@usebetterdev/tenant-core 0.1.0 → 0.2.0-beta.12
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 +33 -20
- package/dist/adapter.d.ts +3 -3
- package/dist/adapter.d.ts.map +1 -1
- package/dist/api.d.ts +6 -4
- package/dist/api.d.ts.map +1 -1
- package/dist/better-tenant.d.ts +13 -18
- package/dist/better-tenant.d.ts.map +1 -1
- package/dist/index.cjs +197 -122
- 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 +196 -119
- package/dist/index.js.map +1 -1
- package/dist/request.d.ts.map +1 -1
- package/dist/resolver.d.ts +6 -5
- package/dist/resolver.d.ts.map +1 -1
- package/dist/telemetry.d.ts +6 -4
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/types.d.ts +35 -16
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,30 +1,22 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @usebetterdev/tenant-core
|
|
2
2
|
|
|
3
|
-
Core library for request-scoped multi-tenancy with Postgres RLS. Provides tenant context (AsyncLocalStorage), resolver strategies, adapter contract, and tenant API. No database driver dependency — use with
|
|
3
|
+
Core library for request-scoped multi-tenancy with Postgres RLS. Provides tenant context (AsyncLocalStorage), resolver strategies, adapter contract, and tenant API. No database driver dependency — use with the Drizzle adapter or a custom adapter.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
pnpm add @
|
|
8
|
+
pnpm add @usebetterdev/tenant
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
## Quick start
|
|
12
12
|
|
|
13
13
|
```ts
|
|
14
|
-
import { betterTenant
|
|
15
|
-
import {
|
|
14
|
+
import { betterTenant } from "@usebetterdev/tenant";
|
|
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
|
-
tenantTables: ["projects", "tasks"],
|
|
22
|
-
getTenantRepository: (database) => ({ create, update, list, delete }), // adapter provides this
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
// In middleware: resolve tenantId (e.g. tenant.resolveTenant(request)), then:
|
|
26
|
-
await tenant.runWithTenantAndDatabase(tenantId, adapter, async () => {
|
|
27
|
-
await next();
|
|
28
20
|
});
|
|
29
21
|
|
|
30
22
|
// In handlers:
|
|
@@ -36,15 +28,16 @@ const database = tenant.getDatabase(); // tenant-scoped DB handle (use only
|
|
|
36
28
|
|
|
37
29
|
| API | Description |
|
|
38
30
|
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
39
|
-
| `betterTenant(config)` | Create instance with
|
|
31
|
+
| `betterTenant(config)` | Create instance with `database` provider and resolver. |
|
|
40
32
|
| `getContext()` | Current `TenantContext \| undefined` (tenantId, tenant?, database?). |
|
|
41
|
-
| `getDatabase()`
|
|
33
|
+
| `tenant.getDatabase()` | Tenant-scoped database handle, or `undefined` outside request scope. Use only this for tenant-scoped tables. |
|
|
42
34
|
| `runWithTenant(tenantId, fn)` | Run `fn` with context only (no DB). For tests or when adapter not used. |
|
|
43
|
-
| `runWithTenantAndDatabase(tenantId,
|
|
44
|
-
| `resolveTenant(request, config)`
|
|
45
|
-
| `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. |
|
|
46
38
|
| `runAsSystem(fn)` | Run with RLS bypass (adapter must implement `runAsSystem`). |
|
|
47
|
-
| `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`. |
|
|
48
41
|
|
|
49
42
|
## Adapter contract
|
|
50
43
|
|
|
@@ -59,6 +52,26 @@ Types: `TenantScopedDatabase` (opaque handle), `SystemDatabase` (opaque handle f
|
|
|
59
52
|
|
|
60
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) => ...`.
|
|
61
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
|
+
|
|
62
75
|
## Telemetry
|
|
63
76
|
|
|
64
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/adapter.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TenantAdapter,
|
|
1
|
+
import type { TenantAdapter, RunWithTenantAndDatabaseOptions } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Returns the tenant-scoped database handle when inside a runWithTenantAndDatabase (or runAs) scope.
|
|
4
4
|
*
|
|
@@ -6,7 +6,7 @@ import type { TenantAdapter, TenantScopedDatabase, RunWithTenantAndDatabaseOptio
|
|
|
6
6
|
* Config requires an adapter when tenant-scoped DB is used; getDatabase() is then available inside
|
|
7
7
|
* request handlers that ran via runWithTenantAndDatabase(tenantId, adapter, next).
|
|
8
8
|
*/
|
|
9
|
-
export declare function getDatabase():
|
|
9
|
+
export declare function getDatabase(): unknown;
|
|
10
10
|
/**
|
|
11
11
|
* Sets AsyncLocalStorage context and runs fn with the adapter's tenant-scoped DB.
|
|
12
12
|
*
|
|
@@ -20,5 +20,5 @@ export declare function getDatabase(): TenantScopedDatabase | undefined;
|
|
|
20
20
|
* When options.loadTenant is true and options.getTenantRepository and adapter.runAsSystem are set,
|
|
21
21
|
* loads the full Tenant row into context so getContext().tenant is available (or undefined if not found).
|
|
22
22
|
*/
|
|
23
|
-
export declare function runWithTenantAndDatabase<T>(tenantId: string, adapter: TenantAdapter, fn: (database:
|
|
23
|
+
export declare function runWithTenantAndDatabase<TDb, SDb, T>(tenantId: string, adapter: TenantAdapter<TDb, SDb>, fn: (database: TDb) => Promise<T>, options?: RunWithTenantAndDatabaseOptions<SDb>): Promise<T>;
|
|
24
24
|
//# sourceMappingURL=adapter.d.ts.map
|
package/dist/adapter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EAEb
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EAEb,+BAA+B,EAChC,MAAM,YAAY,CAAC;AAGpB;;;;;;GAMG;AACH,wBAAgB,WAAW,IAAI,OAAO,CAGrC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,EACxD,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,GAC7C,OAAO,CAAC,CAAC,CAAC,CAiBZ"}
|
package/dist/api.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TenantAdapter, Tenant,
|
|
1
|
+
import type { TenantAdapter, Tenant, TenantRepository } from "./types.js";
|
|
2
2
|
export interface CreateTenantData {
|
|
3
3
|
name: string;
|
|
4
4
|
slug: string;
|
|
@@ -13,8 +13,10 @@ 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
|
-
export declare function createTenantApi(adapter: TenantAdapter, getTenantRepository: (database:
|
|
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[]>;
|
|
@@ -24,10 +26,10 @@ export declare function createTenantApi(adapter: TenantAdapter, getTenantReposit
|
|
|
24
26
|
* runAs(tenantId, fn): same as request flow — set context + run inside adapter.runWithTenant
|
|
25
27
|
* so code can use getDatabase() and run tenant-scoped queries. Use for cron, per-tenant migrations.
|
|
26
28
|
*/
|
|
27
|
-
export declare function runAs<T>(tenantId: string, adapter: TenantAdapter
|
|
29
|
+
export declare function runAs<TDb, T>(tenantId: string, adapter: TenantAdapter<TDb>, fn: (database: TDb) => Promise<T>): Promise<T>;
|
|
28
30
|
/**
|
|
29
31
|
* runAsSystem(fn): run fn with a DB handle that has RLS bypass.
|
|
30
32
|
* Optionally set context so getContext() reflects system mode.
|
|
31
33
|
*/
|
|
32
|
-
export declare function runAsSystem<T>(adapter: TenantAdapter, fn: (database:
|
|
34
|
+
export declare function runAsSystem<SDb, T>(adapter: TenantAdapter<unknown, SDb>, fn: (database: SDb) => Promise<T>): Promise<T>;
|
|
33
35
|
//# sourceMappingURL=api.d.ts.map
|
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,
|
|
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;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;2BAYzC,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;EAUtD;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,8 +1,6 @@
|
|
|
1
|
-
import type { BetterTenantConfig, Tenant, ResolvableRequest,
|
|
2
|
-
import {
|
|
3
|
-
import { getDatabase, runWithTenantAndDatabase } from "./adapter.js";
|
|
1
|
+
import type { BetterTenantConfig, Tenant, ResolvableRequest, TenantContext } from "./types.js";
|
|
2
|
+
import { runWithTenant } from "./context.js";
|
|
4
3
|
import { type HandleRequestOptions } from "./handle-request.js";
|
|
5
|
-
import { runAs } from "./api.js";
|
|
6
4
|
export interface TenantApi {
|
|
7
5
|
createTenant(data: {
|
|
8
6
|
name: string;
|
|
@@ -18,23 +16,20 @@ export interface TenantApi {
|
|
|
18
16
|
}): Promise<Tenant[]>;
|
|
19
17
|
deleteTenant(tenantId: string): Promise<void>;
|
|
20
18
|
}
|
|
21
|
-
export interface BetterTenantInstance {
|
|
22
|
-
getContext:
|
|
23
|
-
getDatabase:
|
|
19
|
+
export interface BetterTenantInstance<TDb = unknown, SDb = unknown> {
|
|
20
|
+
getContext: () => TenantContext | undefined;
|
|
21
|
+
getDatabase: () => TDb | undefined;
|
|
24
22
|
runWithTenant: typeof runWithTenant;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
runAs: <T>(tenantId: string, fn: (database: TDb) => Promise<T>) => Promise<T>;
|
|
24
|
+
runAsSystem: <T>(fn: (database: SDb) => Promise<T>) => Promise<T>;
|
|
25
|
+
resolveTenant: (request: ResolvableRequest) => Promise<string | undefined>;
|
|
26
|
+
/** Human-readable descriptions of the configured resolution strategies. */
|
|
27
|
+
resolverStrategies: string[];
|
|
30
28
|
handleRequest: <Req extends ResolvableRequest, Result>(request: Req, next: () => Promise<Result>, options?: Omit<HandleRequestOptions<Req, Result>, "resolveTenant" | "adapter">) => Promise<Result>;
|
|
31
|
-
|
|
32
|
-
api: TenantApi;
|
|
33
|
-
};
|
|
29
|
+
api: TenantApi;
|
|
34
30
|
}
|
|
35
31
|
/**
|
|
36
|
-
* Creates the Better Tenant instance. Config must include adapter and
|
|
37
|
-
* When getTenantRepository is provided, tenant.api is available for CRUD on the tenants table.
|
|
32
|
+
* Creates the Better Tenant instance. Config must include adapter, tenantResolver, and getTenantRepository.
|
|
38
33
|
*/
|
|
39
|
-
export declare function betterTenant(config: BetterTenantConfig): BetterTenantInstance
|
|
34
|
+
export declare function betterTenant<TDb, SDb>(config: BetterTenantConfig<TDb, SDb>): BetterTenantInstance<TDb, SDb>;
|
|
40
35
|
//# 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;AAwC7B,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,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,CACb,OAAO,EAAE,iBAAiB,KACvB,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACjC,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,CAqDhC"}
|