@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.
@@ -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,3 @@
1
+ export { validateSchemaName, createTenantSchema, syncTenantSchema, dropTenantSchema } from './provisioner.js';
2
+ export { resolveTenant, extractTenantId, setSchemaContext } from './resolver.js';
3
+ //# sourceMappingURL=index.js.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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.7",
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
  }