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.
- package/.claude/settings.local.json +14 -0
- package/LICENSE +21 -0
- package/README.md +262 -0
- package/bin/drizzle-multitenant.js +2 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +866 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/context-DBerWr50.d.ts +76 -0
- package/dist/cross-schema/index.d.ts +262 -0
- package/dist/cross-schema/index.js +219 -0
- package/dist/cross-schema/index.js.map +1 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +935 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/express.d.ts +93 -0
- package/dist/integrations/express.js +110 -0
- package/dist/integrations/express.js.map +1 -0
- package/dist/integrations/fastify.d.ts +92 -0
- package/dist/integrations/fastify.js +236 -0
- package/dist/integrations/fastify.js.map +1 -0
- package/dist/integrations/hono.d.ts +2 -0
- package/dist/integrations/hono.js +3 -0
- package/dist/integrations/hono.js.map +1 -0
- package/dist/integrations/nestjs/index.d.ts +386 -0
- package/dist/integrations/nestjs/index.js +9828 -0
- package/dist/integrations/nestjs/index.js.map +1 -0
- package/dist/migrator/index.d.ts +208 -0
- package/dist/migrator/index.js +390 -0
- package/dist/migrator/index.js.map +1 -0
- package/dist/types-DKVaTaIb.d.ts +130 -0
- package/package.json +102 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,935 @@
|
|
|
1
|
+
import { Pool } from 'pg';
|
|
2
|
+
import { drizzle } from 'drizzle-orm/node-postgres';
|
|
3
|
+
import { LRUCache } from 'lru-cache';
|
|
4
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
5
|
+
import { readdir, readFile } from 'fs/promises';
|
|
6
|
+
import { join, basename } from 'path';
|
|
7
|
+
import { sql, getTableName } from 'drizzle-orm';
|
|
8
|
+
|
|
9
|
+
// src/config.ts
|
|
10
|
+
function defineConfig(config) {
|
|
11
|
+
validateConfig(config);
|
|
12
|
+
return config;
|
|
13
|
+
}
|
|
14
|
+
function validateConfig(config) {
|
|
15
|
+
if (!config.connection.url) {
|
|
16
|
+
throw new Error("[drizzle-multitenant] connection.url is required");
|
|
17
|
+
}
|
|
18
|
+
if (!config.isolation.strategy) {
|
|
19
|
+
throw new Error("[drizzle-multitenant] isolation.strategy is required");
|
|
20
|
+
}
|
|
21
|
+
if (config.isolation.strategy !== "schema") {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`[drizzle-multitenant] isolation.strategy "${config.isolation.strategy}" is not yet supported. Only "schema" is currently available.`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
if (!config.isolation.schemaNameTemplate) {
|
|
27
|
+
throw new Error("[drizzle-multitenant] isolation.schemaNameTemplate is required");
|
|
28
|
+
}
|
|
29
|
+
if (typeof config.isolation.schemaNameTemplate !== "function") {
|
|
30
|
+
throw new Error("[drizzle-multitenant] isolation.schemaNameTemplate must be a function");
|
|
31
|
+
}
|
|
32
|
+
if (!config.schemas.tenant) {
|
|
33
|
+
throw new Error("[drizzle-multitenant] schemas.tenant is required");
|
|
34
|
+
}
|
|
35
|
+
if (config.isolation.maxPools !== void 0 && config.isolation.maxPools < 1) {
|
|
36
|
+
throw new Error("[drizzle-multitenant] isolation.maxPools must be at least 1");
|
|
37
|
+
}
|
|
38
|
+
if (config.isolation.poolTtlMs !== void 0 && config.isolation.poolTtlMs < 0) {
|
|
39
|
+
throw new Error("[drizzle-multitenant] isolation.poolTtlMs must be non-negative");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/types.ts
|
|
44
|
+
var DEFAULT_CONFIG = {
|
|
45
|
+
maxPools: 50,
|
|
46
|
+
poolTtlMs: 60 * 60 * 1e3,
|
|
47
|
+
// 1 hour
|
|
48
|
+
cleanupIntervalMs: 6e4,
|
|
49
|
+
// 1 minute
|
|
50
|
+
poolConfig: {
|
|
51
|
+
max: 10,
|
|
52
|
+
idleTimeoutMillis: 3e4,
|
|
53
|
+
connectionTimeoutMillis: 5e3
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// src/pool.ts
|
|
58
|
+
var PoolManager = class {
|
|
59
|
+
constructor(config) {
|
|
60
|
+
this.config = config;
|
|
61
|
+
const maxPools = config.isolation.maxPools ?? DEFAULT_CONFIG.maxPools;
|
|
62
|
+
this.pools = new LRUCache({
|
|
63
|
+
max: maxPools,
|
|
64
|
+
dispose: (entry, key) => {
|
|
65
|
+
this.disposePoolEntry(entry, key);
|
|
66
|
+
},
|
|
67
|
+
noDisposeOnSet: true
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
pools;
|
|
71
|
+
tenantIdBySchema = /* @__PURE__ */ new Map();
|
|
72
|
+
sharedPool = null;
|
|
73
|
+
sharedDb = null;
|
|
74
|
+
cleanupInterval = null;
|
|
75
|
+
disposed = false;
|
|
76
|
+
/**
|
|
77
|
+
* Get or create a database connection for a tenant
|
|
78
|
+
*/
|
|
79
|
+
getDb(tenantId) {
|
|
80
|
+
this.ensureNotDisposed();
|
|
81
|
+
const schemaName = this.config.isolation.schemaNameTemplate(tenantId);
|
|
82
|
+
let entry = this.pools.get(schemaName);
|
|
83
|
+
if (!entry) {
|
|
84
|
+
entry = this.createPoolEntry(tenantId, schemaName);
|
|
85
|
+
this.pools.set(schemaName, entry);
|
|
86
|
+
this.tenantIdBySchema.set(schemaName, tenantId);
|
|
87
|
+
void this.config.hooks?.onPoolCreated?.(tenantId);
|
|
88
|
+
}
|
|
89
|
+
entry.lastAccess = Date.now();
|
|
90
|
+
return entry.db;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get or create the shared database connection
|
|
94
|
+
*/
|
|
95
|
+
getSharedDb() {
|
|
96
|
+
this.ensureNotDisposed();
|
|
97
|
+
if (!this.sharedDb) {
|
|
98
|
+
this.sharedPool = new Pool({
|
|
99
|
+
connectionString: this.config.connection.url,
|
|
100
|
+
...DEFAULT_CONFIG.poolConfig,
|
|
101
|
+
...this.config.connection.poolConfig
|
|
102
|
+
});
|
|
103
|
+
this.sharedPool.on("error", (err) => {
|
|
104
|
+
void this.config.hooks?.onError?.("shared", err);
|
|
105
|
+
});
|
|
106
|
+
this.sharedDb = drizzle(this.sharedPool, {
|
|
107
|
+
schema: this.config.schemas.shared
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return this.sharedDb;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Get schema name for a tenant
|
|
114
|
+
*/
|
|
115
|
+
getSchemaName(tenantId) {
|
|
116
|
+
return this.config.isolation.schemaNameTemplate(tenantId);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Check if a pool exists for a tenant
|
|
120
|
+
*/
|
|
121
|
+
hasPool(tenantId) {
|
|
122
|
+
const schemaName = this.config.isolation.schemaNameTemplate(tenantId);
|
|
123
|
+
return this.pools.has(schemaName);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Get count of active pools
|
|
127
|
+
*/
|
|
128
|
+
getPoolCount() {
|
|
129
|
+
return this.pools.size;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Get all active tenant IDs
|
|
133
|
+
*/
|
|
134
|
+
getActiveTenantIds() {
|
|
135
|
+
return Array.from(this.tenantIdBySchema.values());
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Manually evict a tenant pool
|
|
139
|
+
*/
|
|
140
|
+
async evictPool(tenantId) {
|
|
141
|
+
const schemaName = this.config.isolation.schemaNameTemplate(tenantId);
|
|
142
|
+
const entry = this.pools.get(schemaName);
|
|
143
|
+
if (entry) {
|
|
144
|
+
this.pools.delete(schemaName);
|
|
145
|
+
this.tenantIdBySchema.delete(schemaName);
|
|
146
|
+
await this.closePool(entry.pool, tenantId);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Start automatic cleanup of idle pools
|
|
151
|
+
*/
|
|
152
|
+
startCleanup() {
|
|
153
|
+
if (this.cleanupInterval) return;
|
|
154
|
+
const poolTtlMs = this.config.isolation.poolTtlMs ?? DEFAULT_CONFIG.poolTtlMs;
|
|
155
|
+
const cleanupIntervalMs = DEFAULT_CONFIG.cleanupIntervalMs;
|
|
156
|
+
this.cleanupInterval = setInterval(() => {
|
|
157
|
+
void this.cleanupIdlePools(poolTtlMs);
|
|
158
|
+
}, cleanupIntervalMs);
|
|
159
|
+
this.cleanupInterval.unref();
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Stop automatic cleanup
|
|
163
|
+
*/
|
|
164
|
+
stopCleanup() {
|
|
165
|
+
if (this.cleanupInterval) {
|
|
166
|
+
clearInterval(this.cleanupInterval);
|
|
167
|
+
this.cleanupInterval = null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Dispose all pools and cleanup resources
|
|
172
|
+
*/
|
|
173
|
+
async dispose() {
|
|
174
|
+
if (this.disposed) return;
|
|
175
|
+
this.disposed = true;
|
|
176
|
+
this.stopCleanup();
|
|
177
|
+
const closePromises = [];
|
|
178
|
+
for (const [schemaName, entry] of this.pools.entries()) {
|
|
179
|
+
const tenantId = this.tenantIdBySchema.get(schemaName);
|
|
180
|
+
closePromises.push(this.closePool(entry.pool, tenantId ?? schemaName));
|
|
181
|
+
}
|
|
182
|
+
this.pools.clear();
|
|
183
|
+
this.tenantIdBySchema.clear();
|
|
184
|
+
if (this.sharedPool) {
|
|
185
|
+
closePromises.push(this.closePool(this.sharedPool, "shared"));
|
|
186
|
+
this.sharedPool = null;
|
|
187
|
+
this.sharedDb = null;
|
|
188
|
+
}
|
|
189
|
+
await Promise.all(closePromises);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Create a new pool entry for a tenant
|
|
193
|
+
*/
|
|
194
|
+
createPoolEntry(tenantId, schemaName) {
|
|
195
|
+
const pool = new Pool({
|
|
196
|
+
connectionString: this.config.connection.url,
|
|
197
|
+
...DEFAULT_CONFIG.poolConfig,
|
|
198
|
+
...this.config.connection.poolConfig,
|
|
199
|
+
options: `-c search_path=${schemaName},public`
|
|
200
|
+
});
|
|
201
|
+
pool.on("error", async (err) => {
|
|
202
|
+
void this.config.hooks?.onError?.(tenantId, err);
|
|
203
|
+
await this.evictPool(tenantId);
|
|
204
|
+
});
|
|
205
|
+
const db = drizzle(pool, {
|
|
206
|
+
schema: this.config.schemas.tenant
|
|
207
|
+
});
|
|
208
|
+
return {
|
|
209
|
+
db,
|
|
210
|
+
pool,
|
|
211
|
+
lastAccess: Date.now(),
|
|
212
|
+
schemaName
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Dispose a pool entry (called by LRU cache)
|
|
217
|
+
*/
|
|
218
|
+
disposePoolEntry(entry, schemaName) {
|
|
219
|
+
const tenantId = this.tenantIdBySchema.get(schemaName);
|
|
220
|
+
this.tenantIdBySchema.delete(schemaName);
|
|
221
|
+
void this.closePool(entry.pool, tenantId ?? schemaName).then(() => {
|
|
222
|
+
if (tenantId) {
|
|
223
|
+
void this.config.hooks?.onPoolEvicted?.(tenantId);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Close a pool gracefully
|
|
229
|
+
*/
|
|
230
|
+
async closePool(pool, identifier) {
|
|
231
|
+
try {
|
|
232
|
+
await pool.end();
|
|
233
|
+
} catch (error) {
|
|
234
|
+
void this.config.hooks?.onError?.(identifier, error);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Cleanup pools that have been idle for too long
|
|
239
|
+
*/
|
|
240
|
+
async cleanupIdlePools(poolTtlMs) {
|
|
241
|
+
const now = Date.now();
|
|
242
|
+
const toEvict = [];
|
|
243
|
+
for (const [schemaName, entry] of this.pools.entries()) {
|
|
244
|
+
if (now - entry.lastAccess > poolTtlMs) {
|
|
245
|
+
toEvict.push(schemaName);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
for (const schemaName of toEvict) {
|
|
249
|
+
const tenantId = this.tenantIdBySchema.get(schemaName);
|
|
250
|
+
if (tenantId) {
|
|
251
|
+
await this.evictPool(tenantId);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Ensure the manager hasn't been disposed
|
|
257
|
+
*/
|
|
258
|
+
ensureNotDisposed() {
|
|
259
|
+
if (this.disposed) {
|
|
260
|
+
throw new Error("[drizzle-multitenant] TenantManager has been disposed");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// src/manager.ts
|
|
266
|
+
function createTenantManager(config) {
|
|
267
|
+
const poolManager = new PoolManager(config);
|
|
268
|
+
poolManager.startCleanup();
|
|
269
|
+
return {
|
|
270
|
+
getDb(tenantId) {
|
|
271
|
+
return poolManager.getDb(tenantId);
|
|
272
|
+
},
|
|
273
|
+
getSharedDb() {
|
|
274
|
+
return poolManager.getSharedDb();
|
|
275
|
+
},
|
|
276
|
+
getSchemaName(tenantId) {
|
|
277
|
+
return poolManager.getSchemaName(tenantId);
|
|
278
|
+
},
|
|
279
|
+
hasPool(tenantId) {
|
|
280
|
+
return poolManager.hasPool(tenantId);
|
|
281
|
+
},
|
|
282
|
+
getPoolCount() {
|
|
283
|
+
return poolManager.getPoolCount();
|
|
284
|
+
},
|
|
285
|
+
getActiveTenantIds() {
|
|
286
|
+
return poolManager.getActiveTenantIds();
|
|
287
|
+
},
|
|
288
|
+
async evictPool(tenantId) {
|
|
289
|
+
await poolManager.evictPool(tenantId);
|
|
290
|
+
},
|
|
291
|
+
async dispose() {
|
|
292
|
+
await poolManager.dispose();
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function createTenantContext(manager) {
|
|
297
|
+
const storage = new AsyncLocalStorage();
|
|
298
|
+
function getTenantOrNull() {
|
|
299
|
+
return storage.getStore();
|
|
300
|
+
}
|
|
301
|
+
function getTenant() {
|
|
302
|
+
const context = getTenantOrNull();
|
|
303
|
+
if (!context) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
"[drizzle-multitenant] No tenant context found. Make sure you are calling this within runWithTenant()."
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
return context;
|
|
309
|
+
}
|
|
310
|
+
function getTenantId() {
|
|
311
|
+
return getTenant().tenantId;
|
|
312
|
+
}
|
|
313
|
+
function getTenantDb() {
|
|
314
|
+
const tenantId = getTenantId();
|
|
315
|
+
return manager.getDb(tenantId);
|
|
316
|
+
}
|
|
317
|
+
function getSharedDb() {
|
|
318
|
+
return manager.getSharedDb();
|
|
319
|
+
}
|
|
320
|
+
function isInTenantContext() {
|
|
321
|
+
return getTenantOrNull() !== void 0;
|
|
322
|
+
}
|
|
323
|
+
function runWithTenant(context, callback) {
|
|
324
|
+
if (!context.tenantId) {
|
|
325
|
+
throw new Error("[drizzle-multitenant] tenantId is required in context");
|
|
326
|
+
}
|
|
327
|
+
return storage.run(context, callback);
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
runWithTenant,
|
|
331
|
+
getTenant,
|
|
332
|
+
getTenantOrNull,
|
|
333
|
+
getTenantId,
|
|
334
|
+
getTenantDb,
|
|
335
|
+
getSharedDb,
|
|
336
|
+
isInTenantContext
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
var DEFAULT_MIGRATIONS_TABLE = "__drizzle_migrations";
|
|
340
|
+
var Migrator = class {
|
|
341
|
+
constructor(tenantConfig, migratorConfig) {
|
|
342
|
+
this.tenantConfig = tenantConfig;
|
|
343
|
+
this.migratorConfig = migratorConfig;
|
|
344
|
+
this.migrationsTable = migratorConfig.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE;
|
|
345
|
+
}
|
|
346
|
+
migrationsTable;
|
|
347
|
+
/**
|
|
348
|
+
* Migrate all tenants in parallel
|
|
349
|
+
*/
|
|
350
|
+
async migrateAll(options = {}) {
|
|
351
|
+
const {
|
|
352
|
+
concurrency = 10,
|
|
353
|
+
onProgress,
|
|
354
|
+
onError,
|
|
355
|
+
dryRun = false
|
|
356
|
+
} = options;
|
|
357
|
+
const tenantIds = await this.migratorConfig.tenantDiscovery();
|
|
358
|
+
const migrations = await this.loadMigrations();
|
|
359
|
+
const results = [];
|
|
360
|
+
let aborted = false;
|
|
361
|
+
for (let i = 0; i < tenantIds.length && !aborted; i += concurrency) {
|
|
362
|
+
const batch = tenantIds.slice(i, i + concurrency);
|
|
363
|
+
const batchResults = await Promise.all(
|
|
364
|
+
batch.map(async (tenantId) => {
|
|
365
|
+
if (aborted) {
|
|
366
|
+
return this.createSkippedResult(tenantId);
|
|
367
|
+
}
|
|
368
|
+
try {
|
|
369
|
+
onProgress?.(tenantId, "starting");
|
|
370
|
+
const result = await this.migrateTenant(tenantId, migrations, { dryRun, onProgress });
|
|
371
|
+
onProgress?.(tenantId, result.success ? "completed" : "failed");
|
|
372
|
+
return result;
|
|
373
|
+
} catch (error) {
|
|
374
|
+
onProgress?.(tenantId, "failed");
|
|
375
|
+
const action = onError?.(tenantId, error);
|
|
376
|
+
if (action === "abort") {
|
|
377
|
+
aborted = true;
|
|
378
|
+
}
|
|
379
|
+
return this.createErrorResult(tenantId, error);
|
|
380
|
+
}
|
|
381
|
+
})
|
|
382
|
+
);
|
|
383
|
+
results.push(...batchResults);
|
|
384
|
+
}
|
|
385
|
+
if (aborted) {
|
|
386
|
+
const remaining = tenantIds.slice(results.length);
|
|
387
|
+
for (const tenantId of remaining) {
|
|
388
|
+
results.push(this.createSkippedResult(tenantId));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return this.aggregateResults(results);
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Migrate a single tenant
|
|
395
|
+
*/
|
|
396
|
+
async migrateTenant(tenantId, migrations, options = {}) {
|
|
397
|
+
const startTime = Date.now();
|
|
398
|
+
const schemaName = this.tenantConfig.isolation.schemaNameTemplate(tenantId);
|
|
399
|
+
const appliedMigrations = [];
|
|
400
|
+
const pool = await this.createPool(schemaName);
|
|
401
|
+
try {
|
|
402
|
+
await this.migratorConfig.hooks?.beforeTenant?.(tenantId);
|
|
403
|
+
await this.ensureMigrationsTable(pool, schemaName);
|
|
404
|
+
const allMigrations = migrations ?? await this.loadMigrations();
|
|
405
|
+
const applied = await this.getAppliedMigrations(pool, schemaName);
|
|
406
|
+
const appliedSet = new Set(applied.map((m) => m.name));
|
|
407
|
+
const pending = allMigrations.filter((m) => !appliedSet.has(m.name));
|
|
408
|
+
if (options.dryRun) {
|
|
409
|
+
return {
|
|
410
|
+
tenantId,
|
|
411
|
+
schemaName,
|
|
412
|
+
success: true,
|
|
413
|
+
appliedMigrations: pending.map((m) => m.name),
|
|
414
|
+
durationMs: Date.now() - startTime
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
for (const migration of pending) {
|
|
418
|
+
const migrationStart = Date.now();
|
|
419
|
+
options.onProgress?.(tenantId, "migrating", migration.name);
|
|
420
|
+
await this.migratorConfig.hooks?.beforeMigration?.(tenantId, migration.name);
|
|
421
|
+
await this.applyMigration(pool, schemaName, migration);
|
|
422
|
+
await this.migratorConfig.hooks?.afterMigration?.(
|
|
423
|
+
tenantId,
|
|
424
|
+
migration.name,
|
|
425
|
+
Date.now() - migrationStart
|
|
426
|
+
);
|
|
427
|
+
appliedMigrations.push(migration.name);
|
|
428
|
+
}
|
|
429
|
+
const result = {
|
|
430
|
+
tenantId,
|
|
431
|
+
schemaName,
|
|
432
|
+
success: true,
|
|
433
|
+
appliedMigrations,
|
|
434
|
+
durationMs: Date.now() - startTime
|
|
435
|
+
};
|
|
436
|
+
await this.migratorConfig.hooks?.afterTenant?.(tenantId, result);
|
|
437
|
+
return result;
|
|
438
|
+
} catch (error) {
|
|
439
|
+
const result = {
|
|
440
|
+
tenantId,
|
|
441
|
+
schemaName,
|
|
442
|
+
success: false,
|
|
443
|
+
appliedMigrations,
|
|
444
|
+
error: error.message,
|
|
445
|
+
durationMs: Date.now() - startTime
|
|
446
|
+
};
|
|
447
|
+
await this.migratorConfig.hooks?.afterTenant?.(tenantId, result);
|
|
448
|
+
return result;
|
|
449
|
+
} finally {
|
|
450
|
+
await pool.end();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Migrate specific tenants
|
|
455
|
+
*/
|
|
456
|
+
async migrateTenants(tenantIds, options = {}) {
|
|
457
|
+
const migrations = await this.loadMigrations();
|
|
458
|
+
const results = [];
|
|
459
|
+
const { concurrency = 10, onProgress, onError } = options;
|
|
460
|
+
for (let i = 0; i < tenantIds.length; i += concurrency) {
|
|
461
|
+
const batch = tenantIds.slice(i, i + concurrency);
|
|
462
|
+
const batchResults = await Promise.all(
|
|
463
|
+
batch.map(async (tenantId) => {
|
|
464
|
+
try {
|
|
465
|
+
onProgress?.(tenantId, "starting");
|
|
466
|
+
const result = await this.migrateTenant(tenantId, migrations, { dryRun: options.dryRun ?? false, onProgress });
|
|
467
|
+
onProgress?.(tenantId, result.success ? "completed" : "failed");
|
|
468
|
+
return result;
|
|
469
|
+
} catch (error) {
|
|
470
|
+
onProgress?.(tenantId, "failed");
|
|
471
|
+
onError?.(tenantId, error);
|
|
472
|
+
return this.createErrorResult(tenantId, error);
|
|
473
|
+
}
|
|
474
|
+
})
|
|
475
|
+
);
|
|
476
|
+
results.push(...batchResults);
|
|
477
|
+
}
|
|
478
|
+
return this.aggregateResults(results);
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Get migration status for all tenants
|
|
482
|
+
*/
|
|
483
|
+
async getStatus() {
|
|
484
|
+
const tenantIds = await this.migratorConfig.tenantDiscovery();
|
|
485
|
+
const migrations = await this.loadMigrations();
|
|
486
|
+
const statuses = [];
|
|
487
|
+
for (const tenantId of tenantIds) {
|
|
488
|
+
statuses.push(await this.getTenantStatus(tenantId, migrations));
|
|
489
|
+
}
|
|
490
|
+
return statuses;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Get migration status for a specific tenant
|
|
494
|
+
*/
|
|
495
|
+
async getTenantStatus(tenantId, migrations) {
|
|
496
|
+
const schemaName = this.tenantConfig.isolation.schemaNameTemplate(tenantId);
|
|
497
|
+
const pool = await this.createPool(schemaName);
|
|
498
|
+
try {
|
|
499
|
+
const allMigrations = migrations ?? await this.loadMigrations();
|
|
500
|
+
const tableExists = await this.migrationsTableExists(pool, schemaName);
|
|
501
|
+
if (!tableExists) {
|
|
502
|
+
return {
|
|
503
|
+
tenantId,
|
|
504
|
+
schemaName,
|
|
505
|
+
appliedCount: 0,
|
|
506
|
+
pendingCount: allMigrations.length,
|
|
507
|
+
pendingMigrations: allMigrations.map((m) => m.name),
|
|
508
|
+
status: allMigrations.length > 0 ? "behind" : "ok"
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
const applied = await this.getAppliedMigrations(pool, schemaName);
|
|
512
|
+
const appliedSet = new Set(applied.map((m) => m.name));
|
|
513
|
+
const pending = allMigrations.filter((m) => !appliedSet.has(m.name));
|
|
514
|
+
return {
|
|
515
|
+
tenantId,
|
|
516
|
+
schemaName,
|
|
517
|
+
appliedCount: applied.length,
|
|
518
|
+
pendingCount: pending.length,
|
|
519
|
+
pendingMigrations: pending.map((m) => m.name),
|
|
520
|
+
status: pending.length > 0 ? "behind" : "ok"
|
|
521
|
+
};
|
|
522
|
+
} catch (error) {
|
|
523
|
+
return {
|
|
524
|
+
tenantId,
|
|
525
|
+
schemaName,
|
|
526
|
+
appliedCount: 0,
|
|
527
|
+
pendingCount: 0,
|
|
528
|
+
pendingMigrations: [],
|
|
529
|
+
status: "error",
|
|
530
|
+
error: error.message
|
|
531
|
+
};
|
|
532
|
+
} finally {
|
|
533
|
+
await pool.end();
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Create a new tenant schema and optionally apply migrations
|
|
538
|
+
*/
|
|
539
|
+
async createTenant(tenantId, options = {}) {
|
|
540
|
+
const { migrate = true } = options;
|
|
541
|
+
const schemaName = this.tenantConfig.isolation.schemaNameTemplate(tenantId);
|
|
542
|
+
const pool = new Pool({
|
|
543
|
+
connectionString: this.tenantConfig.connection.url,
|
|
544
|
+
...this.tenantConfig.connection.poolConfig
|
|
545
|
+
});
|
|
546
|
+
try {
|
|
547
|
+
await pool.query(`CREATE SCHEMA IF NOT EXISTS "${schemaName}"`);
|
|
548
|
+
if (migrate) {
|
|
549
|
+
await this.migrateTenant(tenantId);
|
|
550
|
+
}
|
|
551
|
+
} finally {
|
|
552
|
+
await pool.end();
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Drop a tenant schema
|
|
557
|
+
*/
|
|
558
|
+
async dropTenant(tenantId, options = {}) {
|
|
559
|
+
const { cascade = true } = options;
|
|
560
|
+
const schemaName = this.tenantConfig.isolation.schemaNameTemplate(tenantId);
|
|
561
|
+
const pool = new Pool({
|
|
562
|
+
connectionString: this.tenantConfig.connection.url,
|
|
563
|
+
...this.tenantConfig.connection.poolConfig
|
|
564
|
+
});
|
|
565
|
+
try {
|
|
566
|
+
const cascadeSql = cascade ? "CASCADE" : "RESTRICT";
|
|
567
|
+
await pool.query(`DROP SCHEMA IF EXISTS "${schemaName}" ${cascadeSql}`);
|
|
568
|
+
} finally {
|
|
569
|
+
await pool.end();
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Check if a tenant schema exists
|
|
574
|
+
*/
|
|
575
|
+
async tenantExists(tenantId) {
|
|
576
|
+
const schemaName = this.tenantConfig.isolation.schemaNameTemplate(tenantId);
|
|
577
|
+
const pool = new Pool({
|
|
578
|
+
connectionString: this.tenantConfig.connection.url,
|
|
579
|
+
...this.tenantConfig.connection.poolConfig
|
|
580
|
+
});
|
|
581
|
+
try {
|
|
582
|
+
const result = await pool.query(
|
|
583
|
+
`SELECT 1 FROM information_schema.schemata WHERE schema_name = $1`,
|
|
584
|
+
[schemaName]
|
|
585
|
+
);
|
|
586
|
+
return result.rowCount !== null && result.rowCount > 0;
|
|
587
|
+
} finally {
|
|
588
|
+
await pool.end();
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Load migration files from the migrations folder
|
|
593
|
+
*/
|
|
594
|
+
async loadMigrations() {
|
|
595
|
+
const files = await readdir(this.migratorConfig.migrationsFolder);
|
|
596
|
+
const migrations = [];
|
|
597
|
+
for (const file of files) {
|
|
598
|
+
if (!file.endsWith(".sql")) continue;
|
|
599
|
+
const filePath = join(this.migratorConfig.migrationsFolder, file);
|
|
600
|
+
const content = await readFile(filePath, "utf-8");
|
|
601
|
+
const match = file.match(/^(\d+)_/);
|
|
602
|
+
const timestamp = match?.[1] ? parseInt(match[1], 10) : 0;
|
|
603
|
+
migrations.push({
|
|
604
|
+
name: basename(file, ".sql"),
|
|
605
|
+
path: filePath,
|
|
606
|
+
sql: content,
|
|
607
|
+
timestamp
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
return migrations.sort((a, b) => a.timestamp - b.timestamp);
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Create a pool for a specific schema
|
|
614
|
+
*/
|
|
615
|
+
async createPool(schemaName) {
|
|
616
|
+
return new Pool({
|
|
617
|
+
connectionString: this.tenantConfig.connection.url,
|
|
618
|
+
...this.tenantConfig.connection.poolConfig,
|
|
619
|
+
options: `-c search_path="${schemaName}",public`
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Ensure migrations table exists
|
|
624
|
+
*/
|
|
625
|
+
async ensureMigrationsTable(pool, schemaName) {
|
|
626
|
+
await pool.query(`
|
|
627
|
+
CREATE TABLE IF NOT EXISTS "${schemaName}"."${this.migrationsTable}" (
|
|
628
|
+
id SERIAL PRIMARY KEY,
|
|
629
|
+
name VARCHAR(255) NOT NULL UNIQUE,
|
|
630
|
+
applied_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
631
|
+
)
|
|
632
|
+
`);
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Check if migrations table exists
|
|
636
|
+
*/
|
|
637
|
+
async migrationsTableExists(pool, schemaName) {
|
|
638
|
+
const result = await pool.query(
|
|
639
|
+
`SELECT 1 FROM information_schema.tables
|
|
640
|
+
WHERE table_schema = $1 AND table_name = $2`,
|
|
641
|
+
[schemaName, this.migrationsTable]
|
|
642
|
+
);
|
|
643
|
+
return result.rowCount !== null && result.rowCount > 0;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Get applied migrations for a schema
|
|
647
|
+
*/
|
|
648
|
+
async getAppliedMigrations(pool, schemaName) {
|
|
649
|
+
const result = await pool.query(
|
|
650
|
+
`SELECT id, name, applied_at FROM "${schemaName}"."${this.migrationsTable}" ORDER BY id`
|
|
651
|
+
);
|
|
652
|
+
return result.rows.map((row) => ({
|
|
653
|
+
id: row.id,
|
|
654
|
+
name: row.name,
|
|
655
|
+
appliedAt: row.applied_at
|
|
656
|
+
}));
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Apply a migration to a schema
|
|
660
|
+
*/
|
|
661
|
+
async applyMigration(pool, schemaName, migration) {
|
|
662
|
+
const client = await pool.connect();
|
|
663
|
+
try {
|
|
664
|
+
await client.query("BEGIN");
|
|
665
|
+
await client.query(migration.sql);
|
|
666
|
+
await client.query(
|
|
667
|
+
`INSERT INTO "${schemaName}"."${this.migrationsTable}" (name) VALUES ($1)`,
|
|
668
|
+
[migration.name]
|
|
669
|
+
);
|
|
670
|
+
await client.query("COMMIT");
|
|
671
|
+
} catch (error) {
|
|
672
|
+
await client.query("ROLLBACK");
|
|
673
|
+
throw error;
|
|
674
|
+
} finally {
|
|
675
|
+
client.release();
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Create a skipped result
|
|
680
|
+
*/
|
|
681
|
+
createSkippedResult(tenantId) {
|
|
682
|
+
return {
|
|
683
|
+
tenantId,
|
|
684
|
+
schemaName: this.tenantConfig.isolation.schemaNameTemplate(tenantId),
|
|
685
|
+
success: false,
|
|
686
|
+
appliedMigrations: [],
|
|
687
|
+
error: "Skipped due to abort",
|
|
688
|
+
durationMs: 0
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Create an error result
|
|
693
|
+
*/
|
|
694
|
+
createErrorResult(tenantId, error) {
|
|
695
|
+
return {
|
|
696
|
+
tenantId,
|
|
697
|
+
schemaName: this.tenantConfig.isolation.schemaNameTemplate(tenantId),
|
|
698
|
+
success: false,
|
|
699
|
+
appliedMigrations: [],
|
|
700
|
+
error: error.message,
|
|
701
|
+
durationMs: 0
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Aggregate migration results
|
|
706
|
+
*/
|
|
707
|
+
aggregateResults(results) {
|
|
708
|
+
return {
|
|
709
|
+
total: results.length,
|
|
710
|
+
succeeded: results.filter((r) => r.success).length,
|
|
711
|
+
failed: results.filter((r) => !r.success && r.error !== "Skipped due to abort").length,
|
|
712
|
+
skipped: results.filter((r) => r.error === "Skipped due to abort").length,
|
|
713
|
+
details: results
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
function createMigrator(tenantConfig, migratorConfig) {
|
|
718
|
+
return new Migrator(tenantConfig, migratorConfig);
|
|
719
|
+
}
|
|
720
|
+
var CrossSchemaQueryBuilder = class {
|
|
721
|
+
constructor(context) {
|
|
722
|
+
this.context = context;
|
|
723
|
+
}
|
|
724
|
+
fromTable = null;
|
|
725
|
+
joins = [];
|
|
726
|
+
selectFields = {};
|
|
727
|
+
whereCondition = null;
|
|
728
|
+
orderByFields = [];
|
|
729
|
+
limitValue = null;
|
|
730
|
+
offsetValue = null;
|
|
731
|
+
/**
|
|
732
|
+
* Set the main table to query from
|
|
733
|
+
*/
|
|
734
|
+
from(source, table) {
|
|
735
|
+
const schemaName = this.getSchemaName(source);
|
|
736
|
+
this.fromTable = { table, source, schemaName };
|
|
737
|
+
return this;
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Add an inner join
|
|
741
|
+
*/
|
|
742
|
+
innerJoin(source, table, condition) {
|
|
743
|
+
return this.addJoin(source, table, condition, "inner");
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Add a left join
|
|
747
|
+
*/
|
|
748
|
+
leftJoin(source, table, condition) {
|
|
749
|
+
return this.addJoin(source, table, condition, "left");
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Add a right join
|
|
753
|
+
*/
|
|
754
|
+
rightJoin(source, table, condition) {
|
|
755
|
+
return this.addJoin(source, table, condition, "right");
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Add a full outer join
|
|
759
|
+
*/
|
|
760
|
+
fullJoin(source, table, condition) {
|
|
761
|
+
return this.addJoin(source, table, condition, "full");
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Select specific fields
|
|
765
|
+
*/
|
|
766
|
+
select(fields) {
|
|
767
|
+
this.selectFields = fields;
|
|
768
|
+
return this;
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Add a where condition
|
|
772
|
+
*/
|
|
773
|
+
where(condition) {
|
|
774
|
+
this.whereCondition = condition;
|
|
775
|
+
return this;
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Add order by
|
|
779
|
+
*/
|
|
780
|
+
orderBy(...fields) {
|
|
781
|
+
this.orderByFields = fields;
|
|
782
|
+
return this;
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Set limit
|
|
786
|
+
*/
|
|
787
|
+
limit(value) {
|
|
788
|
+
this.limitValue = value;
|
|
789
|
+
return this;
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Set offset
|
|
793
|
+
*/
|
|
794
|
+
offset(value) {
|
|
795
|
+
this.offsetValue = value;
|
|
796
|
+
return this;
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Execute the query and return typed results
|
|
800
|
+
*/
|
|
801
|
+
async execute() {
|
|
802
|
+
if (!this.fromTable) {
|
|
803
|
+
throw new Error("[drizzle-multitenant] No table specified. Use .from() first.");
|
|
804
|
+
}
|
|
805
|
+
const sqlQuery = this.buildSql();
|
|
806
|
+
const result = await this.context.tenantDb.execute(sqlQuery);
|
|
807
|
+
return result.rows;
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Build the SQL query
|
|
811
|
+
*/
|
|
812
|
+
buildSql() {
|
|
813
|
+
if (!this.fromTable) {
|
|
814
|
+
throw new Error("[drizzle-multitenant] No table specified");
|
|
815
|
+
}
|
|
816
|
+
const parts = [];
|
|
817
|
+
const selectParts = Object.entries(this.selectFields).map(([alias, column]) => {
|
|
818
|
+
const columnName = column.name;
|
|
819
|
+
return sql`${sql.raw(`"${columnName}"`)} as ${sql.raw(`"${alias}"`)}`;
|
|
820
|
+
});
|
|
821
|
+
if (selectParts.length === 0) {
|
|
822
|
+
parts.push(sql`SELECT *`);
|
|
823
|
+
} else {
|
|
824
|
+
parts.push(sql`SELECT ${sql.join(selectParts, sql`, `)}`);
|
|
825
|
+
}
|
|
826
|
+
const fromTableRef = this.getFullTableName(this.fromTable.schemaName, this.fromTable.table);
|
|
827
|
+
parts.push(sql` FROM ${sql.raw(fromTableRef)}`);
|
|
828
|
+
for (const join2 of this.joins) {
|
|
829
|
+
const joinTableRef = this.getFullTableName(join2.schemaName, join2.table);
|
|
830
|
+
const joinType = this.getJoinKeyword(join2.type);
|
|
831
|
+
parts.push(sql` ${sql.raw(joinType)} ${sql.raw(joinTableRef)} ON ${join2.condition}`);
|
|
832
|
+
}
|
|
833
|
+
if (this.whereCondition) {
|
|
834
|
+
parts.push(sql` WHERE ${this.whereCondition}`);
|
|
835
|
+
}
|
|
836
|
+
if (this.orderByFields.length > 0) {
|
|
837
|
+
parts.push(sql` ORDER BY ${sql.join(this.orderByFields, sql`, `)}`);
|
|
838
|
+
}
|
|
839
|
+
if (this.limitValue !== null) {
|
|
840
|
+
parts.push(sql` LIMIT ${sql.raw(this.limitValue.toString())}`);
|
|
841
|
+
}
|
|
842
|
+
if (this.offsetValue !== null) {
|
|
843
|
+
parts.push(sql` OFFSET ${sql.raw(this.offsetValue.toString())}`);
|
|
844
|
+
}
|
|
845
|
+
return sql.join(parts, sql``);
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Add a join to the query
|
|
849
|
+
*/
|
|
850
|
+
addJoin(source, table, condition, type) {
|
|
851
|
+
const schemaName = this.getSchemaName(source);
|
|
852
|
+
this.joins.push({ table, source, schemaName, condition, type });
|
|
853
|
+
return this;
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Get schema name for a source
|
|
857
|
+
*/
|
|
858
|
+
getSchemaName(source) {
|
|
859
|
+
if (source === "tenant") {
|
|
860
|
+
return this.context.tenantSchema ?? "tenant";
|
|
861
|
+
}
|
|
862
|
+
return this.context.sharedSchema ?? "public";
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Get fully qualified table name
|
|
866
|
+
*/
|
|
867
|
+
getFullTableName(schemaName, table) {
|
|
868
|
+
const tableName = getTableName(table);
|
|
869
|
+
return `"${schemaName}"."${tableName}"`;
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Get SQL keyword for join type
|
|
873
|
+
*/
|
|
874
|
+
getJoinKeyword(type) {
|
|
875
|
+
switch (type) {
|
|
876
|
+
case "inner":
|
|
877
|
+
return "INNER JOIN";
|
|
878
|
+
case "left":
|
|
879
|
+
return "LEFT JOIN";
|
|
880
|
+
case "right":
|
|
881
|
+
return "RIGHT JOIN";
|
|
882
|
+
case "full":
|
|
883
|
+
return "FULL OUTER JOIN";
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
function createCrossSchemaQuery(context) {
|
|
888
|
+
return new CrossSchemaQueryBuilder(context);
|
|
889
|
+
}
|
|
890
|
+
async function withSharedLookup(config) {
|
|
891
|
+
const {
|
|
892
|
+
tenantDb,
|
|
893
|
+
tenantTable,
|
|
894
|
+
sharedTable,
|
|
895
|
+
foreignKey,
|
|
896
|
+
sharedKey = "id",
|
|
897
|
+
sharedFields,
|
|
898
|
+
where: whereCondition
|
|
899
|
+
} = config;
|
|
900
|
+
const tenantTableName = getTableName(tenantTable);
|
|
901
|
+
const sharedTableName = getTableName(sharedTable);
|
|
902
|
+
const sharedFieldList = sharedFields.map((field) => `s."${String(field)}"`).join(", ");
|
|
903
|
+
const queryParts = [
|
|
904
|
+
`SELECT t.*, ${sharedFieldList}`,
|
|
905
|
+
`FROM "${tenantTableName}" t`,
|
|
906
|
+
`LEFT JOIN "public"."${sharedTableName}" s ON t."${String(foreignKey)}" = s."${String(sharedKey)}"`
|
|
907
|
+
];
|
|
908
|
+
if (whereCondition) {
|
|
909
|
+
queryParts.push("WHERE");
|
|
910
|
+
}
|
|
911
|
+
const sqlQuery = sql.raw(queryParts.join(" "));
|
|
912
|
+
const result = await tenantDb.execute(sqlQuery);
|
|
913
|
+
return result.rows;
|
|
914
|
+
}
|
|
915
|
+
async function crossSchemaRaw(db, options) {
|
|
916
|
+
const { tenantSchema, sharedSchema, sql: rawSql } = options;
|
|
917
|
+
const processedSql = rawSql.replace(/\$tenant\./g, `"${tenantSchema}".`).replace(/\$shared\./g, `"${sharedSchema}".`);
|
|
918
|
+
const query = sql.raw(processedSql);
|
|
919
|
+
const result = await db.execute(query);
|
|
920
|
+
return result.rows;
|
|
921
|
+
}
|
|
922
|
+
function buildCrossSchemaSelect(fields, tenantSchema, _sharedSchema) {
|
|
923
|
+
const columns = Object.entries(fields).map(([alias, column]) => {
|
|
924
|
+
const columnName = column.name;
|
|
925
|
+
return `"${columnName}" as "${alias}"`;
|
|
926
|
+
});
|
|
927
|
+
const getSchema = () => {
|
|
928
|
+
return tenantSchema;
|
|
929
|
+
};
|
|
930
|
+
return { columns, getSchema };
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
export { CrossSchemaQueryBuilder, DEFAULT_CONFIG, Migrator, buildCrossSchemaSelect, createCrossSchemaQuery, createMigrator, createTenantContext, createTenantManager, crossSchemaRaw, defineConfig, withSharedLookup };
|
|
934
|
+
//# sourceMappingURL=index.js.map
|
|
935
|
+
//# sourceMappingURL=index.js.map
|