drizzle-multitenant 1.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.
@@ -0,0 +1,219 @@
1
+ import { sql, getTableName } from 'drizzle-orm';
2
+
3
+ // src/cross-schema/cross-schema.ts
4
+ var CrossSchemaQueryBuilder = class {
5
+ constructor(context) {
6
+ this.context = context;
7
+ }
8
+ fromTable = null;
9
+ joins = [];
10
+ selectFields = {};
11
+ whereCondition = null;
12
+ orderByFields = [];
13
+ limitValue = null;
14
+ offsetValue = null;
15
+ /**
16
+ * Set the main table to query from
17
+ */
18
+ from(source, table) {
19
+ const schemaName = this.getSchemaName(source);
20
+ this.fromTable = { table, source, schemaName };
21
+ return this;
22
+ }
23
+ /**
24
+ * Add an inner join
25
+ */
26
+ innerJoin(source, table, condition) {
27
+ return this.addJoin(source, table, condition, "inner");
28
+ }
29
+ /**
30
+ * Add a left join
31
+ */
32
+ leftJoin(source, table, condition) {
33
+ return this.addJoin(source, table, condition, "left");
34
+ }
35
+ /**
36
+ * Add a right join
37
+ */
38
+ rightJoin(source, table, condition) {
39
+ return this.addJoin(source, table, condition, "right");
40
+ }
41
+ /**
42
+ * Add a full outer join
43
+ */
44
+ fullJoin(source, table, condition) {
45
+ return this.addJoin(source, table, condition, "full");
46
+ }
47
+ /**
48
+ * Select specific fields
49
+ */
50
+ select(fields) {
51
+ this.selectFields = fields;
52
+ return this;
53
+ }
54
+ /**
55
+ * Add a where condition
56
+ */
57
+ where(condition) {
58
+ this.whereCondition = condition;
59
+ return this;
60
+ }
61
+ /**
62
+ * Add order by
63
+ */
64
+ orderBy(...fields) {
65
+ this.orderByFields = fields;
66
+ return this;
67
+ }
68
+ /**
69
+ * Set limit
70
+ */
71
+ limit(value) {
72
+ this.limitValue = value;
73
+ return this;
74
+ }
75
+ /**
76
+ * Set offset
77
+ */
78
+ offset(value) {
79
+ this.offsetValue = value;
80
+ return this;
81
+ }
82
+ /**
83
+ * Execute the query and return typed results
84
+ */
85
+ async execute() {
86
+ if (!this.fromTable) {
87
+ throw new Error("[drizzle-multitenant] No table specified. Use .from() first.");
88
+ }
89
+ const sqlQuery = this.buildSql();
90
+ const result = await this.context.tenantDb.execute(sqlQuery);
91
+ return result.rows;
92
+ }
93
+ /**
94
+ * Build the SQL query
95
+ */
96
+ buildSql() {
97
+ if (!this.fromTable) {
98
+ throw new Error("[drizzle-multitenant] No table specified");
99
+ }
100
+ const parts = [];
101
+ const selectParts = Object.entries(this.selectFields).map(([alias, column]) => {
102
+ const columnName = column.name;
103
+ return sql`${sql.raw(`"${columnName}"`)} as ${sql.raw(`"${alias}"`)}`;
104
+ });
105
+ if (selectParts.length === 0) {
106
+ parts.push(sql`SELECT *`);
107
+ } else {
108
+ parts.push(sql`SELECT ${sql.join(selectParts, sql`, `)}`);
109
+ }
110
+ const fromTableRef = this.getFullTableName(this.fromTable.schemaName, this.fromTable.table);
111
+ parts.push(sql` FROM ${sql.raw(fromTableRef)}`);
112
+ for (const join of this.joins) {
113
+ const joinTableRef = this.getFullTableName(join.schemaName, join.table);
114
+ const joinType = this.getJoinKeyword(join.type);
115
+ parts.push(sql` ${sql.raw(joinType)} ${sql.raw(joinTableRef)} ON ${join.condition}`);
116
+ }
117
+ if (this.whereCondition) {
118
+ parts.push(sql` WHERE ${this.whereCondition}`);
119
+ }
120
+ if (this.orderByFields.length > 0) {
121
+ parts.push(sql` ORDER BY ${sql.join(this.orderByFields, sql`, `)}`);
122
+ }
123
+ if (this.limitValue !== null) {
124
+ parts.push(sql` LIMIT ${sql.raw(this.limitValue.toString())}`);
125
+ }
126
+ if (this.offsetValue !== null) {
127
+ parts.push(sql` OFFSET ${sql.raw(this.offsetValue.toString())}`);
128
+ }
129
+ return sql.join(parts, sql``);
130
+ }
131
+ /**
132
+ * Add a join to the query
133
+ */
134
+ addJoin(source, table, condition, type) {
135
+ const schemaName = this.getSchemaName(source);
136
+ this.joins.push({ table, source, schemaName, condition, type });
137
+ return this;
138
+ }
139
+ /**
140
+ * Get schema name for a source
141
+ */
142
+ getSchemaName(source) {
143
+ if (source === "tenant") {
144
+ return this.context.tenantSchema ?? "tenant";
145
+ }
146
+ return this.context.sharedSchema ?? "public";
147
+ }
148
+ /**
149
+ * Get fully qualified table name
150
+ */
151
+ getFullTableName(schemaName, table) {
152
+ const tableName = getTableName(table);
153
+ return `"${schemaName}"."${tableName}"`;
154
+ }
155
+ /**
156
+ * Get SQL keyword for join type
157
+ */
158
+ getJoinKeyword(type) {
159
+ switch (type) {
160
+ case "inner":
161
+ return "INNER JOIN";
162
+ case "left":
163
+ return "LEFT JOIN";
164
+ case "right":
165
+ return "RIGHT JOIN";
166
+ case "full":
167
+ return "FULL OUTER JOIN";
168
+ }
169
+ }
170
+ };
171
+ function createCrossSchemaQuery(context) {
172
+ return new CrossSchemaQueryBuilder(context);
173
+ }
174
+ async function withSharedLookup(config) {
175
+ const {
176
+ tenantDb,
177
+ tenantTable,
178
+ sharedTable,
179
+ foreignKey,
180
+ sharedKey = "id",
181
+ sharedFields,
182
+ where: whereCondition
183
+ } = config;
184
+ const tenantTableName = getTableName(tenantTable);
185
+ const sharedTableName = getTableName(sharedTable);
186
+ const sharedFieldList = sharedFields.map((field) => `s."${String(field)}"`).join(", ");
187
+ const queryParts = [
188
+ `SELECT t.*, ${sharedFieldList}`,
189
+ `FROM "${tenantTableName}" t`,
190
+ `LEFT JOIN "public"."${sharedTableName}" s ON t."${String(foreignKey)}" = s."${String(sharedKey)}"`
191
+ ];
192
+ if (whereCondition) {
193
+ queryParts.push("WHERE");
194
+ }
195
+ const sqlQuery = sql.raw(queryParts.join(" "));
196
+ const result = await tenantDb.execute(sqlQuery);
197
+ return result.rows;
198
+ }
199
+ async function crossSchemaRaw(db, options) {
200
+ const { tenantSchema, sharedSchema, sql: rawSql } = options;
201
+ const processedSql = rawSql.replace(/\$tenant\./g, `"${tenantSchema}".`).replace(/\$shared\./g, `"${sharedSchema}".`);
202
+ const query = sql.raw(processedSql);
203
+ const result = await db.execute(query);
204
+ return result.rows;
205
+ }
206
+ function buildCrossSchemaSelect(fields, tenantSchema, _sharedSchema) {
207
+ const columns = Object.entries(fields).map(([alias, column]) => {
208
+ const columnName = column.name;
209
+ return `"${columnName}" as "${alias}"`;
210
+ });
211
+ const getSchema = () => {
212
+ return tenantSchema;
213
+ };
214
+ return { columns, getSchema };
215
+ }
216
+
217
+ export { CrossSchemaQueryBuilder, buildCrossSchemaSelect, createCrossSchemaQuery, crossSchemaRaw, withSharedLookup };
218
+ //# sourceMappingURL=index.js.map
219
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/cross-schema/cross-schema.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { sql, getTableName } from 'drizzle-orm';\nimport type { NodePgDatabase } from 'drizzle-orm/node-postgres';\nimport type { SQL, Table, Column } from 'drizzle-orm';\nimport type {\n CrossSchemaContext,\n SchemaSource,\n JoinType,\n JoinCondition,\n SharedLookupConfig,\n CrossSchemaRawOptions,\n} from './types.js';\n\n/**\n * Cross-schema query builder for joining tenant and shared data\n *\n * @example\n * ```typescript\n * const query = createCrossSchemaQuery({\n * tenantDb: tenants.getDb('tenant-uuid'),\n * sharedDb: tenants.getSharedDb(),\n * });\n *\n * const results = await query\n * .from('tenant', orders)\n * .leftJoin('shared', subscriptionPlans, eq(orders.planId, subscriptionPlans.id))\n * .select({\n * orderId: orders.id,\n * planName: subscriptionPlans.name,\n * })\n * .execute();\n * ```\n */\nexport class CrossSchemaQueryBuilder<\n TTenantSchema extends Record<string, unknown> = Record<string, unknown>,\n TSharedSchema extends Record<string, unknown> = Record<string, unknown>,\n> {\n private fromTable: { table: Table; source: SchemaSource; schemaName: string } | null = null;\n private joins: Array<{\n table: Table;\n source: SchemaSource;\n schemaName: string;\n condition: JoinCondition;\n type: JoinType;\n }> = [];\n private selectFields: Record<string, Column> = {};\n private whereCondition: SQL<unknown> | null = null;\n private orderByFields: SQL<unknown>[] = [];\n private limitValue: number | null = null;\n private offsetValue: number | null = null;\n\n constructor(private readonly context: CrossSchemaContext<TTenantSchema, TSharedSchema>) {}\n\n /**\n * Set the main table to query from\n */\n from<T extends Table>(source: SchemaSource, table: T): this {\n const schemaName = this.getSchemaName(source);\n this.fromTable = { table, source, schemaName };\n return this;\n }\n\n /**\n * Add an inner join\n */\n innerJoin<T extends Table>(source: SchemaSource, table: T, condition: JoinCondition): this {\n return this.addJoin(source, table, condition, 'inner');\n }\n\n /**\n * Add a left join\n */\n leftJoin<T extends Table>(source: SchemaSource, table: T, condition: JoinCondition): this {\n return this.addJoin(source, table, condition, 'left');\n }\n\n /**\n * Add a right join\n */\n rightJoin<T extends Table>(source: SchemaSource, table: T, condition: JoinCondition): this {\n return this.addJoin(source, table, condition, 'right');\n }\n\n /**\n * Add a full outer join\n */\n fullJoin<T extends Table>(source: SchemaSource, table: T, condition: JoinCondition): this {\n return this.addJoin(source, table, condition, 'full');\n }\n\n /**\n * Select specific fields\n */\n select<T extends Record<string, Column>>(fields: T): this {\n this.selectFields = fields;\n return this;\n }\n\n /**\n * Add a where condition\n */\n where(condition: SQL<unknown>): this {\n this.whereCondition = condition;\n return this;\n }\n\n /**\n * Add order by\n */\n orderBy(...fields: SQL<unknown>[]): this {\n this.orderByFields = fields;\n return this;\n }\n\n /**\n * Set limit\n */\n limit(value: number): this {\n this.limitValue = value;\n return this;\n }\n\n /**\n * Set offset\n */\n offset(value: number): this {\n this.offsetValue = value;\n return this;\n }\n\n /**\n * Execute the query and return typed results\n */\n async execute<TResult = Record<string, unknown>>(): Promise<TResult[]> {\n if (!this.fromTable) {\n throw new Error('[drizzle-multitenant] No table specified. Use .from() first.');\n }\n\n const sqlQuery = this.buildSql();\n\n // Use the tenant db to execute (it has access to both schemas via search_path)\n const result = await this.context.tenantDb.execute(sqlQuery);\n\n return result.rows as TResult[];\n }\n\n /**\n * Build the SQL query\n */\n private buildSql(): SQL<unknown> {\n if (!this.fromTable) {\n throw new Error('[drizzle-multitenant] No table specified');\n }\n\n const parts: SQL<unknown>[] = [];\n\n // SELECT clause\n const selectParts = Object.entries(this.selectFields).map(([alias, column]) => {\n const columnName = column.name;\n return sql`${sql.raw(`\"${columnName}\"`)} as ${sql.raw(`\"${alias}\"`)}`;\n });\n\n if (selectParts.length === 0) {\n parts.push(sql`SELECT *`);\n } else {\n parts.push(sql`SELECT ${sql.join(selectParts, sql`, `)}`);\n }\n\n // FROM clause\n const fromTableRef = this.getFullTableName(this.fromTable.schemaName, this.fromTable.table);\n parts.push(sql` FROM ${sql.raw(fromTableRef)}`);\n\n // JOIN clauses\n for (const join of this.joins) {\n const joinTableRef = this.getFullTableName(join.schemaName, join.table);\n const joinType = this.getJoinKeyword(join.type);\n parts.push(sql` ${sql.raw(joinType)} ${sql.raw(joinTableRef)} ON ${join.condition}`);\n }\n\n // WHERE clause\n if (this.whereCondition) {\n parts.push(sql` WHERE ${this.whereCondition}`);\n }\n\n // ORDER BY clause\n if (this.orderByFields.length > 0) {\n parts.push(sql` ORDER BY ${sql.join(this.orderByFields, sql`, `)}`);\n }\n\n // LIMIT clause\n if (this.limitValue !== null) {\n parts.push(sql` LIMIT ${sql.raw(this.limitValue.toString())}`);\n }\n\n // OFFSET clause\n if (this.offsetValue !== null) {\n parts.push(sql` OFFSET ${sql.raw(this.offsetValue.toString())}`);\n }\n\n return sql.join(parts, sql``);\n }\n\n /**\n * Add a join to the query\n */\n private addJoin<T extends Table>(\n source: SchemaSource,\n table: T,\n condition: JoinCondition,\n type: JoinType\n ): this {\n const schemaName = this.getSchemaName(source);\n this.joins.push({ table, source, schemaName, condition, type });\n return this;\n }\n\n /**\n * Get schema name for a source\n */\n private getSchemaName(source: SchemaSource): string {\n if (source === 'tenant') {\n return this.context.tenantSchema ?? 'tenant';\n }\n return this.context.sharedSchema ?? 'public';\n }\n\n /**\n * Get fully qualified table name\n */\n private getFullTableName(schemaName: string, table: Table): string {\n const tableName = getTableName(table);\n return `\"${schemaName}\".\"${tableName}\"`;\n }\n\n /**\n * Get SQL keyword for join type\n */\n private getJoinKeyword(type: JoinType): string {\n switch (type) {\n case 'inner':\n return 'INNER JOIN';\n case 'left':\n return 'LEFT JOIN';\n case 'right':\n return 'RIGHT JOIN';\n case 'full':\n return 'FULL OUTER JOIN';\n }\n }\n}\n\n/**\n * Create a cross-schema query builder\n */\nexport function createCrossSchemaQuery<\n TTenantSchema extends Record<string, unknown> = Record<string, unknown>,\n TSharedSchema extends Record<string, unknown> = Record<string, unknown>,\n>(\n context: CrossSchemaContext<TTenantSchema, TSharedSchema>\n): CrossSchemaQueryBuilder<TTenantSchema, TSharedSchema> {\n return new CrossSchemaQueryBuilder(context);\n}\n\n/**\n * Helper for common pattern: tenant table with shared lookup\n *\n * @example\n * ```typescript\n * const ordersWithPlans = await withSharedLookup({\n * tenantDb,\n * sharedDb,\n * tenantTable: orders,\n * sharedTable: subscriptionPlans,\n * foreignKey: 'planId',\n * sharedFields: ['name', 'features', 'price'],\n * });\n * ```\n */\nexport async function withSharedLookup<\n TTenantTable extends Table,\n TSharedTable extends Table,\n TSharedFields extends keyof TSharedTable['_']['columns'],\n>(config: SharedLookupConfig<TTenantTable, TSharedTable, TSharedFields>): Promise<Array<Record<string, unknown>>> {\n const {\n tenantDb,\n tenantTable,\n sharedTable,\n foreignKey,\n sharedKey = 'id' as keyof TSharedTable['_']['columns'],\n sharedFields,\n where: whereCondition,\n } = config;\n\n // Get table names using Drizzle's utility\n const tenantTableName = getTableName(tenantTable);\n const sharedTableName = getTableName(sharedTable);\n\n // Build field list for shared table\n const sharedFieldList = sharedFields\n .map((field) => `s.\"${String(field)}\"`)\n .join(', ');\n\n // Build the query\n const queryParts = [\n `SELECT t.*, ${sharedFieldList}`,\n `FROM \"${tenantTableName}\" t`,\n `LEFT JOIN \"public\".\"${sharedTableName}\" s ON t.\"${String(foreignKey)}\" = s.\"${String(sharedKey)}\"`,\n ];\n\n if (whereCondition) {\n queryParts.push('WHERE');\n // We'll need to use raw SQL for the where condition\n }\n\n const sqlQuery = sql.raw(queryParts.join(' '));\n\n const result = await tenantDb.execute(sqlQuery);\n\n return result.rows as Array<Record<string, unknown>>;\n}\n\n/**\n * Execute raw cross-schema SQL with type safety\n *\n * @example\n * ```typescript\n * const result = await crossSchemaRaw<{ userName: string; planName: string }>({\n * tenantSchema: 'tenant_abc123',\n * sharedSchema: 'public',\n * sql: `\n * SELECT u.name as \"userName\", p.name as \"planName\"\n * FROM $tenant.users u\n * JOIN $shared.plans p ON u.plan_id = p.id\n * `,\n * });\n * ```\n */\nexport async function crossSchemaRaw<TResult = Record<string, unknown>>(\n db: NodePgDatabase<Record<string, unknown>>,\n options: CrossSchemaRawOptions\n): Promise<TResult[]> {\n const { tenantSchema, sharedSchema, sql: rawSql } = options;\n\n // Replace $tenant and $shared placeholders\n const processedSql = rawSql\n .replace(/\\$tenant\\./g, `\"${tenantSchema}\".`)\n .replace(/\\$shared\\./g, `\"${sharedSchema}\".`);\n\n const query = sql.raw(processedSql);\n\n const result = await db.execute(query);\n\n return result.rows as TResult[];\n}\n\n/**\n * Create a typed cross-schema query using SQL template\n *\n * @example\n * ```typescript\n * const users = await crossSchemaSelect(tenantDb, {\n * tenantSchema: 'tenant_abc',\n * sharedSchema: 'public',\n * select: {\n * id: users.id,\n * name: users.name,\n * planName: plans.name,\n * },\n * from: { table: users, schema: 'tenant' },\n * joins: [\n * { table: plans, schema: 'shared', on: eq(users.planId, plans.id), type: 'left' },\n * ],\n * });\n * ```\n */\nexport function buildCrossSchemaSelect<T extends Record<string, Column>>(\n fields: T,\n tenantSchema: string,\n _sharedSchema: string\n): { columns: string[]; getSchema: () => string } {\n const columns = Object.entries(fields).map(([alias, column]) => {\n const columnName = column.name;\n return `\"${columnName}\" as \"${alias}\"`;\n });\n\n const getSchema = (): string => {\n // This would need more context to determine which schema a column belongs to\n // For now, return tenant schema as default\n return tenantSchema;\n };\n\n return { columns, getSchema };\n}\n"]}
@@ -0,0 +1,71 @@
1
+ import { C as Config, T as TenantManager } from './types-DKVaTaIb.js';
2
+ export { b as ConnectionConfig, D as DEFAULT_CONFIG, H as Hooks, I as IsolationConfig, c as IsolationStrategy, M as MetricsConfig, P as PoolEntry, d as SchemasConfig, S as SharedDb, a as TenantDb } from './types-DKVaTaIb.js';
3
+ export { B as BaseTenantContext, a as TenantContext, T as TenantContextData, c as createTenantContext } from './context-DBerWr50.js';
4
+ export { AppliedMigration, CreateTenantOptions, DropTenantOptions, MigrateOptions, MigrationErrorHandler, MigrationFile, MigrationHooks, MigrationProgressCallback, MigrationResults, Migrator, MigratorConfig, TenantMigrationResult, TenantMigrationStatus, createMigrator } from './migrator/index.js';
5
+ export { ColumnSelection, CrossSchemaContext, CrossSchemaQueryBuilder, CrossSchemaRawOptions, InferSelectedColumns, JoinCondition, JoinDefinition, JoinType, LookupResult, SchemaSource, SharedLookupConfig, TableReference, buildCrossSchemaSelect, createCrossSchemaQuery, crossSchemaRaw, withSharedLookup } from './cross-schema/index.js';
6
+ import 'pg';
7
+ import 'drizzle-orm/node-postgres';
8
+ import 'drizzle-orm';
9
+
10
+ /**
11
+ * Define configuration for drizzle-multitenant
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * import { defineConfig } from 'drizzle-multitenant';
16
+ * import * as tenantSchema from './schemas/tenant';
17
+ * import * as sharedSchema from './schemas/shared';
18
+ *
19
+ * export default defineConfig({
20
+ * connection: {
21
+ * url: process.env.DATABASE_URL!,
22
+ * poolConfig: {
23
+ * max: 10,
24
+ * idleTimeoutMillis: 30000,
25
+ * },
26
+ * },
27
+ * isolation: {
28
+ * strategy: 'schema',
29
+ * schemaNameTemplate: (tenantId) => `tenant_${tenantId.replace(/-/g, '_')}`,
30
+ * maxPools: 50,
31
+ * poolTtlMs: 60 * 60 * 1000,
32
+ * },
33
+ * schemas: {
34
+ * tenant: tenantSchema,
35
+ * shared: sharedSchema,
36
+ * },
37
+ * });
38
+ * ```
39
+ */
40
+ declare function defineConfig<TTenantSchema extends Record<string, unknown>, TSharedSchema extends Record<string, unknown> = Record<string, unknown>>(config: Config<TTenantSchema, TSharedSchema>): Config<TTenantSchema, TSharedSchema>;
41
+
42
+ /**
43
+ * Create a tenant manager instance
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * import { createTenantManager, defineConfig } from 'drizzle-multitenant';
48
+ *
49
+ * const config = defineConfig({
50
+ * connection: { url: process.env.DATABASE_URL! },
51
+ * isolation: {
52
+ * strategy: 'schema',
53
+ * schemaNameTemplate: (id) => `tenant_${id}`,
54
+ * },
55
+ * schemas: { tenant: tenantSchema },
56
+ * });
57
+ *
58
+ * const tenants = createTenantManager(config);
59
+ *
60
+ * // Get database for a specific tenant
61
+ * const db = tenants.getDb('tenant-uuid');
62
+ * const users = await db.select().from(schema.users);
63
+ *
64
+ * // Get shared database
65
+ * const sharedDb = tenants.getSharedDb();
66
+ * const plans = await sharedDb.select().from(sharedSchema.plans);
67
+ * ```
68
+ */
69
+ declare function createTenantManager<TTenantSchema extends Record<string, unknown>, TSharedSchema extends Record<string, unknown> = Record<string, unknown>>(config: Config<TTenantSchema, TSharedSchema>): TenantManager<TTenantSchema, TSharedSchema>;
70
+
71
+ export { Config, TenantManager, createTenantManager, defineConfig };