@volcanicminds/tools 0.0.7 → 0.0.8
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/lib/tenant/index.d.ts +4 -0
- package/dist/lib/tenant/index.d.ts.map +1 -0
- package/dist/lib/tenant/index.js +3 -0
- package/dist/lib/tenant/index.js.map +1 -0
- package/dist/lib/tenant/provisioner.d.ts +22 -0
- package/dist/lib/tenant/provisioner.d.ts.map +1 -0
- package/dist/lib/tenant/provisioner.js +45 -0
- package/dist/lib/tenant/provisioner.js.map +1 -0
- package/dist/lib/tenant/resolver.d.ts +17 -0
- package/dist/lib/tenant/resolver.d.ts.map +1 -0
- package/dist/lib/tenant/resolver.js +29 -0
- package/dist/lib/tenant/resolver.js.map +1 -0
- package/dist/lib/tenant/types.d.ts +15 -0
- package/dist/lib/tenant/types.d.ts.map +1 -0
- package/dist/lib/tenant/types.js +2 -0
- package/dist/lib/tenant/types.js.map +1 -0
- package/lib/tenant/index.ts +19 -0
- package/lib/tenant/provisioner.ts +96 -0
- package/lib/tenant/resolver.ts +62 -0
- package/lib/tenant/types.ts +29 -0
- package/package.json +9 -1
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type { DataSourceLike, QueryRunnerLike, RepositoryLike } from './types.js';
|
|
2
|
+
export { TenantEx, TenantStatus, ProvisionResult, validateSchemaName, createTenantSchema, syncTenantSchema, dropTenantSchema } from './provisioner.js';
|
|
3
|
+
export { TenantResolverConfig, resolveTenant, extractTenantId, setSchemaContext } from './resolver.js';
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../lib/tenant/index.ts"],"names":[],"mappings":"AAIA,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAGjF,OAAO,EACL,QAAQ,EACR,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,gBAAgB,EACjB,MAAM,kBAAkB,CAAA;AAGzB,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../lib/tenant/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAIL,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,gBAAgB,EACjB,MAAM,kBAAkB,CAAA;AAGzB,OAAO,EAAwB,aAAa,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { DataSourceLike } from './types.js';
|
|
2
|
+
export type TenantStatus = 'active' | 'suspended' | 'archived';
|
|
3
|
+
export interface TenantEx {
|
|
4
|
+
name: string;
|
|
5
|
+
slug: string;
|
|
6
|
+
schemaName: string;
|
|
7
|
+
status: TenantStatus;
|
|
8
|
+
config: Record<string, unknown>;
|
|
9
|
+
createdAt: Date;
|
|
10
|
+
updatedAt: Date;
|
|
11
|
+
deletedAt?: Date;
|
|
12
|
+
}
|
|
13
|
+
export interface ProvisionResult {
|
|
14
|
+
success: boolean;
|
|
15
|
+
schemaName: string;
|
|
16
|
+
error?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function validateSchemaName(name: string): boolean;
|
|
19
|
+
export declare function createTenantSchema(dataSource: DataSourceLike, schemaName: string): Promise<ProvisionResult>;
|
|
20
|
+
export declare function syncTenantSchema(dataSource: DataSourceLike, schemaName: string): Promise<void>;
|
|
21
|
+
export declare function dropTenantSchema(dataSource: DataSourceLike, schemaName: string): Promise<void>;
|
|
22
|
+
//# sourceMappingURL=provisioner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provisioner.d.ts","sourceRoot":"","sources":["../../../lib/tenant/provisioner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAEhD,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,WAAW,GAAG,UAAU,CAAA;AAmB9D,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,YAAY,CAAA;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,SAAS,EAAE,IAAI,CAAA;IACf,SAAS,EAAE,IAAI,CAAA;IACf,SAAS,CAAC,EAAE,IAAI,CAAA;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAOD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExD;AAKD,wBAAsB,kBAAkB,CAAC,UAAU,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAgBjH;AAMD,wBAAsB,gBAAgB,CAAC,UAAU,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAUpG;AAKD,wBAAsB,gBAAgB,CAAC,UAAU,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQpG"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const SCHEMA_REGEX = /^[a-z][a-z0-9_]*$/;
|
|
2
|
+
export function validateSchemaName(name) {
|
|
3
|
+
return SCHEMA_REGEX.test(name);
|
|
4
|
+
}
|
|
5
|
+
export async function createTenantSchema(dataSource, schemaName) {
|
|
6
|
+
if (!validateSchemaName(schemaName)) {
|
|
7
|
+
return { success: false, schemaName, error: 'Invalid schema name format' };
|
|
8
|
+
}
|
|
9
|
+
const qr = dataSource.createQueryRunner();
|
|
10
|
+
await qr.connect();
|
|
11
|
+
try {
|
|
12
|
+
await qr.query(`CREATE SCHEMA IF NOT EXISTS "${schemaName}"`);
|
|
13
|
+
return { success: true, schemaName };
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
17
|
+
return { success: false, schemaName, error: message };
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
await qr.release();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function syncTenantSchema(dataSource, schemaName) {
|
|
24
|
+
const qr = dataSource.createQueryRunner();
|
|
25
|
+
await qr.connect();
|
|
26
|
+
try {
|
|
27
|
+
await qr.query(`SET search_path TO "${schemaName}"`);
|
|
28
|
+
await dataSource.synchronize();
|
|
29
|
+
await qr.query(`SET search_path TO "public"`);
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
await qr.release();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function dropTenantSchema(dataSource, schemaName) {
|
|
36
|
+
const qr = dataSource.createQueryRunner();
|
|
37
|
+
await qr.connect();
|
|
38
|
+
try {
|
|
39
|
+
await qr.query(`DROP SCHEMA IF EXISTS "${schemaName}" CASCADE`);
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
await qr.release();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=provisioner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provisioner.js","sourceRoot":"","sources":["../../../lib/tenant/provisioner.ts"],"names":[],"mappings":"AAsCA,MAAM,YAAY,GAAG,mBAAmB,CAAA;AAKxC,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,OAAO,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AAChC,CAAC;AAKD,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,UAA0B,EAAE,UAAkB;IACrF,IAAI,CAAC,kBAAkB,CAAC,UAAU,CAAC,EAAE,CAAC;QACpC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,4BAA4B,EAAE,CAAA;IAC5E,CAAC;IAED,MAAM,EAAE,GAAG,UAAU,CAAC,iBAAiB,EAAE,CAAA;IACzC,MAAM,EAAE,CAAC,OAAO,EAAE,CAAA;IAClB,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,KAAK,CAAC,gCAAgC,UAAU,GAAG,CAAC,CAAA;QAC7D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,CAAA;IACtC,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACtB,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAChE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,CAAA;IACvD,CAAC;YAAS,CAAC;QACT,MAAM,EAAE,CAAC,OAAO,EAAE,CAAA;IACpB,CAAC;AACH,CAAC;AAMD,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,UAA0B,EAAE,UAAkB;IACnF,MAAM,EAAE,GAAG,UAAU,CAAC,iBAAiB,EAAE,CAAA;IACzC,MAAM,EAAE,CAAC,OAAO,EAAE,CAAA;IAClB,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,KAAK,CAAC,uBAAuB,UAAU,GAAG,CAAC,CAAA;QACpD,MAAM,UAAU,CAAC,WAAW,EAAE,CAAA;QAC9B,MAAM,EAAE,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAA;IAC/C,CAAC;YAAS,CAAC;QACT,MAAM,EAAE,CAAC,OAAO,EAAE,CAAA;IACpB,CAAC;AACH,CAAC;AAKD,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,UAA0B,EAAE,UAAkB;IACnF,MAAM,EAAE,GAAG,UAAU,CAAC,iBAAiB,EAAE,CAAA;IACzC,MAAM,EAAE,CAAC,OAAO,EAAE,CAAA;IAClB,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,KAAK,CAAC,0BAA0B,UAAU,WAAW,CAAC,CAAA;IACjE,CAAC;YAAS,CAAC;QACT,MAAM,EAAE,CAAC,OAAO,EAAE,CAAA;IACpB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DataSourceLike, RepositoryLike } from './types.js';
|
|
2
|
+
import type { TenantEx } from './provisioner.js';
|
|
3
|
+
export interface TenantResolverConfig<T extends TenantEx> {
|
|
4
|
+
dataSource: DataSourceLike;
|
|
5
|
+
tenantRepository: RepositoryLike<T>;
|
|
6
|
+
headerName?: string;
|
|
7
|
+
subdomainEnabled?: boolean;
|
|
8
|
+
baseDomain?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function resolveTenant<T extends TenantEx>(repository: RepositoryLike<T>, tenantIdOrSlug: string): Promise<T | null>;
|
|
11
|
+
export declare function extractTenantId(headers: Record<string, string | string[] | undefined>, host: string | undefined, config: {
|
|
12
|
+
headerName?: string;
|
|
13
|
+
subdomainEnabled?: boolean;
|
|
14
|
+
baseDomain?: string;
|
|
15
|
+
}): string | null;
|
|
16
|
+
export declare function setSchemaContext(dataSource: DataSourceLike, schemaName: string): Promise<() => Promise<void>>;
|
|
17
|
+
//# sourceMappingURL=resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../../../lib/tenant/resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAEhD,MAAM,WAAW,oBAAoB,CAAC,CAAC,SAAS,QAAQ;IACtD,UAAU,EAAE,cAAc,CAAA;IAC1B,gBAAgB,EAAE,cAAc,CAAC,CAAC,CAAC,CAAA;IACnC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAOD,wBAAsB,aAAa,CAAC,CAAC,SAAS,QAAQ,EACpD,UAAU,EAAE,cAAc,CAAC,CAAC,CAAC,EAC7B,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAGnB;AAKD,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,EACtD,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,MAAM,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/E,MAAM,GAAG,IAAI,CAef;AAMD,wBAAsB,gBAAgB,CAAC,UAAU,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CASnH"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
2
|
+
export async function resolveTenant(repository, tenantIdOrSlug) {
|
|
3
|
+
const where = UUID_RE.test(tenantIdOrSlug) ? { id: tenantIdOrSlug } : { slug: tenantIdOrSlug };
|
|
4
|
+
return repository.findOne({ where });
|
|
5
|
+
}
|
|
6
|
+
export function extractTenantId(headers, host, config) {
|
|
7
|
+
const headerName = config.headerName || 'x-tenant-id';
|
|
8
|
+
const fromHeader = headers[headerName];
|
|
9
|
+
if (fromHeader && typeof fromHeader === 'string')
|
|
10
|
+
return fromHeader;
|
|
11
|
+
if (config.subdomainEnabled && config.baseDomain && host) {
|
|
12
|
+
if (host.endsWith(config.baseDomain)) {
|
|
13
|
+
const subdomain = host.replace(`.${config.baseDomain}`, '').split('.')[0];
|
|
14
|
+
if (subdomain && subdomain !== 'www')
|
|
15
|
+
return subdomain;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
export async function setSchemaContext(dataSource, schemaName) {
|
|
21
|
+
const qr = dataSource.createQueryRunner();
|
|
22
|
+
await qr.connect();
|
|
23
|
+
await qr.query(`SET search_path TO "${schemaName}", "public"`);
|
|
24
|
+
return async () => {
|
|
25
|
+
await qr.query(`SET search_path TO "public"`);
|
|
26
|
+
await qr.release();
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolver.js","sourceRoot":"","sources":["../../../lib/tenant/resolver.ts"],"names":[],"mappings":"AAWA,MAAM,OAAO,GAAG,4EAA4E,CAAA;AAK5F,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,UAA6B,EAC7B,cAAsB;IAEtB,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,CAAA;IAC9F,OAAO,UAAU,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,CAAC,CAAA;AACtC,CAAC;AAKD,MAAM,UAAU,eAAe,CAC7B,OAAsD,EACtD,IAAwB,EACxB,MAAgF;IAGhF,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,IAAI,aAAa,CAAA;IACrD,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;IACtC,IAAI,UAAU,IAAI,OAAO,UAAU,KAAK,QAAQ;QAAE,OAAO,UAAU,CAAA;IAGnE,IAAI,MAAM,CAAC,gBAAgB,IAAI,MAAM,CAAC,UAAU,IAAI,IAAI,EAAE,CAAC;QACzD,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;YACrC,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,UAAU,EAAE,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;YACzE,IAAI,SAAS,IAAI,SAAS,KAAK,KAAK;gBAAE,OAAO,SAAS,CAAA;QACxD,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAA;AACb,CAAC;AAMD,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,UAA0B,EAAE,UAAkB;IACnF,MAAM,EAAE,GAAG,UAAU,CAAC,iBAAiB,EAAE,CAAA;IACzC,MAAM,EAAE,CAAC,OAAO,EAAE,CAAA;IAClB,MAAM,EAAE,CAAC,KAAK,CAAC,uBAAuB,UAAU,aAAa,CAAC,CAAA;IAE9D,OAAO,KAAK,IAAI,EAAE;QAChB,MAAM,EAAE,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAA;QAC7C,MAAM,EAAE,CAAC,OAAO,EAAE,CAAA;IACpB,CAAC,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface QueryRunnerLike {
|
|
2
|
+
connect(): Promise<void>;
|
|
3
|
+
query(query: string, parameters?: unknown[]): Promise<unknown>;
|
|
4
|
+
release(): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
export interface DataSourceLike {
|
|
7
|
+
createQueryRunner(): QueryRunnerLike;
|
|
8
|
+
synchronize(): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export interface RepositoryLike<T> {
|
|
11
|
+
findOne(options: {
|
|
12
|
+
where: Record<string, unknown>;
|
|
13
|
+
}): Promise<T | null>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../lib/tenant/types.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,eAAe;IAC9B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACxB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC9D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACzB;AAKD,MAAM,WAAW,cAAc;IAC7B,iBAAiB,IAAI,eAAe,CAAA;IACpC,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CAC7B;AAKD,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B,OAAO,CAAC,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;CACxE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../lib/tenant/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// TenantEx Module - Multi-tenant utilities for Volcanic Backend
|
|
2
|
+
// Zero external dependencies - uses duck-typed interfaces
|
|
3
|
+
|
|
4
|
+
// Types
|
|
5
|
+
export type { DataSourceLike, QueryRunnerLike, RepositoryLike } from './types.js'
|
|
6
|
+
|
|
7
|
+
// Entity Interface & Provisioning
|
|
8
|
+
export {
|
|
9
|
+
TenantEx,
|
|
10
|
+
TenantStatus,
|
|
11
|
+
ProvisionResult,
|
|
12
|
+
validateSchemaName,
|
|
13
|
+
createTenantSchema,
|
|
14
|
+
syncTenantSchema,
|
|
15
|
+
dropTenantSchema
|
|
16
|
+
} from './provisioner.js'
|
|
17
|
+
|
|
18
|
+
// Tenant Resolution
|
|
19
|
+
export { TenantResolverConfig, resolveTenant, extractTenantId, setSchemaContext } from './resolver.js'
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { DataSourceLike } from './types.js'
|
|
2
|
+
|
|
3
|
+
export type TenantStatus = 'active' | 'suspended' | 'archived'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Interface for multi-tenant entities.
|
|
7
|
+
* Consumer entities should extend BaseEntity and implement this interface.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { TenantEx } from "@volcanicminds/tools/tenant"
|
|
12
|
+
* import { Entity, PrimaryGeneratedColumn, Column, ... } from "typeorm"
|
|
13
|
+
*
|
|
14
|
+
* @Entity({ name: "tenants", schema: "public" })
|
|
15
|
+
* export class Tenant extends BaseEntity implements TenantEx {
|
|
16
|
+
* @PrimaryGeneratedColumn("uuid") id!: string
|
|
17
|
+
* @Column() name!: string
|
|
18
|
+
* // ...
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export interface TenantEx {
|
|
23
|
+
name: string
|
|
24
|
+
slug: string
|
|
25
|
+
schemaName: string
|
|
26
|
+
status: TenantStatus
|
|
27
|
+
config: Record<string, unknown>
|
|
28
|
+
createdAt: Date
|
|
29
|
+
updatedAt: Date
|
|
30
|
+
deletedAt?: Date
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ProvisionResult {
|
|
34
|
+
success: boolean
|
|
35
|
+
schemaName: string
|
|
36
|
+
error?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const SCHEMA_REGEX = /^[a-z][a-z0-9_]*$/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validates a schema name for PostgreSQL compatibility.
|
|
43
|
+
*/
|
|
44
|
+
export function validateSchemaName(name: string): boolean {
|
|
45
|
+
return SCHEMA_REGEX.test(name)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Creates a new PostgreSQL schema for a tenant.
|
|
50
|
+
*/
|
|
51
|
+
export async function createTenantSchema(dataSource: DataSourceLike, schemaName: string): Promise<ProvisionResult> {
|
|
52
|
+
if (!validateSchemaName(schemaName)) {
|
|
53
|
+
return { success: false, schemaName, error: 'Invalid schema name format' }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const qr = dataSource.createQueryRunner()
|
|
57
|
+
await qr.connect()
|
|
58
|
+
try {
|
|
59
|
+
await qr.query(`CREATE SCHEMA IF NOT EXISTS "${schemaName}"`)
|
|
60
|
+
return { success: true, schemaName }
|
|
61
|
+
} catch (err: unknown) {
|
|
62
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
63
|
+
return { success: false, schemaName, error: message }
|
|
64
|
+
} finally {
|
|
65
|
+
await qr.release()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Synchronizes TypeORM entities to a tenant schema.
|
|
71
|
+
* WARNING: Use with caution in production. Prefer migrations.
|
|
72
|
+
*/
|
|
73
|
+
export async function syncTenantSchema(dataSource: DataSourceLike, schemaName: string): Promise<void> {
|
|
74
|
+
const qr = dataSource.createQueryRunner()
|
|
75
|
+
await qr.connect()
|
|
76
|
+
try {
|
|
77
|
+
await qr.query(`SET search_path TO "${schemaName}"`)
|
|
78
|
+
await dataSource.synchronize()
|
|
79
|
+
await qr.query(`SET search_path TO "public"`)
|
|
80
|
+
} finally {
|
|
81
|
+
await qr.release()
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Drops a tenant schema and all its contents. DESTRUCTIVE.
|
|
87
|
+
*/
|
|
88
|
+
export async function dropTenantSchema(dataSource: DataSourceLike, schemaName: string): Promise<void> {
|
|
89
|
+
const qr = dataSource.createQueryRunner()
|
|
90
|
+
await qr.connect()
|
|
91
|
+
try {
|
|
92
|
+
await qr.query(`DROP SCHEMA IF EXISTS "${schemaName}" CASCADE`)
|
|
93
|
+
} finally {
|
|
94
|
+
await qr.release()
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { DataSourceLike, RepositoryLike } from './types.js'
|
|
2
|
+
import type { TenantEx } from './provisioner.js'
|
|
3
|
+
|
|
4
|
+
export interface TenantResolverConfig<T extends TenantEx> {
|
|
5
|
+
dataSource: DataSourceLike
|
|
6
|
+
tenantRepository: RepositoryLike<T>
|
|
7
|
+
headerName?: string
|
|
8
|
+
subdomainEnabled?: boolean
|
|
9
|
+
baseDomain?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolves a tenant by ID or slug.
|
|
16
|
+
*/
|
|
17
|
+
export async function resolveTenant<T extends TenantEx>(
|
|
18
|
+
repository: RepositoryLike<T>,
|
|
19
|
+
tenantIdOrSlug: string
|
|
20
|
+
): Promise<T | null> {
|
|
21
|
+
const where = UUID_RE.test(tenantIdOrSlug) ? { id: tenantIdOrSlug } : { slug: tenantIdOrSlug }
|
|
22
|
+
return repository.findOne({ where })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Extracts tenant identifier from request headers or subdomain.
|
|
27
|
+
*/
|
|
28
|
+
export function extractTenantId(
|
|
29
|
+
headers: Record<string, string | string[] | undefined>,
|
|
30
|
+
host: string | undefined,
|
|
31
|
+
config: { headerName?: string; subdomainEnabled?: boolean; baseDomain?: string }
|
|
32
|
+
): string | null {
|
|
33
|
+
// 1. Try header
|
|
34
|
+
const headerName = config.headerName || 'x-tenant-id'
|
|
35
|
+
const fromHeader = headers[headerName]
|
|
36
|
+
if (fromHeader && typeof fromHeader === 'string') return fromHeader
|
|
37
|
+
|
|
38
|
+
// 2. Try subdomain
|
|
39
|
+
if (config.subdomainEnabled && config.baseDomain && host) {
|
|
40
|
+
if (host.endsWith(config.baseDomain)) {
|
|
41
|
+
const subdomain = host.replace(`.${config.baseDomain}`, '').split('.')[0]
|
|
42
|
+
if (subdomain && subdomain !== 'www') return subdomain
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Sets the PostgreSQL search_path for a tenant schema.
|
|
51
|
+
* Returns a cleanup function to reset to public.
|
|
52
|
+
*/
|
|
53
|
+
export async function setSchemaContext(dataSource: DataSourceLike, schemaName: string): Promise<() => Promise<void>> {
|
|
54
|
+
const qr = dataSource.createQueryRunner()
|
|
55
|
+
await qr.connect()
|
|
56
|
+
await qr.query(`SET search_path TO "${schemaName}", "public"`)
|
|
57
|
+
|
|
58
|
+
return async () => {
|
|
59
|
+
await qr.query(`SET search_path TO "public"`)
|
|
60
|
+
await qr.release()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal type definitions for ORM compatibility.
|
|
3
|
+
* These are duck-typed interfaces that match the methods we use.
|
|
4
|
+
* Works with TypeORM or any ORM with compatible signatures.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Minimal QueryRunner interface.
|
|
9
|
+
*/
|
|
10
|
+
export interface QueryRunnerLike {
|
|
11
|
+
connect(): Promise<void>
|
|
12
|
+
query(query: string, parameters?: unknown[]): Promise<unknown>
|
|
13
|
+
release(): Promise<void>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Minimal DataSource interface.
|
|
18
|
+
*/
|
|
19
|
+
export interface DataSourceLike {
|
|
20
|
+
createQueryRunner(): QueryRunnerLike
|
|
21
|
+
synchronize(): Promise<void>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Minimal Repository interface.
|
|
26
|
+
*/
|
|
27
|
+
export interface RepositoryLike<T> {
|
|
28
|
+
findOne(options: { where: Record<string, unknown> }): Promise<T | null>
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volcanicminds/tools",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "Tools for the volcanic (minds) backend",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"volcanic",
|
|
@@ -68,6 +68,11 @@
|
|
|
68
68
|
"types": "./dist/lib/transfer/index.d.ts",
|
|
69
69
|
"import": "./dist/lib/transfer/index.js",
|
|
70
70
|
"require": "./dist/lib/transfer/index.js"
|
|
71
|
+
},
|
|
72
|
+
"./tenant": {
|
|
73
|
+
"types": "./dist/lib/tenant/index.d.ts",
|
|
74
|
+
"import": "./dist/lib/tenant/index.js",
|
|
75
|
+
"require": "./dist/lib/tenant/index.js"
|
|
71
76
|
}
|
|
72
77
|
},
|
|
73
78
|
"main": "dist/index.js",
|
|
@@ -137,6 +142,9 @@
|
|
|
137
142
|
],
|
|
138
143
|
"transfer": [
|
|
139
144
|
"dist/lib/transfer/index.d.ts"
|
|
145
|
+
],
|
|
146
|
+
"tenant": [
|
|
147
|
+
"dist/lib/tenant/index.d.ts"
|
|
140
148
|
]
|
|
141
149
|
}
|
|
142
150
|
}
|