@forinda/kickjs-multi-tenant 1.3.1 → 1.4.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/dist/index.d.ts +4 -79
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +65 -1
- package/dist/tenant.adapter.d.ts +40 -0
- package/dist/tenant.adapter.d.ts.map +1 -0
- package/dist/types.d.ts +38 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +8 -7
package/dist/index.d.ts
CHANGED
|
@@ -1,79 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
/** Tenant information resolved from the request */
|
|
6
|
-
interface TenantInfo {
|
|
7
|
-
/** Unique tenant identifier */
|
|
8
|
-
id: string;
|
|
9
|
-
/** Optional tenant name */
|
|
10
|
-
name?: string;
|
|
11
|
-
/** Optional tenant-specific config/metadata */
|
|
12
|
-
metadata?: Record<string, any>;
|
|
13
|
-
}
|
|
14
|
-
/** Strategy for resolving the tenant from a request */
|
|
15
|
-
type TenantResolutionStrategy = 'header' | 'subdomain' | 'path' | 'query' | ((req: any) => TenantInfo | null | Promise<TenantInfo | null>);
|
|
16
|
-
interface MultiTenantOptions {
|
|
17
|
-
/**
|
|
18
|
-
* How to resolve the tenant from the request.
|
|
19
|
-
* - 'header' — reads X-Tenant-ID header (default)
|
|
20
|
-
* - 'subdomain' — extracts from subdomain (tenant.example.com)
|
|
21
|
-
* - 'path' — extracts from first path segment (/tenant-id/...)
|
|
22
|
-
* - 'query' — reads ?tenantId= query param
|
|
23
|
-
* - function — custom resolver
|
|
24
|
-
*/
|
|
25
|
-
strategy?: TenantResolutionStrategy;
|
|
26
|
-
/** Header name when strategy is 'header' (default: 'x-tenant-id') */
|
|
27
|
-
headerName?: string;
|
|
28
|
-
/** Query param name when strategy is 'query' (default: 'tenantId') */
|
|
29
|
-
queryParam?: string;
|
|
30
|
-
/**
|
|
31
|
-
* Called after tenant is resolved. Use for validation, loading tenant
|
|
32
|
-
* config from DB, or rejecting unknown tenants.
|
|
33
|
-
*/
|
|
34
|
-
onTenantResolved?: (tenant: TenantInfo, req: any) => void | Promise<void>;
|
|
35
|
-
/** Return a 403 if no tenant can be resolved (default: true) */
|
|
36
|
-
required?: boolean;
|
|
37
|
-
/** Routes to skip tenant resolution (e.g., health checks) */
|
|
38
|
-
excludeRoutes?: string[];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Multi-tenancy adapter for KickJS.
|
|
43
|
-
*
|
|
44
|
-
* Resolves the tenant from each request and makes it available
|
|
45
|
-
* via DI (`@Inject(TENANT_CONTEXT)`) and `req.tenant`.
|
|
46
|
-
*
|
|
47
|
-
* @example
|
|
48
|
-
* ```ts
|
|
49
|
-
* import { TenantAdapter, TENANT_CONTEXT } from '@forinda/kickjs-multi-tenant'
|
|
50
|
-
*
|
|
51
|
-
* bootstrap({
|
|
52
|
-
* modules,
|
|
53
|
-
* adapters: [
|
|
54
|
-
* new TenantAdapter({
|
|
55
|
-
* strategy: 'header',
|
|
56
|
-
* onTenantResolved: async (tenant) => {
|
|
57
|
-
* // Load tenant config from DB, validate, etc.
|
|
58
|
-
* },
|
|
59
|
-
* }),
|
|
60
|
-
* ],
|
|
61
|
-
* })
|
|
62
|
-
*
|
|
63
|
-
* // In a service:
|
|
64
|
-
* @Service()
|
|
65
|
-
* class UserService {
|
|
66
|
-
* constructor(@Inject(TENANT_CONTEXT) private tenant: TenantInfo) {}
|
|
67
|
-
* }
|
|
68
|
-
* ```
|
|
69
|
-
*/
|
|
70
|
-
declare class TenantAdapter implements AppAdapter {
|
|
71
|
-
name: string;
|
|
72
|
-
private options;
|
|
73
|
-
constructor(options?: MultiTenantOptions);
|
|
74
|
-
middleware(): AdapterMiddleware[];
|
|
75
|
-
beforeStart(_app: any, container: Container): void;
|
|
76
|
-
private resolveTenant;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export { type MultiTenantOptions, TENANT_CONTEXT, TenantAdapter, type TenantInfo, type TenantResolutionStrategy };
|
|
1
|
+
export { TenantAdapter } from './tenant.adapter';
|
|
2
|
+
export { TENANT_CONTEXT } from './types';
|
|
3
|
+
export type { TenantInfo, MultiTenantOptions, TenantResolutionStrategy } from './types';
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AACxC,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,SAAS,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1 +1,65 @@
|
|
|
1
|
-
|
|
1
|
+
import { Logger as s, Scope as o } from "@forinda/kickjs-core";
|
|
2
|
+
var i = /* @__PURE__ */ Symbol("TenantContext"), u = s.for("MultiTenant"), d = class {
|
|
3
|
+
name = "TenantAdapter";
|
|
4
|
+
options;
|
|
5
|
+
constructor(t = {}) {
|
|
6
|
+
this.options = {
|
|
7
|
+
strategy: t.strategy ?? "header",
|
|
8
|
+
required: t.required ?? !0,
|
|
9
|
+
headerName: t.headerName ?? "x-tenant-id",
|
|
10
|
+
queryParam: t.queryParam ?? "tenantId",
|
|
11
|
+
...t
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
middleware() {
|
|
15
|
+
return [{
|
|
16
|
+
handler: async (t, n, e) => {
|
|
17
|
+
if (this.options.excludeRoutes?.some((r) => t.path.startsWith(r))) return e();
|
|
18
|
+
const a = await this.resolveTenant(t);
|
|
19
|
+
if (!a) {
|
|
20
|
+
if (this.options.required) {
|
|
21
|
+
n.status(403).json({ message: "Tenant not found. Provide a valid tenant identifier." });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
return e();
|
|
25
|
+
}
|
|
26
|
+
t.tenant = a, this.options.onTenantResolved && await this.options.onTenantResolved(a, t), e();
|
|
27
|
+
},
|
|
28
|
+
phase: "beforeGlobal"
|
|
29
|
+
}];
|
|
30
|
+
}
|
|
31
|
+
beforeStart(t, n) {
|
|
32
|
+
n.registerFactory(i, () => ({
|
|
33
|
+
id: "default",
|
|
34
|
+
name: "Default Tenant"
|
|
35
|
+
}), o.SINGLETON), u.info(`Tenant resolution: ${typeof this.options.strategy == "function" ? "custom" : this.options.strategy}`);
|
|
36
|
+
}
|
|
37
|
+
async resolveTenant(t) {
|
|
38
|
+
const n = this.options.strategy;
|
|
39
|
+
if (typeof n == "function") return n(t);
|
|
40
|
+
switch (n) {
|
|
41
|
+
case "header": {
|
|
42
|
+
const e = t.get(this.options.headerName);
|
|
43
|
+
return e ? { id: e } : null;
|
|
44
|
+
}
|
|
45
|
+
case "subdomain": {
|
|
46
|
+
const e = t.hostname.split(".");
|
|
47
|
+
return e.length >= 3 ? { id: e[0] } : null;
|
|
48
|
+
}
|
|
49
|
+
case "path": {
|
|
50
|
+
const e = t.path.split("/").filter(Boolean);
|
|
51
|
+
return e.length > 0 ? { id: e[0] } : null;
|
|
52
|
+
}
|
|
53
|
+
case "query": {
|
|
54
|
+
const e = t.query[this.options.queryParam];
|
|
55
|
+
return e ? { id: e } : null;
|
|
56
|
+
}
|
|
57
|
+
default:
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
export {
|
|
63
|
+
i as TENANT_CONTEXT,
|
|
64
|
+
d as TenantAdapter
|
|
65
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type AppAdapter, type AdapterMiddleware, type Container } from '@forinda/kickjs-core';
|
|
2
|
+
import { type MultiTenantOptions } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Multi-tenancy adapter for KickJS.
|
|
5
|
+
*
|
|
6
|
+
* Resolves the tenant from each request and makes it available
|
|
7
|
+
* via DI (`@Inject(TENANT_CONTEXT)`) and `req.tenant`.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { TenantAdapter, TENANT_CONTEXT } from '@forinda/kickjs-multi-tenant'
|
|
12
|
+
*
|
|
13
|
+
* bootstrap({
|
|
14
|
+
* modules,
|
|
15
|
+
* adapters: [
|
|
16
|
+
* new TenantAdapter({
|
|
17
|
+
* strategy: 'header',
|
|
18
|
+
* onTenantResolved: async (tenant) => {
|
|
19
|
+
* // Load tenant config from DB, validate, etc.
|
|
20
|
+
* },
|
|
21
|
+
* }),
|
|
22
|
+
* ],
|
|
23
|
+
* })
|
|
24
|
+
*
|
|
25
|
+
* // In a service:
|
|
26
|
+
* @Service()
|
|
27
|
+
* class UserService {
|
|
28
|
+
* constructor(@Inject(TENANT_CONTEXT) private tenant: TenantInfo) {}
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare class TenantAdapter implements AppAdapter {
|
|
33
|
+
name: string;
|
|
34
|
+
private options;
|
|
35
|
+
constructor(options?: MultiTenantOptions);
|
|
36
|
+
middleware(): AdapterMiddleware[];
|
|
37
|
+
beforeStart(_app: any, container: Container): void;
|
|
38
|
+
private resolveTenant;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=tenant.adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant.adapter.d.ts","sourceRoot":"","sources":["../src/tenant.adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,SAAS,EAEf,MAAM,sBAAsB,CAAA;AAE7B,OAAO,EAGL,KAAK,kBAAkB,EAExB,MAAM,SAAS,CAAA;AAIhB;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,qBAAa,aAAc,YAAW,UAAU;IAC9C,IAAI,SAAkB;IACtB,OAAO,CAAC,OAAO,CAGK;gBAER,OAAO,GAAE,kBAAuB;IAU5C,UAAU,IAAI,iBAAiB,EAAE;IAoCjC,WAAW,CAAC,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,SAAS,GAAG,IAAI;YAapC,aAAa;CAmC5B"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** DI token for the current tenant context */
|
|
2
|
+
export declare const TENANT_CONTEXT: unique symbol;
|
|
3
|
+
/** Tenant information resolved from the request */
|
|
4
|
+
export interface TenantInfo {
|
|
5
|
+
/** Unique tenant identifier */
|
|
6
|
+
id: string;
|
|
7
|
+
/** Optional tenant name */
|
|
8
|
+
name?: string;
|
|
9
|
+
/** Optional tenant-specific config/metadata */
|
|
10
|
+
metadata?: Record<string, any>;
|
|
11
|
+
}
|
|
12
|
+
/** Strategy for resolving the tenant from a request */
|
|
13
|
+
export type TenantResolutionStrategy = 'header' | 'subdomain' | 'path' | 'query' | ((req: any) => TenantInfo | null | Promise<TenantInfo | null>);
|
|
14
|
+
export interface MultiTenantOptions {
|
|
15
|
+
/**
|
|
16
|
+
* How to resolve the tenant from the request.
|
|
17
|
+
* - 'header' — reads X-Tenant-ID header (default)
|
|
18
|
+
* - 'subdomain' — extracts from subdomain (tenant.example.com)
|
|
19
|
+
* - 'path' — extracts from first path segment (/tenant-id/...)
|
|
20
|
+
* - 'query' — reads ?tenantId= query param
|
|
21
|
+
* - function — custom resolver
|
|
22
|
+
*/
|
|
23
|
+
strategy?: TenantResolutionStrategy;
|
|
24
|
+
/** Header name when strategy is 'header' (default: 'x-tenant-id') */
|
|
25
|
+
headerName?: string;
|
|
26
|
+
/** Query param name when strategy is 'query' (default: 'tenantId') */
|
|
27
|
+
queryParam?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Called after tenant is resolved. Use for validation, loading tenant
|
|
30
|
+
* config from DB, or rejecting unknown tenants.
|
|
31
|
+
*/
|
|
32
|
+
onTenantResolved?: (tenant: TenantInfo, req: any) => void | Promise<void>;
|
|
33
|
+
/** Return a 403 if no tenant can be resolved (default: true) */
|
|
34
|
+
required?: boolean;
|
|
35
|
+
/** Routes to skip tenant resolution (e.g., health checks) */
|
|
36
|
+
excludeRoutes?: string[];
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAC9C,eAAO,MAAM,cAAc,eAA0B,CAAA;AAErD,mDAAmD;AACnD,MAAM,WAAW,UAAU;IACzB,+BAA+B;IAC/B,EAAE,EAAE,MAAM,CAAA;IACV,2BAA2B;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAC/B;AAED,uDAAuD;AACvD,MAAM,MAAM,wBAAwB,GAChC,QAAQ,GACR,WAAW,GACX,MAAM,GACN,OAAO,GACP,CAAC,CAAC,GAAG,EAAE,GAAG,KAAK,UAAU,GAAG,IAAI,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC,CAAA;AAElE,MAAM,WAAW,kBAAkB;IACjC;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,wBAAwB,CAAA;IAEnC,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAA;IAEnB,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAA;IAEnB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEzE,gEAAgE;IAChE,QAAQ,CAAC,EAAE,OAAO,CAAA;IAElB,6DAA6D;IAC7D,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;CACzB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forinda/kickjs-multi-tenant",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Multi-tenancy helpers for KickJS — tenant resolution, scoped DI, and database routing",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kickjs",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"@forinda/kickjs-mailer",
|
|
34
34
|
"@forinda/kickjs-queue",
|
|
35
35
|
"@forinda/kickjs-devtools",
|
|
36
|
-
"@forinda/kickjs-notifications"
|
|
36
|
+
"@forinda/kickjs-notifications",
|
|
37
|
+
"vite"
|
|
37
38
|
],
|
|
38
39
|
"type": "module",
|
|
39
40
|
"main": "dist/index.js",
|
|
@@ -49,12 +50,11 @@
|
|
|
49
50
|
],
|
|
50
51
|
"dependencies": {
|
|
51
52
|
"reflect-metadata": "^0.2.2",
|
|
52
|
-
"@forinda/kickjs-core": "1.
|
|
53
|
-
"@forinda/kickjs-http": "1.
|
|
53
|
+
"@forinda/kickjs-core": "1.4.0",
|
|
54
|
+
"@forinda/kickjs-http": "1.4.0"
|
|
54
55
|
},
|
|
55
56
|
"devDependencies": {
|
|
56
57
|
"@types/node": "^24.5.2",
|
|
57
|
-
"tsup": "^8.5.0",
|
|
58
58
|
"typescript": "^5.9.2"
|
|
59
59
|
},
|
|
60
60
|
"publishConfig": {
|
|
@@ -75,8 +75,9 @@
|
|
|
75
75
|
"url": "https://github.com/forinda/kick-js/issues"
|
|
76
76
|
},
|
|
77
77
|
"scripts": {
|
|
78
|
-
"build": "
|
|
79
|
-
"
|
|
78
|
+
"build": "vite build && pnpm build:types",
|
|
79
|
+
"build:types": "tsc -p tsconfig.build.json",
|
|
80
|
+
"dev": "vite build --watch",
|
|
80
81
|
"typecheck": "tsc --noEmit",
|
|
81
82
|
"clean": "rm -rf dist .turbo"
|
|
82
83
|
}
|