Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -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;
},
};
}