@forinda/kickjs-multi-tenant 2.0.1 → 2.2.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.mts +85 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +114 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +26 -10
- package/dist/index.d.ts +0 -4
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -65
- package/dist/tenant.adapter.d.ts +0 -40
- package/dist/tenant.adapter.d.ts.map +0 -1
- package/dist/types.d.ts +0 -38
- package/dist/types.d.ts.map +0 -1
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
|
|
2
|
+
import { AdapterContext, AdapterMiddleware, AppAdapter } from "@forinda/kickjs";
|
|
3
|
+
|
|
4
|
+
//#region src/types.d.ts
|
|
5
|
+
/** DI token for the current tenant context */
|
|
6
|
+
declare const TENANT_CONTEXT: unique symbol;
|
|
7
|
+
/** Tenant information resolved from the request */
|
|
8
|
+
interface TenantInfo {
|
|
9
|
+
/** Unique tenant identifier */
|
|
10
|
+
id: string;
|
|
11
|
+
/** Optional tenant name */
|
|
12
|
+
name?: string;
|
|
13
|
+
/** Optional tenant-specific config/metadata */
|
|
14
|
+
metadata?: Record<string, any>;
|
|
15
|
+
}
|
|
16
|
+
/** Strategy for resolving the tenant from a request */
|
|
17
|
+
type TenantResolutionStrategy = 'header' | 'subdomain' | 'path' | 'query' | ((req: any) => TenantInfo | null | Promise<TenantInfo | null>);
|
|
18
|
+
interface MultiTenantOptions {
|
|
19
|
+
/**
|
|
20
|
+
* How to resolve the tenant from the request.
|
|
21
|
+
* - 'header' — reads X-Tenant-ID header (default)
|
|
22
|
+
* - 'subdomain' — extracts from subdomain (tenant.example.com)
|
|
23
|
+
* - 'path' — extracts from first path segment (/tenant-id/...)
|
|
24
|
+
* - 'query' — reads ?tenantId= query param
|
|
25
|
+
* - function — custom resolver
|
|
26
|
+
*/
|
|
27
|
+
strategy?: TenantResolutionStrategy;
|
|
28
|
+
/** Header name when strategy is 'header' (default: 'x-tenant-id') */
|
|
29
|
+
headerName?: string;
|
|
30
|
+
/** Query param name when strategy is 'query' (default: 'tenantId') */
|
|
31
|
+
queryParam?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Called after tenant is resolved. Use for validation, loading tenant
|
|
34
|
+
* config from DB, or rejecting unknown tenants.
|
|
35
|
+
*/
|
|
36
|
+
onTenantResolved?: (tenant: TenantInfo, req: any) => void | Promise<void>;
|
|
37
|
+
/** Return a 403 if no tenant can be resolved (default: true) */
|
|
38
|
+
required?: boolean;
|
|
39
|
+
/** Routes to skip tenant resolution (e.g., health checks) */
|
|
40
|
+
excludeRoutes?: string[];
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/tenant.adapter.d.ts
|
|
44
|
+
/**
|
|
45
|
+
* Multi-tenancy adapter for KickJS.
|
|
46
|
+
*
|
|
47
|
+
* Resolves the tenant from each request and makes it available
|
|
48
|
+
* via DI (`@Inject(TENANT_CONTEXT)`) and `req.tenant`.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* import { TenantAdapter, TENANT_CONTEXT } from '@forinda/kickjs-multi-tenant'
|
|
53
|
+
*
|
|
54
|
+
* bootstrap({
|
|
55
|
+
* modules,
|
|
56
|
+
* adapters: [
|
|
57
|
+
* new TenantAdapter({
|
|
58
|
+
* strategy: 'header',
|
|
59
|
+
* onTenantResolved: async (tenant) => {
|
|
60
|
+
* // Load tenant config from DB, validate, etc.
|
|
61
|
+
* },
|
|
62
|
+
* }),
|
|
63
|
+
* ],
|
|
64
|
+
* })
|
|
65
|
+
*
|
|
66
|
+
* // In a service:
|
|
67
|
+
* @Service()
|
|
68
|
+
* class UserService {
|
|
69
|
+
* constructor(@Inject(TENANT_CONTEXT) private tenant: TenantInfo) {}
|
|
70
|
+
* }
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
declare class TenantAdapter implements AppAdapter {
|
|
74
|
+
name: string;
|
|
75
|
+
private options;
|
|
76
|
+
constructor(options?: MultiTenantOptions);
|
|
77
|
+
middleware(): AdapterMiddleware[];
|
|
78
|
+
beforeStart({
|
|
79
|
+
container
|
|
80
|
+
}: AdapterContext): void;
|
|
81
|
+
private resolveTenant;
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
export { type MultiTenantOptions, TENANT_CONTEXT, TenantAdapter, type TenantInfo, type TenantResolutionStrategy };
|
|
85
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/tenant.adapter.ts"],"mappings":";;;;;cACa,cAAA;;UAGI,UAAA;EAHoC;EAKnD,EAAA;EALmD;EAOnD,IAAA;EAJe;EAMf,QAAA,GAAW,MAAA;AAAA;;KAID,wBAAA,iDAKN,GAAA,UAAa,UAAA,UAAoB,OAAA,CAAQ,UAAA;AAAA,UAE9B,kBAAA;EAXf;;;;AAIF;;;;EAgBE,QAAA,GAAW,wBAAA;EAX0B;EAcrC,UAAA;EAd4C;EAiB5C,UAAA;EAjBiB;;;;EAuBjB,gBAAA,IAAoB,MAAA,EAAQ,UAAA,EAAY,GAAA,iBAAoB,OAAA;EArB7C;EAwBf,QAAA;;EAGA,aAAA;AAAA;;;;AA/CF;;;;;AAGA;;;;;;;;;;AAUA;;;;;;;;;;;;;cCgCa,aAAA,YAAyB,UAAA;EACpC,IAAA;EAAA,QACQ,OAAA;cAKI,OAAA,GAAS,kBAAA;EAUrB,UAAA,CAAA,GAAc,iBAAA;EAoCd,WAAA,CAAA;IAAc;EAAA,GAAa,cAAA;EAAA,QAab,aAAA;AAAA"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @forinda/kickjs-multi-tenant v2.2.0
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) Felix Orinda
|
|
5
|
+
*
|
|
6
|
+
* This source code is licensed under the MIT license found in the
|
|
7
|
+
* LICENSE file in the root directory of this source tree.
|
|
8
|
+
*
|
|
9
|
+
* @license MIT
|
|
10
|
+
*/
|
|
11
|
+
import { Logger, Scope } from "@forinda/kickjs";
|
|
12
|
+
//#region src/types.ts
|
|
13
|
+
/** DI token for the current tenant context */
|
|
14
|
+
const TENANT_CONTEXT = Symbol("TenantContext");
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/tenant.adapter.ts
|
|
17
|
+
const log = Logger.for("MultiTenant");
|
|
18
|
+
/**
|
|
19
|
+
* Multi-tenancy adapter for KickJS.
|
|
20
|
+
*
|
|
21
|
+
* Resolves the tenant from each request and makes it available
|
|
22
|
+
* via DI (`@Inject(TENANT_CONTEXT)`) and `req.tenant`.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { TenantAdapter, TENANT_CONTEXT } from '@forinda/kickjs-multi-tenant'
|
|
27
|
+
*
|
|
28
|
+
* bootstrap({
|
|
29
|
+
* modules,
|
|
30
|
+
* adapters: [
|
|
31
|
+
* new TenantAdapter({
|
|
32
|
+
* strategy: 'header',
|
|
33
|
+
* onTenantResolved: async (tenant) => {
|
|
34
|
+
* // Load tenant config from DB, validate, etc.
|
|
35
|
+
* },
|
|
36
|
+
* }),
|
|
37
|
+
* ],
|
|
38
|
+
* })
|
|
39
|
+
*
|
|
40
|
+
* // In a service:
|
|
41
|
+
* @Service()
|
|
42
|
+
* class UserService {
|
|
43
|
+
* constructor(@Inject(TENANT_CONTEXT) private tenant: TenantInfo) {}
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
var TenantAdapter = class {
|
|
48
|
+
name = "TenantAdapter";
|
|
49
|
+
options;
|
|
50
|
+
constructor(options = {}) {
|
|
51
|
+
this.options = {
|
|
52
|
+
strategy: options.strategy ?? "header",
|
|
53
|
+
required: options.required ?? true,
|
|
54
|
+
headerName: options.headerName ?? "x-tenant-id",
|
|
55
|
+
queryParam: options.queryParam ?? "tenantId",
|
|
56
|
+
...options
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
middleware() {
|
|
60
|
+
return [{
|
|
61
|
+
handler: async (req, res, next) => {
|
|
62
|
+
if (this.options.excludeRoutes?.some((r) => req.path.startsWith(r))) return next();
|
|
63
|
+
const tenant = await this.resolveTenant(req);
|
|
64
|
+
if (!tenant) {
|
|
65
|
+
if (this.options.required) {
|
|
66
|
+
res.status(403).json({ message: "Tenant not found. Provide a valid tenant identifier." });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
return next();
|
|
70
|
+
}
|
|
71
|
+
req.tenant = tenant;
|
|
72
|
+
if (this.options.onTenantResolved) await this.options.onTenantResolved(tenant, req);
|
|
73
|
+
next();
|
|
74
|
+
},
|
|
75
|
+
phase: "beforeGlobal"
|
|
76
|
+
}];
|
|
77
|
+
}
|
|
78
|
+
beforeStart({ container }) {
|
|
79
|
+
container.registerFactory(TENANT_CONTEXT, () => ({
|
|
80
|
+
id: "default",
|
|
81
|
+
name: "Default Tenant"
|
|
82
|
+
}), Scope.SINGLETON);
|
|
83
|
+
log.info(`Tenant resolution: ${typeof this.options.strategy === "function" ? "custom" : this.options.strategy}`);
|
|
84
|
+
}
|
|
85
|
+
async resolveTenant(req) {
|
|
86
|
+
const strategy = this.options.strategy;
|
|
87
|
+
if (typeof strategy === "function") return strategy(req);
|
|
88
|
+
switch (strategy) {
|
|
89
|
+
case "header": {
|
|
90
|
+
const tenantId = req.get(this.options.headerName);
|
|
91
|
+
return tenantId ? { id: tenantId } : null;
|
|
92
|
+
}
|
|
93
|
+
case "subdomain": {
|
|
94
|
+
const parts = req.hostname.split(".");
|
|
95
|
+
if (parts.length >= 3) return { id: parts[0] };
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
case "path": {
|
|
99
|
+
const segments = req.path.split("/").filter(Boolean);
|
|
100
|
+
if (segments.length > 0) return { id: segments[0] };
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
case "query": {
|
|
104
|
+
const tenantId = req.query[this.options.queryParam];
|
|
105
|
+
return tenantId ? { id: tenantId } : null;
|
|
106
|
+
}
|
|
107
|
+
default: return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
//#endregion
|
|
112
|
+
export { TENANT_CONTEXT, TenantAdapter };
|
|
113
|
+
|
|
114
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/types.ts","../src/tenant.adapter.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","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'\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 next()\n },\n phase: 'beforeGlobal',\n },\n ]\n }\n\n beforeStart({ container }: AdapterContext): void {\n // Register a factory that reads the tenant from the current request context\n // This requires request-scoped resolution — for now, register as a placeholder\n container.registerFactory(\n TENANT_CONTEXT,\n () => ({ id: 'default', name: 'Default Tenant' }) as TenantInfo,\n Scope.SINGLETON,\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"],"mappings":";;;;;;;;;;;;;AACA,MAAa,iBAAiB,OAAO,gBAAgB;;;ACcrD,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;AAGlD,UAAM;;GAER,OAAO;GACR,CACF;;CAGH,YAAY,EAAE,aAAmC;AAG/C,YAAU,gBACR,uBACO;GAAE,IAAI;GAAW,MAAM;GAAkB,GAChD,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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forinda/kickjs-multi-tenant",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Multi-tenancy helpers for KickJS — tenant resolution, scoped DI, and database routing",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kickjs",
|
|
@@ -37,20 +37,37 @@
|
|
|
37
37
|
"vite"
|
|
38
38
|
],
|
|
39
39
|
"type": "module",
|
|
40
|
-
"main": "dist/index.
|
|
41
|
-
"types": "dist/index.d.
|
|
40
|
+
"main": "dist/index.mjs",
|
|
41
|
+
"types": "dist/index.d.mts",
|
|
42
42
|
"exports": {
|
|
43
43
|
".": {
|
|
44
|
-
"import": "./dist/index.
|
|
45
|
-
"types": "./dist/index.d.
|
|
44
|
+
"import": "./dist/index.mjs",
|
|
45
|
+
"types": "./dist/index.d.mts"
|
|
46
46
|
}
|
|
47
47
|
},
|
|
48
48
|
"files": [
|
|
49
49
|
"dist"
|
|
50
50
|
],
|
|
51
|
+
"wireit": {
|
|
52
|
+
"build": {
|
|
53
|
+
"command": "tsdown",
|
|
54
|
+
"files": [
|
|
55
|
+
"src/**/*.ts",
|
|
56
|
+
"tsdown.config.ts",
|
|
57
|
+
"tsconfig.json",
|
|
58
|
+
"package.json"
|
|
59
|
+
],
|
|
60
|
+
"output": [
|
|
61
|
+
"dist/**"
|
|
62
|
+
],
|
|
63
|
+
"dependencies": [
|
|
64
|
+
"../core:build"
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
},
|
|
51
68
|
"dependencies": {
|
|
52
69
|
"reflect-metadata": "^0.2.2",
|
|
53
|
-
"@forinda/kickjs": "2.0
|
|
70
|
+
"@forinda/kickjs": "2.2.0"
|
|
54
71
|
},
|
|
55
72
|
"devDependencies": {
|
|
56
73
|
"@types/node": "^25.0.0",
|
|
@@ -74,10 +91,9 @@
|
|
|
74
91
|
"url": "https://github.com/forinda/kick-js/issues"
|
|
75
92
|
},
|
|
76
93
|
"scripts": {
|
|
77
|
-
"build": "
|
|
78
|
-
"
|
|
79
|
-
"dev": "vite build --watch",
|
|
94
|
+
"build": "wireit",
|
|
95
|
+
"dev": "tsdown --watch",
|
|
80
96
|
"typecheck": "tsc --noEmit",
|
|
81
|
-
"clean": "rm -rf dist .
|
|
97
|
+
"clean": "rm -rf dist .wireit"
|
|
82
98
|
}
|
|
83
99
|
}
|
package/dist/index.d.ts
DELETED
package/dist/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { Logger as s, Scope as o } from "@forinda/kickjs";
|
|
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({ container: t }) {
|
|
32
|
-
t.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
|
-
};
|
package/dist/tenant.adapter.d.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { type AppAdapter, type AdapterContext, type AdapterMiddleware } from '@forinda/kickjs';
|
|
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({ container }: AdapterContext): void;
|
|
38
|
-
private resolveTenant;
|
|
39
|
-
}
|
|
40
|
-
//# sourceMappingURL=tenant.adapter.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tenant.adapter.d.ts","sourceRoot":"","sources":["../src/tenant.adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,iBAAiB,EAEvB,MAAM,iBAAiB,CAAA;AAExB,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,EAAE,SAAS,EAAE,EAAE,cAAc,GAAG,IAAI;YAalC,aAAa;CAmC5B"}
|
package/dist/types.d.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
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
|
package/dist/types.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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"}
|