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; /** 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; /** Store-backed listing. Throws STORE_NOT_CONFIGURED when no store. */ listAll(query?: ListTenantsQuery): Promise; /** Store-backed create. Throws STORE_NOT_CONFIGURED when no store. */ createTenant(input: CreateTenantInput): Promise; /** Store-backed update. Throws STORE_NOT_CONFIGURED when no store. */ updateTenant(id: string, patch: UpdateTenantInput): Promise; /** Store-backed soft-delete. Throws STORE_NOT_CONFIGURED when no store. */ deleteTenant(id: string): Promise; /** 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; }, }; }