@usebetterdev/tenant-core 0.2.0-beta.9 → 0.3.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,18 +12,11 @@ pnpm add @usebetterdev/tenant
12
12
 
13
13
  ```ts
14
14
  import { betterTenant } from "@usebetterdev/tenant";
15
- import { drizzleAdapter } from "@usebetterdev/tenant/drizzle"; // or your adapter
15
+ import { drizzleDatabase } from "@usebetterdev/tenant/drizzle"; // or prismaDatabase
16
16
 
17
- const adapter = drizzleAdapter(db);
18
17
  const tenant = betterTenant({
19
- adapter,
18
+ database: drizzleDatabase(db),
20
19
  tenantResolver: { header: "x-tenant-id" },
21
- getTenantRepository: (database) => ({ create, update, list, delete }), // adapter provides this
22
- });
23
-
24
- // In middleware: resolve tenantId (e.g. tenant.resolveTenant(request)), then:
25
- await tenant.runWithTenantAndDatabase(tenantId, adapter, async () => {
26
- await next();
27
20
  });
28
21
 
29
22
  // In handlers:
@@ -35,15 +28,16 @@ const database = tenant.getDatabase(); // tenant-scoped DB handle (use only
35
28
 
36
29
  | API | Description |
37
30
  | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
38
- | `betterTenant(config)` | Create instance with adapter, resolver, optional `getTenantRepository`. |
31
+ | `betterTenant(config)` | Create instance with `database` provider and resolver. |
39
32
  | `getContext()` | Current `TenantContext \| undefined` (tenantId, tenant?, database?). |
40
33
  | `tenant.getDatabase()` | Tenant-scoped database handle, or `undefined` outside request scope. Use only this for tenant-scoped tables. |
41
34
  | `runWithTenant(tenantId, fn)` | Run `fn` with context only (no DB). For tests or when adapter not used. |
42
- | `runWithTenantAndDatabase(tenantId, adapter, fn)` | Set context and run `fn` with adapter's tenant-scoped DB. Call from middleware. |
43
- | `resolveTenant(request, config)` / `resolveTenantAsync` | Resolve tenant id from request (header, path, subdomain, jwt, custom). |
44
- | `runAs(tenantId, adapter, fn)` | Same as request flow; use for cron or per-tenant jobs. |
35
+ | `tenant.runWithTenantAndDatabase(tenantId, fn)` | Set context and run `fn` with adapter's tenant-scoped DB. Call from middleware. |
36
+ | `resolveTenant(request, config)` | Resolve tenant id from request (header, path, subdomain, jwt, custom). Returns `Promise<string \| undefined>`. |
37
+ | `tenant.runAs(tenantId, fn)` | Same as request flow; use for cron or per-tenant jobs. |
45
38
  | `runAsSystem(fn)` | Run with RLS bypass (adapter must implement `runAsSystem`). |
46
- | `tenant.api` | `createTenant`, `updateTenant`, `listTenants`, `deleteTenant` (when `getTenantRepository` is set). |
39
+ | `tenant.api` | `createTenant`, `updateTenant`, `listTenants`, `deleteTenant`. |
40
+ | `TenantRepository.getBySlug(slug)` | Look up a tenant by slug. Used internally for slug-to-UUID auto-resolution. Returns `Tenant \| null`. |
47
41
 
48
42
  ## Adapter contract
49
43
 
@@ -58,6 +52,26 @@ Types: `TenantScopedDatabase` (opaque handle), `SystemDatabase` (opaque handle f
58
52
 
59
53
  Resolution order: **header → path → subdomain → jwt → custom**. Configure `tenantResolver` with e.g. `header: "x-tenant-id"`, `path: "/t/:tenantId"`, `subdomain: true`, `jwt: { claim: "tenant_id" }`, or `custom: (req) => ...`.
60
54
 
55
+ ### Slug-to-UUID resolution
56
+
57
+ After the resolver extracts an identifier, it is automatically normalized to a UUID:
58
+
59
+ - **UUID** — passes through unchanged.
60
+ - **Slug** (e.g. `"acme"`) — looked up in the tenants table via `getBySlug`.
61
+ - **Custom transform** — if `resolveToId` is set, it is called instead of the above (takes full precedence).
62
+
63
+ This means subdomain resolution (`"acme"`) or header-based slugs automatically resolve to UUIDs.
64
+
65
+ ### `resolveToId`
66
+
67
+ Optional transform added to `TenantResolverConfig`:
68
+
69
+ ```ts
70
+ resolveToId?: (identifier: string) => string | Promise<string>;
71
+ ```
72
+
73
+ Use this to map custom domains, external IDs, or any non-slug identifier to a tenant UUID.
74
+
61
75
  ## Telemetry
62
76
 
63
77
  Anonymous telemetry is **on by default** to help improve the library. Data is sent to `https://telemetry.usebetter.dev` and includes runtime, framework, and redacted config (no PII or secrets). Telemetry is disabled in `NODE_ENV=test`.
