@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 +28 -14
- package/dist/api.d.ts +4 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/better-tenant.d.ts +9 -10
- package/dist/better-tenant.d.ts.map +1 -1
- package/dist/console-endpoints.d.ts +13 -0
- package/dist/console-endpoints.d.ts.map +1 -0
- package/dist/index.cjs +236 -105
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +235 -103
- package/dist/index.js.map +1 -1
- package/dist/resolver.d.ts +6 -5
- package/dist/resolver.d.ts.map +1 -1
- package/dist/telemetry.d.ts +1 -4
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/types.d.ts +32 -6
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
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 {
|
|
15
|
+
import { drizzleDatabase } from "@usebetterdev/tenant/drizzle"; // or prismaDatabase
|
|
16
16
|
|
|
17
|
-
const adapter = drizzleAdapter(db);
|
|
18
17
|
const tenant = betterTenant({
|
|
19
|
-
|
|
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
|
|
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,
|
|
43
|
-
| `resolveTenant(request, config)`
|
|
44
|
-
| `runAs(tenantId,
|
|
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
|
|
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;
|
|
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"}
|
package/dist/better-tenant.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BetterTenantConfig, Tenant, ResolvableRequest, TenantContext
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
api: TenantApi;
|
|
31
|
-
};
|
|
31
|
+
api: TenantApi;
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
-
* Creates the Better Tenant instance. Config must include adapter and
|
|
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,
|
|
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) =>
|
|
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) =>
|
|
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 =
|
|
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) =>
|
|
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) =>
|
|
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({
|
|
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
|
|
552
|
+
function describeStrategies(config) {
|
|
553
|
+
const strategies = [];
|
|
582
554
|
if (config.header !== void 0) {
|
|
583
|
-
|
|
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
|
|
590
|
-
|
|
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
|
-
|
|
596
|
-
if (v !== void 0) {
|
|
597
|
-
return v;
|
|
598
|
-
}
|
|
562
|
+
strategies.push("subdomain");
|
|
599
563
|
}
|
|
600
564
|
if (config.jwt !== void 0) {
|
|
601
|
-
const
|
|
602
|
-
|
|
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
|
-
|
|
608
|
-
if (typeof v === "string" && v.length > 0) {
|
|
609
|
-
return v;
|
|
610
|
-
}
|
|
569
|
+
strategies.push("custom resolver");
|
|
611
570
|
}
|
|
612
|
-
return
|
|
571
|
+
return strategies;
|
|
613
572
|
}
|
|
614
|
-
async function
|
|
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
|
|
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 {
|
|
772
|
+
const { database, tenantResolver, loadTenant } = config;
|
|
773
|
+
const { adapter, getTenantRepository } = database;
|
|
651
774
|
sendInitTelemetry(config, config.telemetry);
|
|
652
|
-
const api =
|
|
653
|
-
const
|
|
654
|
-
const
|
|
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
|
-
|
|
660
|
-
|
|
800
|
+
runAs: (tenantId, fn) => runWithTenantAndDatabase(
|
|
801
|
+
tenantId,
|
|
802
|
+
adapter,
|
|
803
|
+
fn,
|
|
804
|
+
runWithTenantAndDatabaseOptions
|
|
805
|
+
),
|
|
661
806
|
runAsSystem: (fn) => runAsSystem(adapter, fn),
|
|
662
|
-
resolveTenant:
|
|
663
|
-
|
|
807
|
+
resolveTenant: resolveAndNormalize,
|
|
808
|
+
resolverStrategies,
|
|
664
809
|
handleRequest: (request, next, options) => handleRequest(request, next, {
|
|
665
810
|
...options,
|
|
666
|
-
resolveTenant:
|
|
811
|
+
resolveTenant: resolveAndNormalize,
|
|
667
812
|
adapter
|
|
668
813
|
}),
|
|
669
|
-
|
|
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
|
});
|