Initial Commit
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
import {
|
||||
createTenantContext,
|
||||
NullTenantContext,
|
||||
type Tenant,
|
||||
type TenantContext,
|
||||
type TenantResolver,
|
||||
} from "@suaveplan/api-tenant";
|
||||
import type { Context } from "hono";
|
||||
import {
|
||||
TENANT_PLUGIN_CODES,
|
||||
TenantPluginError,
|
||||
} from "../error-codes/error-codes.js";
|
||||
import { getTenantPluginInstruments } from "../otel/instruments.js";
|
||||
import type {
|
||||
CreateTenantInput,
|
||||
ListTenantsQuery,
|
||||
TenantStore,
|
||||
UpdateTenantInput,
|
||||
} from "../store/store.js";
|
||||
|
||||
/**
|
||||
* Inputs required to build the canonical `TenantService` implementation.
|
||||
* The resolver is always present (composed at `onInit`); the store is
|
||||
* optional — when absent, CRUD methods throw STORE_NOT_CONFIGURED.
|
||||
*/
|
||||
export interface CreateTenantServiceOptions {
|
||||
readonly resolver: TenantResolver;
|
||||
readonly store?: TenantStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service slot registered by `createTenantPlugin` in the host
|
||||
* `ServiceRegistry`. Downstream plugins and route handlers call
|
||||
* `ctx.getService("tenantService")` for the helper bag of tenant ops.
|
||||
*/
|
||||
export interface TenantService {
|
||||
/** Look up the tenant for the current request by re-running the resolver chain. */
|
||||
resolveFrom(c: Context): Promise<TenantContext>;
|
||||
/** Same as {@link resolveFrom} but throws when no tenant is resolved. */
|
||||
requireFrom(c: Context): TenantContext;
|
||||
/** Read the tenant set on the Hono context by the middleware (or NullTenantContext). */
|
||||
current(c: Context): TenantContext;
|
||||
/** Direct store lookup. Returns `null` when store is absent. */
|
||||
getById(id: string): Promise<Tenant | null>;
|
||||
/** Store-backed listing. Throws STORE_NOT_CONFIGURED when no store. */
|
||||
listAll(query?: ListTenantsQuery): Promise<readonly Tenant[]>;
|
||||
/** Store-backed create. Throws STORE_NOT_CONFIGURED when no store. */
|
||||
createTenant(input: CreateTenantInput): Promise<Tenant>;
|
||||
/** Store-backed update. Throws STORE_NOT_CONFIGURED when no store. */
|
||||
updateTenant(id: string, patch: UpdateTenantInput): Promise<Tenant>;
|
||||
/** Store-backed soft-delete. Throws STORE_NOT_CONFIGURED when no store. */
|
||||
deleteTenant(id: string): Promise<void>;
|
||||
/** The composed resolver — exposed for diagnostics + advanced use. */
|
||||
getResolver(): TenantResolver;
|
||||
}
|
||||
|
||||
function requireStore(store: TenantStore | undefined, op: string): TenantStore {
|
||||
if (store === undefined) {
|
||||
throw new TenantPluginError(
|
||||
TENANT_PLUGIN_CODES.STORE_NOT_CONFIGURED,
|
||||
`tenantService.${op} requires a TenantStore but none was configured`,
|
||||
{ context: { op, statusCode: 500 } }
|
||||
);
|
||||
}
|
||||
return store;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct the canonical `TenantService` backed by the supplied
|
||||
* resolver chain and optional store. Every CRUD method increments
|
||||
* `tenant_plugin.management_ops_total` with the resolved outcome.
|
||||
*/
|
||||
export function createTenantService(
|
||||
options: CreateTenantServiceOptions
|
||||
): TenantService {
|
||||
const { resolver, store } = options;
|
||||
|
||||
return {
|
||||
async resolveFrom(c) {
|
||||
const tenant = await resolver.resolve(c);
|
||||
return tenant ? createTenantContext(tenant) : NullTenantContext;
|
||||
},
|
||||
requireFrom(c) {
|
||||
const tenant = c.get("tenant");
|
||||
if (tenant === undefined) {
|
||||
throw new TenantPluginError(
|
||||
TENANT_PLUGIN_CODES.TENANT_NOT_FOUND,
|
||||
"No tenant resolved for the current request",
|
||||
{ context: { statusCode: 404 } }
|
||||
);
|
||||
}
|
||||
return createTenantContext(tenant);
|
||||
},
|
||||
current(c) {
|
||||
const tenant = c.get("tenant");
|
||||
return tenant ? createTenantContext(tenant) : NullTenantContext;
|
||||
},
|
||||
async getById(id) {
|
||||
const instruments = getTenantPluginInstruments();
|
||||
if (store === undefined) {
|
||||
instruments.managementOpsTotal.add(1, {
|
||||
op: "read",
|
||||
outcome: "failure",
|
||||
});
|
||||
throw new TenantPluginError(
|
||||
TENANT_PLUGIN_CODES.STORE_NOT_CONFIGURED,
|
||||
"tenantService.getById requires a TenantStore but none was configured",
|
||||
{ context: { op: "read", statusCode: 500 } }
|
||||
);
|
||||
}
|
||||
try {
|
||||
const result = await store.getById(id);
|
||||
instruments.managementOpsTotal.add(1, {
|
||||
op: "read",
|
||||
outcome: "success",
|
||||
});
|
||||
return result;
|
||||
} catch (err) {
|
||||
instruments.managementOpsTotal.add(1, {
|
||||
op: "read",
|
||||
outcome: "failure",
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
async listAll(query) {
|
||||
const instruments = getTenantPluginInstruments();
|
||||
const s = requireStore(store, "listAll");
|
||||
try {
|
||||
const rows = await s.listAll(query);
|
||||
instruments.managementOpsTotal.add(1, {
|
||||
op: "list",
|
||||
outcome: "success",
|
||||
});
|
||||
return rows;
|
||||
} catch (err) {
|
||||
instruments.managementOpsTotal.add(1, {
|
||||
op: "list",
|
||||
outcome: "failure",
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
async createTenant(input) {
|
||||
const instruments = getTenantPluginInstruments();
|
||||
const s = requireStore(store, "createTenant");
|
||||
try {
|
||||
const tenant = await s.create(input);
|
||||
instruments.managementOpsTotal.add(1, {
|
||||
op: "create",
|
||||
outcome: "success",
|
||||
});
|
||||
return tenant;
|
||||
} catch (err) {
|
||||
instruments.managementOpsTotal.add(1, {
|
||||
op: "create",
|
||||
outcome: "failure",
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
async updateTenant(id, patch) {
|
||||
const instruments = getTenantPluginInstruments();
|
||||
const s = requireStore(store, "updateTenant");
|
||||
try {
|
||||
const tenant = await s.update(id, patch);
|
||||
instruments.managementOpsTotal.add(1, {
|
||||
op: "update",
|
||||
outcome: "success",
|
||||
});
|
||||
return tenant;
|
||||
} catch (err) {
|
||||
instruments.managementOpsTotal.add(1, {
|
||||
op: "update",
|
||||
outcome: "failure",
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
async deleteTenant(id) {
|
||||
const instruments = getTenantPluginInstruments();
|
||||
const s = requireStore(store, "deleteTenant");
|
||||
try {
|
||||
await s.delete(id);
|
||||
instruments.managementOpsTotal.add(1, {
|
||||
op: "delete",
|
||||
outcome: "success",
|
||||
});
|
||||
} catch (err) {
|
||||
instruments.managementOpsTotal.add(1, {
|
||||
op: "delete",
|
||||
outcome: "failure",
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
getResolver() {
|
||||
return resolver;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user