package/dist/api.d.ts CHANGED
@@ -13,12 +13,16 @@ export interface ListTenantsOptions {
13
13
  }
14
14
  /**
15
15
  * Tenant CRUD API. All methods run via adapter.runAsSystem and getTenantRepository.
16
+ * The runAsSystem check is deferred to call time so that betterTenant() can be
17
+ * constructed with adapters that don't implement runAsSystem (e.g. test mocks).
16
18
  */
17
19
  export declare function createTenantApi<TDb, SDb>(adapter: TenantAdapter<TDb, SDb>, getTenantRepository: (database: SDb) => TenantRepository): {
18
20
  createTenant(data: CreateTenantData): Promise<Tenant>;
19
21
  updateTenant(tenantId: string, data: UpdateTenantData): Promise<Tenant>;
20
22
  listTenants(options?: ListTenantsOptions): Promise<Tenant[]>;
21
23
  deleteTenant(tenantId: string): Promise<void>;
24
+ getTenant(tenantId: string): Promise<Tenant | null>;
25
+ countTenants(): Promise<number>;
22
26
  };
23
27
  /**
24
28
  * runAs(tenantId, fn): same as request flow — set context + run inside adapter.runWithTenant
package/dist/api.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAO1E,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAwBD;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,GAAG,EACtC,OAAO,EAAE,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,EAChC,mBAAmB,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,gBAAgB;uBAM7B,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC;2BAgB/C,MAAM,QACV,gBAAgB,GACrB,OAAO,CAAC,MAAM,CAAC;0BAYS,kBAAkB,GAAQ,OAAO,CAAC,MAAM,EAAE,CAAC;2BAWzC,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;EAStD;AAED;;;GAGG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,CAAC,EAChC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,aAAa,CAAC,GAAG,CAAC,EAC3B,EAAE,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,GAChC,OAAO,CAAC,CAAC,CAAC,CAEZ;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,CAAC,EACtC,OAAO,EAAE,aAAa,CAAC,OAAO,EAAE,GAAG,CAAC,EACpC,EAAE,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,GAChC,OAAO,CAAC,CAAC,CAAC,CAGZ"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAM1E,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAaD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,GAAG,EACtC,OAAO,EAAE,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,EAChC,mBAAmB,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,gBAAgB;uBAG7B,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC;2BAiB/C,MAAM,QACV,gBAAgB,GACrB,OAAO,CAAC,MAAM,CAAC;0BAaS,kBAAkB,GAAQ,OAAO,CAAC,MAAM,EAAE,CAAC;2BASzC,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;wBAUzB,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;oBAUnC,OAAO,CAAC,MAAM,CAAC;EAOxC;AAED;;;GAGG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,CAAC,EAChC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,aAAa,CAAC,GAAG,CAAC,EAC3B,EAAE,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,GAChC,OAAO,CAAC,CAAC,CAAC,CAEZ;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,CAAC,EACtC,OAAO,EAAE,aAAa,CAAC,OAAO,EAAE,GAAG,CAAC,EACpC,EAAE,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,GAChC,OAAO,CAAC,CAAC,CAAC,CAGZ"}
@@ -1,4 +1,4 @@
1
- import type { BetterTenantConfig, Tenant, ResolvableRequest, TenantContext, TenantAdapter, RunWithTenantAndDatabaseOptions } from "./types.js";
1
+ import type { BetterTenantConfig, Tenant, ResolvableRequest, TenantContext } from "./types.js";
2
2
  import { runWithTenant } from "./context.js";
3
3
  import { type HandleRequestOptions } from "./handle-request.js";
4
4
  export interface TenantApi {
@@ -15,24 +15,23 @@ export interface TenantApi {
15
15
  offset?: number;
16
16
  }): Promise<Tenant[]>;
17
17
  deleteTenant(tenantId: string): Promise<void>;
18
+ getTenant(tenantId: string): Promise<Tenant | null>;
19
+ countTenants(): Promise<number>;
18
20
  }
19
21
  export interface BetterTenantInstance<TDb = unknown, SDb = unknown> {
20
22
  getContext: () => TenantContext | undefined;
21
23
  getDatabase: () => TDb | undefined;
22
24
  runWithTenant: typeof runWithTenant;
23
- runWithTenantAndDatabase: <T>(tenantId: string, adapter: TenantAdapter<TDb, SDb>, fn: (database: TDb) => Promise<T>, options?: RunWithTenantAndDatabaseOptions<SDb>) => Promise<T>;
24
- runAs: <T>(tenantId: string, adapter: TenantAdapter<TDb>, fn: (database: TDb) => Promise<T>) => Promise<T>;
25
+ runAs: <T>(tenantId: string, fn: (database: TDb) => Promise<T>) => Promise<T>;
25
26
  runAsSystem: <T>(fn: (database: SDb) => Promise<T>) => Promise<T>;
26
- resolveTenant: (request: ResolvableRequest) => string | undefined;
27
- resolveTenantAsync: (request: ResolvableRequest) => Promise<string | undefined>;
27
+ resolveTenant: (request: ResolvableRequest) => Promise<string | undefined>;
28
+ /** Human-readable descriptions of the configured resolution strategies. */
29
+ resolverStrategies: string[];
28
30
  handleRequest: <Req extends ResolvableRequest, Result>(request: Req, next: () => Promise<Result>, options?: Omit<HandleRequestOptions<Req, Result>, "resolveTenant" | "adapter">) => Promise<Result>;
