202 lines
7.2 KiB
TypeScript
202 lines
7.2 KiB
TypeScript
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;
|
|
},
|
|
};
|
|
}
|