@forinda/kickjs-multi-tenant 3.1.3 → 4.0.0
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 +11 -30
- package/dist/index.d.mts +24 -16
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +110 -67
- package/dist/index.mjs.map +1 -1
- package/package.json +12 -11
package/README.md
CHANGED
|
@@ -1,57 +1,38 @@
|
|
|
1
1
|
# @forinda/kickjs-multi-tenant
|
|
2
2
|
|
|
3
|
-
Multi-tenancy
|
|
3
|
+
Multi-tenancy for KickJS — tenant resolution from header/subdomain/path/query/custom, request-scoped DI via AsyncLocalStorage, and per-tenant DB routing through the `prisma` / `drizzle` tenant adapters.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
# Using the KickJS CLI (recommended)
|
|
9
8
|
kick add multi-tenant
|
|
10
|
-
|
|
11
|
-
# Manual install
|
|
12
|
-
pnpm add @forinda/kickjs-multi-tenant
|
|
13
9
|
```
|
|
14
10
|
|
|
15
|
-
## Features
|
|
16
|
-
|
|
17
|
-
- `TenantAdapter` — lifecycle adapter that resolves tenant from requests
|
|
18
|
-
- `TENANT_CONTEXT` token for injecting tenant info via DI (request-scoped via AsyncLocalStorage)
|
|
19
|
-
- `getCurrentTenant()` — functional helper for use outside DI
|
|
20
|
-
- Pluggable resolution strategies: header, subdomain, path, query, or custom
|
|
21
|
-
- Per-tenant database switching: database, schema, or discriminator modes
|
|
22
|
-
- `TENANT_DB` token for injecting per-tenant database connections
|
|
23
|
-
- Integration with `@forinda/kickjs-auth` for tenant-scoped RBAC
|
|
24
|
-
|
|
25
11
|
## Quick Example
|
|
26
12
|
|
|
27
|
-
```
|
|
13
|
+
```ts
|
|
14
|
+
import { bootstrap } from '@forinda/kickjs'
|
|
28
15
|
import { TenantAdapter, TENANT_CONTEXT, type TenantInfo } from '@forinda/kickjs-multi-tenant'
|
|
29
16
|
import { Inject, Service } from '@forinda/kickjs'
|
|
17
|
+
import { modules } from './modules'
|
|
30
18
|
|
|
31
|
-
bootstrap({
|
|
19
|
+
export const app = await bootstrap({
|
|
32
20
|
modules,
|
|
33
|
-
adapters: [
|
|
34
|
-
new TenantAdapter({
|
|
35
|
-
strategy: 'header',
|
|
36
|
-
headerName: 'X-Tenant-ID',
|
|
37
|
-
}),
|
|
38
|
-
],
|
|
21
|
+
adapters: [TenantAdapter({ strategy: 'header', headerName: 'X-Tenant-ID' })],
|
|
39
22
|
})
|
|
40
23
|
|
|
41
|
-
// Access tenant in any service
|
|
42
24
|
@Service()
|
|
43
25
|
class DataService {
|
|
44
|
-
@Inject(TENANT_CONTEXT) private tenant
|
|
45
|
-
|
|
46
|
-
async getData() {
|
|
47
|
-
return this.repo.findByTenant(this.tenant.id)
|
|
48
|
-
}
|
|
26
|
+
constructor(@Inject(TENANT_CONTEXT) private tenant: TenantInfo) {}
|
|
27
|
+
getData() { return this.repo.findByTenant(this.tenant.id) }
|
|
49
28
|
}
|
|
50
29
|
```
|
|
51
30
|
|
|
31
|
+
For sharded / multi-realm setups use `TenantAdapter.scoped('eu', { ... })` — name composes as `TenantAdapter:eu` so `dependsOn` lookups stay unambiguous.
|
|
32
|
+
|
|
52
33
|
## Documentation
|
|
53
34
|
|
|
54
|
-
[
|
|
35
|
+
[forinda.github.io/kick-js/guide/multi-tenancy](https://forinda.github.io/kick-js/guide/multi-tenancy)
|
|
55
36
|
|
|
56
37
|
## License
|
|
57
38
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import
|
|
2
|
+
import * as _$_forinda_kickjs0 from "@forinda/kickjs";
|
|
3
3
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
4
|
|
|
5
5
|
//#region src/database.d.ts
|
|
@@ -52,8 +52,6 @@ type TenantDatabase = DatabasePerTenantConfig | SchemaPerTenantConfig | Discrimi
|
|
|
52
52
|
declare const TENANT_DB: unique symbol;
|
|
53
53
|
//#endregion
|
|
54
54
|
//#region src/types.d.ts
|
|
55
|
-
/** DI token for the current tenant context */
|
|
56
|
-
declare const TENANT_CONTEXT: unique symbol;
|
|
57
55
|
/** Tenant information resolved from the request */
|
|
58
56
|
interface TenantInfo {
|
|
59
57
|
/** Unique tenant identifier */
|
|
@@ -63,6 +61,14 @@ interface TenantInfo {
|
|
|
63
61
|
/** Optional tenant-specific config/metadata */
|
|
64
62
|
metadata?: Record<string, any>;
|
|
65
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* DI token for the current tenant context.
|
|
66
|
+
*
|
|
67
|
+
* Backed by AsyncLocalStorage — registered as `Scope.TRANSIENT` so each
|
|
68
|
+
* resolution returns the request-scoped tenant. Throws if resolved
|
|
69
|
+
* outside a request that ran the tenant middleware.
|
|
70
|
+
*/
|
|
71
|
+
declare const TENANT_CONTEXT: _$_forinda_kickjs0.InjectionToken<TenantInfo>;
|
|
66
72
|
/** Strategy for resolving the tenant from a request */
|
|
67
73
|
type TenantResolutionStrategy = 'header' | 'subdomain' | 'path' | 'query' | ((req: any) => TenantInfo | null | Promise<TenantInfo | null>);
|
|
68
74
|
interface MultiTenantOptions {
|
|
@@ -100,8 +106,8 @@ interface MultiTenantOptions {
|
|
|
100
106
|
/**
|
|
101
107
|
* Multi-tenancy adapter for KickJS.
|
|
102
108
|
*
|
|
103
|
-
* Resolves the tenant from each request and makes it available
|
|
104
|
-
*
|
|
109
|
+
* Resolves the tenant from each request and makes it available via DI
|
|
110
|
+
* (`@Inject(TENANT_CONTEXT)`) and `req.tenant`.
|
|
105
111
|
*
|
|
106
112
|
* @example
|
|
107
113
|
* ```ts
|
|
@@ -110,7 +116,7 @@ interface MultiTenantOptions {
|
|
|
110
116
|
* bootstrap({
|
|
111
117
|
* modules,
|
|
112
118
|
* adapters: [
|
|
113
|
-
*
|
|
119
|
+
* TenantAdapter({
|
|
114
120
|
* strategy: 'header',
|
|
115
121
|
* onTenantResolved: async (tenant) => {
|
|
116
122
|
* // Load tenant config from DB, validate, etc.
|
|
@@ -125,17 +131,19 @@ interface MultiTenantOptions {
|
|
|
125
131
|
* constructor(@Inject(TENANT_CONTEXT) private tenant: TenantInfo) {}
|
|
126
132
|
* }
|
|
127
133
|
* ```
|
|
134
|
+
*
|
|
135
|
+
* Multiple shards or independent tenant pipelines? Use `.scoped()` for
|
|
136
|
+
* a per-shard instance — each one gets its own `name` (e.g.
|
|
137
|
+
* `TenantAdapter:eu`) so `dependsOn` lookups stay unambiguous:
|
|
138
|
+
*
|
|
139
|
+
* ```ts
|
|
140
|
+
* adapters: [
|
|
141
|
+
* TenantAdapter.scoped('eu', { strategy: 'header', headerName: 'x-eu-tenant' }),
|
|
142
|
+
* TenantAdapter.scoped('us', { strategy: 'header', headerName: 'x-us-tenant' }),
|
|
143
|
+
* ]
|
|
144
|
+
* ```
|
|
128
145
|
*/
|
|
129
|
-
declare
|
|
130
|
-
name: string;
|
|
131
|
-
private options;
|
|
132
|
-
constructor(options?: MultiTenantOptions);
|
|
133
|
-
middleware(): AdapterMiddleware[];
|
|
134
|
-
beforeStart({
|
|
135
|
-
container
|
|
136
|
-
}: AdapterContext): void;
|
|
137
|
-
private resolveTenant;
|
|
138
|
-
}
|
|
146
|
+
declare const TenantAdapter: _$_forinda_kickjs0.AdapterFactory<MultiTenantOptions, unknown>;
|
|
139
147
|
//#endregion
|
|
140
148
|
//#region src/tenant.context.d.ts
|
|
141
149
|
/**
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/database.ts","../src/types.ts","../src/tenant.adapter.ts","../src/tenant.context.ts"],"mappings":";;;;;;;;;AASA;;;;UAAiB,sBAAA;EACf,IAAA;EACA,IAAA;EACA,QAAA;EACA,IAAA;EACA,QAAA;AAAA;AAAA,UAGe,uBAAA;EACf,IAAA;;EAEA,OAAA,GAAU,QAAA,aAAqB,sBAAA,GAAyB,OAAA,CAAQ,sBAAA;EAAA;EAEhE,IAAA;IAAS,GAAA;IAAc,GAAA;IAAc,WAAA;EAAA;EAF3B;EAIV,KAAA;IAAU,GAAA;EAAA;AAAA;AAAA,UAGK,qBAAA;EACf,IAAA;EANqC;EAQrC,UAAA;EANU;EAQV,cAAA;AAAA;AAAA,UAGe,mBAAA;EACf,IAAA;;EAEA,UAAA;EAVA;EAYA,MAAA;AAAA;AAAA,KAGU,cAAA,GAAiB,uBAAA,GAA0B,qBAAA,GAAwB,mBAAA;;cAGlE,SAAA;;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/database.ts","../src/types.ts","../src/tenant.adapter.ts","../src/tenant.context.ts"],"mappings":";;;;;;;;;AASA;;;;UAAiB,sBAAA;EACf,IAAA;EACA,IAAA;EACA,QAAA;EACA,IAAA;EACA,QAAA;AAAA;AAAA,UAGe,uBAAA;EACf,IAAA;;EAEA,OAAA,GAAU,QAAA,aAAqB,sBAAA,GAAyB,OAAA,CAAQ,sBAAA;EAAA;EAEhE,IAAA;IAAS,GAAA;IAAc,GAAA;IAAc,WAAA;EAAA;EAF3B;EAIV,KAAA;IAAU,GAAA;EAAA;AAAA;AAAA,UAGK,qBAAA;EACf,IAAA;EANqC;EAQrC,UAAA;EANU;EAQV,cAAA;AAAA;AAAA,UAGe,mBAAA;EACf,IAAA;;EAEA,UAAA;EAVA;EAYA,MAAA;AAAA;AAAA,KAGU,cAAA,GAAiB,uBAAA,GAA0B,qBAAA,GAAwB,mBAAA;;cAGlE,SAAA;;;;UC3CI,UAAA;;EAEf,EAAA;EDIe;ECFf,IAAA;;EAEA,QAAA,GAAW,MAAA;AAAA;;;;;;;ADQb;cCEa,cAAA,EAAc,kBAAA,CAAA,cAAA,CAAA,UAAA;;KAGf,wBAAA,iDAKN,GAAA,UAAa,UAAA,UAAoB,OAAA,CAAQ,UAAA;AAAA,UAE9B,kBAAA;EDTyC;;;;;;;;ECkBxD,QAAA,GAAW,wBAAA;EDhBX;ECmBA,UAAA;EDnBuB;ECsBvB,UAAA;EDpBA;;;;EC0BA,gBAAA,IAAoB,MAAA,EAAQ,UAAA,EAAY,GAAA,iBAAoB,OAAA;EDvBxB;EC0BpC,QAAA;ED1BoC;EC6BpC,aAAA;ED1BA;;;;AAKF;EC4BE,QAAA,GAbmE,cAAA;AAAA;;;;;;ADzCrE;;;;;;;;;;;AAQA;;;;;;;;;;;;;;;;;;;;;;AAUA;;;;cEqBa,aAAA,EAAa,kBAAA,CAAA,cAAA,CAAA,kBAAA;;;;;;;;;;;AF/B1B;;;;;;;;iBGQgB,gBAAA,CAAA,GAAoB,UAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs-multi-tenant
|
|
2
|
+
* @forinda/kickjs-multi-tenant v4.0.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,11 +8,18 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
import { Logger, Scope } from "@forinda/kickjs";
|
|
11
|
+
import { Logger, Scope, createToken, defineAdapter } from "@forinda/kickjs";
|
|
12
|
+
import { PROTOCOL_VERSION } from "@forinda/kickjs-devtools-kit";
|
|
12
13
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
13
14
|
//#region src/types.ts
|
|
14
|
-
/**
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* DI token for the current tenant context.
|
|
17
|
+
*
|
|
18
|
+
* Backed by AsyncLocalStorage — registered as `Scope.TRANSIENT` so each
|
|
19
|
+
* resolution returns the request-scoped tenant. Throws if resolved
|
|
20
|
+
* outside a request that ran the tenant middleware.
|
|
21
|
+
*/
|
|
22
|
+
const TENANT_CONTEXT = createToken("kick/tenant/Context");
|
|
16
23
|
//#endregion
|
|
17
24
|
//#region src/tenant.context.ts
|
|
18
25
|
/**
|
|
@@ -45,8 +52,8 @@ const log = Logger.for("MultiTenant");
|
|
|
45
52
|
/**
|
|
46
53
|
* Multi-tenancy adapter for KickJS.
|
|
47
54
|
*
|
|
48
|
-
* Resolves the tenant from each request and makes it available
|
|
49
|
-
*
|
|
55
|
+
* Resolves the tenant from each request and makes it available via DI
|
|
56
|
+
* (`@Inject(TENANT_CONTEXT)`) and `req.tenant`.
|
|
50
57
|
*
|
|
51
58
|
* @example
|
|
52
59
|
* ```ts
|
|
@@ -55,7 +62,7 @@ const log = Logger.for("MultiTenant");
|
|
|
55
62
|
* bootstrap({
|
|
56
63
|
* modules,
|
|
57
64
|
* adapters: [
|
|
58
|
-
*
|
|
65
|
+
* TenantAdapter({
|
|
59
66
|
* strategy: 'header',
|
|
60
67
|
* onTenantResolved: async (tenant) => {
|
|
61
68
|
* // Load tenant config from DB, validate, etc.
|
|
@@ -70,72 +77,108 @@ const log = Logger.for("MultiTenant");
|
|
|
70
77
|
* constructor(@Inject(TENANT_CONTEXT) private tenant: TenantInfo) {}
|
|
71
78
|
* }
|
|
72
79
|
* ```
|
|
80
|
+
*
|
|
81
|
+
* Multiple shards or independent tenant pipelines? Use `.scoped()` for
|
|
82
|
+
* a per-shard instance — each one gets its own `name` (e.g.
|
|
83
|
+
* `TenantAdapter:eu`) so `dependsOn` lookups stay unambiguous:
|
|
84
|
+
*
|
|
85
|
+
* ```ts
|
|
86
|
+
* adapters: [
|
|
87
|
+
* TenantAdapter.scoped('eu', { strategy: 'header', headerName: 'x-eu-tenant' }),
|
|
88
|
+
* TenantAdapter.scoped('us', { strategy: 'header', headerName: 'x-us-tenant' }),
|
|
89
|
+
* ]
|
|
90
|
+
* ```
|
|
73
91
|
*/
|
|
74
|
-
|
|
75
|
-
name
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
const TenantAdapter = defineAdapter({
|
|
93
|
+
name: "TenantAdapter",
|
|
94
|
+
defaults: {
|
|
95
|
+
strategy: "header",
|
|
96
|
+
required: true,
|
|
97
|
+
headerName: "x-tenant-id",
|
|
98
|
+
queryParam: "tenantId"
|
|
99
|
+
},
|
|
100
|
+
build: (options) => {
|
|
101
|
+
let tenantsResolved = 0;
|
|
102
|
+
let tenantsRejected = 0;
|
|
103
|
+
return {
|
|
104
|
+
introspect() {
|
|
105
|
+
return {
|
|
106
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
107
|
+
name: "TenantAdapter",
|
|
108
|
+
kind: "adapter",
|
|
109
|
+
state: {
|
|
110
|
+
strategy: typeof options.strategy === "function" ? "custom" : options.strategy ?? null,
|
|
111
|
+
required: options.required ?? true,
|
|
112
|
+
headerName: options.headerName ?? null,
|
|
113
|
+
queryParam: options.queryParam ?? null
|
|
114
|
+
},
|
|
115
|
+
tokens: {
|
|
116
|
+
provides: ["kick/tenant/Context"],
|
|
117
|
+
requires: []
|
|
118
|
+
},
|
|
119
|
+
metrics: {
|
|
120
|
+
tenantsResolved,
|
|
121
|
+
tenantsRejected
|
|
95
122
|
}
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
req.tenant = tenant;
|
|
99
|
-
if (this.options.onTenantResolved) await this.options.onTenantResolved(tenant, req);
|
|
100
|
-
tenantStorage.run(tenant, () => next());
|
|
123
|
+
};
|
|
101
124
|
},
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
case "query": {
|
|
132
|
-
const tenantId = req.query[this.options.queryParam];
|
|
133
|
-
return tenantId ? { id: tenantId } : null;
|
|
125
|
+
middleware() {
|
|
126
|
+
return [{
|
|
127
|
+
handler: async (req, res, next) => {
|
|
128
|
+
if (options.excludeRoutes?.some((r) => req.path.startsWith(r))) return next();
|
|
129
|
+
const tenant = await resolveTenant(req, options);
|
|
130
|
+
if (!tenant) {
|
|
131
|
+
if (options.required) {
|
|
132
|
+
tenantsRejected++;
|
|
133
|
+
res.status(403).json({ message: "Tenant not found. Provide a valid tenant identifier." });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
return next();
|
|
137
|
+
}
|
|
138
|
+
tenantsResolved++;
|
|
139
|
+
req.tenant = tenant;
|
|
140
|
+
if (options.onTenantResolved) await options.onTenantResolved(tenant, req);
|
|
141
|
+
tenantStorage.run(tenant, () => next());
|
|
142
|
+
},
|
|
143
|
+
phase: "beforeGlobal"
|
|
144
|
+
}];
|
|
145
|
+
},
|
|
146
|
+
beforeStart({ container }) {
|
|
147
|
+
container.registerFactory(TENANT_CONTEXT, () => {
|
|
148
|
+
const tenant = getCurrentTenant();
|
|
149
|
+
if (!tenant) throw new Error("TENANT_CONTEXT resolved outside request scope. Ensure TenantAdapter middleware is active and the code runs within a request.");
|
|
150
|
+
return tenant;
|
|
151
|
+
}, Scope.TRANSIENT);
|
|
152
|
+
log.info(`Tenant resolution: ${typeof options.strategy === "function" ? "custom" : options.strategy}`);
|
|
134
153
|
}
|
|
135
|
-
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
async function resolveTenant(req, options) {
|
|
158
|
+
const strategy = options.strategy;
|
|
159
|
+
if (typeof strategy === "function") return strategy(req);
|
|
160
|
+
switch (strategy) {
|
|
161
|
+
case "header": {
|
|
162
|
+
const tenantId = req.get(options.headerName);
|
|
163
|
+
return tenantId ? { id: tenantId } : null;
|
|
164
|
+
}
|
|
165
|
+
case "subdomain": {
|
|
166
|
+
const parts = req.hostname.split(".");
|
|
167
|
+
if (parts.length >= 3) return { id: parts[0] };
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
case "path": {
|
|
171
|
+
const segments = req.path.split("/").filter(Boolean);
|
|
172
|
+
if (segments.length > 0) return { id: segments[0] };
|
|
173
|
+
return null;
|
|
136
174
|
}
|
|
175
|
+
case "query": {
|
|
176
|
+
const tenantId = req.query[options.queryParam];
|
|
177
|
+
return tenantId ? { id: tenantId } : null;
|
|
178
|
+
}
|
|
179
|
+
default: return null;
|
|
137
180
|
}
|
|
138
|
-
}
|
|
181
|
+
}
|
|
139
182
|
//#endregion
|
|
140
183
|
//#region src/database.ts
|
|
141
184
|
/** DI token for the current tenant's database connection */
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/types.ts","../src/tenant.context.ts","../src/tenant.adapter.ts","../src/database.ts"],"sourcesContent":["/** DI token for the current tenant context */\nexport const TENANT_CONTEXT = Symbol('TenantContext')\n\n/** Tenant information resolved from the request */\nexport interface TenantInfo {\n /** Unique tenant identifier */\n id: string\n /** Optional tenant name */\n name?: string\n /** Optional tenant-specific config/metadata */\n metadata?: Record<string, any>\n}\n\n/** Strategy for resolving the tenant from a request */\nexport type TenantResolutionStrategy =\n | 'header'\n | 'subdomain'\n | 'path'\n | 'query'\n | ((req: any) => TenantInfo | null | Promise<TenantInfo | null>)\n\nexport interface MultiTenantOptions {\n /**\n * How to resolve the tenant from the request.\n * - 'header' — reads X-Tenant-ID header (default)\n * - 'subdomain' — extracts from subdomain (tenant.example.com)\n * - 'path' — extracts from first path segment (/tenant-id/...)\n * - 'query' — reads ?tenantId= query param\n * - function — custom resolver\n */\n strategy?: TenantResolutionStrategy\n\n /** Header name when strategy is 'header' (default: 'x-tenant-id') */\n headerName?: string\n\n /** Query param name when strategy is 'query' (default: 'tenantId') */\n queryParam?: string\n\n /**\n * Called after tenant is resolved. Use for validation, loading tenant\n * config from DB, or rejecting unknown tenants.\n */\n onTenantResolved?: (tenant: TenantInfo, req: any) => void | Promise<void>\n\n /** Return a 403 if no tenant can be resolved (default: true) */\n required?: boolean\n\n /** Routes to skip tenant resolution (e.g., health checks) */\n excludeRoutes?: string[]\n\n /**\n * Per-tenant database switching configuration.\n * When set, a TENANT_DB DI token is registered that resolves to\n * the current tenant's database connection.\n */\n database?: import('./database').TenantDatabase\n}\n","import { AsyncLocalStorage } from 'node:async_hooks'\nimport type { TenantInfo } from './types'\n\n/**\n * AsyncLocalStorage instance that holds the current request's tenant.\n * Used internally by TenantAdapter to make tenant resolution request-scoped.\n */\nexport const tenantStorage = new AsyncLocalStorage<TenantInfo>()\n\n/**\n * Get the current request's tenant from AsyncLocalStorage.\n *\n * Returns `undefined` when called outside a request scope (e.g.,\n * during startup, in a background job, or in tests without setup).\n *\n * @example\n * ```ts\n * import { getCurrentTenant } from '@forinda/kickjs-multi-tenant'\n *\n * function logForTenant(message: string) {\n * const tenant = getCurrentTenant()\n * console.log(`[${tenant?.id ?? 'no-tenant'}] ${message}`)\n * }\n * ```\n */\nexport function getCurrentTenant(): TenantInfo | undefined {\n return tenantStorage.getStore()\n}\n","import {\n Logger,\n type AppAdapter,\n type AdapterContext,\n type AdapterMiddleware,\n Scope,\n} from '@forinda/kickjs'\nimport type { Request, Response, NextFunction } from 'express'\nimport {\n TENANT_CONTEXT,\n type TenantInfo,\n type MultiTenantOptions,\n type TenantResolutionStrategy,\n} from './types'\nimport { tenantStorage, getCurrentTenant } from './tenant.context'\n\nconst log = Logger.for('MultiTenant')\n\n/**\n * Multi-tenancy adapter for KickJS.\n *\n * Resolves the tenant from each request and makes it available\n * via DI (`@Inject(TENANT_CONTEXT)`) and `req.tenant`.\n *\n * @example\n * ```ts\n * import { TenantAdapter, TENANT_CONTEXT } from '@forinda/kickjs-multi-tenant'\n *\n * bootstrap({\n * modules,\n * adapters: [\n * new TenantAdapter({\n * strategy: 'header',\n * onTenantResolved: async (tenant) => {\n * // Load tenant config from DB, validate, etc.\n * },\n * }),\n * ],\n * })\n *\n * // In a service:\n * @Service()\n * class UserService {\n * constructor(@Inject(TENANT_CONTEXT) private tenant: TenantInfo) {}\n * }\n * ```\n */\nexport class TenantAdapter implements AppAdapter {\n name = 'TenantAdapter'\n private options: Required<\n Pick<MultiTenantOptions, 'strategy' | 'required' | 'headerName' | 'queryParam'>\n > &\n MultiTenantOptions\n\n constructor(options: MultiTenantOptions = {}) {\n this.options = {\n strategy: options.strategy ?? 'header',\n required: options.required ?? true,\n headerName: options.headerName ?? 'x-tenant-id',\n queryParam: options.queryParam ?? 'tenantId',\n ...options,\n }\n }\n\n middleware(): AdapterMiddleware[] {\n return [\n {\n handler: async (req: Request, res: Response, next: NextFunction) => {\n // Skip excluded routes\n if (this.options.excludeRoutes?.some((r) => req.path.startsWith(r))) {\n return next()\n }\n\n const tenant = await this.resolveTenant(req)\n\n if (!tenant) {\n if (this.options.required) {\n res\n .status(403)\n .json({ message: 'Tenant not found. Provide a valid tenant identifier.' })\n return\n }\n return next()\n }\n\n // Attach to request\n ;(req as any).tenant = tenant\n\n // Call hook\n if (this.options.onTenantResolved) {\n await this.options.onTenantResolved(tenant, req)\n }\n\n // Wrap the rest of the request in AsyncLocalStorage so\n // @Inject(TENANT_CONTEXT) and getCurrentTenant() return\n // the correct tenant for this request.\n tenantStorage.run(tenant, () => next())\n },\n phase: 'beforeGlobal',\n },\n ]\n }\n\n beforeStart({ container }: AdapterContext): void {\n // Register as TRANSIENT so each resolution reads from AsyncLocalStorage\n container.registerFactory(\n TENANT_CONTEXT,\n () => {\n const tenant = getCurrentTenant()\n if (!tenant) {\n throw new Error(\n 'TENANT_CONTEXT resolved outside request scope. ' +\n 'Ensure TenantAdapter middleware is active and the code runs within a request.',\n )\n }\n return tenant\n },\n Scope.TRANSIENT,\n )\n log.info(\n `Tenant resolution: ${typeof this.options.strategy === 'function' ? 'custom' : this.options.strategy}`,\n )\n }\n\n private async resolveTenant(req: Request): Promise<TenantInfo | null> {\n const strategy = this.options.strategy\n\n if (typeof strategy === 'function') {\n return strategy(req)\n }\n\n switch (strategy) {\n case 'header': {\n const tenantId = req.get(this.options.headerName)\n return tenantId ? { id: tenantId } : null\n }\n case 'subdomain': {\n const host = req.hostname\n const parts = host.split('.')\n if (parts.length >= 3) {\n return { id: parts[0] }\n }\n return null\n }\n case 'path': {\n const segments = req.path.split('/').filter(Boolean)\n if (segments.length > 0) {\n return { id: segments[0] }\n }\n return null\n }\n case 'query': {\n const tenantId = req.query[this.options.queryParam] as string\n return tenantId ? { id: tenantId } : null\n }\n default:\n return null\n }\n }\n}\n","/**\n * Per-tenant database switching configuration.\n *\n * Three isolation modes, from strongest to weakest:\n * - `database` — each tenant has its own database\n * - `schema` — shared database, separate schemas (PostgreSQL)\n * - `discriminator` — shared tables with a tenant_id column\n */\n\nexport interface DatabaseConnectionInfo {\n host: string\n port?: number\n database: string\n user: string\n password: string\n}\n\nexport interface DatabasePerTenantConfig {\n mode: 'database'\n /** Resolve connection info for a tenant */\n resolve: (tenantId: string) => DatabaseConnectionInfo | Promise<DatabaseConnectionInfo>\n /** Connection pool settings */\n pool?: { min?: number; max?: number; idleTimeout?: number }\n /** Cache resolved connections (default TTL: 300_000ms = 5 min) */\n cache?: { ttl?: number }\n}\n\nexport interface SchemaPerTenantConfig {\n mode: 'schema'\n /** Base connection URL (shared database) */\n connection: string\n /** Schema name template. `${tenantId}` is replaced at runtime. Default: `'tenant_${tenantId}'` */\n schemaTemplate?: string\n}\n\nexport interface DiscriminatorConfig {\n mode: 'discriminator'\n /** Base connection URL (shared everything) */\n connection: string\n /** Column name used to scope queries. Default: `'tenant_id'` */\n column?: string\n}\n\nexport type TenantDatabase = DatabasePerTenantConfig | SchemaPerTenantConfig | DiscriminatorConfig\n\n/** DI token for the current tenant's database connection */\nexport const TENANT_DB = Symbol('TenantDB')\n"],"mappings":";;;;;;;;;;;;;;AACA,MAAa,iBAAiB,OAAO,gBAAgB;;;;;;;ACMrD,MAAa,gBAAgB,IAAI,mBAA+B;;;;;;;;;;;;;;;;;AAkBhE,SAAgB,mBAA2C;AACzD,QAAO,cAAc,UAAU;;;;ACVjC,MAAM,MAAM,OAAO,IAAI,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BrC,IAAa,gBAAb,MAAiD;CAC/C,OAAO;CACP;CAKA,YAAY,UAA8B,EAAE,EAAE;AAC5C,OAAK,UAAU;GACb,UAAU,QAAQ,YAAY;GAC9B,UAAU,QAAQ,YAAY;GAC9B,YAAY,QAAQ,cAAc;GAClC,YAAY,QAAQ,cAAc;GAClC,GAAG;GACJ;;CAGH,aAAkC;AAChC,SAAO,CACL;GACE,SAAS,OAAO,KAAc,KAAe,SAAuB;AAElE,QAAI,KAAK,QAAQ,eAAe,MAAM,MAAM,IAAI,KAAK,WAAW,EAAE,CAAC,CACjE,QAAO,MAAM;IAGf,MAAM,SAAS,MAAM,KAAK,cAAc,IAAI;AAE5C,QAAI,CAAC,QAAQ;AACX,SAAI,KAAK,QAAQ,UAAU;AACzB,UACG,OAAO,IAAI,CACX,KAAK,EAAE,SAAS,wDAAwD,CAAC;AAC5E;;AAEF,YAAO,MAAM;;AAIb,QAAY,SAAS;AAGvB,QAAI,KAAK,QAAQ,iBACf,OAAM,KAAK,QAAQ,iBAAiB,QAAQ,IAAI;AAMlD,kBAAc,IAAI,cAAc,MAAM,CAAC;;GAEzC,OAAO;GACR,CACF;;CAGH,YAAY,EAAE,aAAmC;AAE/C,YAAU,gBACR,sBACM;GACJ,MAAM,SAAS,kBAAkB;AACjC,OAAI,CAAC,OACH,OAAM,IAAI,MACR,+HAED;AAEH,UAAO;KAET,MAAM,UACP;AACD,MAAI,KACF,sBAAsB,OAAO,KAAK,QAAQ,aAAa,aAAa,WAAW,KAAK,QAAQ,WAC7F;;CAGH,MAAc,cAAc,KAA0C;EACpE,MAAM,WAAW,KAAK,QAAQ;AAE9B,MAAI,OAAO,aAAa,WACtB,QAAO,SAAS,IAAI;AAGtB,UAAQ,UAAR;GACE,KAAK,UAAU;IACb,MAAM,WAAW,IAAI,IAAI,KAAK,QAAQ,WAAW;AACjD,WAAO,WAAW,EAAE,IAAI,UAAU,GAAG;;GAEvC,KAAK,aAAa;IAEhB,MAAM,QADO,IAAI,SACE,MAAM,IAAI;AAC7B,QAAI,MAAM,UAAU,EAClB,QAAO,EAAE,IAAI,MAAM,IAAI;AAEzB,WAAO;;GAET,KAAK,QAAQ;IACX,MAAM,WAAW,IAAI,KAAK,MAAM,IAAI,CAAC,OAAO,QAAQ;AACpD,QAAI,SAAS,SAAS,EACpB,QAAO,EAAE,IAAI,SAAS,IAAI;AAE5B,WAAO;;GAET,KAAK,SAAS;IACZ,MAAM,WAAW,IAAI,MAAM,KAAK,QAAQ;AACxC,WAAO,WAAW,EAAE,IAAI,UAAU,GAAG;;GAEvC,QACE,QAAO;;;;;;;AC9Gf,MAAa,YAAY,OAAO,WAAW"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/types.ts","../src/tenant.context.ts","../src/tenant.adapter.ts","../src/database.ts"],"sourcesContent":["import { createToken } from '@forinda/kickjs'\n\n/** Tenant information resolved from the request */\nexport interface TenantInfo {\n /** Unique tenant identifier */\n id: string\n /** Optional tenant name */\n name?: string\n /** Optional tenant-specific config/metadata */\n metadata?: Record<string, any>\n}\n\n/**\n * DI token for the current tenant context.\n *\n * Backed by AsyncLocalStorage — registered as `Scope.TRANSIENT` so each\n * resolution returns the request-scoped tenant. Throws if resolved\n * outside a request that ran the tenant middleware.\n */\nexport const TENANT_CONTEXT = createToken<TenantInfo>('kick/tenant/Context')\n\n/** Strategy for resolving the tenant from a request */\nexport type TenantResolutionStrategy =\n | 'header'\n | 'subdomain'\n | 'path'\n | 'query'\n | ((req: any) => TenantInfo | null | Promise<TenantInfo | null>)\n\nexport interface MultiTenantOptions {\n /**\n * How to resolve the tenant from the request.\n * - 'header' — reads X-Tenant-ID header (default)\n * - 'subdomain' — extracts from subdomain (tenant.example.com)\n * - 'path' — extracts from first path segment (/tenant-id/...)\n * - 'query' — reads ?tenantId= query param\n * - function — custom resolver\n */\n strategy?: TenantResolutionStrategy\n\n /** Header name when strategy is 'header' (default: 'x-tenant-id') */\n headerName?: string\n\n /** Query param name when strategy is 'query' (default: 'tenantId') */\n queryParam?: string\n\n /**\n * Called after tenant is resolved. Use for validation, loading tenant\n * config from DB, or rejecting unknown tenants.\n */\n onTenantResolved?: (tenant: TenantInfo, req: any) => void | Promise<void>\n\n /** Return a 403 if no tenant can be resolved (default: true) */\n required?: boolean\n\n /** Routes to skip tenant resolution (e.g., health checks) */\n excludeRoutes?: string[]\n\n /**\n * Per-tenant database switching configuration.\n * When set, a TENANT_DB DI token is registered that resolves to\n * the current tenant's database connection.\n */\n database?: import('./database').TenantDatabase\n}\n","import { AsyncLocalStorage } from 'node:async_hooks'\nimport type { TenantInfo } from './types'\n\n/**\n * AsyncLocalStorage instance that holds the current request's tenant.\n * Used internally by TenantAdapter to make tenant resolution request-scoped.\n */\nexport const tenantStorage = new AsyncLocalStorage<TenantInfo>()\n\n/**\n * Get the current request's tenant from AsyncLocalStorage.\n *\n * Returns `undefined` when called outside a request scope (e.g.,\n * during startup, in a background job, or in tests without setup).\n *\n * @example\n * ```ts\n * import { getCurrentTenant } from '@forinda/kickjs-multi-tenant'\n *\n * function logForTenant(message: string) {\n * const tenant = getCurrentTenant()\n * console.log(`[${tenant?.id ?? 'no-tenant'}] ${message}`)\n * }\n * ```\n */\nexport function getCurrentTenant(): TenantInfo | undefined {\n return tenantStorage.getStore()\n}\n","import { Logger, Scope, defineAdapter, type AdapterMiddleware } from '@forinda/kickjs'\nimport { PROTOCOL_VERSION, type IntrospectionSnapshot } from '@forinda/kickjs-devtools-kit'\nimport type { Request, Response, NextFunction } from 'express'\nimport { TENANT_CONTEXT, type TenantInfo, type MultiTenantOptions } from './types'\nimport { tenantStorage, getCurrentTenant } from './tenant.context'\n\nconst log = Logger.for('MultiTenant')\n\n/**\n * Multi-tenancy adapter for KickJS.\n *\n * Resolves the tenant from each request and makes it available via DI\n * (`@Inject(TENANT_CONTEXT)`) and `req.tenant`.\n *\n * @example\n * ```ts\n * import { TenantAdapter, TENANT_CONTEXT } from '@forinda/kickjs-multi-tenant'\n *\n * bootstrap({\n * modules,\n * adapters: [\n * TenantAdapter({\n * strategy: 'header',\n * onTenantResolved: async (tenant) => {\n * // Load tenant config from DB, validate, etc.\n * },\n * }),\n * ],\n * })\n *\n * // In a service:\n * @Service()\n * class UserService {\n * constructor(@Inject(TENANT_CONTEXT) private tenant: TenantInfo) {}\n * }\n * ```\n *\n * Multiple shards or independent tenant pipelines? Use `.scoped()` for\n * a per-shard instance — each one gets its own `name` (e.g.\n * `TenantAdapter:eu`) so `dependsOn` lookups stay unambiguous:\n *\n * ```ts\n * adapters: [\n * TenantAdapter.scoped('eu', { strategy: 'header', headerName: 'x-eu-tenant' }),\n * TenantAdapter.scoped('us', { strategy: 'header', headerName: 'x-us-tenant' }),\n * ]\n * ```\n */\nexport const TenantAdapter = defineAdapter<MultiTenantOptions>({\n name: 'TenantAdapter',\n defaults: {\n strategy: 'header',\n required: true,\n headerName: 'x-tenant-id',\n queryParam: 'tenantId',\n },\n build: (options) => {\n // Tracked across the request middleware so introspect() can report\n // a coarse \"tenants resolved since boot\" counter to DevTools without\n // any per-request overhead beyond the increment itself.\n let tenantsResolved = 0\n let tenantsRejected = 0\n\n return {\n // ── DevTools introspection (architecture.md §23) ───────────────\n introspect(): IntrospectionSnapshot {\n return {\n protocolVersion: PROTOCOL_VERSION,\n name: 'TenantAdapter',\n kind: 'adapter',\n state: {\n strategy:\n typeof options.strategy === 'function' ? 'custom' : (options.strategy ?? null),\n required: options.required ?? true,\n headerName: options.headerName ?? null,\n queryParam: options.queryParam ?? null,\n },\n tokens: { provides: ['kick/tenant/Context'], requires: [] },\n metrics: {\n tenantsResolved,\n tenantsRejected,\n },\n }\n },\n\n middleware(): AdapterMiddleware[] {\n return [\n {\n handler: async (req: Request, res: Response, next: NextFunction) => {\n if (options.excludeRoutes?.some((r) => req.path.startsWith(r))) {\n return next()\n }\n\n const tenant = await resolveTenant(req, options)\n\n if (!tenant) {\n if (options.required) {\n tenantsRejected++\n res\n .status(403)\n .json({ message: 'Tenant not found. Provide a valid tenant identifier.' })\n return\n }\n return next()\n }\n\n tenantsResolved++\n ;(req as unknown as { tenant: TenantInfo }).tenant = tenant\n\n if (options.onTenantResolved) {\n await options.onTenantResolved(tenant, req)\n }\n\n // Wrap the rest of the request in AsyncLocalStorage so\n // @Inject(TENANT_CONTEXT) and getCurrentTenant() return the\n // correct tenant for this request.\n tenantStorage.run(tenant, () => next())\n },\n phase: 'beforeGlobal',\n },\n ]\n },\n\n beforeStart({ container }) {\n // TRANSIENT so each resolution reads from AsyncLocalStorage.\n container.registerFactory(\n TENANT_CONTEXT,\n () => {\n const tenant = getCurrentTenant()\n if (!tenant) {\n throw new Error(\n 'TENANT_CONTEXT resolved outside request scope. ' +\n 'Ensure TenantAdapter middleware is active and the code runs within a request.',\n )\n }\n return tenant\n },\n Scope.TRANSIENT,\n )\n log.info(\n `Tenant resolution: ${typeof options.strategy === 'function' ? 'custom' : options.strategy}`,\n )\n },\n }\n },\n})\n\nasync function resolveTenant(\n req: Request,\n options: MultiTenantOptions,\n): Promise<TenantInfo | null> {\n const strategy = options.strategy\n\n if (typeof strategy === 'function') {\n return strategy(req)\n }\n\n switch (strategy) {\n case 'header': {\n const tenantId = req.get(options.headerName!)\n return tenantId ? { id: tenantId } : null\n }\n case 'subdomain': {\n const host = req.hostname\n const parts = host.split('.')\n if (parts.length >= 3) {\n return { id: parts[0] }\n }\n return null\n }\n case 'path': {\n const segments = req.path.split('/').filter(Boolean)\n if (segments.length > 0) {\n return { id: segments[0] }\n }\n return null\n }\n case 'query': {\n const tenantId = req.query[options.queryParam!] as string\n return tenantId ? { id: tenantId } : null\n }\n default:\n return null\n }\n}\n","/**\n * Per-tenant database switching configuration.\n *\n * Three isolation modes, from strongest to weakest:\n * - `database` — each tenant has its own database\n * - `schema` — shared database, separate schemas (PostgreSQL)\n * - `discriminator` — shared tables with a tenant_id column\n */\n\nexport interface DatabaseConnectionInfo {\n host: string\n port?: number\n database: string\n user: string\n password: string\n}\n\nexport interface DatabasePerTenantConfig {\n mode: 'database'\n /** Resolve connection info for a tenant */\n resolve: (tenantId: string) => DatabaseConnectionInfo | Promise<DatabaseConnectionInfo>\n /** Connection pool settings */\n pool?: { min?: number; max?: number; idleTimeout?: number }\n /** Cache resolved connections (default TTL: 300_000ms = 5 min) */\n cache?: { ttl?: number }\n}\n\nexport interface SchemaPerTenantConfig {\n mode: 'schema'\n /** Base connection URL (shared database) */\n connection: string\n /** Schema name template. `${tenantId}` is replaced at runtime. Default: `'tenant_${tenantId}'` */\n schemaTemplate?: string\n}\n\nexport interface DiscriminatorConfig {\n mode: 'discriminator'\n /** Base connection URL (shared everything) */\n connection: string\n /** Column name used to scope queries. Default: `'tenant_id'` */\n column?: string\n}\n\nexport type TenantDatabase = DatabasePerTenantConfig | SchemaPerTenantConfig | DiscriminatorConfig\n\n/** DI token for the current tenant's database connection */\nexport const TENANT_DB = Symbol('TenantDB')\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAmBA,MAAa,iBAAiB,YAAwB,sBAAsB;;;;;;;ACZ5E,MAAa,gBAAgB,IAAI,mBAA+B;;;;;;;;;;;;;;;;;AAkBhE,SAAgB,mBAA2C;AACzD,QAAO,cAAc,UAAU;;;;ACpBjC,MAAM,MAAM,OAAO,IAAI,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CrC,MAAa,gBAAgB,cAAkC;CAC7D,MAAM;CACN,UAAU;EACR,UAAU;EACV,UAAU;EACV,YAAY;EACZ,YAAY;EACb;CACD,QAAQ,YAAY;EAIlB,IAAI,kBAAkB;EACtB,IAAI,kBAAkB;AAEtB,SAAO;GAEL,aAAoC;AAClC,WAAO;KACL,iBAAiB;KACjB,MAAM;KACN,MAAM;KACN,OAAO;MACL,UACE,OAAO,QAAQ,aAAa,aAAa,WAAY,QAAQ,YAAY;MAC3E,UAAU,QAAQ,YAAY;MAC9B,YAAY,QAAQ,cAAc;MAClC,YAAY,QAAQ,cAAc;MACnC;KACD,QAAQ;MAAE,UAAU,CAAC,sBAAsB;MAAE,UAAU,EAAE;MAAE;KAC3D,SAAS;MACP;MACA;MACD;KACF;;GAGH,aAAkC;AAChC,WAAO,CACL;KACE,SAAS,OAAO,KAAc,KAAe,SAAuB;AAClE,UAAI,QAAQ,eAAe,MAAM,MAAM,IAAI,KAAK,WAAW,EAAE,CAAC,CAC5D,QAAO,MAAM;MAGf,MAAM,SAAS,MAAM,cAAc,KAAK,QAAQ;AAEhD,UAAI,CAAC,QAAQ;AACX,WAAI,QAAQ,UAAU;AACpB;AACA,YACG,OAAO,IAAI,CACX,KAAK,EAAE,SAAS,wDAAwD,CAAC;AAC5E;;AAEF,cAAO,MAAM;;AAGf;AACE,UAA0C,SAAS;AAErD,UAAI,QAAQ,iBACV,OAAM,QAAQ,iBAAiB,QAAQ,IAAI;AAM7C,oBAAc,IAAI,cAAc,MAAM,CAAC;;KAEzC,OAAO;KACR,CACF;;GAGH,YAAY,EAAE,aAAa;AAEzB,cAAU,gBACR,sBACM;KACJ,MAAM,SAAS,kBAAkB;AACjC,SAAI,CAAC,OACH,OAAM,IAAI,MACR,+HAED;AAEH,YAAO;OAET,MAAM,UACP;AACD,QAAI,KACF,sBAAsB,OAAO,QAAQ,aAAa,aAAa,WAAW,QAAQ,WACnF;;GAEJ;;CAEJ,CAAC;AAEF,eAAe,cACb,KACA,SAC4B;CAC5B,MAAM,WAAW,QAAQ;AAEzB,KAAI,OAAO,aAAa,WACtB,QAAO,SAAS,IAAI;AAGtB,SAAQ,UAAR;EACE,KAAK,UAAU;GACb,MAAM,WAAW,IAAI,IAAI,QAAQ,WAAY;AAC7C,UAAO,WAAW,EAAE,IAAI,UAAU,GAAG;;EAEvC,KAAK,aAAa;GAEhB,MAAM,QADO,IAAI,SACE,MAAM,IAAI;AAC7B,OAAI,MAAM,UAAU,EAClB,QAAO,EAAE,IAAI,MAAM,IAAI;AAEzB,UAAO;;EAET,KAAK,QAAQ;GACX,MAAM,WAAW,IAAI,KAAK,MAAM,IAAI,CAAC,OAAO,QAAQ;AACpD,OAAI,SAAS,SAAS,EACpB,QAAO,EAAE,IAAI,SAAS,IAAI;AAE5B,UAAO;;EAET,KAAK,SAAS;GACZ,MAAM,WAAW,IAAI,MAAM,QAAQ;AACnC,UAAO,WAAW,EAAE,IAAI,UAAU,GAAG;;EAEvC,QACE,QAAO;;;;;;ACxIb,MAAa,YAAY,OAAO,WAAW"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forinda/kickjs-multi-tenant",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Multi-tenancy helpers for KickJS — tenant resolution, scoped DI, and database routing",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kickjs",
|
|
@@ -21,13 +21,10 @@
|
|
|
21
21
|
"@forinda/kickjs",
|
|
22
22
|
"@forinda/kickjs-auth",
|
|
23
23
|
"@forinda/kickjs-cli",
|
|
24
|
-
"@forinda/kickjs-config",
|
|
25
|
-
"@forinda/kickjs-core",
|
|
26
24
|
"@forinda/kickjs-cron",
|
|
27
25
|
"@forinda/kickjs-devtools",
|
|
28
26
|
"@forinda/kickjs-drizzle",
|
|
29
27
|
"@forinda/kickjs-graphql",
|
|
30
|
-
"@forinda/kickjs-http",
|
|
31
28
|
"@forinda/kickjs-mailer",
|
|
32
29
|
"@forinda/kickjs-multi-tenant",
|
|
33
30
|
"@forinda/kickjs-notifications",
|
|
@@ -63,18 +60,21 @@
|
|
|
63
60
|
"output": [
|
|
64
61
|
"dist/**"
|
|
65
62
|
],
|
|
66
|
-
"dependencies": [
|
|
67
|
-
"../core:build"
|
|
68
|
-
]
|
|
63
|
+
"dependencies": []
|
|
69
64
|
}
|
|
70
65
|
},
|
|
71
66
|
"dependencies": {
|
|
72
|
-
"reflect-metadata": "^0.2.2"
|
|
67
|
+
"reflect-metadata": "^0.2.2",
|
|
68
|
+
"@forinda/kickjs-devtools-kit": "4.0.0"
|
|
73
69
|
},
|
|
74
70
|
"devDependencies": {
|
|
75
|
-
"@types/
|
|
76
|
-
"
|
|
77
|
-
"
|
|
71
|
+
"@types/express": "^5.0.6",
|
|
72
|
+
"@types/node": "^25.6.0",
|
|
73
|
+
"express": "^5.1.0",
|
|
74
|
+
"typescript": "^6.0.3",
|
|
75
|
+
"unplugin-swc": "^1.5.7",
|
|
76
|
+
"vitest": "^4.1.5",
|
|
77
|
+
"@forinda/kickjs": "4.0.0"
|
|
78
78
|
},
|
|
79
79
|
"publishConfig": {
|
|
80
80
|
"access": "public"
|
|
@@ -100,6 +100,7 @@
|
|
|
100
100
|
"scripts": {
|
|
101
101
|
"build": "wireit",
|
|
102
102
|
"dev": "tsdown --watch",
|
|
103
|
+
"test": "vitest run",
|
|
103
104
|
"typecheck": "tsc --noEmit",
|
|
104
105
|
"clean": "rm -rf dist .wireit"
|
|
105
106
|
}
|