29
- tenant: {
30
- api: TenantApi;
31
- };
31
+ api: TenantApi;
32
32
  }
33
33
  /**
34
- * Creates the Better Tenant instance. Config must include adapter and tenantResolver.
35
- * When getTenantRepository is provided, tenant.api is available for CRUD on the tenants table.
34
+ * Creates the Better Tenant instance. Config must include adapter, tenantResolver, and getTenantRepository.
36
35
  */
37
36
  export declare function betterTenant<TDb, SDb>(config: BetterTenantConfig<TDb, SDb>): BetterTenantInstance<TDb, SDb>;
38
37
  //# sourceMappingURL=better-tenant.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"better-tenant.d.ts","sourceRoot":"","sources":["../src/better-tenant.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,MAAM,EACN,iBAAiB,EACjB,aAAa,EACb,aAAa,EACb,+BAA+B,EAChC,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAc,aAAa,EAAE,MAAM,cAAc,CAAC;AAEzD,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAQ7B,MAAM,WAAW,SAAS;IACxB,YAAY,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACpE,YAAY,CACV,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GACrC,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,WAAW,CAAC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC9E,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/C;AAED,MAAM,WAAW,oBAAoB,CAAC,GAAG,GAAG,OAAO,EAAE,GAAG,GAAG,OAAO;IAChE,UAAU,EAAE,MAAM,aAAa,GAAG,SAAS,CAAC;IAC5C,WAAW,EAAE,MAAM,GAAG,GAAG,SAAS,CAAC;IACnC,aAAa,EAAE,OAAO,aAAa,CAAC;IACpC,wBAAwB,EAAE,CAAC,CAAC,EAC1B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,EAChC,EAAE,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,EACjC,OAAO,CAAC,EAAE,+BAA+B,CAAC,GAAG,CAAC,KAC3C,OAAO,CAAC,CAAC,CAAC,CAAC;IAChB,KAAK,EAAE,CAAC,CAAC,EACP,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,aAAa,CAAC,GAAG,CAAC,EAC3B,EAAE,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,KAC9B,OAAO,CAAC,CAAC,CAAC,CAAC;IAChB,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAClE,aAAa,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,MAAM,GAAG,SAAS,CAAC;IAClE,kBAAkB,EAAE,CAClB,OAAO,EAAE,iBAAiB,KACvB,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACjC,aAAa,EAAE,CAAC,GAAG,SAAS,iBAAiB,EAAE,MAAM,EACnD,OAAO,EAAE,GAAG,EACZ,IAAI,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,EAC3B,OAAO,CAAC,EAAE,IAAI,CAAC,oBAAoB,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC,KAC3E,OAAO,CAAC,MAAM,CAAC,CAAC;IACrB,MAAM,EAAE;QAAE,GAAG,EAAE,SAAS,CAAA;KAAE,CAAC;CAC5B;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,kBAAkB,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,oBAAoB,CAAC,GAAG,EAAE,GAAG,CAAC,CAmC3G"}
1
+ {"version":3,"file":"better-tenant.d.ts","sourceRoot":"","sources":["../src/better-tenant.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,MAAM,EAIN,iBAAiB,EACjB,aAAa,EAEd,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAc,aAAa,EAAE,MAAM,cAAc,CAAC;AAEzD,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAyC7B,MAAM,WAAW,SAAS;IACxB,YAAY,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACpE,YAAY,CACV,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GACrC,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,WAAW,CAAC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC9E,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACpD,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,oBAAoB,CAAC,GAAG,GAAG,OAAO,EAAE,GAAG,GAAG,OAAO;IAChE,UAAU,EAAE,MAAM,aAAa,GAAG,SAAS,CAAC;IAC5C,WAAW,EAAE,MAAM,GAAG,GAAG,SAAS,CAAC;IACnC,aAAa,EAAE,OAAO,aAAa,CAAC;IACpC,KAAK,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC9E,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAClE,aAAa,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC3E,2EAA2E;IAC3E,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,aAAa,EAAE,CAAC,GAAG,SAAS,iBAAiB,EAAE,MAAM,EACnD,OAAO,EAAE,GAAG,EACZ,IAAI,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,EAC3B,OAAO,CAAC,EAAE,IAAI,CACZ,oBAAoB,CAAC,GAAG,EAAE,MAAM,CAAC,EACjC,eAAe,GAAG,SAAS,CAC5B,KACE,OAAO,CAAC,MAAM,CAAC,CAAC;IACrB,GAAG,EAAE,SAAS,CAAC;CAChB;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,GAAG,EACnC,MAAM,EAAE,kBAAkB,CAAC,GAAG,EAAE,GAAG,CAAC,GACnC,oBAAoB,CAAC,GAAG,EAAE,GAAG,CAAC,CA+DhC"}
@@ -0,0 +1,13 @@
1
+ import type { TenantApi } from "./better-tenant.js";
2
+ interface ConsoleEndpoint {
3
+ method: "GET" | "POST" | "PATCH" | "DELETE";
4
+ path: string;
5
+ handler: (request: unknown) => Promise<{
6
+ status: number;
7
+ body: unknown;
8
+ }>;
9
+ requiredPermission: "read" | "write" | "admin";
10
+ }
11
+ export declare function createTenantConsoleEndpoints(api: TenantApi): ConsoleEndpoint[];
12
+ export {};
13
+ //# sourceMappingURL=console-endpoints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console-endpoints.d.ts","sourceRoot":"","sources":["../src/console-endpoints.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAQpD,UAAU,eAAe;IACvB,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC1E,kBAAkB,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;CAChD;AAoBD,wBAAgB,4BAA4B,CAC1C,GAAG,EAAE,SAAS,GACb,eAAe,EAAE,CAgInB"}
package/dist/index.cjs CHANGED
@@ -24,14 +24,13 @@ __export(index_exports, {
24
24
  TenantNotResolvedError: () => TenantNotResolvedError,
25
25
  betterTenant: () => betterTenant,
26
26
  createTenantApi: () => createTenantApi,
27
+ describeStrategies: () => describeStrategies,
27
28
  getContext: () => getContext,
28
29
  handleRequest: () => handleRequest,
29
30
  resolveTenant: () => resolveTenant,
30
- resolveTenantAsync: () => resolveTenantAsync,
31
31
  runAs: () => runAs,
32
32
  runAsSystem: () => runAsSystem,
33
33
  runWithTenant: () => runWithTenant,
34
- runWithTenantAndDatabase: () => runWithTenantAndDatabase,
35
34
  sendCliTelemetry: () => sendCliTelemetry,
36
35
  toResolvableRequest: () => toResolvableRequest
37
36
  });
@@ -55,7 +54,6 @@ function getTelemetryTenantConfig(config) {
55
54
  custom: !!r.custom
56
55
  },
57
56
  tenantTablesCount: 0,
58
- hasGetTenantRepository: !!config.getTenantRepository,
59
57
  loadTenant: config.loadTenant,
60
58
  basePathSet: !!config.basePath,
61
59
  plugins: (config.plugins ?? []).map((p) => String(p.id))
@@ -365,7 +363,6 @@ async function handleRequest(request, next, options) {
365
363
 
366
364
  // src/api.ts
367
365
  var DEFAULT_LIST_LIMIT = 50;
368
- var MAX_LIST_LIMIT = 50;
369
366
  function requireRunAsSystem(adapter) {
370
367
  if (!adapter.runAsSystem) {
371
368
  throw new Error(
@@ -374,17 +371,7 @@ function requireRunAsSystem(adapter) {
374
371
  }
375
372
  return adapter.runAsSystem;
376
373
  }
377
- function requireTenantRepository(getTenantRepository) {
378
- if (!getTenantRepository) {
379
- throw new Error(
380
- "better-tenant: tenant.api requires getTenantRepository in config (adapter provides CRUD for tenants table)"
381
- );
382
- }
383
- return getTenantRepository;
384
- }
385
374
  function createTenantApi(adapter, getTenantRepository) {
386
- const runAsSystem2 = requireRunAsSystem(adapter);
387
- const getRepository = requireTenantRepository(getTenantRepository);
388
375
  return {
389
376
  async createTenant(data) {
390
377
  if (!data.name?.trim()) {
@@ -393,8 +380,9 @@ function createTenantApi(adapter, getTenantRepository) {
393
380
  if (!data.slug?.trim()) {
394
381
  throw new Error("better-tenant: createTenant requires slug");
395
382
  }
383
+ const runAsSystem2 = requireRunAsSystem(adapter);
396
384
  return runAsSystem2(
397
- (database) => getRepository(database).create({
385
+ (database) => getTenantRepository(database).create({
398
386
  name: data.name.trim(),
399
387
  slug: data.slug.trim()
400
388
  })
@@ -404,29 +392,44 @@ function createTenantApi(adapter, getTenantRepository) {
404
392
  if (!tenantId?.trim()) {
405
393
  throw new Error("better-tenant: updateTenant requires tenantId");
406
394
  }
395
+ const runAsSystem2 = requireRunAsSystem(adapter);
407
396
  return runAsSystem2(
408
- (database) => getRepository(database).update(tenantId, {
397
+ (database) => getTenantRepository(database).update(tenantId, {
409
398
  ...data.name !== void 0 && { name: data.name },
410
399
  ...data.slug !== void 0 && { slug: data.slug }
411
400
  })
412
401
  );
413
402
  },
414
403
  async listTenants(options = {}) {
415
- const limit = Math.min(
416
- options.limit ?? DEFAULT_LIST_LIMIT,
417
- MAX_LIST_LIMIT
418
- );
404
+ const limit = options.limit ?? DEFAULT_LIST_LIMIT;
419
405
  const offset = Math.max(0, options.offset ?? 0);
406
+ const runAsSystem2 = requireRunAsSystem(adapter);
420
407
  return runAsSystem2(
421
- (database) => getRepository(database).list({ limit, offset })
408
+ (database) => getTenantRepository(database).list({ limit, offset })
422
409
  );
423
410
  },
424
411
  async deleteTenant(tenantId) {
425
412
  if (!tenantId?.trim()) {
426
413
  throw new Error("better-tenant: deleteTenant requires tenantId");
427
414
  }
415
+ const runAsSystem2 = requireRunAsSystem(adapter);
416
+ return runAsSystem2(
417
+ (database) => getTenantRepository(database).delete(tenantId)
418
+ );
419
+ },
420
+ async getTenant(tenantId) {
421
+ if (!tenantId?.trim()) {
422
+ throw new Error("better-tenant: getTenant requires tenantId");
423
+ }
424
+ const runAsSystem2 = requireRunAsSystem(adapter);
425
+ return runAsSystem2(
426
+ (database) => getTenantRepository(database).getById(tenantId)
427
+ );
428
+ },
429
+ async countTenants() {
430
+ const runAsSystem2 = requireRunAsSystem(adapter);
428
431
  return runAsSystem2(
429
- (database) => getRepository(database).delete(tenantId)
432
+ (database) => getTenantRepository(database).count()
430
433
  );
431
434
  }
432
435
  };
@@ -436,7 +439,7 @@ async function runAs(tenantId, adapter, fn) {
436
439
  }
437
440
  async function runAsSystem(adapter, fn) {
438
441
  const run = requireRunAsSystem(adapter);
439
- return runWithContext({ tenantId: "", isSystem: true }, () => run(fn));
442
+ return runWithContext({ isSystem: true }, () => run(fn));
440
443
  }
441
444
 
442
445
  // src/resolver.ts
@@ -518,39 +521,7 @@ function decodeJwtPayload(token) {
518
521
  return null;
519
522
  }
520
523
  }
521
- function resolveFromJwt(request, config) {
522
- const getToken = request.getToken;
523
- if (!getToken) {
524
- return void 0;
525
- }
526
- const token = typeof getToken === "function" ? getToken() : getToken;
527
- const value = token instanceof Promise ? void 0 : token ?? void 0;
528
- const resolved = value ?? void 0;
529
- if (!resolved) {
530
- return void 0;
531
- }
532
- const claim = typeof config === "string" ? config : config.claim;
533
- if (!claim) {
534
- return void 0;
535
- }
536
- const verifyToken = typeof config === "object" ? config.verifyToken : void 0;
537
- let payload;
538
- if (verifyToken) {
539
- const result = verifyToken(resolved);
540
- if (result instanceof Promise) {
541
- return void 0;
542
- }
543
- payload = result;
544
- } else {
545
- payload = decodeJwtPayload(resolved);
546
- }
547
- if (!payload) {
548
- return void 0;
549
- }
550
- const claimValue = payload[claim];
551
- return typeof claimValue === "string" && claimValue.length > 0 ? claimValue : void 0;
552
- }
553
- async function resolveFromJwtAsync(request, config) {
524
+ async function resolveFromJwt(request, config) {
554
525
  const getToken = request.getToken;
555
526
  if (!getToken) {
556
527
  return void 0;
@@ -578,40 +549,28 @@ async function resolveFromJwtAsync(request, config) {
578
549
  const claimValue = payload[claim];
579
550
  return typeof claimValue === "string" && claimValue.length > 0 ? claimValue : void 0;
580
551
  }
581
- function resolveTenant(request, config) {
552
+ function describeStrategies(config) {
553
+ const strategies = [];
582
554
  if (config.header !== void 0) {
583
- const v = resolveFromHeader(request, config.header);
584
- if (v !== void 0) {
585
- return v;
586
- }
555
+ strategies.push(`header '${config.header}'`);
587
556
  }
588
557
  if (config.path !== void 0) {
589
- const v = resolveFromPath(request, config.path);
590
- if (v !== void 0) {
591
- return v;
592
- }
558
+ const pattern = typeof config.path === "string" ? config.path : config.path.pattern;
559
+ strategies.push(`path '${pattern}'`);
593
560
  }
594
561
  if (config.subdomain !== void 0 && config.subdomain !== false) {
595
- const v = resolveFromSubdomain(request, config.subdomain);
596
- if (v !== void 0) {
597
- return v;
598
- }
562
+ strategies.push("subdomain");
599
563
  }
600
564
  if (config.jwt !== void 0) {
601
- const v = resolveFromJwt(request, config.jwt);
602
- if (v !== void 0) {
603
- return v;
604
- }
565
+ const claim = typeof config.jwt === "string" ? config.jwt : config.jwt.claim;
566
+ strategies.push(`jwt claim '${claim}'`);
605
567
  }
606
568
  if (config.custom !== void 0) {
607
- const v = config.custom(request);
608
- if (typeof v === "string" && v.length > 0) {
609
- return v;
610
- }
569
+ strategies.push("custom resolver");
611
570
  }
612
- return void 0;
571
+ return strategies;
613
572
  }
614
- async function resolveTenantAsync(request, config) {
573
+ async function resolveTenant(request, config) {
615
574
  if (config.header !== void 0) {
616
575
  const v = resolveFromHeader(request, config.header);
617
576
  if (v !== void 0) {
@@ -631,7 +590,7 @@ async function resolveTenantAsync(request, config) {
631
590
  }
632
591
  }
633
592
  if (config.jwt !== void 0) {
634
- const v = await resolveFromJwtAsync(request, config.jwt);
593
+ const v = await resolveFromJwt(request, config.jwt);
635
594
  if (v !== void 0) {
636
595
  return v;
637
596
  }
@@ -645,41 +604,214 @@ async function resolveTenantAsync(request, config) {
645
604
  return void 0;
646
605
  }
647
606
 
607
+ // src/console-endpoints.ts
608
+ function toConsoleRequest(request) {
609
+ if (request && typeof request === "object") {
610
+ return request;
611
+ }
612
+ return {};
613
+ }
614
+ function parsePositiveInt(value, max) {
615
+ if (value === void 0) {
616
+ return void 0;
617
+ }
618
+ const parsed = Number(value);
619
+ if (!Number.isFinite(parsed) || parsed < 0) {
620
+ return void 0;
621
+ }
622
+ return Math.min(Math.floor(parsed), max);
623
+ }
624
+ function createTenantConsoleEndpoints(api) {
625
+ return [
626
+ {
627
+ method: "GET",
628
+ path: "/tenants",
629
+ requiredPermission: "read",
630
+ async handler(request) {
631
+ try {
632
+ const { query } = toConsoleRequest(request);
633
+ const limit = parsePositiveInt(query?.limit, 1e3);
634
+ const offset = parsePositiveInt(query?.offset, Number.MAX_SAFE_INTEGER);
635
+ const options = {};
636
+ if (limit !== void 0) {
637
+ options.limit = limit;
638
+ }
639
+ if (offset !== void 0) {
640
+ options.offset = offset;
641
+ }
642
+ const tenants = await api.listTenants(options);
643
+ return { status: 200, body: tenants };
644
+ } catch (error) {
645
+ return { status: 500, body: { error: "Internal server error" } };
646
+ }
647
+ }
648
+ },
649
+ {
650
+ method: "GET",
651
+ path: "/tenants/:id",
652
+ requiredPermission: "read",
653
+ async handler(request) {
654
+ try {
655
+ const { params } = toConsoleRequest(request);
656
+ const id = params?.id?.trim();
657
+ if (!id) {
658
+ return { status: 400, body: { error: "Missing tenant id" } };
659
+ }
660
+ const tenant = await api.getTenant(id);
661
+ if (!tenant) {
662
+ return { status: 404, body: { error: "Tenant not found" } };
663
+ }
664
+ return { status: 200, body: tenant };
665
+ } catch {
666
+ return { status: 500, body: { error: "Internal server error" } };
667
+ }
668
+ }
669
+ },
670
+ {
671
+ method: "POST",
672
+ path: "/tenants",
673
+ requiredPermission: "write",
674
+ async handler(request) {
675
+ try {
676
+ const { body } = toConsoleRequest(request);
677
+ if (!body || typeof body !== "object") {
678
+ return { status: 400, body: { error: "Request body is required" } };
679
+ }
680
+ const name = typeof body.name === "string" ? body.name : "";
681
+ const slug = typeof body.slug === "string" ? body.slug : "";
682
+ if (!name.trim() || !slug.trim()) {
683
+ return { status: 400, body: { error: "name and slug are required" } };
684
+ }
685
+ const tenant = await api.createTenant({ name, slug });
686
+ return { status: 201, body: tenant };
687
+ } catch {
688
+ return { status: 500, body: { error: "Internal server error" } };
689
+ }
690
+ }
691
+ },
692
+ {
693
+ method: "PATCH",
694
+ path: "/tenants/:id",
695
+ requiredPermission: "write",
696
+ async handler(request) {
697
+ try {
698
+ const { params, body } = toConsoleRequest(request);
699
+ const id = params?.id?.trim();
700
+ if (!id) {
701
+ return { status: 400, body: { error: "Missing tenant id" } };
702
+ }
703
+ if (!body || typeof body !== "object") {
704
+ return { status: 400, body: { error: "Request body is required" } };
705
+ }
706
+ const data = {};
707
+ if (typeof body.name === "string") {
708
+ data.name = body.name;
709
+ }
710
+ if (typeof body.slug === "string") {
711
+ data.slug = body.slug;
712
+ }
713
+ const tenant = await api.updateTenant(id, data);
714
+ return { status: 200, body: tenant };
715
+ } catch {
716
+ return { status: 500, body: { error: "Internal server error" } };
717
+ }
718
+ }
719
+ },
720
+ {
721
+ method: "DELETE",
722
+ path: "/tenants/:id",
723
+ requiredPermission: "admin",
724
+ async handler(request) {
725
+ try {
726
+ const { params } = toConsoleRequest(request);
727
+ const id = params?.id?.trim();
728
+ if (!id) {
729
+ return { status: 400, body: { error: "Missing tenant id" } };
730
+ }
731
+ await api.deleteTenant(id);
732
+ return { status: 204, body: null };
733
+ } catch {
734
+ return { status: 500, body: { error: "Internal server error" } };
735
+ }
736
+ }
737
+ },
738
+ {
739
+ method: "GET",
740
+ path: "/stats",
741
+ requiredPermission: "read",
742
+ async handler() {
743
+ try {
744
+ const tenantCount = await api.countTenants();
745
+ return { status: 200, body: { tenantCount } };
746
+ } catch (error) {
747
+ return { status: 500, body: { error: "Internal server error" } };
748
+ }
749
+ }
750
+ }
751
+ ];
752
+ }
753
+
648
754
  // src/better-tenant.ts
755
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
756
+ async function resolveIdentifierToId(identifier, resolverConfig, adapter, getTenantRepository) {
757
+ if (resolverConfig.resolveToId) {
758
+ return resolverConfig.resolveToId(identifier);
759
+ }
760
+ if (UUID_RE.test(identifier)) {
761
+ return identifier;
762
+ }
763
+ if (adapter.runAsSystem) {
764
+ const tenant = await adapter.runAsSystem(
765
+ (systemDb) => getTenantRepository(systemDb).getBySlug(identifier)
766
+ );
767
+ return tenant?.id;
768
+ }
769
+ return identifier;
770
+ }
649
771
  function betterTenant(config) {
650
- const { adapter, tenantResolver, getTenantRepository, loadTenant } = config;
772
+ const { database, tenantResolver, loadTenant } = config;
773
+ const { adapter, getTenantRepository } = database;
651
774
  sendInitTelemetry(config, config.telemetry);
652
- const api = getTenantRepository ? createTenantApi(adapter, getTenantRepository) : createStubTenantApi();
653
- const shouldLoadTenant = getTenantRepository != null && loadTenant !== false;
654
- const runWithTenantAndDatabaseOptions = shouldLoadTenant ? { loadTenant: true, getTenantRepository } : void 0;
775
+ const api = createTenantApi(adapter, getTenantRepository);
776
+ const runWithTenantAndDatabaseOptions = loadTenant !== false ? { loadTenant: true, getTenantRepository } : void 0;
777
+ const resolverStrategies = describeStrategies(tenantResolver);
778
+ async function resolveAndNormalize(request) {
779
+ const raw = await resolveTenant(request, tenantResolver);
780
+ if (!raw) return void 0;
781
+ return resolveIdentifierToId(
782
+ raw,
783
+ tenantResolver,
784
+ adapter,
785
+ getTenantRepository
786
+ );
787
+ }
788
+ if (config.console) {
789
+ const endpoints = createTenantConsoleEndpoints(api);
790
+ config.console.registerProduct({
791
+ id: "tenant",
792
+ name: "Better Tenant",
793
+ endpoints
794
+ });
795
+ }
655
796
  return {
656
797
  getContext,
657
798
  getDatabase: () => getDatabase(),
658
799
  runWithTenant,
659
- runWithTenantAndDatabase: (tenantId, _adapter, fn) => runWithTenantAndDatabase(tenantId, adapter, fn, runWithTenantAndDatabaseOptions),
660
- runAs: (tenantId, _adapter, fn) => runAs(tenantId, adapter, fn),
800
+ runAs: (tenantId, fn) => runWithTenantAndDatabase(
801
+ tenantId,
802
+ adapter,
803
+ fn,
804
+ runWithTenantAndDatabaseOptions
805
+ ),
661
806
  runAsSystem: (fn) => runAsSystem(adapter, fn),
662
- resolveTenant: (request) => resolveTenant(request, tenantResolver),
663
- resolveTenantAsync: (request) => resolveTenantAsync(request, tenantResolver),
807
+ resolveTenant: resolveAndNormalize,
808
+ resolverStrategies,
664
809
  handleRequest: (request, next, options) => handleRequest(request, next, {
665
810
  ...options,
666
- resolveTenant: (input) => resolveTenantAsync(input, tenantResolver),
811
+ resolveTenant: resolveAndNormalize,
667
812
  adapter
668
813
  }),
669
- tenant: { api }
670
- };
671
- }
672
- function createStubTenantApi() {
673
- const err = () => {
674
- throw new Error(
675
- "better-tenant: tenant.api requires getTenantRepository in config"
676
- );
677
- };
678
- return {
679
- createTenant: () => err(),
680
- updateTenant: () => err(),
681
- listTenants: () => err(),
682
- deleteTenant: () => err()
814
+ api
683
815
  };
684
816
  }
685
817
 
@@ -773,14 +905,13 @@ function toResolvableRequest(request) {
773
905
  TenantNotResolvedError,
774
906
  betterTenant,
775
907
  createTenantApi,
908
+ describeStrategies,
776
909
  getContext,
777
910
  handleRequest,
778
911
  resolveTenant,
779
- resolveTenantAsync,
780
912
  runAs,
781
913
  runAsSystem,
782
914
  runWithTenant,
783
- runWithTenantAndDatabase,
784
915
  sendCliTelemetry,
785
916
  toResolvableRequest
786
917
  });