drizzle-multitenant 1.2.0 → 1.3.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/README.md +28 -8
- package/dist/cli/index.js +1809 -5949
- package/dist/{context-Vki959ri.d.ts → context-BBLPNjmk.d.ts} +1 -1
- package/dist/cross-schema/index.js +1 -426
- package/dist/export/index.d.ts +395 -0
- package/dist/export/index.js +9 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +34 -4745
- package/dist/integrations/express.d.ts +3 -3
- package/dist/integrations/express.js +1 -110
- package/dist/integrations/fastify.d.ts +3 -3
- package/dist/integrations/fastify.js +1 -236
- package/dist/integrations/hono.js +0 -3
- package/dist/integrations/nestjs/index.d.ts +1 -1
- package/dist/integrations/nestjs/index.js +3 -11006
- package/dist/lint/index.d.ts +475 -0
- package/dist/lint/index.js +5 -0
- package/dist/metrics/index.d.ts +530 -0
- package/dist/metrics/index.js +3 -0
- package/dist/migrator/index.d.ts +116 -4
- package/dist/migrator/index.js +34 -2990
- package/dist/{migrator-BDgFzSh8.d.ts → migrator-B7oPKe73.d.ts} +245 -2
- package/dist/scaffold/index.d.ts +330 -0
- package/dist/scaffold/index.js +277 -0
- package/dist/{types-BhK96FPC.d.ts → types-CGqsPe2Q.d.ts} +49 -1
- package/package.json +18 -1
- package/dist/cli/index.js.map +0 -1
- package/dist/cross-schema/index.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/integrations/express.js.map +0 -1
- package/dist/integrations/fastify.js.map +0 -1
- package/dist/integrations/hono.js.map +0 -1
- package/dist/integrations/nestjs/index.js.map +0 -1
- package/dist/migrator/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,1634 +1,16 @@
|
|
|
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 { createHash } from 'crypto';
|
|
8
|
-
import { sql, getTableName } from 'drizzle-orm';
|
|
9
|
-
|
|
10
|
-
// src/config.ts
|
|
11
|
-
function defineConfig(config) {
|
|
12
|
-
validateConfig(config);
|
|
13
|
-
return config;
|
|
14
|
-
}
|
|
15
|
-
function validateConfig(config) {
|
|
16
|
-
if (!config.connection.url) {
|
|
17
|
-
throw new Error("[drizzle-multitenant] connection.url is required");
|
|
18
|
-
}
|
|
19
|
-
if (!config.isolation.strategy) {
|
|
20
|
-
throw new Error("[drizzle-multitenant] isolation.strategy is required");
|
|
21
|
-
}
|
|
22
|
-
if (config.isolation.strategy !== "schema") {
|
|
23
|
-
throw new Error(
|
|
24
|
-
`[drizzle-multitenant] isolation.strategy "${config.isolation.strategy}" is not yet supported. Only "schema" is currently available.`
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
if (!config.isolation.schemaNameTemplate) {
|
|
28
|
-
throw new Error("[drizzle-multitenant] isolation.schemaNameTemplate is required");
|
|
29
|
-
}
|
|
30
|
-
if (typeof config.isolation.schemaNameTemplate !== "function") {
|
|
31
|
-
throw new Error("[drizzle-multitenant] isolation.schemaNameTemplate must be a function");
|
|
32
|
-
}
|
|
33
|
-
if (!config.schemas.tenant) {
|
|
34
|
-
throw new Error("[drizzle-multitenant] schemas.tenant is required");
|
|
35
|
-
}
|
|
36
|
-
if (config.isolation.maxPools !== void 0 && config.isolation.maxPools < 1) {
|
|
37
|
-
throw new Error("[drizzle-multitenant] isolation.maxPools must be at least 1");
|
|
38
|
-
}
|
|
39
|
-
if (config.isolation.poolTtlMs !== void 0 && config.isolation.poolTtlMs < 0) {
|
|
40
|
-
throw new Error("[drizzle-multitenant] isolation.poolTtlMs must be non-negative");
|
|
41
|
-
}
|
|
42
|
-
if (config.connection.retry) {
|
|
43
|
-
const retry = config.connection.retry;
|
|
44
|
-
if (retry.maxAttempts !== void 0 && retry.maxAttempts < 1) {
|
|
45
|
-
throw new Error("[drizzle-multitenant] connection.retry.maxAttempts must be at least 1");
|
|
46
|
-
}
|
|
47
|
-
if (retry.initialDelayMs !== void 0 && retry.initialDelayMs < 0) {
|
|
48
|
-
throw new Error("[drizzle-multitenant] connection.retry.initialDelayMs must be non-negative");
|
|
49
|
-
}
|
|
50
|
-
if (retry.maxDelayMs !== void 0 && retry.maxDelayMs < 0) {
|
|
51
|
-
throw new Error("[drizzle-multitenant] connection.retry.maxDelayMs must be non-negative");
|
|
52
|
-
}
|
|
53
|
-
if (retry.backoffMultiplier !== void 0 && retry.backoffMultiplier < 1) {
|
|
54
|
-
throw new Error("[drizzle-multitenant] connection.retry.backoffMultiplier must be at least 1");
|
|
55
|
-
}
|
|
56
|
-
if (retry.initialDelayMs !== void 0 && retry.maxDelayMs !== void 0 && retry.initialDelayMs > retry.maxDelayMs) {
|
|
57
|
-
throw new Error(
|
|
58
|
-
"[drizzle-multitenant] connection.retry.initialDelayMs cannot be greater than maxDelayMs"
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// src/types.ts
|
|
65
|
-
var DEFAULT_CONFIG = {
|
|
66
|
-
maxPools: 50,
|
|
67
|
-
poolTtlMs: 60 * 60 * 1e3,
|
|
68
|
-
// 1 hour
|
|
69
|
-
cleanupIntervalMs: 6e4,
|
|
70
|
-
// 1 minute
|
|
71
|
-
poolConfig: {
|
|
72
|
-
max: 10,
|
|
73
|
-
idleTimeoutMillis: 3e4,
|
|
74
|
-
connectionTimeoutMillis: 5e3
|
|
75
|
-
},
|
|
76
|
-
retry: {
|
|
77
|
-
maxAttempts: 3,
|
|
78
|
-
initialDelayMs: 100,
|
|
79
|
-
maxDelayMs: 5e3,
|
|
80
|
-
backoffMultiplier: 2,
|
|
81
|
-
jitter: true
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
// src/debug.ts
|
|
86
|
-
var PREFIX = "[drizzle-multitenant]";
|
|
87
|
-
var DEFAULT_SLOW_QUERY_THRESHOLD = 1e3;
|
|
88
|
-
var DebugLogger = class {
|
|
89
|
-
enabled;
|
|
90
|
-
logQueries;
|
|
91
|
-
logPoolEvents;
|
|
92
|
-
slowQueryThreshold;
|
|
93
|
-
logger;
|
|
94
|
-
constructor(config) {
|
|
95
|
-
this.enabled = config?.enabled ?? false;
|
|
96
|
-
this.logQueries = config?.logQueries ?? true;
|
|
97
|
-
this.logPoolEvents = config?.logPoolEvents ?? true;
|
|
98
|
-
this.slowQueryThreshold = config?.slowQueryThreshold ?? DEFAULT_SLOW_QUERY_THRESHOLD;
|
|
99
|
-
this.logger = config?.logger ?? this.defaultLogger;
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Check if debug mode is enabled
|
|
103
|
-
*/
|
|
104
|
-
isEnabled() {
|
|
105
|
-
return this.enabled;
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Log a query execution
|
|
109
|
-
*/
|
|
110
|
-
logQuery(tenantId, query, durationMs) {
|
|
111
|
-
if (!this.enabled || !this.logQueries) return;
|
|
112
|
-
const isSlowQuery = durationMs >= this.slowQueryThreshold;
|
|
113
|
-
const type = isSlowQuery ? "slow_query" : "query";
|
|
114
|
-
const context = {
|
|
115
|
-
type,
|
|
116
|
-
tenantId,
|
|
117
|
-
query: this.truncateQuery(query),
|
|
118
|
-
durationMs
|
|
119
|
-
};
|
|
120
|
-
if (isSlowQuery) {
|
|
121
|
-
this.logger(
|
|
122
|
-
`${PREFIX} tenant=${tenantId} SLOW_QUERY duration=${durationMs}ms query="${this.truncateQuery(query)}"`,
|
|
123
|
-
context
|
|
124
|
-
);
|
|
125
|
-
} else {
|
|
126
|
-
this.logger(
|
|
127
|
-
`${PREFIX} tenant=${tenantId} query="${this.truncateQuery(query)}" duration=${durationMs}ms`,
|
|
128
|
-
context
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Log pool creation
|
|
134
|
-
*/
|
|
135
|
-
logPoolCreated(tenantId, schemaName) {
|
|
136
|
-
if (!this.enabled || !this.logPoolEvents) return;
|
|
137
|
-
const context = {
|
|
138
|
-
type: "pool_created",
|
|
139
|
-
tenantId,
|
|
140
|
-
schemaName
|
|
141
|
-
};
|
|
142
|
-
this.logger(
|
|
143
|
-
`${PREFIX} tenant=${tenantId} POOL_CREATED schema=${schemaName}`,
|
|
144
|
-
context
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Log pool eviction
|
|
149
|
-
*/
|
|
150
|
-
logPoolEvicted(tenantId, schemaName, reason) {
|
|
151
|
-
if (!this.enabled || !this.logPoolEvents) return;
|
|
152
|
-
const context = {
|
|
153
|
-
type: "pool_evicted",
|
|
154
|
-
tenantId,
|
|
155
|
-
schemaName,
|
|
156
|
-
metadata: reason ? { reason } : void 0
|
|
157
|
-
};
|
|
158
|
-
const reasonStr = reason ? ` reason=${reason}` : "";
|
|
159
|
-
this.logger(
|
|
160
|
-
`${PREFIX} tenant=${tenantId} POOL_EVICTED schema=${schemaName}${reasonStr}`,
|
|
161
|
-
context
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Log pool error
|
|
166
|
-
*/
|
|
167
|
-
logPoolError(tenantId, error) {
|
|
168
|
-
if (!this.enabled || !this.logPoolEvents) return;
|
|
169
|
-
const context = {
|
|
170
|
-
type: "pool_error",
|
|
171
|
-
tenantId,
|
|
172
|
-
error: error.message
|
|
173
|
-
};
|
|
174
|
-
this.logger(
|
|
175
|
-
`${PREFIX} tenant=${tenantId} POOL_ERROR error="${error.message}"`,
|
|
176
|
-
context
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Log warmup event
|
|
181
|
-
*/
|
|
182
|
-
logWarmup(tenantId, success, durationMs, alreadyWarm) {
|
|
183
|
-
if (!this.enabled || !this.logPoolEvents) return;
|
|
184
|
-
const context = {
|
|
185
|
-
type: "warmup",
|
|
186
|
-
tenantId,
|
|
187
|
-
durationMs,
|
|
188
|
-
metadata: { success, alreadyWarm }
|
|
189
|
-
};
|
|
190
|
-
const status = alreadyWarm ? "already_warm" : success ? "success" : "failed";
|
|
191
|
-
this.logger(
|
|
192
|
-
`${PREFIX} tenant=${tenantId} WARMUP status=${status} duration=${durationMs}ms`,
|
|
193
|
-
context
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* Log connection retry event
|
|
198
|
-
*/
|
|
199
|
-
logConnectionRetry(identifier, attempt, maxAttempts, error, delayMs) {
|
|
200
|
-
if (!this.enabled || !this.logPoolEvents) return;
|
|
201
|
-
const context = {
|
|
202
|
-
type: "connection_retry",
|
|
203
|
-
tenantId: identifier,
|
|
204
|
-
error: error.message,
|
|
205
|
-
metadata: { attempt, maxAttempts, delayMs }
|
|
206
|
-
};
|
|
207
|
-
this.logger(
|
|
208
|
-
`${PREFIX} tenant=${identifier} CONNECTION_RETRY attempt=${attempt}/${maxAttempts} delay=${delayMs}ms error="${error.message}"`,
|
|
209
|
-
context
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* Log connection success after retries
|
|
214
|
-
*/
|
|
215
|
-
logConnectionSuccess(identifier, attempts, totalTimeMs) {
|
|
216
|
-
if (!this.enabled || !this.logPoolEvents) return;
|
|
217
|
-
const context = {
|
|
218
|
-
type: "pool_created",
|
|
219
|
-
tenantId: identifier,
|
|
220
|
-
durationMs: totalTimeMs,
|
|
221
|
-
metadata: { attempts }
|
|
222
|
-
};
|
|
223
|
-
if (attempts > 1) {
|
|
224
|
-
this.logger(
|
|
225
|
-
`${PREFIX} tenant=${identifier} CONNECTION_SUCCESS attempts=${attempts} totalTime=${totalTimeMs}ms`,
|
|
226
|
-
context
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Log a custom debug message
|
|
232
|
-
*/
|
|
233
|
-
log(message, context) {
|
|
234
|
-
if (!this.enabled) return;
|
|
235
|
-
this.logger(`${PREFIX} ${message}`, context);
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Default logger implementation using console
|
|
239
|
-
*/
|
|
240
|
-
defaultLogger(message, _context) {
|
|
241
|
-
console.log(message);
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* Truncate long queries for readability
|
|
245
|
-
*/
|
|
246
|
-
truncateQuery(query, maxLength = 100) {
|
|
247
|
-
const normalized = query.replace(/\s+/g, " ").trim();
|
|
248
|
-
if (normalized.length <= maxLength) {
|
|
249
|
-
return normalized;
|
|
250
|
-
}
|
|
251
|
-
return normalized.substring(0, maxLength - 3) + "...";
|
|
252
|
-
}
|
|
253
|
-
};
|
|
254
|
-
function createDebugLogger(config) {
|
|
255
|
-
return new DebugLogger(config);
|
|
256
|
-
}
|
|
257
|
-
var PoolCache = class {
|
|
258
|
-
cache;
|
|
259
|
-
poolTtlMs;
|
|
260
|
-
onDispose;
|
|
261
|
-
constructor(options) {
|
|
262
|
-
this.poolTtlMs = options.poolTtlMs;
|
|
263
|
-
this.onDispose = options.onDispose;
|
|
264
|
-
this.cache = new LRUCache({
|
|
265
|
-
max: options.maxPools,
|
|
266
|
-
dispose: (entry, key) => {
|
|
267
|
-
void this.handleDispose(key, entry);
|
|
268
|
-
},
|
|
269
|
-
noDisposeOnSet: true
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* Get a pool entry from cache
|
|
274
|
-
*
|
|
275
|
-
* This does NOT update the last access time automatically.
|
|
276
|
-
* Use `touch()` to update access time when needed.
|
|
277
|
-
*/
|
|
278
|
-
get(schemaName) {
|
|
279
|
-
return this.cache.get(schemaName);
|
|
280
|
-
}
|
|
281
|
-
/**
|
|
282
|
-
* Set a pool entry in cache
|
|
283
|
-
*
|
|
284
|
-
* If the cache is full, the least recently used entry will be evicted.
|
|
285
|
-
*/
|
|
286
|
-
set(schemaName, entry) {
|
|
287
|
-
this.cache.set(schemaName, entry);
|
|
288
|
-
}
|
|
289
|
-
/**
|
|
290
|
-
* Check if a pool exists in cache
|
|
291
|
-
*/
|
|
292
|
-
has(schemaName) {
|
|
293
|
-
return this.cache.has(schemaName);
|
|
294
|
-
}
|
|
295
|
-
/**
|
|
296
|
-
* Delete a pool from cache
|
|
297
|
-
*
|
|
298
|
-
* Note: This triggers the dispose callback if configured.
|
|
299
|
-
*/
|
|
300
|
-
delete(schemaName) {
|
|
301
|
-
return this.cache.delete(schemaName);
|
|
302
|
-
}
|
|
303
|
-
/**
|
|
304
|
-
* Get the number of pools in cache
|
|
305
|
-
*/
|
|
306
|
-
size() {
|
|
307
|
-
return this.cache.size;
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* Get all schema names in cache
|
|
311
|
-
*/
|
|
312
|
-
keys() {
|
|
313
|
-
return Array.from(this.cache.keys());
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* Iterate over all entries in cache
|
|
317
|
-
*
|
|
318
|
-
* @yields [schemaName, entry] pairs
|
|
319
|
-
*/
|
|
320
|
-
*entries() {
|
|
321
|
-
for (const [key, value] of this.cache.entries()) {
|
|
322
|
-
yield [key, value];
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
/**
|
|
326
|
-
* Clear all pools from cache
|
|
327
|
-
*
|
|
328
|
-
* Each pool's dispose callback will be triggered by the LRU cache.
|
|
329
|
-
*/
|
|
330
|
-
async clear() {
|
|
331
|
-
this.cache.clear();
|
|
332
|
-
await Promise.resolve();
|
|
333
|
-
}
|
|
334
|
-
/**
|
|
335
|
-
* Evict the least recently used pool
|
|
336
|
-
*
|
|
337
|
-
* @returns The schema name of the evicted pool, or undefined if cache is empty
|
|
338
|
-
*/
|
|
339
|
-
evictLRU() {
|
|
340
|
-
const keys = Array.from(this.cache.keys());
|
|
341
|
-
if (keys.length === 0) {
|
|
342
|
-
return void 0;
|
|
343
|
-
}
|
|
344
|
-
const lruKey = keys[keys.length - 1];
|
|
345
|
-
this.cache.delete(lruKey);
|
|
346
|
-
return lruKey;
|
|
347
|
-
}
|
|
348
|
-
/**
|
|
349
|
-
* Evict pools that have exceeded TTL
|
|
350
|
-
*
|
|
351
|
-
* @returns Array of schema names that were evicted
|
|
352
|
-
*/
|
|
353
|
-
async evictExpired() {
|
|
354
|
-
if (!this.poolTtlMs) {
|
|
355
|
-
return [];
|
|
356
|
-
}
|
|
357
|
-
const now = Date.now();
|
|
358
|
-
const toEvict = [];
|
|
359
|
-
for (const [schemaName, entry] of this.cache.entries()) {
|
|
360
|
-
if (now - entry.lastAccess > this.poolTtlMs) {
|
|
361
|
-
toEvict.push(schemaName);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
for (const schemaName of toEvict) {
|
|
365
|
-
this.cache.delete(schemaName);
|
|
366
|
-
}
|
|
367
|
-
return toEvict;
|
|
368
|
-
}
|
|
369
|
-
/**
|
|
370
|
-
* Update last access time for a pool
|
|
371
|
-
*
|
|
372
|
-
* This moves the pool to the front of the LRU list.
|
|
373
|
-
*/
|
|
374
|
-
touch(schemaName) {
|
|
375
|
-
const entry = this.cache.get(schemaName);
|
|
376
|
-
if (entry) {
|
|
377
|
-
entry.lastAccess = Date.now();
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
/**
|
|
381
|
-
* Get the maximum number of pools allowed in cache
|
|
382
|
-
*/
|
|
383
|
-
getMaxPools() {
|
|
384
|
-
return this.cache.max;
|
|
385
|
-
}
|
|
386
|
-
/**
|
|
387
|
-
* Get the configured TTL in milliseconds
|
|
388
|
-
*/
|
|
389
|
-
getTtlMs() {
|
|
390
|
-
return this.poolTtlMs;
|
|
391
|
-
}
|
|
392
|
-
/**
|
|
393
|
-
* Check if an entry has expired based on TTL
|
|
394
|
-
*/
|
|
395
|
-
isExpired(entry) {
|
|
396
|
-
if (!this.poolTtlMs) {
|
|
397
|
-
return false;
|
|
398
|
-
}
|
|
399
|
-
return Date.now() - entry.lastAccess > this.poolTtlMs;
|
|
400
|
-
}
|
|
401
|
-
/**
|
|
402
|
-
* Handle disposal of a cache entry
|
|
403
|
-
*/
|
|
404
|
-
async handleDispose(schemaName, entry) {
|
|
405
|
-
if (this.onDispose) {
|
|
406
|
-
await this.onDispose(schemaName, entry);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
// src/pool/retry/retry-handler.ts
|
|
412
|
-
function isRetryableError(error) {
|
|
413
|
-
const message = error.message.toLowerCase();
|
|
414
|
-
if (message.includes("econnrefused") || message.includes("econnreset") || message.includes("etimedout") || message.includes("enotfound") || message.includes("connection refused") || message.includes("connection reset") || message.includes("connection terminated") || message.includes("connection timed out") || message.includes("timeout expired") || message.includes("socket hang up")) {
|
|
415
|
-
return true;
|
|
416
|
-
}
|
|
417
|
-
if (message.includes("too many connections") || message.includes("sorry, too many clients") || message.includes("the database system is starting up") || message.includes("the database system is shutting down") || message.includes("server closed the connection unexpectedly") || message.includes("could not connect to server")) {
|
|
418
|
-
return true;
|
|
419
|
-
}
|
|
420
|
-
if (message.includes("ssl connection") || message.includes("ssl handshake")) {
|
|
421
|
-
return true;
|
|
422
|
-
}
|
|
423
|
-
return false;
|
|
424
|
-
}
|
|
425
|
-
function sleep(ms) {
|
|
426
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
427
|
-
}
|
|
428
|
-
var RetryHandler = class {
|
|
429
|
-
config;
|
|
430
|
-
constructor(config) {
|
|
431
|
-
this.config = {
|
|
432
|
-
maxAttempts: config?.maxAttempts ?? DEFAULT_CONFIG.retry.maxAttempts,
|
|
433
|
-
initialDelayMs: config?.initialDelayMs ?? DEFAULT_CONFIG.retry.initialDelayMs,
|
|
434
|
-
maxDelayMs: config?.maxDelayMs ?? DEFAULT_CONFIG.retry.maxDelayMs,
|
|
435
|
-
backoffMultiplier: config?.backoffMultiplier ?? DEFAULT_CONFIG.retry.backoffMultiplier,
|
|
436
|
-
jitter: config?.jitter ?? DEFAULT_CONFIG.retry.jitter,
|
|
437
|
-
isRetryable: config?.isRetryable ?? isRetryableError,
|
|
438
|
-
onRetry: config?.onRetry
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
/**
|
|
442
|
-
* Execute an operation with retry logic
|
|
443
|
-
*
|
|
444
|
-
* @param operation - The async operation to execute
|
|
445
|
-
* @param overrideConfig - Optional config to override defaults for this call
|
|
446
|
-
* @returns Result with metadata about attempts and timing
|
|
447
|
-
*/
|
|
448
|
-
async withRetry(operation, overrideConfig) {
|
|
449
|
-
const config = overrideConfig ? { ...this.config, ...overrideConfig } : this.config;
|
|
450
|
-
const startTime = Date.now();
|
|
451
|
-
let lastError = null;
|
|
452
|
-
for (let attempt = 0; attempt < config.maxAttempts; attempt++) {
|
|
453
|
-
try {
|
|
454
|
-
const result = await operation();
|
|
455
|
-
return {
|
|
456
|
-
result,
|
|
457
|
-
attempts: attempt + 1,
|
|
458
|
-
totalTimeMs: Date.now() - startTime
|
|
459
|
-
};
|
|
460
|
-
} catch (error) {
|
|
461
|
-
lastError = error;
|
|
462
|
-
const isLastAttempt = attempt >= config.maxAttempts - 1;
|
|
463
|
-
const checkRetryable = config.isRetryable ?? this.isRetryable;
|
|
464
|
-
if (isLastAttempt || !checkRetryable(lastError)) {
|
|
465
|
-
throw lastError;
|
|
466
|
-
}
|
|
467
|
-
const delay = this.calculateDelay(attempt, config);
|
|
468
|
-
config.onRetry?.(attempt + 1, lastError, delay);
|
|
469
|
-
await sleep(delay);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
throw lastError ?? new Error("Retry failed with no error");
|
|
473
|
-
}
|
|
474
|
-
/**
|
|
475
|
-
* Calculate delay with exponential backoff and optional jitter
|
|
476
|
-
*
|
|
477
|
-
* @param attempt - Current attempt number (0-indexed)
|
|
478
|
-
* @param config - Retry configuration
|
|
479
|
-
* @returns Delay in milliseconds
|
|
480
|
-
*/
|
|
481
|
-
calculateDelay(attempt, config) {
|
|
482
|
-
const cfg = config ? { ...this.config, ...config } : this.config;
|
|
483
|
-
const exponentialDelay = cfg.initialDelayMs * Math.pow(cfg.backoffMultiplier, attempt);
|
|
484
|
-
const cappedDelay = Math.min(exponentialDelay, cfg.maxDelayMs);
|
|
485
|
-
if (cfg.jitter) {
|
|
486
|
-
const jitterFactor = 1 + Math.random() * 0.25;
|
|
487
|
-
return Math.floor(cappedDelay * jitterFactor);
|
|
488
|
-
}
|
|
489
|
-
return Math.floor(cappedDelay);
|
|
490
|
-
}
|
|
491
|
-
/**
|
|
492
|
-
* Check if an error is retryable
|
|
493
|
-
*
|
|
494
|
-
* Uses the configured isRetryable function or the default implementation.
|
|
495
|
-
*/
|
|
496
|
-
isRetryable(error) {
|
|
497
|
-
return (this.config.isRetryable ?? isRetryableError)(error);
|
|
498
|
-
}
|
|
499
|
-
/**
|
|
500
|
-
* Get the current configuration
|
|
501
|
-
*/
|
|
502
|
-
getConfig() {
|
|
503
|
-
return { ...this.config };
|
|
504
|
-
}
|
|
505
|
-
/**
|
|
506
|
-
* Get the maximum number of attempts
|
|
507
|
-
*/
|
|
508
|
-
getMaxAttempts() {
|
|
509
|
-
return this.config.maxAttempts;
|
|
510
|
-
}
|
|
511
|
-
};
|
|
512
|
-
|
|
513
|
-
// src/pool/health/health-checker.ts
|
|
514
|
-
var HealthChecker = class {
|
|
515
|
-
constructor(deps) {
|
|
516
|
-
this.deps = deps;
|
|
517
|
-
}
|
|
518
|
-
/**
|
|
519
|
-
* Check health of all pools and connections
|
|
520
|
-
*
|
|
521
|
-
* Verifies the health of tenant pools and optionally the shared database.
|
|
522
|
-
* Returns detailed status information for monitoring and load balancer integration.
|
|
523
|
-
*
|
|
524
|
-
* @example
|
|
525
|
-
* ```typescript
|
|
526
|
-
* // Basic health check
|
|
527
|
-
* const health = await healthChecker.checkHealth();
|
|
528
|
-
* console.log(health.healthy); // true/false
|
|
529
|
-
*
|
|
530
|
-
* // Check specific tenants only
|
|
531
|
-
* const health = await healthChecker.checkHealth({
|
|
532
|
-
* tenantIds: ['tenant-1', 'tenant-2'],
|
|
533
|
-
* ping: true,
|
|
534
|
-
* pingTimeoutMs: 3000,
|
|
535
|
-
* });
|
|
536
|
-
* ```
|
|
537
|
-
*/
|
|
538
|
-
async checkHealth(options = {}) {
|
|
539
|
-
const startTime = Date.now();
|
|
540
|
-
const {
|
|
541
|
-
ping = true,
|
|
542
|
-
pingTimeoutMs = 5e3,
|
|
543
|
-
includeShared = true,
|
|
544
|
-
tenantIds
|
|
545
|
-
} = options;
|
|
546
|
-
const poolHealthResults = [];
|
|
547
|
-
let sharedDbStatus = "ok";
|
|
548
|
-
let sharedDbResponseTimeMs;
|
|
549
|
-
let sharedDbError;
|
|
550
|
-
const poolsToCheck = this.getPoolsToCheck(tenantIds);
|
|
551
|
-
const poolChecks = poolsToCheck.map(async ({ schemaName, tenantId, entry }) => {
|
|
552
|
-
return this.checkPoolHealth(tenantId, schemaName, entry, ping, pingTimeoutMs);
|
|
553
|
-
});
|
|
554
|
-
poolHealthResults.push(...await Promise.all(poolChecks));
|
|
555
|
-
const sharedPool = this.deps.getSharedPool();
|
|
556
|
-
if (includeShared && sharedPool) {
|
|
557
|
-
const sharedResult = await this.checkSharedDbHealth(sharedPool, ping, pingTimeoutMs);
|
|
558
|
-
sharedDbStatus = sharedResult.status;
|
|
559
|
-
sharedDbResponseTimeMs = sharedResult.responseTimeMs;
|
|
560
|
-
sharedDbError = sharedResult.error;
|
|
561
|
-
}
|
|
562
|
-
const degradedPools = poolHealthResults.filter((p) => p.status === "degraded").length;
|
|
563
|
-
const unhealthyPools = poolHealthResults.filter((p) => p.status === "unhealthy").length;
|
|
564
|
-
const healthy = unhealthyPools === 0 && sharedDbStatus !== "unhealthy";
|
|
565
|
-
const result = {
|
|
566
|
-
healthy,
|
|
567
|
-
pools: poolHealthResults,
|
|
568
|
-
sharedDb: sharedDbStatus,
|
|
569
|
-
totalPools: poolHealthResults.length,
|
|
570
|
-
degradedPools,
|
|
571
|
-
unhealthyPools,
|
|
572
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
573
|
-
durationMs: Date.now() - startTime
|
|
574
|
-
};
|
|
575
|
-
if (sharedDbResponseTimeMs !== void 0) {
|
|
576
|
-
result.sharedDbResponseTimeMs = sharedDbResponseTimeMs;
|
|
577
|
-
}
|
|
578
|
-
if (sharedDbError !== void 0) {
|
|
579
|
-
result.sharedDbError = sharedDbError;
|
|
580
|
-
}
|
|
581
|
-
return result;
|
|
582
|
-
}
|
|
583
|
-
/**
|
|
584
|
-
* Get pools to check based on options
|
|
585
|
-
*/
|
|
586
|
-
getPoolsToCheck(tenantIds) {
|
|
587
|
-
const poolsToCheck = [];
|
|
588
|
-
if (tenantIds && tenantIds.length > 0) {
|
|
589
|
-
for (const tenantId of tenantIds) {
|
|
590
|
-
const schemaName = this.deps.getSchemaName(tenantId);
|
|
591
|
-
const entry = this.deps.getPoolEntry(schemaName);
|
|
592
|
-
if (entry) {
|
|
593
|
-
poolsToCheck.push({ schemaName, tenantId, entry });
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
} else {
|
|
597
|
-
for (const [schemaName, entry] of this.deps.getPoolEntries()) {
|
|
598
|
-
const tenantId = this.deps.getTenantIdBySchema(schemaName) ?? schemaName;
|
|
599
|
-
poolsToCheck.push({ schemaName, tenantId, entry });
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
return poolsToCheck;
|
|
603
|
-
}
|
|
604
|
-
/**
|
|
605
|
-
* Check health of a single tenant pool
|
|
606
|
-
*/
|
|
607
|
-
async checkPoolHealth(tenantId, schemaName, entry, ping, pingTimeoutMs) {
|
|
608
|
-
const pool = entry.pool;
|
|
609
|
-
const totalConnections = pool.totalCount;
|
|
610
|
-
const idleConnections = pool.idleCount;
|
|
611
|
-
const waitingRequests = pool.waitingCount;
|
|
612
|
-
let status = "ok";
|
|
613
|
-
let responseTimeMs;
|
|
614
|
-
let error;
|
|
615
|
-
if (waitingRequests > 0) {
|
|
616
|
-
status = "degraded";
|
|
617
|
-
}
|
|
618
|
-
if (ping) {
|
|
619
|
-
const pingResult = await this.executePingQuery(pool, pingTimeoutMs);
|
|
620
|
-
responseTimeMs = pingResult.responseTimeMs;
|
|
621
|
-
if (!pingResult.success) {
|
|
622
|
-
status = "unhealthy";
|
|
623
|
-
error = pingResult.error;
|
|
624
|
-
} else if (pingResult.responseTimeMs && pingResult.responseTimeMs > pingTimeoutMs / 2) {
|
|
625
|
-
if (status === "ok") {
|
|
626
|
-
status = "degraded";
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
const result = {
|
|
631
|
-
tenantId,
|
|
632
|
-
schemaName,
|
|
633
|
-
status,
|
|
634
|
-
totalConnections,
|
|
635
|
-
idleConnections,
|
|
636
|
-
waitingRequests
|
|
637
|
-
};
|
|
638
|
-
if (responseTimeMs !== void 0) {
|
|
639
|
-
result.responseTimeMs = responseTimeMs;
|
|
640
|
-
}
|
|
641
|
-
if (error !== void 0) {
|
|
642
|
-
result.error = error;
|
|
643
|
-
}
|
|
644
|
-
return result;
|
|
645
|
-
}
|
|
646
|
-
/**
|
|
647
|
-
* Check health of shared database
|
|
648
|
-
*/
|
|
649
|
-
async checkSharedDbHealth(sharedPool, ping, pingTimeoutMs) {
|
|
650
|
-
let status = "ok";
|
|
651
|
-
let responseTimeMs;
|
|
652
|
-
let error;
|
|
653
|
-
const waitingRequests = sharedPool.waitingCount;
|
|
654
|
-
if (waitingRequests > 0) {
|
|
655
|
-
status = "degraded";
|
|
656
|
-
}
|
|
657
|
-
if (ping) {
|
|
658
|
-
const pingResult = await this.executePingQuery(sharedPool, pingTimeoutMs);
|
|
659
|
-
responseTimeMs = pingResult.responseTimeMs;
|
|
660
|
-
if (!pingResult.success) {
|
|
661
|
-
status = "unhealthy";
|
|
662
|
-
error = pingResult.error;
|
|
663
|
-
} else if (pingResult.responseTimeMs && pingResult.responseTimeMs > pingTimeoutMs / 2) {
|
|
664
|
-
if (status === "ok") {
|
|
665
|
-
status = "degraded";
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
const result = { status };
|
|
670
|
-
if (responseTimeMs !== void 0) {
|
|
671
|
-
result.responseTimeMs = responseTimeMs;
|
|
672
|
-
}
|
|
673
|
-
if (error !== void 0) {
|
|
674
|
-
result.error = error;
|
|
675
|
-
}
|
|
676
|
-
return result;
|
|
677
|
-
}
|
|
678
|
-
/**
|
|
679
|
-
* Execute a ping query with timeout
|
|
680
|
-
*/
|
|
681
|
-
async executePingQuery(pool, timeoutMs) {
|
|
682
|
-
const startTime = Date.now();
|
|
683
|
-
try {
|
|
684
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
685
|
-
setTimeout(() => reject(new Error("Health check ping timeout")), timeoutMs);
|
|
686
|
-
});
|
|
687
|
-
const queryPromise = pool.query("SELECT 1");
|
|
688
|
-
await Promise.race([queryPromise, timeoutPromise]);
|
|
689
|
-
return {
|
|
690
|
-
success: true,
|
|
691
|
-
responseTimeMs: Date.now() - startTime
|
|
692
|
-
};
|
|
693
|
-
} catch (err) {
|
|
694
|
-
return {
|
|
695
|
-
success: false,
|
|
696
|
-
responseTimeMs: Date.now() - startTime,
|
|
697
|
-
error: err.message
|
|
698
|
-
};
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
/**
|
|
702
|
-
* Determine overall health status from pool health results
|
|
703
|
-
*/
|
|
704
|
-
determineOverallHealth(pools, sharedDbStatus = "ok") {
|
|
705
|
-
const unhealthyPools = pools.filter((p) => p.status === "unhealthy").length;
|
|
706
|
-
return unhealthyPools === 0 && sharedDbStatus !== "unhealthy";
|
|
707
|
-
}
|
|
708
|
-
};
|
|
709
|
-
|
|
710
|
-
// src/pool.ts
|
|
711
|
-
var PoolManager = class {
|
|
712
|
-
constructor(config) {
|
|
713
|
-
this.config = config;
|
|
714
|
-
const maxPools = config.isolation.maxPools ?? DEFAULT_CONFIG.maxPools;
|
|
715
|
-
const poolTtlMs = config.isolation.poolTtlMs ?? DEFAULT_CONFIG.poolTtlMs;
|
|
716
|
-
this.debugLogger = createDebugLogger(config.debug);
|
|
717
|
-
this.retryHandler = new RetryHandler(config.connection.retry);
|
|
718
|
-
this.poolCache = new PoolCache({
|
|
719
|
-
maxPools,
|
|
720
|
-
poolTtlMs,
|
|
721
|
-
onDispose: (schemaName, entry) => {
|
|
722
|
-
this.disposePoolEntry(entry, schemaName);
|
|
723
|
-
}
|
|
724
|
-
});
|
|
725
|
-
this.healthChecker = new HealthChecker({
|
|
726
|
-
getPoolEntries: () => this.poolCache.entries(),
|
|
727
|
-
getTenantIdBySchema: (schemaName) => this.tenantIdBySchema.get(schemaName),
|
|
728
|
-
getPoolEntry: (schemaName) => this.poolCache.get(schemaName),
|
|
729
|
-
getSchemaName: (tenantId) => this.config.isolation.schemaNameTemplate(tenantId),
|
|
730
|
-
getSharedPool: () => this.sharedPool
|
|
731
|
-
});
|
|
732
|
-
}
|
|
733
|
-
poolCache;
|
|
734
|
-
tenantIdBySchema = /* @__PURE__ */ new Map();
|
|
735
|
-
pendingConnections = /* @__PURE__ */ new Map();
|
|
736
|
-
sharedPool = null;
|
|
737
|
-
sharedDb = null;
|
|
738
|
-
sharedDbPending = null;
|
|
739
|
-
cleanupInterval = null;
|
|
740
|
-
disposed = false;
|
|
741
|
-
debugLogger;
|
|
742
|
-
retryHandler;
|
|
743
|
-
healthChecker;
|
|
744
|
-
/**
|
|
745
|
-
* Get or create a database connection for a tenant
|
|
746
|
-
*/
|
|
747
|
-
getDb(tenantId) {
|
|
748
|
-
this.ensureNotDisposed();
|
|
749
|
-
const schemaName = this.config.isolation.schemaNameTemplate(tenantId);
|
|
750
|
-
let entry = this.poolCache.get(schemaName);
|
|
751
|
-
if (!entry) {
|
|
752
|
-
entry = this.createPoolEntry(tenantId, schemaName);
|
|
753
|
-
this.poolCache.set(schemaName, entry);
|
|
754
|
-
this.tenantIdBySchema.set(schemaName, tenantId);
|
|
755
|
-
this.debugLogger.logPoolCreated(tenantId, schemaName);
|
|
756
|
-
void this.config.hooks?.onPoolCreated?.(tenantId);
|
|
757
|
-
}
|
|
758
|
-
this.poolCache.touch(schemaName);
|
|
759
|
-
return entry.db;
|
|
760
|
-
}
|
|
761
|
-
/**
|
|
762
|
-
* Get or create a database connection for a tenant with retry and validation
|
|
763
|
-
*
|
|
764
|
-
* This async version validates the connection by executing a ping query
|
|
765
|
-
* and retries on transient failures with exponential backoff.
|
|
766
|
-
*
|
|
767
|
-
* @example
|
|
768
|
-
* ```typescript
|
|
769
|
-
* // Get tenant database with automatic retry
|
|
770
|
-
* const db = await manager.getDbAsync('tenant-123');
|
|
771
|
-
*
|
|
772
|
-
* // Queries will use the validated connection
|
|
773
|
-
* const users = await db.select().from(users);
|
|
774
|
-
* ```
|
|
775
|
-
*/
|
|
776
|
-
async getDbAsync(tenantId) {
|
|
777
|
-
this.ensureNotDisposed();
|
|
778
|
-
const schemaName = this.config.isolation.schemaNameTemplate(tenantId);
|
|
779
|
-
let entry = this.poolCache.get(schemaName);
|
|
780
|
-
if (entry) {
|
|
781
|
-
this.poolCache.touch(schemaName);
|
|
782
|
-
return entry.db;
|
|
783
|
-
}
|
|
784
|
-
const pending = this.pendingConnections.get(schemaName);
|
|
785
|
-
if (pending) {
|
|
786
|
-
entry = await pending;
|
|
787
|
-
this.poolCache.touch(schemaName);
|
|
788
|
-
return entry.db;
|
|
789
|
-
}
|
|
790
|
-
const connectionPromise = this.connectWithRetry(tenantId, schemaName);
|
|
791
|
-
this.pendingConnections.set(schemaName, connectionPromise);
|
|
792
|
-
try {
|
|
793
|
-
entry = await connectionPromise;
|
|
794
|
-
this.poolCache.set(schemaName, entry);
|
|
795
|
-
this.tenantIdBySchema.set(schemaName, tenantId);
|
|
796
|
-
this.debugLogger.logPoolCreated(tenantId, schemaName);
|
|
797
|
-
void this.config.hooks?.onPoolCreated?.(tenantId);
|
|
798
|
-
this.poolCache.touch(schemaName);
|
|
799
|
-
return entry.db;
|
|
800
|
-
} finally {
|
|
801
|
-
this.pendingConnections.delete(schemaName);
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
/**
|
|
805
|
-
* Connect to a tenant database with retry logic
|
|
806
|
-
*/
|
|
807
|
-
async connectWithRetry(tenantId, schemaName) {
|
|
808
|
-
const retryConfig = this.retryHandler.getConfig();
|
|
809
|
-
const maxAttempts = retryConfig.maxAttempts;
|
|
810
|
-
const result = await this.retryHandler.withRetry(
|
|
811
|
-
async () => {
|
|
812
|
-
const entry = this.createPoolEntry(tenantId, schemaName);
|
|
813
|
-
try {
|
|
814
|
-
await entry.pool.query("SELECT 1");
|
|
815
|
-
return entry;
|
|
816
|
-
} catch (error) {
|
|
817
|
-
try {
|
|
818
|
-
await entry.pool.end();
|
|
819
|
-
} catch {
|
|
820
|
-
}
|
|
821
|
-
throw error;
|
|
822
|
-
}
|
|
823
|
-
},
|
|
824
|
-
{
|
|
825
|
-
onRetry: (attempt, error, delayMs) => {
|
|
826
|
-
this.debugLogger.logConnectionRetry(tenantId, attempt, maxAttempts, error, delayMs);
|
|
827
|
-
retryConfig.onRetry?.(attempt, error, delayMs);
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
);
|
|
831
|
-
this.debugLogger.logConnectionSuccess(tenantId, result.attempts, result.totalTimeMs);
|
|
832
|
-
return result.result;
|
|
833
|
-
}
|
|
834
|
-
/**
|
|
835
|
-
* Get or create the shared database connection
|
|
836
|
-
*/
|
|
837
|
-
getSharedDb() {
|
|
838
|
-
this.ensureNotDisposed();
|
|
839
|
-
if (!this.sharedDb) {
|
|
840
|
-
this.sharedPool = new Pool({
|
|
841
|
-
connectionString: this.config.connection.url,
|
|
842
|
-
...DEFAULT_CONFIG.poolConfig,
|
|
843
|
-
...this.config.connection.poolConfig
|
|
844
|
-
});
|
|
845
|
-
this.sharedPool.on("error", (err) => {
|
|
846
|
-
void this.config.hooks?.onError?.("shared", err);
|
|
847
|
-
});
|
|
848
|
-
this.sharedDb = drizzle(this.sharedPool, {
|
|
849
|
-
schema: this.config.schemas.shared
|
|
850
|
-
});
|
|
851
|
-
}
|
|
852
|
-
return this.sharedDb;
|
|
853
|
-
}
|
|
854
|
-
/**
|
|
855
|
-
* Get or create the shared database connection with retry and validation
|
|
856
|
-
*
|
|
857
|
-
* This async version validates the connection by executing a ping query
|
|
858
|
-
* and retries on transient failures with exponential backoff.
|
|
859
|
-
*
|
|
860
|
-
* @example
|
|
861
|
-
* ```typescript
|
|
862
|
-
* // Get shared database with automatic retry
|
|
863
|
-
* const sharedDb = await manager.getSharedDbAsync();
|
|
864
|
-
*
|
|
865
|
-
* // Queries will use the validated connection
|
|
866
|
-
* const plans = await sharedDb.select().from(plans);
|
|
867
|
-
* ```
|
|
868
|
-
*/
|
|
869
|
-
async getSharedDbAsync() {
|
|
870
|
-
this.ensureNotDisposed();
|
|
871
|
-
if (this.sharedDb) {
|
|
872
|
-
return this.sharedDb;
|
|
873
|
-
}
|
|
874
|
-
if (this.sharedDbPending) {
|
|
875
|
-
return this.sharedDbPending;
|
|
876
|
-
}
|
|
877
|
-
this.sharedDbPending = this.connectSharedWithRetry();
|
|
878
|
-
try {
|
|
879
|
-
const db = await this.sharedDbPending;
|
|
880
|
-
return db;
|
|
881
|
-
} finally {
|
|
882
|
-
this.sharedDbPending = null;
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
/**
|
|
886
|
-
* Connect to shared database with retry logic
|
|
887
|
-
*/
|
|
888
|
-
async connectSharedWithRetry() {
|
|
889
|
-
const retryConfig = this.retryHandler.getConfig();
|
|
890
|
-
const maxAttempts = retryConfig.maxAttempts;
|
|
891
|
-
const result = await this.retryHandler.withRetry(
|
|
892
|
-
async () => {
|
|
893
|
-
const pool = new Pool({
|
|
894
|
-
connectionString: this.config.connection.url,
|
|
895
|
-
...DEFAULT_CONFIG.poolConfig,
|
|
896
|
-
...this.config.connection.poolConfig
|
|
897
|
-
});
|
|
898
|
-
try {
|
|
899
|
-
await pool.query("SELECT 1");
|
|
900
|
-
pool.on("error", (err) => {
|
|
901
|
-
void this.config.hooks?.onError?.("shared", err);
|
|
902
|
-
});
|
|
903
|
-
this.sharedPool = pool;
|
|
904
|
-
this.sharedDb = drizzle(pool, {
|
|
905
|
-
schema: this.config.schemas.shared
|
|
906
|
-
});
|
|
907
|
-
return this.sharedDb;
|
|
908
|
-
} catch (error) {
|
|
909
|
-
try {
|
|
910
|
-
await pool.end();
|
|
911
|
-
} catch {
|
|
912
|
-
}
|
|
913
|
-
throw error;
|
|
914
|
-
}
|
|
915
|
-
},
|
|
916
|
-
{
|
|
917
|
-
onRetry: (attempt, error, delayMs) => {
|
|
918
|
-
this.debugLogger.logConnectionRetry("shared", attempt, maxAttempts, error, delayMs);
|
|
919
|
-
retryConfig.onRetry?.(attempt, error, delayMs);
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
);
|
|
923
|
-
this.debugLogger.logConnectionSuccess("shared", result.attempts, result.totalTimeMs);
|
|
924
|
-
return result.result;
|
|
925
|
-
}
|
|
926
|
-
/**
|
|
927
|
-
* Get schema name for a tenant
|
|
928
|
-
*/
|
|
929
|
-
getSchemaName(tenantId) {
|
|
930
|
-
return this.config.isolation.schemaNameTemplate(tenantId);
|
|
931
|
-
}
|
|
932
|
-
/**
|
|
933
|
-
* Check if a pool exists for a tenant
|
|
934
|
-
*/
|
|
935
|
-
hasPool(tenantId) {
|
|
936
|
-
const schemaName = this.config.isolation.schemaNameTemplate(tenantId);
|
|
937
|
-
return this.poolCache.has(schemaName);
|
|
938
|
-
}
|
|
939
|
-
/**
|
|
940
|
-
* Get count of active pools
|
|
941
|
-
*/
|
|
942
|
-
getPoolCount() {
|
|
943
|
-
return this.poolCache.size();
|
|
944
|
-
}
|
|
945
|
-
/**
|
|
946
|
-
* Get all active tenant IDs
|
|
947
|
-
*/
|
|
948
|
-
getActiveTenantIds() {
|
|
949
|
-
return Array.from(this.tenantIdBySchema.values());
|
|
950
|
-
}
|
|
951
|
-
/**
|
|
952
|
-
* Get the retry configuration
|
|
953
|
-
*/
|
|
954
|
-
getRetryConfig() {
|
|
955
|
-
return this.retryHandler.getConfig();
|
|
956
|
-
}
|
|
957
|
-
/**
|
|
958
|
-
* Pre-warm pools for specified tenants to reduce cold start latency
|
|
959
|
-
*
|
|
960
|
-
* Uses automatic retry with exponential backoff for connection failures.
|
|
961
|
-
*/
|
|
962
|
-
async warmup(tenantIds, options = {}) {
|
|
963
|
-
this.ensureNotDisposed();
|
|
964
|
-
const startTime = Date.now();
|
|
965
|
-
const { concurrency = 10, ping = true, onProgress } = options;
|
|
966
|
-
const results = [];
|
|
967
|
-
for (let i = 0; i < tenantIds.length; i += concurrency) {
|
|
968
|
-
const batch = tenantIds.slice(i, i + concurrency);
|
|
969
|
-
const batchResults = await Promise.all(
|
|
970
|
-
batch.map(async (tenantId) => {
|
|
971
|
-
const tenantStart = Date.now();
|
|
972
|
-
onProgress?.(tenantId, "starting");
|
|
973
|
-
try {
|
|
974
|
-
const alreadyWarm = this.hasPool(tenantId);
|
|
975
|
-
if (ping) {
|
|
976
|
-
await this.getDbAsync(tenantId);
|
|
977
|
-
} else {
|
|
978
|
-
this.getDb(tenantId);
|
|
979
|
-
}
|
|
980
|
-
const durationMs = Date.now() - tenantStart;
|
|
981
|
-
onProgress?.(tenantId, "completed");
|
|
982
|
-
this.debugLogger.logWarmup(tenantId, true, durationMs, alreadyWarm);
|
|
983
|
-
return {
|
|
984
|
-
tenantId,
|
|
985
|
-
success: true,
|
|
986
|
-
alreadyWarm,
|
|
987
|
-
durationMs
|
|
988
|
-
};
|
|
989
|
-
} catch (error) {
|
|
990
|
-
const durationMs = Date.now() - tenantStart;
|
|
991
|
-
onProgress?.(tenantId, "failed");
|
|
992
|
-
this.debugLogger.logWarmup(tenantId, false, durationMs, false);
|
|
993
|
-
return {
|
|
994
|
-
tenantId,
|
|
995
|
-
success: false,
|
|
996
|
-
alreadyWarm: false,
|
|
997
|
-
durationMs,
|
|
998
|
-
error: error.message
|
|
999
|
-
};
|
|
1000
|
-
}
|
|
1001
|
-
})
|
|
1002
|
-
);
|
|
1003
|
-
results.push(...batchResults);
|
|
1004
|
-
}
|
|
1005
|
-
return {
|
|
1006
|
-
total: results.length,
|
|
1007
|
-
succeeded: results.filter((r) => r.success).length,
|
|
1008
|
-
failed: results.filter((r) => !r.success).length,
|
|
1009
|
-
alreadyWarm: results.filter((r) => r.alreadyWarm).length,
|
|
1010
|
-
durationMs: Date.now() - startTime,
|
|
1011
|
-
details: results
|
|
1012
|
-
};
|
|
1013
|
-
}
|
|
1014
|
-
/**
|
|
1015
|
-
* Get current metrics for all pools
|
|
1016
|
-
*
|
|
1017
|
-
* Collects metrics on demand with zero overhead when not called.
|
|
1018
|
-
* Returns raw data that can be formatted for any monitoring system.
|
|
1019
|
-
*
|
|
1020
|
-
* @example
|
|
1021
|
-
* ```typescript
|
|
1022
|
-
* const metrics = manager.getMetrics();
|
|
1023
|
-
* console.log(metrics.pools.total); // 15
|
|
1024
|
-
*
|
|
1025
|
-
* // Format for Prometheus
|
|
1026
|
-
* for (const pool of metrics.pools.tenants) {
|
|
1027
|
-
* gauge.labels(pool.tenantId).set(pool.connections.idle);
|
|
1028
|
-
* }
|
|
1029
|
-
* ```
|
|
1030
|
-
*/
|
|
1031
|
-
getMetrics() {
|
|
1032
|
-
this.ensureNotDisposed();
|
|
1033
|
-
const maxPools = this.config.isolation.maxPools ?? DEFAULT_CONFIG.maxPools;
|
|
1034
|
-
const tenantMetrics = [];
|
|
1035
|
-
for (const [schemaName, entry] of this.poolCache.entries()) {
|
|
1036
|
-
const tenantId = this.tenantIdBySchema.get(schemaName) ?? schemaName;
|
|
1037
|
-
const pool = entry.pool;
|
|
1038
|
-
tenantMetrics.push({
|
|
1039
|
-
tenantId,
|
|
1040
|
-
schemaName,
|
|
1041
|
-
connections: {
|
|
1042
|
-
total: pool.totalCount,
|
|
1043
|
-
idle: pool.idleCount,
|
|
1044
|
-
waiting: pool.waitingCount
|
|
1045
|
-
},
|
|
1046
|
-
lastAccessedAt: new Date(entry.lastAccess).toISOString()
|
|
1047
|
-
});
|
|
1048
|
-
}
|
|
1049
|
-
return {
|
|
1050
|
-
pools: {
|
|
1051
|
-
total: tenantMetrics.length,
|
|
1052
|
-
maxPools,
|
|
1053
|
-
tenants: tenantMetrics
|
|
1054
|
-
},
|
|
1055
|
-
shared: {
|
|
1056
|
-
initialized: this.sharedPool !== null,
|
|
1057
|
-
connections: this.sharedPool ? {
|
|
1058
|
-
total: this.sharedPool.totalCount,
|
|
1059
|
-
idle: this.sharedPool.idleCount,
|
|
1060
|
-
waiting: this.sharedPool.waitingCount
|
|
1061
|
-
} : null
|
|
1062
|
-
},
|
|
1063
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1064
|
-
};
|
|
1065
|
-
}
|
|
1066
|
-
/**
|
|
1067
|
-
* Check health of all pools and connections
|
|
1068
|
-
*
|
|
1069
|
-
* Verifies the health of tenant pools and optionally the shared database.
|
|
1070
|
-
* Returns detailed status information for monitoring and load balancer integration.
|
|
1071
|
-
*
|
|
1072
|
-
* @example
|
|
1073
|
-
* ```typescript
|
|
1074
|
-
* // Basic health check
|
|
1075
|
-
* const health = await manager.healthCheck();
|
|
1076
|
-
* console.log(health.healthy); // true/false
|
|
1077
|
-
*
|
|
1078
|
-
* // Use with Express endpoint
|
|
1079
|
-
* app.get('/health', async (req, res) => {
|
|
1080
|
-
* const health = await manager.healthCheck();
|
|
1081
|
-
* res.status(health.healthy ? 200 : 503).json(health);
|
|
1082
|
-
* });
|
|
1083
|
-
*
|
|
1084
|
-
* // Check specific tenants only
|
|
1085
|
-
* const health = await manager.healthCheck({
|
|
1086
|
-
* tenantIds: ['tenant-1', 'tenant-2'],
|
|
1087
|
-
* ping: true,
|
|
1088
|
-
* pingTimeoutMs: 3000,
|
|
1089
|
-
* });
|
|
1090
|
-
* ```
|
|
1091
|
-
*/
|
|
1092
|
-
async healthCheck(options = {}) {
|
|
1093
|
-
this.ensureNotDisposed();
|
|
1094
|
-
return this.healthChecker.checkHealth(options);
|
|
1095
|
-
}
|
|
1096
|
-
/**
|
|
1097
|
-
* Manually evict a tenant pool
|
|
1098
|
-
*/
|
|
1099
|
-
async evictPool(tenantId, reason = "manual") {
|
|
1100
|
-
const schemaName = this.config.isolation.schemaNameTemplate(tenantId);
|
|
1101
|
-
const entry = this.poolCache.get(schemaName);
|
|
1102
|
-
if (entry) {
|
|
1103
|
-
this.debugLogger.logPoolEvicted(tenantId, schemaName, reason);
|
|
1104
|
-
this.poolCache.delete(schemaName);
|
|
1105
|
-
this.tenantIdBySchema.delete(schemaName);
|
|
1106
|
-
await this.closePool(entry.pool, tenantId);
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
/**
|
|
1110
|
-
* Start automatic cleanup of idle pools
|
|
1111
|
-
*/
|
|
1112
|
-
startCleanup() {
|
|
1113
|
-
if (this.cleanupInterval) return;
|
|
1114
|
-
const cleanupIntervalMs = DEFAULT_CONFIG.cleanupIntervalMs;
|
|
1115
|
-
this.cleanupInterval = setInterval(() => {
|
|
1116
|
-
void this.cleanupIdlePools();
|
|
1117
|
-
}, cleanupIntervalMs);
|
|
1118
|
-
this.cleanupInterval.unref();
|
|
1119
|
-
}
|
|
1120
|
-
/**
|
|
1121
|
-
* Stop automatic cleanup
|
|
1122
|
-
*/
|
|
1123
|
-
stopCleanup() {
|
|
1124
|
-
if (this.cleanupInterval) {
|
|
1125
|
-
clearInterval(this.cleanupInterval);
|
|
1126
|
-
this.cleanupInterval = null;
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
/**
|
|
1130
|
-
* Dispose all pools and cleanup resources
|
|
1131
|
-
*/
|
|
1132
|
-
async dispose() {
|
|
1133
|
-
if (this.disposed) return;
|
|
1134
|
-
this.disposed = true;
|
|
1135
|
-
this.stopCleanup();
|
|
1136
|
-
const closePromises = [];
|
|
1137
|
-
for (const [schemaName, entry] of this.poolCache.entries()) {
|
|
1138
|
-
const tenantId = this.tenantIdBySchema.get(schemaName);
|
|
1139
|
-
closePromises.push(this.closePool(entry.pool, tenantId ?? schemaName));
|
|
1140
|
-
}
|
|
1141
|
-
await this.poolCache.clear();
|
|
1142
|
-
this.tenantIdBySchema.clear();
|
|
1143
|
-
if (this.sharedPool) {
|
|
1144
|
-
closePromises.push(this.closePool(this.sharedPool, "shared"));
|
|
1145
|
-
this.sharedPool = null;
|
|
1146
|
-
this.sharedDb = null;
|
|
1147
|
-
}
|
|
1148
|
-
await Promise.all(closePromises);
|
|
1149
|
-
}
|
|
1150
|
-
/**
|
|
1151
|
-
* Create a new pool entry for a tenant
|
|
1152
|
-
*/
|
|
1153
|
-
createPoolEntry(tenantId, schemaName) {
|
|
1154
|
-
const pool = new Pool({
|
|
1155
|
-
connectionString: this.config.connection.url,
|
|
1156
|
-
...DEFAULT_CONFIG.poolConfig,
|
|
1157
|
-
...this.config.connection.poolConfig,
|
|
1158
|
-
options: `-c search_path=${schemaName},public`
|
|
1159
|
-
});
|
|
1160
|
-
pool.on("error", async (err) => {
|
|
1161
|
-
this.debugLogger.logPoolError(tenantId, err);
|
|
1162
|
-
void this.config.hooks?.onError?.(tenantId, err);
|
|
1163
|
-
await this.evictPool(tenantId, "error");
|
|
1164
|
-
});
|
|
1165
|
-
const db = drizzle(pool, {
|
|
1166
|
-
schema: this.config.schemas.tenant
|
|
1167
|
-
});
|
|
1168
|
-
return {
|
|
1169
|
-
db,
|
|
1170
|
-
pool,
|
|
1171
|
-
lastAccess: Date.now(),
|
|
1172
|
-
schemaName
|
|
1173
|
-
};
|
|
1174
|
-
}
|
|
1175
|
-
/**
|
|
1176
|
-
* Dispose a pool entry (called by LRU cache)
|
|
1177
|
-
*/
|
|
1178
|
-
disposePoolEntry(entry, schemaName) {
|
|
1179
|
-
const tenantId = this.tenantIdBySchema.get(schemaName);
|
|
1180
|
-
this.tenantIdBySchema.delete(schemaName);
|
|
1181
|
-
if (tenantId) {
|
|
1182
|
-
this.debugLogger.logPoolEvicted(tenantId, schemaName, "lru_eviction");
|
|
1183
|
-
}
|
|
1184
|
-
void this.closePool(entry.pool, tenantId ?? schemaName).then(() => {
|
|
1185
|
-
if (tenantId) {
|
|
1186
|
-
void this.config.hooks?.onPoolEvicted?.(tenantId);
|
|
1187
|
-
}
|
|
1188
|
-
});
|
|
1189
|
-
}
|
|
1190
|
-
/**
|
|
1191
|
-
* Close a pool gracefully
|
|
1192
|
-
*/
|
|
1193
|
-
async closePool(pool, identifier) {
|
|
1194
|
-
try {
|
|
1195
|
-
await pool.end();
|
|
1196
|
-
} catch (error) {
|
|
1197
|
-
void this.config.hooks?.onError?.(identifier, error);
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
/**
|
|
1201
|
-
* Cleanup pools that have been idle for too long
|
|
1202
|
-
*/
|
|
1203
|
-
async cleanupIdlePools() {
|
|
1204
|
-
const evictedSchemas = await this.poolCache.evictExpired();
|
|
1205
|
-
for (const schemaName of evictedSchemas) {
|
|
1206
|
-
const tenantId = this.tenantIdBySchema.get(schemaName);
|
|
1207
|
-
if (tenantId) {
|
|
1208
|
-
this.debugLogger.logPoolEvicted(tenantId, schemaName, "ttl_expired");
|
|
1209
|
-
this.tenantIdBySchema.delete(schemaName);
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
/**
|
|
1214
|
-
* Ensure the manager hasn't been disposed
|
|
1215
|
-
*/
|
|
1216
|
-
ensureNotDisposed() {
|
|
1217
|
-
if (this.disposed) {
|
|
1218
|
-
throw new Error("[drizzle-multitenant] TenantManager has been disposed");
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
};
|
|
1222
|
-
|
|
1223
|
-
// src/manager.ts
|
|
1224
|
-
function createTenantManager(config) {
|
|
1225
|
-
const poolManager = new PoolManager(config);
|
|
1226
|
-
poolManager.startCleanup();
|
|
1227
|
-
return {
|
|
1228
|
-
getDb(tenantId) {
|
|
1229
|
-
return poolManager.getDb(tenantId);
|
|
1230
|
-
},
|
|
1231
|
-
async getDbAsync(tenantId) {
|
|
1232
|
-
return poolManager.getDbAsync(tenantId);
|
|
1233
|
-
},
|
|
1234
|
-
getSharedDb() {
|
|
1235
|
-
return poolManager.getSharedDb();
|
|
1236
|
-
},
|
|
1237
|
-
async getSharedDbAsync() {
|
|
1238
|
-
return poolManager.getSharedDbAsync();
|
|
1239
|
-
},
|
|
1240
|
-
getSchemaName(tenantId) {
|
|
1241
|
-
return poolManager.getSchemaName(tenantId);
|
|
1242
|
-
},
|
|
1243
|
-
hasPool(tenantId) {
|
|
1244
|
-
return poolManager.hasPool(tenantId);
|
|
1245
|
-
},
|
|
1246
|
-
getPoolCount() {
|
|
1247
|
-
return poolManager.getPoolCount();
|
|
1248
|
-
},
|
|
1249
|
-
getActiveTenantIds() {
|
|
1250
|
-
return poolManager.getActiveTenantIds();
|
|
1251
|
-
},
|
|
1252
|
-
getRetryConfig() {
|
|
1253
|
-
return poolManager.getRetryConfig();
|
|
1254
|
-
},
|
|
1255
|
-
async evictPool(tenantId) {
|
|
1256
|
-
await poolManager.evictPool(tenantId);
|
|
1257
|
-
},
|
|
1258
|
-
async warmup(tenantIds, options) {
|
|
1259
|
-
return poolManager.warmup(tenantIds, options);
|
|
1260
|
-
},
|
|
1261
|
-
async healthCheck(options) {
|
|
1262
|
-
return poolManager.healthCheck(options);
|
|
1263
|
-
},
|
|
1264
|
-
getMetrics() {
|
|
1265
|
-
return poolManager.getMetrics();
|
|
1266
|
-
},
|
|
1267
|
-
async dispose() {
|
|
1268
|
-
await poolManager.dispose();
|
|
1269
|
-
}
|
|
1270
|
-
};
|
|
1271
|
-
}
|
|
1272
|
-
function createTenantContext(manager) {
|
|
1273
|
-
const storage = new AsyncLocalStorage();
|
|
1274
|
-
function getTenantOrNull() {
|
|
1275
|
-
return storage.getStore();
|
|
1276
|
-
}
|
|
1277
|
-
function getTenant() {
|
|
1278
|
-
const context = getTenantOrNull();
|
|
1279
|
-
if (!context) {
|
|
1280
|
-
throw new Error(
|
|
1281
|
-
"[drizzle-multitenant] No tenant context found. Make sure you are calling this within runWithTenant()."
|
|
1282
|
-
);
|
|
1283
|
-
}
|
|
1284
|
-
return context;
|
|
1285
|
-
}
|
|
1286
|
-
function getTenantId() {
|
|
1287
|
-
return getTenant().tenantId;
|
|
1288
|
-
}
|
|
1289
|
-
function getTenantDb() {
|
|
1290
|
-
const tenantId = getTenantId();
|
|
1291
|
-
return manager.getDb(tenantId);
|
|
1292
|
-
}
|
|
1293
|
-
function getSharedDb() {
|
|
1294
|
-
return manager.getSharedDb();
|
|
1295
|
-
}
|
|
1296
|
-
function isInTenantContext() {
|
|
1297
|
-
return getTenantOrNull() !== void 0;
|
|
1298
|
-
}
|
|
1299
|
-
function runWithTenant(context, callback) {
|
|
1300
|
-
if (!context.tenantId) {
|
|
1301
|
-
throw new Error("[drizzle-multitenant] tenantId is required in context");
|
|
1302
|
-
}
|
|
1303
|
-
return storage.run(context, callback);
|
|
1304
|
-
}
|
|
1305
|
-
return {
|
|
1306
|
-
runWithTenant,
|
|
1307
|
-
getTenant,
|
|
1308
|
-
getTenantOrNull,
|
|
1309
|
-
getTenantId,
|
|
1310
|
-
getTenantDb,
|
|
1311
|
-
getSharedDb,
|
|
1312
|
-
isInTenantContext
|
|
1313
|
-
};
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
// src/migrator/table-format.ts
|
|
1317
|
-
async function detectTableFormat(pool, schemaName, tableName) {
|
|
1318
|
-
const tableExists = await pool.query(
|
|
1319
|
-
`SELECT EXISTS (
|
|
1
|
+
import {Pool}from'pg';import {drizzle}from'drizzle-orm/node-postgres';import {LRUCache}from'lru-cache';import {AsyncLocalStorage}from'async_hooks';import {readdir,readFile}from'fs/promises';import {join,basename}from'path';import {createHash}from'crypto';import {existsSync}from'fs';import {sql,getTableName}from'drizzle-orm';function Se(c){return be(c),c}function be(c){if(!c.connection.url)throw new Error("[drizzle-multitenant] connection.url is required");if(!c.isolation.strategy)throw new Error("[drizzle-multitenant] isolation.strategy is required");if(c.isolation.strategy!=="schema")throw new Error(`[drizzle-multitenant] isolation.strategy "${c.isolation.strategy}" is not yet supported. Only "schema" is currently available.`);if(!c.isolation.schemaNameTemplate)throw new Error("[drizzle-multitenant] isolation.schemaNameTemplate is required");if(typeof c.isolation.schemaNameTemplate!="function")throw new Error("[drizzle-multitenant] isolation.schemaNameTemplate must be a function");if(!c.schemas.tenant)throw new Error("[drizzle-multitenant] schemas.tenant is required");if(c.isolation.maxPools!==void 0&&c.isolation.maxPools<1)throw new Error("[drizzle-multitenant] isolation.maxPools must be at least 1");if(c.isolation.poolTtlMs!==void 0&&c.isolation.poolTtlMs<0)throw new Error("[drizzle-multitenant] isolation.poolTtlMs must be non-negative");if(c.connection.retry){let e=c.connection.retry;if(e.maxAttempts!==void 0&&e.maxAttempts<1)throw new Error("[drizzle-multitenant] connection.retry.maxAttempts must be at least 1");if(e.initialDelayMs!==void 0&&e.initialDelayMs<0)throw new Error("[drizzle-multitenant] connection.retry.initialDelayMs must be non-negative");if(e.maxDelayMs!==void 0&&e.maxDelayMs<0)throw new Error("[drizzle-multitenant] connection.retry.maxDelayMs must be non-negative");if(e.backoffMultiplier!==void 0&&e.backoffMultiplier<1)throw new Error("[drizzle-multitenant] connection.retry.backoffMultiplier must be at least 1");if(e.initialDelayMs!==void 0&&e.maxDelayMs!==void 0&&e.initialDelayMs>e.maxDelayMs)throw new Error("[drizzle-multitenant] connection.retry.initialDelayMs cannot be greater than maxDelayMs")}}var T={maxPools:50,poolTtlMs:36e5,cleanupIntervalMs:6e4,poolConfig:{max:10,idleTimeoutMillis:3e4,connectionTimeoutMillis:5e3},retry:{maxAttempts:3,initialDelayMs:100,maxDelayMs:5e3,backoffMultiplier:2,jitter:true}};var R="[drizzle-multitenant]";var W=class{enabled;logQueries;logPoolEvents;slowQueryThreshold;logger;constructor(e){this.enabled=e?.enabled??false,this.logQueries=e?.logQueries??true,this.logPoolEvents=e?.logPoolEvents??true,this.slowQueryThreshold=e?.slowQueryThreshold??1e3,this.logger=e?.logger??this.defaultLogger;}isEnabled(){return this.enabled}logQuery(e,t,n){if(!this.enabled||!this.logQueries)return;let a=n>=this.slowQueryThreshold,s={type:a?"slow_query":"query",tenantId:e,query:this.truncateQuery(t),durationMs:n};a?this.logger(`${R} tenant=${e} SLOW_QUERY duration=${n}ms query="${this.truncateQuery(t)}"`,s):this.logger(`${R} tenant=${e} query="${this.truncateQuery(t)}" duration=${n}ms`,s);}logPoolCreated(e,t){if(!this.enabled||!this.logPoolEvents)return;let n={type:"pool_created",tenantId:e,schemaName:t};this.logger(`${R} tenant=${e} POOL_CREATED schema=${t}`,n);}logPoolEvicted(e,t,n){if(!this.enabled||!this.logPoolEvents)return;let a={type:"pool_evicted",tenantId:e,schemaName:t,metadata:n?{reason:n}:void 0},r=n?` reason=${n}`:"";this.logger(`${R} tenant=${e} POOL_EVICTED schema=${t}${r}`,a);}logPoolError(e,t){if(!this.enabled||!this.logPoolEvents)return;let n={type:"pool_error",tenantId:e,error:t.message};this.logger(`${R} tenant=${e} POOL_ERROR error="${t.message}"`,n);}logWarmup(e,t,n,a){if(!this.enabled||!this.logPoolEvents)return;let r={type:"warmup",tenantId:e,durationMs:n,metadata:{success:t,alreadyWarm:a}},s=a?"already_warm":t?"success":"failed";this.logger(`${R} tenant=${e} WARMUP status=${s} duration=${n}ms`,r);}logConnectionRetry(e,t,n,a,r){if(!this.enabled||!this.logPoolEvents)return;let s={type:"connection_retry",tenantId:e,error:a.message,metadata:{attempt:t,maxAttempts:n,delayMs:r}};this.logger(`${R} tenant=${e} CONNECTION_RETRY attempt=${t}/${n} delay=${r}ms error="${a.message}"`,s);}logConnectionSuccess(e,t,n){if(!this.enabled||!this.logPoolEvents)return;let a={type:"pool_created",tenantId:e,durationMs:n,metadata:{attempts:t}};t>1&&this.logger(`${R} tenant=${e} CONNECTION_SUCCESS attempts=${t} totalTime=${n}ms`,a);}log(e,t){this.enabled&&this.logger(`${R} ${e}`,t);}defaultLogger(e,t){console.log(e);}truncateQuery(e,t=100){let n=e.replace(/\s+/g," ").trim();return n.length<=t?n:n.substring(0,t-3)+"..."}};function G(c){return new W(c)}var x=class{cache;poolTtlMs;onDispose;constructor(e){this.poolTtlMs=e.poolTtlMs,this.onDispose=e.onDispose,this.cache=new LRUCache({max:e.maxPools,dispose:(t,n)=>{this.handleDispose(n,t);},noDisposeOnSet:true});}get(e){return this.cache.get(e)}set(e,t){this.cache.set(e,t);}has(e){return this.cache.has(e)}delete(e){return this.cache.delete(e)}size(){return this.cache.size}keys(){return Array.from(this.cache.keys())}*entries(){for(let[e,t]of this.cache.entries())yield [e,t];}async clear(){this.cache.clear(),await Promise.resolve();}evictLRU(){let e=Array.from(this.cache.keys());if(e.length===0)return;let t=e[e.length-1];return this.cache.delete(t),t}async evictExpired(){if(!this.poolTtlMs)return [];let e=Date.now(),t=[];for(let[n,a]of this.cache.entries())e-a.lastAccess>this.poolTtlMs&&t.push(n);for(let n of t)this.cache.delete(n);return t}touch(e){let t=this.cache.get(e);t&&(t.lastAccess=Date.now());}getMaxPools(){return this.cache.max}getTtlMs(){return this.poolTtlMs}isExpired(e){return this.poolTtlMs?Date.now()-e.lastAccess>this.poolTtlMs:false}async handleDispose(e,t){this.onDispose&&await this.onDispose(e,t);}};function J(c){let e=c.message.toLowerCase();return !!(e.includes("econnrefused")||e.includes("econnreset")||e.includes("etimedout")||e.includes("enotfound")||e.includes("connection refused")||e.includes("connection reset")||e.includes("connection terminated")||e.includes("connection timed out")||e.includes("timeout expired")||e.includes("socket hang up")||e.includes("too many connections")||e.includes("sorry, too many clients")||e.includes("the database system is starting up")||e.includes("the database system is shutting down")||e.includes("server closed the connection unexpectedly")||e.includes("could not connect to server")||e.includes("ssl connection")||e.includes("ssl handshake"))}function Me(c){return new Promise(e=>setTimeout(e,c))}var E=class{config;constructor(e){this.config={maxAttempts:e?.maxAttempts??T.retry.maxAttempts,initialDelayMs:e?.initialDelayMs??T.retry.initialDelayMs,maxDelayMs:e?.maxDelayMs??T.retry.maxDelayMs,backoffMultiplier:e?.backoffMultiplier??T.retry.backoffMultiplier,jitter:e?.jitter??T.retry.jitter,isRetryable:e?.isRetryable??J,onRetry:e?.onRetry};}async withRetry(e,t){let n=t?{...this.config,...t}:this.config,a=Date.now(),r=null;for(let s=0;s<n.maxAttempts;s++)try{return {result:await e(),attempts:s+1,totalTimeMs:Date.now()-a}}catch(i){r=i;let o=s>=n.maxAttempts-1,m=n.isRetryable??this.isRetryable;if(o||!m(r))throw r;let l=this.calculateDelay(s,n);n.onRetry?.(s+1,r,l),await Me(l);}throw r??new Error("Retry failed with no error")}calculateDelay(e,t){let n=t?{...this.config,...t}:this.config,a=n.initialDelayMs*Math.pow(n.backoffMultiplier,e),r=Math.min(a,n.maxDelayMs);if(n.jitter){let s=1+Math.random()*.25;return Math.floor(r*s)}return Math.floor(r)}isRetryable(e){return (this.config.isRetryable??J)(e)}getConfig(){return {...this.config}}getMaxAttempts(){return this.config.maxAttempts}};var P=class{constructor(e){this.deps=e;}async checkHealth(e={}){let t=Date.now(),{ping:n=true,pingTimeoutMs:a=5e3,includeShared:r=true,tenantIds:s}=e,i=[],o="ok",m,l,h=this.getPoolsToCheck(s).map(async({schemaName:S,tenantId:N,entry:M})=>this.checkPoolHealth(N,S,M,n,a));i.push(...await Promise.all(h));let d=this.deps.getSharedPool();if(r&&d){let S=await this.checkSharedDbHealth(d,n,a);o=S.status,m=S.responseTimeMs,l=S.error;}let g=i.filter(S=>S.status==="degraded").length,w=i.filter(S=>S.status==="unhealthy").length,b={healthy:w===0&&o!=="unhealthy",pools:i,sharedDb:o,totalPools:i.length,degradedPools:g,unhealthyPools:w,timestamp:new Date().toISOString(),durationMs:Date.now()-t};return m!==void 0&&(b.sharedDbResponseTimeMs=m),l!==void 0&&(b.sharedDbError=l),b}getPoolsToCheck(e){let t=[];if(e&&e.length>0)for(let n of e){let a=this.deps.getSchemaName(n),r=this.deps.getPoolEntry(a);r&&t.push({schemaName:a,tenantId:n,entry:r});}else for(let[n,a]of this.deps.getPoolEntries()){let r=this.deps.getTenantIdBySchema(n)??n;t.push({schemaName:n,tenantId:r,entry:a});}return t}async checkPoolHealth(e,t,n,a,r){let s=n.pool,i=s.totalCount,o=s.idleCount,m=s.waitingCount,l="ok",u,h;if(m>0&&(l="degraded"),a){let g=await this.executePingQuery(s,r);u=g.responseTimeMs,g.success?g.responseTimeMs&&g.responseTimeMs>r/2&&l==="ok"&&(l="degraded"):(l="unhealthy",h=g.error);}let d={tenantId:e,schemaName:t,status:l,totalConnections:i,idleConnections:o,waitingRequests:m};return u!==void 0&&(d.responseTimeMs=u),h!==void 0&&(d.error=h),d}async checkSharedDbHealth(e,t,n){let a="ok",r,s;if(e.waitingCount>0&&(a="degraded"),t){let m=await this.executePingQuery(e,n);r=m.responseTimeMs,m.success?m.responseTimeMs&&m.responseTimeMs>n/2&&a==="ok"&&(a="degraded"):(a="unhealthy",s=m.error);}let o={status:a};return r!==void 0&&(o.responseTimeMs=r),s!==void 0&&(o.error=s),o}async executePingQuery(e,t){let n=Date.now();try{let a=new Promise((s,i)=>{setTimeout(()=>i(new Error("Health check ping timeout")),t);}),r=e.query("SELECT 1");return await Promise.race([r,a]),{success:!0,responseTimeMs:Date.now()-n}}catch(a){return {success:false,responseTimeMs:Date.now()-n,error:a.message}}}determineOverallHealth(e,t="ok"){return e.filter(a=>a.status==="unhealthy").length===0&&t!=="unhealthy"}};var k=class{constructor(e){this.config=e;let t=e.isolation.maxPools??T.maxPools,n=e.isolation.poolTtlMs??T.poolTtlMs;this.debugLogger=G(e.debug),this.retryHandler=new E(e.connection.retry),this.poolCache=new x({maxPools:t,poolTtlMs:n,onDispose:(a,r)=>{this.disposePoolEntry(r,a);}}),this.healthChecker=new P({getPoolEntries:()=>this.poolCache.entries(),getTenantIdBySchema:a=>this.tenantIdBySchema.get(a),getPoolEntry:a=>this.poolCache.get(a),getSchemaName:a=>this.config.isolation.schemaNameTemplate(a),getSharedPool:()=>this.sharedPool});}poolCache;tenantIdBySchema=new Map;pendingConnections=new Map;sharedPool=null;sharedDb=null;sharedDbPending=null;cleanupInterval=null;disposed=false;debugLogger;retryHandler;healthChecker;getDb(e){this.ensureNotDisposed();let t=this.config.isolation.schemaNameTemplate(e),n=this.poolCache.get(t);return n||(n=this.createPoolEntry(e,t),this.poolCache.set(t,n),this.tenantIdBySchema.set(t,e),this.debugLogger.logPoolCreated(e,t),this.config.hooks?.onPoolCreated?.(e)),this.poolCache.touch(t),n.db}async getDbAsync(e){this.ensureNotDisposed();let t=this.config.isolation.schemaNameTemplate(e),n=this.poolCache.get(t);if(n)return this.poolCache.touch(t),n.db;let a=this.pendingConnections.get(t);if(a)return n=await a,this.poolCache.touch(t),n.db;let r=this.connectWithRetry(e,t);this.pendingConnections.set(t,r);try{return n=await r,this.poolCache.set(t,n),this.tenantIdBySchema.set(t,e),this.debugLogger.logPoolCreated(e,t),this.config.hooks?.onPoolCreated?.(e),this.poolCache.touch(t),n.db}finally{this.pendingConnections.delete(t);}}async connectWithRetry(e,t){let n=this.retryHandler.getConfig(),a=n.maxAttempts,r=await this.retryHandler.withRetry(async()=>{let s=this.createPoolEntry(e,t);try{return await s.pool.query("SELECT 1"),s}catch(i){try{await s.pool.end();}catch{}throw i}},{onRetry:(s,i,o)=>{this.debugLogger.logConnectionRetry(e,s,a,i,o),n.onRetry?.(s,i,o);}});return this.debugLogger.logConnectionSuccess(e,r.attempts,r.totalTimeMs),r.result}getSharedDb(){return this.ensureNotDisposed(),this.sharedDb||(this.sharedPool=new Pool({connectionString:this.config.connection.url,...T.poolConfig,...this.config.connection.poolConfig}),this.sharedPool.on("error",e=>{this.config.hooks?.onError?.("shared",e);}),this.sharedDb=drizzle(this.sharedPool,{schema:this.config.schemas.shared})),this.sharedDb}async getSharedDbAsync(){if(this.ensureNotDisposed(),this.sharedDb)return this.sharedDb;if(this.sharedDbPending)return this.sharedDbPending;this.sharedDbPending=this.connectSharedWithRetry();try{return await this.sharedDbPending}finally{this.sharedDbPending=null;}}async connectSharedWithRetry(){let e=this.retryHandler.getConfig(),t=e.maxAttempts,n=await this.retryHandler.withRetry(async()=>{let a=new Pool({connectionString:this.config.connection.url,...T.poolConfig,...this.config.connection.poolConfig});try{return await a.query("SELECT 1"),a.on("error",r=>{this.config.hooks?.onError?.("shared",r);}),this.sharedPool=a,this.sharedDb=drizzle(a,{schema:this.config.schemas.shared}),this.sharedDb}catch(r){try{await a.end();}catch{}throw r}},{onRetry:(a,r,s)=>{this.debugLogger.logConnectionRetry("shared",a,t,r,s),e.onRetry?.(a,r,s);}});return this.debugLogger.logConnectionSuccess("shared",n.attempts,n.totalTimeMs),n.result}getSchemaName(e){return this.config.isolation.schemaNameTemplate(e)}hasPool(e){let t=this.config.isolation.schemaNameTemplate(e);return this.poolCache.has(t)}getPoolCount(){return this.poolCache.size()}getActiveTenantIds(){return Array.from(this.tenantIdBySchema.values())}getRetryConfig(){return this.retryHandler.getConfig()}async warmup(e,t={}){this.ensureNotDisposed();let n=Date.now(),{concurrency:a=10,ping:r=true,onProgress:s}=t,i=[];for(let o=0;o<e.length;o+=a){let m=e.slice(o,o+a),l=await Promise.all(m.map(async u=>{let h=Date.now();s?.(u,"starting");try{let d=this.hasPool(u);r?await this.getDbAsync(u):this.getDb(u);let g=Date.now()-h;return s?.(u,"completed"),this.debugLogger.logWarmup(u,!0,g,d),{tenantId:u,success:!0,alreadyWarm:d,durationMs:g}}catch(d){let g=Date.now()-h;return s?.(u,"failed"),this.debugLogger.logWarmup(u,false,g,false),{tenantId:u,success:false,alreadyWarm:false,durationMs:g,error:d.message}}}));i.push(...l);}return {total:i.length,succeeded:i.filter(o=>o.success).length,failed:i.filter(o=>!o.success).length,alreadyWarm:i.filter(o=>o.alreadyWarm).length,durationMs:Date.now()-n,details:i}}getMetrics(){this.ensureNotDisposed();let e=this.config.isolation.maxPools??T.maxPools,t=[];for(let[n,a]of this.poolCache.entries()){let r=this.tenantIdBySchema.get(n)??n,s=a.pool;t.push({tenantId:r,schemaName:n,connections:{total:s.totalCount,idle:s.idleCount,waiting:s.waitingCount},lastAccessedAt:new Date(a.lastAccess).toISOString()});}return {pools:{total:t.length,maxPools:e,tenants:t},shared:{initialized:this.sharedPool!==null,connections:this.sharedPool?{total:this.sharedPool.totalCount,idle:this.sharedPool.idleCount,waiting:this.sharedPool.waitingCount}:null},timestamp:new Date().toISOString()}}async healthCheck(e={}){return this.ensureNotDisposed(),this.healthChecker.checkHealth(e)}async evictPool(e,t="manual"){let n=this.config.isolation.schemaNameTemplate(e),a=this.poolCache.get(n);a&&(this.debugLogger.logPoolEvicted(e,n,t),this.poolCache.delete(n),this.tenantIdBySchema.delete(n),await this.closePool(a.pool,e));}startCleanup(){if(this.cleanupInterval)return;let e=T.cleanupIntervalMs;this.cleanupInterval=setInterval(()=>{this.cleanupIdlePools();},e),this.cleanupInterval.unref();}stopCleanup(){this.cleanupInterval&&(clearInterval(this.cleanupInterval),this.cleanupInterval=null);}async dispose(){if(this.disposed)return;this.disposed=true,this.stopCleanup();let e=[];for(let[t,n]of this.poolCache.entries()){let a=this.tenantIdBySchema.get(t);e.push(this.closePool(n.pool,a??t));}await this.poolCache.clear(),this.tenantIdBySchema.clear(),this.sharedPool&&(e.push(this.closePool(this.sharedPool,"shared")),this.sharedPool=null,this.sharedDb=null),await Promise.all(e);}createPoolEntry(e,t){let n=new Pool({connectionString:this.config.connection.url,...T.poolConfig,...this.config.connection.poolConfig,options:`-c search_path=${t},public`});return n.on("error",async r=>{this.debugLogger.logPoolError(e,r),this.config.hooks?.onError?.(e,r),await this.evictPool(e,"error");}),{db:drizzle(n,{schema:this.config.schemas.tenant}),pool:n,lastAccess:Date.now(),schemaName:t}}disposePoolEntry(e,t){let n=this.tenantIdBySchema.get(t);this.tenantIdBySchema.delete(t),n&&this.debugLogger.logPoolEvicted(n,t,"lru_eviction"),this.closePool(e.pool,n??t).then(()=>{n&&this.config.hooks?.onPoolEvicted?.(n);});}async closePool(e,t){try{await e.end();}catch(n){this.config.hooks?.onError?.(t,n);}}async cleanupIdlePools(){let e=await this.poolCache.evictExpired();for(let t of e){let n=this.tenantIdBySchema.get(t);n&&(this.debugLogger.logPoolEvicted(n,t,"ttl_expired"),this.tenantIdBySchema.delete(t));}}ensureNotDisposed(){if(this.disposed)throw new Error("[drizzle-multitenant] TenantManager has been disposed")}};function Re(c){let e=new k(c);return e.startCleanup(),{getDb(t){return e.getDb(t)},async getDbAsync(t){return e.getDbAsync(t)},getSharedDb(){return e.getSharedDb()},async getSharedDbAsync(){return e.getSharedDbAsync()},getSchemaName(t){return e.getSchemaName(t)},hasPool(t){return e.hasPool(t)},getPoolCount(){return e.getPoolCount()},getActiveTenantIds(){return e.getActiveTenantIds()},getRetryConfig(){return e.getRetryConfig()},async evictPool(t){await e.evictPool(t);},async warmup(t,n){return e.warmup(t,n)},async healthCheck(t){return e.healthCheck(t)},getMetrics(){return e.getMetrics()},async dispose(){await e.dispose();}}}function De(c){let e=new AsyncLocalStorage;function t(){return e.getStore()}function n(){let m=t();if(!m)throw new Error("[drizzle-multitenant] No tenant context found. Make sure you are calling this within runWithTenant().");return m}function a(){return n().tenantId}function r(){let m=a();return c.getDb(m)}function s(){return c.getSharedDb()}function i(){return t()!==void 0}function o(m,l){if(!m.tenantId)throw new Error("[drizzle-multitenant] tenantId is required in context");return e.run(m,l)}return {runWithTenant:o,getTenant:n,getTenantOrNull:t,getTenantId:a,getTenantDb:r,getSharedDb:s,isInTenantContext:i}}async function Y(c,e,t){if(!(await c.query(`SELECT EXISTS (
|
|
1320
2
|
SELECT 1 FROM information_schema.tables
|
|
1321
3
|
WHERE table_schema = $1 AND table_name = $2
|
|
1322
|
-
) as exists`,
|
|
1323
|
-
[schemaName, tableName]
|
|
1324
|
-
);
|
|
1325
|
-
if (!tableExists.rows[0]?.exists) {
|
|
1326
|
-
return null;
|
|
1327
|
-
}
|
|
1328
|
-
const columnsResult = await pool.query(
|
|
1329
|
-
`SELECT column_name, data_type
|
|
4
|
+
) as exists`,[e,t])).rows[0]?.exists)return null;let a=await c.query(`SELECT column_name, data_type
|
|
1330
5
|
FROM information_schema.columns
|
|
1331
|
-
WHERE table_schema = $1 AND table_name = $2`,
|
|
1332
|
-
|
|
1333
|
-
);
|
|
1334
|
-
const columnMap = new Map(
|
|
1335
|
-
columnsResult.rows.map((r) => [r.column_name, r.data_type])
|
|
1336
|
-
);
|
|
1337
|
-
if (columnMap.has("name")) {
|
|
1338
|
-
return {
|
|
1339
|
-
format: "name",
|
|
1340
|
-
tableName,
|
|
1341
|
-
columns: {
|
|
1342
|
-
identifier: "name",
|
|
1343
|
-
timestamp: columnMap.has("applied_at") ? "applied_at" : "created_at",
|
|
1344
|
-
timestampType: "timestamp"
|
|
1345
|
-
}
|
|
1346
|
-
};
|
|
1347
|
-
}
|
|
1348
|
-
if (columnMap.has("hash")) {
|
|
1349
|
-
const createdAtType = columnMap.get("created_at");
|
|
1350
|
-
if (createdAtType === "bigint") {
|
|
1351
|
-
return {
|
|
1352
|
-
format: "drizzle-kit",
|
|
1353
|
-
tableName,
|
|
1354
|
-
columns: {
|
|
1355
|
-
identifier: "hash",
|
|
1356
|
-
timestamp: "created_at",
|
|
1357
|
-
timestampType: "bigint"
|
|
1358
|
-
}
|
|
1359
|
-
};
|
|
1360
|
-
}
|
|
1361
|
-
return {
|
|
1362
|
-
format: "hash",
|
|
1363
|
-
tableName,
|
|
1364
|
-
columns: {
|
|
1365
|
-
identifier: "hash",
|
|
1366
|
-
timestamp: "created_at",
|
|
1367
|
-
timestampType: "timestamp"
|
|
1368
|
-
}
|
|
1369
|
-
};
|
|
1370
|
-
}
|
|
1371
|
-
return null;
|
|
1372
|
-
}
|
|
1373
|
-
function getFormatConfig(format, tableName = "__drizzle_migrations") {
|
|
1374
|
-
switch (format) {
|
|
1375
|
-
case "name":
|
|
1376
|
-
return {
|
|
1377
|
-
format: "name",
|
|
1378
|
-
tableName,
|
|
1379
|
-
columns: {
|
|
1380
|
-
identifier: "name",
|
|
1381
|
-
timestamp: "applied_at",
|
|
1382
|
-
timestampType: "timestamp"
|
|
1383
|
-
}
|
|
1384
|
-
};
|
|
1385
|
-
case "hash":
|
|
1386
|
-
return {
|
|
1387
|
-
format: "hash",
|
|
1388
|
-
tableName,
|
|
1389
|
-
columns: {
|
|
1390
|
-
identifier: "hash",
|
|
1391
|
-
timestamp: "created_at",
|
|
1392
|
-
timestampType: "timestamp"
|
|
1393
|
-
}
|
|
1394
|
-
};
|
|
1395
|
-
case "drizzle-kit":
|
|
1396
|
-
return {
|
|
1397
|
-
format: "drizzle-kit",
|
|
1398
|
-
tableName,
|
|
1399
|
-
columns: {
|
|
1400
|
-
identifier: "hash",
|
|
1401
|
-
timestamp: "created_at",
|
|
1402
|
-
timestampType: "bigint"
|
|
1403
|
-
}
|
|
1404
|
-
};
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
var DEFAULT_MIGRATIONS_TABLE = "__drizzle_migrations";
|
|
1408
|
-
var SchemaManager = class {
|
|
1409
|
-
constructor(config, migrationsTable) {
|
|
1410
|
-
this.config = config;
|
|
1411
|
-
this.migrationsTable = migrationsTable ?? DEFAULT_MIGRATIONS_TABLE;
|
|
1412
|
-
}
|
|
1413
|
-
migrationsTable;
|
|
1414
|
-
/**
|
|
1415
|
-
* Get the schema name for a tenant ID using the configured template
|
|
1416
|
-
*
|
|
1417
|
-
* @param tenantId - The tenant identifier
|
|
1418
|
-
* @returns The PostgreSQL schema name
|
|
1419
|
-
*
|
|
1420
|
-
* @example
|
|
1421
|
-
* ```typescript
|
|
1422
|
-
* const schemaName = schemaManager.getSchemaName('tenant-123');
|
|
1423
|
-
* // Returns: 'tenant_tenant-123' (depends on schemaNameTemplate)
|
|
1424
|
-
* ```
|
|
1425
|
-
*/
|
|
1426
|
-
getSchemaName(tenantId) {
|
|
1427
|
-
return this.config.isolation.schemaNameTemplate(tenantId);
|
|
1428
|
-
}
|
|
1429
|
-
/**
|
|
1430
|
-
* Create a PostgreSQL pool for a specific schema
|
|
1431
|
-
*
|
|
1432
|
-
* The pool is configured with `search_path` set to the schema,
|
|
1433
|
-
* allowing queries to run in tenant isolation.
|
|
1434
|
-
*
|
|
1435
|
-
* @param schemaName - The PostgreSQL schema name
|
|
1436
|
-
* @returns A configured Pool instance
|
|
1437
|
-
*
|
|
1438
|
-
* @example
|
|
1439
|
-
* ```typescript
|
|
1440
|
-
* const pool = await schemaManager.createPool('tenant_123');
|
|
1441
|
-
* try {
|
|
1442
|
-
* await pool.query('SELECT * FROM users'); // Queries tenant_123.users
|
|
1443
|
-
* } finally {
|
|
1444
|
-
* await pool.end();
|
|
1445
|
-
* }
|
|
1446
|
-
* ```
|
|
1447
|
-
*/
|
|
1448
|
-
async createPool(schemaName) {
|
|
1449
|
-
return new Pool({
|
|
1450
|
-
connectionString: this.config.connection.url,
|
|
1451
|
-
...this.config.connection.poolConfig,
|
|
1452
|
-
options: `-c search_path="${schemaName}",public`
|
|
1453
|
-
});
|
|
1454
|
-
}
|
|
1455
|
-
/**
|
|
1456
|
-
* Create a PostgreSQL pool without schema-specific search_path
|
|
1457
|
-
*
|
|
1458
|
-
* Used for operations that need to work across schemas or
|
|
1459
|
-
* before a schema exists (like creating the schema itself).
|
|
1460
|
-
*
|
|
1461
|
-
* @returns A Pool instance connected to the database
|
|
1462
|
-
*/
|
|
1463
|
-
async createRootPool() {
|
|
1464
|
-
return new Pool({
|
|
1465
|
-
connectionString: this.config.connection.url,
|
|
1466
|
-
...this.config.connection.poolConfig
|
|
1467
|
-
});
|
|
1468
|
-
}
|
|
1469
|
-
/**
|
|
1470
|
-
* Create a new tenant schema in the database
|
|
1471
|
-
*
|
|
1472
|
-
* @param tenantId - The tenant identifier
|
|
1473
|
-
* @returns Promise that resolves when schema is created
|
|
1474
|
-
*
|
|
1475
|
-
* @example
|
|
1476
|
-
* ```typescript
|
|
1477
|
-
* await schemaManager.createSchema('new-tenant');
|
|
1478
|
-
* ```
|
|
1479
|
-
*/
|
|
1480
|
-
async createSchema(tenantId) {
|
|
1481
|
-
const schemaName = this.getSchemaName(tenantId);
|
|
1482
|
-
const pool = await this.createRootPool();
|
|
1483
|
-
try {
|
|
1484
|
-
await pool.query(`CREATE SCHEMA IF NOT EXISTS "${schemaName}"`);
|
|
1485
|
-
} finally {
|
|
1486
|
-
await pool.end();
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
/**
|
|
1490
|
-
* Drop a tenant schema from the database
|
|
1491
|
-
*
|
|
1492
|
-
* @param tenantId - The tenant identifier
|
|
1493
|
-
* @param options - Drop options (cascade, force)
|
|
1494
|
-
* @returns Promise that resolves when schema is dropped
|
|
1495
|
-
*
|
|
1496
|
-
* @example
|
|
1497
|
-
* ```typescript
|
|
1498
|
-
* // Drop with CASCADE (removes all objects)
|
|
1499
|
-
* await schemaManager.dropSchema('old-tenant', { cascade: true });
|
|
1500
|
-
*
|
|
1501
|
-
* // Drop with RESTRICT (fails if objects exist)
|
|
1502
|
-
* await schemaManager.dropSchema('old-tenant', { cascade: false });
|
|
1503
|
-
* ```
|
|
1504
|
-
*/
|
|
1505
|
-
async dropSchema(tenantId, options = {}) {
|
|
1506
|
-
const { cascade = true } = options;
|
|
1507
|
-
const schemaName = this.getSchemaName(tenantId);
|
|
1508
|
-
const pool = await this.createRootPool();
|
|
1509
|
-
try {
|
|
1510
|
-
const cascadeSql = cascade ? "CASCADE" : "RESTRICT";
|
|
1511
|
-
await pool.query(`DROP SCHEMA IF EXISTS "${schemaName}" ${cascadeSql}`);
|
|
1512
|
-
} finally {
|
|
1513
|
-
await pool.end();
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
/**
|
|
1517
|
-
* Check if a tenant schema exists in the database
|
|
1518
|
-
*
|
|
1519
|
-
* @param tenantId - The tenant identifier
|
|
1520
|
-
* @returns True if schema exists, false otherwise
|
|
1521
|
-
*
|
|
1522
|
-
* @example
|
|
1523
|
-
* ```typescript
|
|
1524
|
-
* if (await schemaManager.schemaExists('tenant-123')) {
|
|
1525
|
-
* console.log('Tenant schema exists');
|
|
1526
|
-
* }
|
|
1527
|
-
* ```
|
|
1528
|
-
*/
|
|
1529
|
-
async schemaExists(tenantId) {
|
|
1530
|
-
const schemaName = this.getSchemaName(tenantId);
|
|
1531
|
-
const pool = await this.createRootPool();
|
|
1532
|
-
try {
|
|
1533
|
-
const result = await pool.query(
|
|
1534
|
-
`SELECT 1 FROM information_schema.schemata WHERE schema_name = $1`,
|
|
1535
|
-
[schemaName]
|
|
1536
|
-
);
|
|
1537
|
-
return result.rowCount !== null && result.rowCount > 0;
|
|
1538
|
-
} finally {
|
|
1539
|
-
await pool.end();
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
/**
|
|
1543
|
-
* List all schemas matching a pattern
|
|
1544
|
-
*
|
|
1545
|
-
* @param pattern - SQL LIKE pattern to filter schemas (optional)
|
|
1546
|
-
* @returns Array of schema names
|
|
1547
|
-
*
|
|
1548
|
-
* @example
|
|
1549
|
-
* ```typescript
|
|
1550
|
-
* // List all tenant schemas
|
|
1551
|
-
* const schemas = await schemaManager.listSchemas('tenant_%');
|
|
1552
|
-
* ```
|
|
1553
|
-
*/
|
|
1554
|
-
async listSchemas(pattern) {
|
|
1555
|
-
const pool = await this.createRootPool();
|
|
1556
|
-
try {
|
|
1557
|
-
const query = pattern ? `SELECT schema_name FROM information_schema.schemata WHERE schema_name LIKE $1 ORDER BY schema_name` : `SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast') ORDER BY schema_name`;
|
|
1558
|
-
const result = await pool.query(
|
|
1559
|
-
query,
|
|
1560
|
-
pattern ? [pattern] : []
|
|
1561
|
-
);
|
|
1562
|
-
return result.rows.map((row) => row.schema_name);
|
|
1563
|
-
} finally {
|
|
1564
|
-
await pool.end();
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
/**
|
|
1568
|
-
* Ensure the migrations table exists with the correct format
|
|
1569
|
-
*
|
|
1570
|
-
* Creates the migrations tracking table if it doesn't exist,
|
|
1571
|
-
* using the appropriate column types based on the format.
|
|
1572
|
-
*
|
|
1573
|
-
* @param pool - Database pool to use
|
|
1574
|
-
* @param schemaName - The schema to create the table in
|
|
1575
|
-
* @param format - The detected/configured table format
|
|
1576
|
-
*
|
|
1577
|
-
* @example
|
|
1578
|
-
* ```typescript
|
|
1579
|
-
* const pool = await schemaManager.createPool('tenant_123');
|
|
1580
|
-
* await schemaManager.ensureMigrationsTable(pool, 'tenant_123', format);
|
|
1581
|
-
* ```
|
|
1582
|
-
*/
|
|
1583
|
-
async ensureMigrationsTable(pool, schemaName, format) {
|
|
1584
|
-
const { identifier, timestamp, timestampType } = format.columns;
|
|
1585
|
-
const identifierCol = identifier === "name" ? "name VARCHAR(255) NOT NULL UNIQUE" : "hash TEXT NOT NULL";
|
|
1586
|
-
const timestampCol = timestampType === "bigint" ? `${timestamp} BIGINT NOT NULL` : `${timestamp} TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP`;
|
|
1587
|
-
await pool.query(`
|
|
1588
|
-
CREATE TABLE IF NOT EXISTS "${schemaName}"."${format.tableName}" (
|
|
6
|
+
WHERE table_schema = $1 AND table_name = $2`,[e,t]),r=new Map(a.rows.map(s=>[s.column_name,s.data_type]));return r.has("name")?{format:"name",tableName:t,columns:{identifier:"name",timestamp:r.has("applied_at")?"applied_at":"created_at",timestampType:"timestamp"}}:r.has("hash")?r.get("created_at")==="bigint"?{format:"drizzle-kit",tableName:t,columns:{identifier:"hash",timestamp:"created_at",timestampType:"bigint"}}:{format:"hash",tableName:t,columns:{identifier:"hash",timestamp:"created_at",timestampType:"timestamp"}}:null}function _(c,e="__drizzle_migrations"){switch(c){case "name":return {format:"name",tableName:e,columns:{identifier:"name",timestamp:"applied_at",timestampType:"timestamp"}};case "hash":return {format:"hash",tableName:e,columns:{identifier:"hash",timestamp:"created_at",timestampType:"timestamp"}};case "drizzle-kit":return {format:"drizzle-kit",tableName:e,columns:{identifier:"hash",timestamp:"created_at",timestampType:"bigint"}}}}var xe="__drizzle_migrations",O=class{constructor(e,t){this.config=e;this.migrationsTable=t??xe;}migrationsTable;getSchemaName(e){return this.config.isolation.schemaNameTemplate(e)}async createPool(e){return new Pool({connectionString:this.config.connection.url,...this.config.connection.poolConfig,options:`-c search_path="${e}",public`})}async createRootPool(){return new Pool({connectionString:this.config.connection.url,...this.config.connection.poolConfig})}async createSchema(e){let t=this.getSchemaName(e),n=await this.createRootPool();try{await n.query(`CREATE SCHEMA IF NOT EXISTS "${t}"`);}finally{await n.end();}}async dropSchema(e,t={}){let{cascade:n=true}=t,a=this.getSchemaName(e),r=await this.createRootPool();try{let s=n?"CASCADE":"RESTRICT";await r.query(`DROP SCHEMA IF EXISTS "${a}" ${s}`);}finally{await r.end();}}async schemaExists(e){let t=this.getSchemaName(e),n=await this.createRootPool();try{let a=await n.query("SELECT 1 FROM information_schema.schemata WHERE schema_name = $1",[t]);return a.rowCount!==null&&a.rowCount>0}finally{await n.end();}}async listSchemas(e){let t=await this.createRootPool();try{let n=e?"SELECT schema_name FROM information_schema.schemata WHERE schema_name LIKE $1 ORDER BY schema_name":"SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast') ORDER BY schema_name";return (await t.query(n,e?[e]:[])).rows.map(r=>r.schema_name)}finally{await t.end();}}async ensureMigrationsTable(e,t,n){let{identifier:a,timestamp:r,timestampType:s}=n.columns,i=a==="name"?"name VARCHAR(255) NOT NULL UNIQUE":"hash TEXT NOT NULL",o=s==="bigint"?`${r} BIGINT NOT NULL`:`${r} TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP`;await e.query(`
|
|
7
|
+
CREATE TABLE IF NOT EXISTS "${t}"."${n.tableName}" (
|
|
1589
8
|
id SERIAL PRIMARY KEY,
|
|
1590
|
-
${
|
|
1591
|
-
${
|
|
9
|
+
${i},
|
|
10
|
+
${o}
|
|
1592
11
|
)
|
|
1593
|
-
`);
|
|
1594
|
-
|
|
1595
|
-
/**
|
|
1596
|
-
* Check if the migrations table exists in a schema
|
|
1597
|
-
*
|
|
1598
|
-
* @param pool - Database pool to use
|
|
1599
|
-
* @param schemaName - The schema to check
|
|
1600
|
-
* @returns True if migrations table exists
|
|
1601
|
-
*
|
|
1602
|
-
* @example
|
|
1603
|
-
* ```typescript
|
|
1604
|
-
* const pool = await schemaManager.createPool('tenant_123');
|
|
1605
|
-
* if (await schemaManager.migrationsTableExists(pool, 'tenant_123')) {
|
|
1606
|
-
* console.log('Migrations table exists');
|
|
1607
|
-
* }
|
|
1608
|
-
* ```
|
|
1609
|
-
*/
|
|
1610
|
-
async migrationsTableExists(pool, schemaName) {
|
|
1611
|
-
const result = await pool.query(
|
|
1612
|
-
`SELECT 1 FROM information_schema.tables
|
|
1613
|
-
WHERE table_schema = $1 AND table_name = $2`,
|
|
1614
|
-
[schemaName, this.migrationsTable]
|
|
1615
|
-
);
|
|
1616
|
-
return result.rowCount !== null && result.rowCount > 0;
|
|
1617
|
-
}
|
|
1618
|
-
/**
|
|
1619
|
-
* Get the configured migrations table name
|
|
1620
|
-
*
|
|
1621
|
-
* @returns The migrations table name
|
|
1622
|
-
*/
|
|
1623
|
-
getMigrationsTableName() {
|
|
1624
|
-
return this.migrationsTable;
|
|
1625
|
-
}
|
|
1626
|
-
};
|
|
1627
|
-
|
|
1628
|
-
// src/migrator/drift/column-analyzer.ts
|
|
1629
|
-
async function introspectColumns(pool, schemaName, tableName) {
|
|
1630
|
-
const result = await pool.query(
|
|
1631
|
-
`SELECT
|
|
12
|
+
`);}async migrationsTableExists(e,t){let n=await e.query(`SELECT 1 FROM information_schema.tables
|
|
13
|
+
WHERE table_schema = $1 AND table_name = $2`,[t,this.migrationsTable]);return n.rowCount!==null&&n.rowCount>0}getMigrationsTableName(){return this.migrationsTable}};async function ee(c,e,t){return (await c.query(`SELECT
|
|
1632
14
|
column_name,
|
|
1633
15
|
data_type,
|
|
1634
16
|
udt_name,
|
|
@@ -1640,94 +22,10 @@ async function introspectColumns(pool, schemaName, tableName) {
|
|
|
1640
22
|
ordinal_position
|
|
1641
23
|
FROM information_schema.columns
|
|
1642
24
|
WHERE table_schema = $1 AND table_name = $2
|
|
1643
|
-
ORDER BY ordinal_position`,
|
|
1644
|
-
[schemaName, tableName]
|
|
1645
|
-
);
|
|
1646
|
-
return result.rows.map((row) => ({
|
|
1647
|
-
name: row.column_name,
|
|
1648
|
-
dataType: row.data_type,
|
|
1649
|
-
udtName: row.udt_name,
|
|
1650
|
-
isNullable: row.is_nullable === "YES",
|
|
1651
|
-
columnDefault: row.column_default,
|
|
1652
|
-
characterMaximumLength: row.character_maximum_length,
|
|
1653
|
-
numericPrecision: row.numeric_precision,
|
|
1654
|
-
numericScale: row.numeric_scale,
|
|
1655
|
-
ordinalPosition: row.ordinal_position
|
|
1656
|
-
}));
|
|
1657
|
-
}
|
|
1658
|
-
function normalizeDefault(value) {
|
|
1659
|
-
if (value === null) return null;
|
|
1660
|
-
return value.replace(/^'(.+)'::.+$/, "$1").replace(/^(.+)::.+$/, "$1").trim();
|
|
1661
|
-
}
|
|
1662
|
-
function compareColumns(reference, target) {
|
|
1663
|
-
const drifts = [];
|
|
1664
|
-
const refColMap = new Map(reference.map((c) => [c.name, c]));
|
|
1665
|
-
const targetColMap = new Map(target.map((c) => [c.name, c]));
|
|
1666
|
-
for (const refCol of reference) {
|
|
1667
|
-
const targetCol = targetColMap.get(refCol.name);
|
|
1668
|
-
if (!targetCol) {
|
|
1669
|
-
drifts.push({
|
|
1670
|
-
column: refCol.name,
|
|
1671
|
-
type: "missing",
|
|
1672
|
-
expected: refCol.dataType,
|
|
1673
|
-
description: `Column "${refCol.name}" (${refCol.dataType}) is missing`
|
|
1674
|
-
});
|
|
1675
|
-
continue;
|
|
1676
|
-
}
|
|
1677
|
-
if (refCol.udtName !== targetCol.udtName) {
|
|
1678
|
-
drifts.push({
|
|
1679
|
-
column: refCol.name,
|
|
1680
|
-
type: "type_mismatch",
|
|
1681
|
-
expected: refCol.udtName,
|
|
1682
|
-
actual: targetCol.udtName,
|
|
1683
|
-
description: `Column "${refCol.name}" type mismatch: expected "${refCol.udtName}", got "${targetCol.udtName}"`
|
|
1684
|
-
});
|
|
1685
|
-
}
|
|
1686
|
-
if (refCol.isNullable !== targetCol.isNullable) {
|
|
1687
|
-
drifts.push({
|
|
1688
|
-
column: refCol.name,
|
|
1689
|
-
type: "nullable_mismatch",
|
|
1690
|
-
expected: refCol.isNullable,
|
|
1691
|
-
actual: targetCol.isNullable,
|
|
1692
|
-
description: `Column "${refCol.name}" nullable mismatch: expected ${refCol.isNullable ? "NULL" : "NOT NULL"}, got ${targetCol.isNullable ? "NULL" : "NOT NULL"}`
|
|
1693
|
-
});
|
|
1694
|
-
}
|
|
1695
|
-
const normalizedRefDefault = normalizeDefault(refCol.columnDefault);
|
|
1696
|
-
const normalizedTargetDefault = normalizeDefault(targetCol.columnDefault);
|
|
1697
|
-
if (normalizedRefDefault !== normalizedTargetDefault) {
|
|
1698
|
-
drifts.push({
|
|
1699
|
-
column: refCol.name,
|
|
1700
|
-
type: "default_mismatch",
|
|
1701
|
-
expected: refCol.columnDefault,
|
|
1702
|
-
actual: targetCol.columnDefault,
|
|
1703
|
-
description: `Column "${refCol.name}" default mismatch: expected "${refCol.columnDefault ?? "none"}", got "${targetCol.columnDefault ?? "none"}"`
|
|
1704
|
-
});
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
for (const targetCol of target) {
|
|
1708
|
-
if (!refColMap.has(targetCol.name)) {
|
|
1709
|
-
drifts.push({
|
|
1710
|
-
column: targetCol.name,
|
|
1711
|
-
type: "extra",
|
|
1712
|
-
actual: targetCol.dataType,
|
|
1713
|
-
description: `Extra column "${targetCol.name}" (${targetCol.dataType}) not in reference`
|
|
1714
|
-
});
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
return drifts;
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
// src/migrator/drift/index-analyzer.ts
|
|
1721
|
-
async function introspectIndexes(pool, schemaName, tableName) {
|
|
1722
|
-
const indexResult = await pool.query(
|
|
1723
|
-
`SELECT indexname, indexdef
|
|
25
|
+
ORDER BY ordinal_position`,[e,t])).rows.map(a=>({name:a.column_name,dataType:a.data_type,udtName:a.udt_name,isNullable:a.is_nullable==="YES",columnDefault:a.column_default,characterMaximumLength:a.character_maximum_length,numericPrecision:a.numeric_precision,numericScale:a.numeric_scale,ordinalPosition:a.ordinal_position}))}function Z(c){return c===null?null:c.replace(/^'(.+)'::.+$/,"$1").replace(/^(.+)::.+$/,"$1").trim()}function te(c,e){let t=[],n=new Map(c.map(r=>[r.name,r])),a=new Map(e.map(r=>[r.name,r]));for(let r of c){let s=a.get(r.name);if(!s){t.push({column:r.name,type:"missing",expected:r.dataType,description:`Column "${r.name}" (${r.dataType}) is missing`});continue}r.udtName!==s.udtName&&t.push({column:r.name,type:"type_mismatch",expected:r.udtName,actual:s.udtName,description:`Column "${r.name}" type mismatch: expected "${r.udtName}", got "${s.udtName}"`}),r.isNullable!==s.isNullable&&t.push({column:r.name,type:"nullable_mismatch",expected:r.isNullable,actual:s.isNullable,description:`Column "${r.name}" nullable mismatch: expected ${r.isNullable?"NULL":"NOT NULL"}, got ${s.isNullable?"NULL":"NOT NULL"}`});let i=Z(r.columnDefault),o=Z(s.columnDefault);i!==o&&t.push({column:r.name,type:"default_mismatch",expected:r.columnDefault,actual:s.columnDefault,description:`Column "${r.name}" default mismatch: expected "${r.columnDefault??"none"}", got "${s.columnDefault??"none"}"`});}for(let r of e)n.has(r.name)||t.push({column:r.name,type:"extra",actual:r.dataType,description:`Extra column "${r.name}" (${r.dataType}) not in reference`});return t}async function ne(c,e,t){let n=await c.query(`SELECT indexname, indexdef
|
|
1724
26
|
FROM pg_indexes
|
|
1725
27
|
WHERE schemaname = $1 AND tablename = $2
|
|
1726
|
-
ORDER BY indexname`,
|
|
1727
|
-
[schemaName, tableName]
|
|
1728
|
-
);
|
|
1729
|
-
const indexDetails = await pool.query(
|
|
1730
|
-
`SELECT
|
|
28
|
+
ORDER BY indexname`,[e,t]),a=await c.query(`SELECT
|
|
1731
29
|
i.relname as indexname,
|
|
1732
30
|
a.attname as column_name,
|
|
1733
31
|
ix.indisunique as is_unique,
|
|
@@ -1738,77 +36,7 @@ async function introspectIndexes(pool, schemaName, tableName) {
|
|
|
1738
36
|
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
|
|
1739
37
|
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
1740
38
|
WHERE n.nspname = $1 AND t.relname = $2
|
|
1741
|
-
ORDER BY i.relname, a.attnum`,
|
|
1742
|
-
[schemaName, tableName]
|
|
1743
|
-
);
|
|
1744
|
-
const indexColumnsMap = /* @__PURE__ */ new Map();
|
|
1745
|
-
for (const row of indexDetails.rows) {
|
|
1746
|
-
const existing = indexColumnsMap.get(row.indexname);
|
|
1747
|
-
if (existing) {
|
|
1748
|
-
existing.columns.push(row.column_name);
|
|
1749
|
-
} else {
|
|
1750
|
-
indexColumnsMap.set(row.indexname, {
|
|
1751
|
-
columns: [row.column_name],
|
|
1752
|
-
isUnique: row.is_unique,
|
|
1753
|
-
isPrimary: row.is_primary
|
|
1754
|
-
});
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
return indexResult.rows.map((row) => {
|
|
1758
|
-
const details = indexColumnsMap.get(row.indexname);
|
|
1759
|
-
return {
|
|
1760
|
-
name: row.indexname,
|
|
1761
|
-
columns: details?.columns ?? [],
|
|
1762
|
-
isUnique: details?.isUnique ?? false,
|
|
1763
|
-
isPrimary: details?.isPrimary ?? false,
|
|
1764
|
-
definition: row.indexdef
|
|
1765
|
-
};
|
|
1766
|
-
});
|
|
1767
|
-
}
|
|
1768
|
-
function compareIndexes(reference, target) {
|
|
1769
|
-
const drifts = [];
|
|
1770
|
-
const refIndexMap = new Map(reference.map((i) => [i.name, i]));
|
|
1771
|
-
const targetIndexMap = new Map(target.map((i) => [i.name, i]));
|
|
1772
|
-
for (const refIndex of reference) {
|
|
1773
|
-
const targetIndex = targetIndexMap.get(refIndex.name);
|
|
1774
|
-
if (!targetIndex) {
|
|
1775
|
-
drifts.push({
|
|
1776
|
-
index: refIndex.name,
|
|
1777
|
-
type: "missing",
|
|
1778
|
-
expected: refIndex.definition,
|
|
1779
|
-
description: `Index "${refIndex.name}" is missing`
|
|
1780
|
-
});
|
|
1781
|
-
continue;
|
|
1782
|
-
}
|
|
1783
|
-
const refCols = refIndex.columns.sort().join(",");
|
|
1784
|
-
const targetCols = targetIndex.columns.sort().join(",");
|
|
1785
|
-
if (refCols !== targetCols || refIndex.isUnique !== targetIndex.isUnique) {
|
|
1786
|
-
drifts.push({
|
|
1787
|
-
index: refIndex.name,
|
|
1788
|
-
type: "definition_mismatch",
|
|
1789
|
-
expected: refIndex.definition,
|
|
1790
|
-
actual: targetIndex.definition,
|
|
1791
|
-
description: `Index "${refIndex.name}" definition differs`
|
|
1792
|
-
});
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
|
-
for (const targetIndex of target) {
|
|
1796
|
-
if (!refIndexMap.has(targetIndex.name)) {
|
|
1797
|
-
drifts.push({
|
|
1798
|
-
index: targetIndex.name,
|
|
1799
|
-
type: "extra",
|
|
1800
|
-
actual: targetIndex.definition,
|
|
1801
|
-
description: `Extra index "${targetIndex.name}" not in reference`
|
|
1802
|
-
});
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
return drifts;
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
// src/migrator/drift/constraint-analyzer.ts
|
|
1809
|
-
async function introspectConstraints(pool, schemaName, tableName) {
|
|
1810
|
-
const result = await pool.query(
|
|
1811
|
-
`SELECT
|
|
39
|
+
ORDER BY i.relname, a.attnum`,[e,t]),r=new Map;for(let s of a.rows){let i=r.get(s.indexname);i?i.columns.push(s.column_name):r.set(s.indexname,{columns:[s.column_name],isUnique:s.is_unique,isPrimary:s.is_primary});}return n.rows.map(s=>{let i=r.get(s.indexname);return {name:s.indexname,columns:i?.columns??[],isUnique:i?.isUnique??false,isPrimary:i?.isPrimary??false,definition:s.indexdef}})}function re(c,e){let t=[],n=new Map(c.map(r=>[r.name,r])),a=new Map(e.map(r=>[r.name,r]));for(let r of c){let s=a.get(r.name);if(!s){t.push({index:r.name,type:"missing",expected:r.definition,description:`Index "${r.name}" is missing`});continue}let i=r.columns.sort().join(","),o=s.columns.sort().join(",");(i!==o||r.isUnique!==s.isUnique)&&t.push({index:r.name,type:"definition_mismatch",expected:r.definition,actual:s.definition,description:`Index "${r.name}" definition differs`});}for(let r of e)n.has(r.name)||t.push({index:r.name,type:"extra",actual:r.definition,description:`Extra index "${r.name}" not in reference`});return t}async function ae(c,e,t){let n=await c.query(`SELECT
|
|
1812
40
|
tc.constraint_name,
|
|
1813
41
|
tc.constraint_type,
|
|
1814
42
|
kcu.column_name,
|
|
@@ -1827,1698 +55,20 @@ async function introspectConstraints(pool, schemaName, tableName) {
|
|
|
1827
55
|
ON tc.constraint_name = cc.constraint_name
|
|
1828
56
|
AND tc.constraint_type = 'CHECK'
|
|
1829
57
|
WHERE tc.table_schema = $1 AND tc.table_name = $2
|
|
1830
|
-
ORDER BY tc.constraint_name, kcu.ordinal_position`,
|
|
1831
|
-
[schemaName, tableName]
|
|
1832
|
-
);
|
|
1833
|
-
const constraintMap = /* @__PURE__ */ new Map();
|
|
1834
|
-
for (const row of result.rows) {
|
|
1835
|
-
const existing = constraintMap.get(row.constraint_name);
|
|
1836
|
-
if (existing) {
|
|
1837
|
-
if (row.column_name && !existing.columns.includes(row.column_name)) {
|
|
1838
|
-
existing.columns.push(row.column_name);
|
|
1839
|
-
}
|
|
1840
|
-
if (row.foreign_column_name && existing.foreignColumns && !existing.foreignColumns.includes(row.foreign_column_name)) {
|
|
1841
|
-
existing.foreignColumns.push(row.foreign_column_name);
|
|
1842
|
-
}
|
|
1843
|
-
} else {
|
|
1844
|
-
const constraint = {
|
|
1845
|
-
name: row.constraint_name,
|
|
1846
|
-
type: row.constraint_type,
|
|
1847
|
-
columns: row.column_name ? [row.column_name] : []
|
|
1848
|
-
};
|
|
1849
|
-
if (row.foreign_table_name) {
|
|
1850
|
-
constraint.foreignTable = row.foreign_table_name;
|
|
1851
|
-
}
|
|
1852
|
-
if (row.foreign_column_name) {
|
|
1853
|
-
constraint.foreignColumns = [row.foreign_column_name];
|
|
1854
|
-
}
|
|
1855
|
-
if (row.check_clause) {
|
|
1856
|
-
constraint.checkExpression = row.check_clause;
|
|
1857
|
-
}
|
|
1858
|
-
constraintMap.set(row.constraint_name, constraint);
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
return Array.from(constraintMap.values());
|
|
1862
|
-
}
|
|
1863
|
-
function compareConstraints(reference, target) {
|
|
1864
|
-
const drifts = [];
|
|
1865
|
-
const refConstraintMap = new Map(reference.map((c) => [c.name, c]));
|
|
1866
|
-
const targetConstraintMap = new Map(target.map((c) => [c.name, c]));
|
|
1867
|
-
for (const refConstraint of reference) {
|
|
1868
|
-
const targetConstraint = targetConstraintMap.get(refConstraint.name);
|
|
1869
|
-
if (!targetConstraint) {
|
|
1870
|
-
drifts.push({
|
|
1871
|
-
constraint: refConstraint.name,
|
|
1872
|
-
type: "missing",
|
|
1873
|
-
expected: `${refConstraint.type} on (${refConstraint.columns.join(", ")})`,
|
|
1874
|
-
description: `Constraint "${refConstraint.name}" (${refConstraint.type}) is missing`
|
|
1875
|
-
});
|
|
1876
|
-
continue;
|
|
1877
|
-
}
|
|
1878
|
-
const refCols = refConstraint.columns.sort().join(",");
|
|
1879
|
-
const targetCols = targetConstraint.columns.sort().join(",");
|
|
1880
|
-
if (refConstraint.type !== targetConstraint.type || refCols !== targetCols) {
|
|
1881
|
-
drifts.push({
|
|
1882
|
-
constraint: refConstraint.name,
|
|
1883
|
-
type: "definition_mismatch",
|
|
1884
|
-
expected: `${refConstraint.type} on (${refConstraint.columns.join(", ")})`,
|
|
1885
|
-
actual: `${targetConstraint.type} on (${targetConstraint.columns.join(", ")})`,
|
|
1886
|
-
description: `Constraint "${refConstraint.name}" definition differs`
|
|
1887
|
-
});
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
for (const targetConstraint of target) {
|
|
1891
|
-
if (!refConstraintMap.has(targetConstraint.name)) {
|
|
1892
|
-
drifts.push({
|
|
1893
|
-
constraint: targetConstraint.name,
|
|
1894
|
-
type: "extra",
|
|
1895
|
-
actual: `${targetConstraint.type} on (${targetConstraint.columns.join(", ")})`,
|
|
1896
|
-
description: `Extra constraint "${targetConstraint.name}" (${targetConstraint.type}) not in reference`
|
|
1897
|
-
});
|
|
1898
|
-
}
|
|
1899
|
-
}
|
|
1900
|
-
return drifts;
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
// src/migrator/drift/drift-detector.ts
|
|
1904
|
-
var DEFAULT_MIGRATIONS_TABLE2 = "__drizzle_migrations";
|
|
1905
|
-
var DriftDetector = class {
|
|
1906
|
-
constructor(tenantConfig, schemaManager, driftConfig) {
|
|
1907
|
-
this.tenantConfig = tenantConfig;
|
|
1908
|
-
this.schemaManager = schemaManager;
|
|
1909
|
-
this.driftConfig = driftConfig;
|
|
1910
|
-
this.migrationsTable = driftConfig.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE2;
|
|
1911
|
-
}
|
|
1912
|
-
migrationsTable;
|
|
1913
|
-
/**
|
|
1914
|
-
* Get the schema name for a tenant ID
|
|
1915
|
-
*/
|
|
1916
|
-
getSchemaName(tenantId) {
|
|
1917
|
-
return this.tenantConfig.isolation.schemaNameTemplate(tenantId);
|
|
1918
|
-
}
|
|
1919
|
-
/**
|
|
1920
|
-
* Create a pool for a schema
|
|
1921
|
-
*/
|
|
1922
|
-
async createPool(schemaName) {
|
|
1923
|
-
return this.schemaManager.createPool(schemaName);
|
|
1924
|
-
}
|
|
1925
|
-
/**
|
|
1926
|
-
* Detect schema drift across all tenants.
|
|
1927
|
-
*
|
|
1928
|
-
* Compares each tenant's schema against a reference tenant (first tenant by default).
|
|
1929
|
-
* Returns a comprehensive report of all differences found.
|
|
1930
|
-
*
|
|
1931
|
-
* @param options - Detection options
|
|
1932
|
-
* @returns Schema drift status with details for each tenant
|
|
1933
|
-
*
|
|
1934
|
-
* @example
|
|
1935
|
-
* ```typescript
|
|
1936
|
-
* // Basic usage - compare all tenants against the first one
|
|
1937
|
-
* const status = await detector.detectDrift();
|
|
1938
|
-
*
|
|
1939
|
-
* // Use a specific tenant as reference
|
|
1940
|
-
* const status = await detector.detectDrift({
|
|
1941
|
-
* referenceTenant: 'golden-tenant',
|
|
1942
|
-
* });
|
|
1943
|
-
*
|
|
1944
|
-
* // Check specific tenants only
|
|
1945
|
-
* const status = await detector.detectDrift({
|
|
1946
|
-
* tenantIds: ['tenant-1', 'tenant-2'],
|
|
1947
|
-
* });
|
|
1948
|
-
*
|
|
1949
|
-
* // Skip index and constraint comparison for faster checks
|
|
1950
|
-
* const status = await detector.detectDrift({
|
|
1951
|
-
* includeIndexes: false,
|
|
1952
|
-
* includeConstraints: false,
|
|
1953
|
-
* });
|
|
1954
|
-
* ```
|
|
1955
|
-
*/
|
|
1956
|
-
async detectDrift(options = {}) {
|
|
1957
|
-
const startTime = Date.now();
|
|
1958
|
-
const {
|
|
1959
|
-
concurrency = 10,
|
|
1960
|
-
includeIndexes = true,
|
|
1961
|
-
includeConstraints = true,
|
|
1962
|
-
excludeTables = [this.migrationsTable],
|
|
1963
|
-
onProgress
|
|
1964
|
-
} = options;
|
|
1965
|
-
const tenantIds = options.tenantIds ?? await this.driftConfig.tenantDiscovery();
|
|
1966
|
-
if (tenantIds.length === 0) {
|
|
1967
|
-
return {
|
|
1968
|
-
referenceTenant: "",
|
|
1969
|
-
total: 0,
|
|
1970
|
-
noDrift: 0,
|
|
1971
|
-
withDrift: 0,
|
|
1972
|
-
error: 0,
|
|
1973
|
-
details: [],
|
|
1974
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1975
|
-
durationMs: Date.now() - startTime
|
|
1976
|
-
};
|
|
1977
|
-
}
|
|
1978
|
-
const referenceTenant = options.referenceTenant ?? tenantIds[0];
|
|
1979
|
-
onProgress?.(referenceTenant, "starting");
|
|
1980
|
-
onProgress?.(referenceTenant, "introspecting");
|
|
1981
|
-
const referenceSchema = await this.introspectSchema(referenceTenant, {
|
|
1982
|
-
includeIndexes,
|
|
1983
|
-
includeConstraints,
|
|
1984
|
-
excludeTables
|
|
1985
|
-
});
|
|
1986
|
-
if (!referenceSchema) {
|
|
1987
|
-
return {
|
|
1988
|
-
referenceTenant,
|
|
1989
|
-
total: tenantIds.length,
|
|
1990
|
-
noDrift: 0,
|
|
1991
|
-
withDrift: 0,
|
|
1992
|
-
error: tenantIds.length,
|
|
1993
|
-
details: tenantIds.map((id) => ({
|
|
1994
|
-
tenantId: id,
|
|
1995
|
-
schemaName: this.getSchemaName(id),
|
|
1996
|
-
hasDrift: false,
|
|
1997
|
-
tables: [],
|
|
1998
|
-
issueCount: 0,
|
|
1999
|
-
error: id === referenceTenant ? "Failed to introspect reference tenant" : "Reference tenant introspection failed"
|
|
2000
|
-
})),
|
|
2001
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2002
|
-
durationMs: Date.now() - startTime
|
|
2003
|
-
};
|
|
2004
|
-
}
|
|
2005
|
-
onProgress?.(referenceTenant, "completed");
|
|
2006
|
-
const tenantsToCheck = tenantIds.filter((id) => id !== referenceTenant);
|
|
2007
|
-
const results = [];
|
|
2008
|
-
results.push({
|
|
2009
|
-
tenantId: referenceTenant,
|
|
2010
|
-
schemaName: referenceSchema.schemaName,
|
|
2011
|
-
hasDrift: false,
|
|
2012
|
-
tables: [],
|
|
2013
|
-
issueCount: 0
|
|
2014
|
-
});
|
|
2015
|
-
for (let i = 0; i < tenantsToCheck.length; i += concurrency) {
|
|
2016
|
-
const batch = tenantsToCheck.slice(i, i + concurrency);
|
|
2017
|
-
const batchResults = await Promise.all(
|
|
2018
|
-
batch.map(async (tenantId) => {
|
|
2019
|
-
try {
|
|
2020
|
-
onProgress?.(tenantId, "starting");
|
|
2021
|
-
onProgress?.(tenantId, "introspecting");
|
|
2022
|
-
const tenantSchema = await this.introspectSchema(tenantId, {
|
|
2023
|
-
includeIndexes,
|
|
2024
|
-
includeConstraints,
|
|
2025
|
-
excludeTables
|
|
2026
|
-
});
|
|
2027
|
-
if (!tenantSchema) {
|
|
2028
|
-
onProgress?.(tenantId, "failed");
|
|
2029
|
-
return {
|
|
2030
|
-
tenantId,
|
|
2031
|
-
schemaName: this.getSchemaName(tenantId),
|
|
2032
|
-
hasDrift: false,
|
|
2033
|
-
tables: [],
|
|
2034
|
-
issueCount: 0,
|
|
2035
|
-
error: "Failed to introspect schema"
|
|
2036
|
-
};
|
|
2037
|
-
}
|
|
2038
|
-
onProgress?.(tenantId, "comparing");
|
|
2039
|
-
const drift = this.compareSchemas(referenceSchema, tenantSchema, {
|
|
2040
|
-
includeIndexes,
|
|
2041
|
-
includeConstraints
|
|
2042
|
-
});
|
|
2043
|
-
onProgress?.(tenantId, "completed");
|
|
2044
|
-
return drift;
|
|
2045
|
-
} catch (error) {
|
|
2046
|
-
onProgress?.(tenantId, "failed");
|
|
2047
|
-
return {
|
|
2048
|
-
tenantId,
|
|
2049
|
-
schemaName: this.getSchemaName(tenantId),
|
|
2050
|
-
hasDrift: false,
|
|
2051
|
-
tables: [],
|
|
2052
|
-
issueCount: 0,
|
|
2053
|
-
error: error.message
|
|
2054
|
-
};
|
|
2055
|
-
}
|
|
2056
|
-
})
|
|
2057
|
-
);
|
|
2058
|
-
results.push(...batchResults);
|
|
2059
|
-
}
|
|
2060
|
-
return {
|
|
2061
|
-
referenceTenant,
|
|
2062
|
-
total: results.length,
|
|
2063
|
-
noDrift: results.filter((r) => !r.hasDrift && !r.error).length,
|
|
2064
|
-
withDrift: results.filter((r) => r.hasDrift && !r.error).length,
|
|
2065
|
-
error: results.filter((r) => !!r.error).length,
|
|
2066
|
-
details: results,
|
|
2067
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2068
|
-
durationMs: Date.now() - startTime
|
|
2069
|
-
};
|
|
2070
|
-
}
|
|
2071
|
-
/**
|
|
2072
|
-
* Compare a specific tenant against a reference tenant.
|
|
2073
|
-
*
|
|
2074
|
-
* @param tenantId - Tenant to check
|
|
2075
|
-
* @param referenceTenantId - Tenant to use as reference
|
|
2076
|
-
* @param options - Introspection options
|
|
2077
|
-
* @returns Drift details for the tenant
|
|
2078
|
-
*
|
|
2079
|
-
* @example
|
|
2080
|
-
* ```typescript
|
|
2081
|
-
* const drift = await detector.compareTenant('tenant-123', 'golden-tenant');
|
|
2082
|
-
* if (drift.hasDrift) {
|
|
2083
|
-
* console.log(`Found ${drift.issueCount} issues`);
|
|
2084
|
-
* }
|
|
2085
|
-
* ```
|
|
2086
|
-
*/
|
|
2087
|
-
async compareTenant(tenantId, referenceTenantId, options = {}) {
|
|
2088
|
-
const {
|
|
2089
|
-
includeIndexes = true,
|
|
2090
|
-
includeConstraints = true,
|
|
2091
|
-
excludeTables = [this.migrationsTable]
|
|
2092
|
-
} = options;
|
|
2093
|
-
const referenceSchema = await this.introspectSchema(referenceTenantId, {
|
|
2094
|
-
includeIndexes,
|
|
2095
|
-
includeConstraints,
|
|
2096
|
-
excludeTables
|
|
2097
|
-
});
|
|
2098
|
-
if (!referenceSchema) {
|
|
2099
|
-
return {
|
|
2100
|
-
tenantId,
|
|
2101
|
-
schemaName: this.getSchemaName(tenantId),
|
|
2102
|
-
hasDrift: false,
|
|
2103
|
-
tables: [],
|
|
2104
|
-
issueCount: 0,
|
|
2105
|
-
error: "Failed to introspect reference tenant"
|
|
2106
|
-
};
|
|
2107
|
-
}
|
|
2108
|
-
const tenantSchema = await this.introspectSchema(tenantId, {
|
|
2109
|
-
includeIndexes,
|
|
2110
|
-
includeConstraints,
|
|
2111
|
-
excludeTables
|
|
2112
|
-
});
|
|
2113
|
-
if (!tenantSchema) {
|
|
2114
|
-
return {
|
|
2115
|
-
tenantId,
|
|
2116
|
-
schemaName: this.getSchemaName(tenantId),
|
|
2117
|
-
hasDrift: false,
|
|
2118
|
-
tables: [],
|
|
2119
|
-
issueCount: 0,
|
|
2120
|
-
error: "Failed to introspect tenant schema"
|
|
2121
|
-
};
|
|
2122
|
-
}
|
|
2123
|
-
return this.compareSchemas(referenceSchema, tenantSchema, {
|
|
2124
|
-
includeIndexes,
|
|
2125
|
-
includeConstraints
|
|
2126
|
-
});
|
|
2127
|
-
}
|
|
2128
|
-
/**
|
|
2129
|
-
* Introspect a tenant's schema structure.
|
|
2130
|
-
*
|
|
2131
|
-
* Retrieves all tables, columns, indexes, and constraints
|
|
2132
|
-
* for a tenant's schema.
|
|
2133
|
-
*
|
|
2134
|
-
* @param tenantId - Tenant to introspect
|
|
2135
|
-
* @param options - Introspection options
|
|
2136
|
-
* @returns Schema structure or null if introspection fails
|
|
2137
|
-
*
|
|
2138
|
-
* @example
|
|
2139
|
-
* ```typescript
|
|
2140
|
-
* const schema = await detector.introspectSchema('tenant-123');
|
|
2141
|
-
* if (schema) {
|
|
2142
|
-
* console.log(`Found ${schema.tables.length} tables`);
|
|
2143
|
-
* for (const table of schema.tables) {
|
|
2144
|
-
* console.log(` ${table.name}: ${table.columns.length} columns`);
|
|
2145
|
-
* }
|
|
2146
|
-
* }
|
|
2147
|
-
* ```
|
|
2148
|
-
*/
|
|
2149
|
-
async introspectSchema(tenantId, options = {}) {
|
|
2150
|
-
const schemaName = this.getSchemaName(tenantId);
|
|
2151
|
-
const pool = await this.createPool(schemaName);
|
|
2152
|
-
try {
|
|
2153
|
-
const tables = await this.introspectTables(pool, schemaName, options);
|
|
2154
|
-
return {
|
|
2155
|
-
tenantId,
|
|
2156
|
-
schemaName,
|
|
2157
|
-
tables,
|
|
2158
|
-
introspectedAt: /* @__PURE__ */ new Date()
|
|
2159
|
-
};
|
|
2160
|
-
} catch {
|
|
2161
|
-
return null;
|
|
2162
|
-
} finally {
|
|
2163
|
-
await pool.end();
|
|
2164
|
-
}
|
|
2165
|
-
}
|
|
2166
|
-
/**
|
|
2167
|
-
* Compare two schema snapshots.
|
|
2168
|
-
*
|
|
2169
|
-
* This method compares pre-introspected schema snapshots,
|
|
2170
|
-
* useful when you already have the schema data available.
|
|
2171
|
-
*
|
|
2172
|
-
* @param reference - Reference (expected) schema
|
|
2173
|
-
* @param target - Target (actual) schema
|
|
2174
|
-
* @param options - Comparison options
|
|
2175
|
-
* @returns Drift details
|
|
2176
|
-
*
|
|
2177
|
-
* @example
|
|
2178
|
-
* ```typescript
|
|
2179
|
-
* const refSchema = await detector.introspectSchema('golden-tenant');
|
|
2180
|
-
* const targetSchema = await detector.introspectSchema('tenant-123');
|
|
2181
|
-
*
|
|
2182
|
-
* if (refSchema && targetSchema) {
|
|
2183
|
-
* const drift = detector.compareSchemas(refSchema, targetSchema);
|
|
2184
|
-
* console.log(`Drift detected: ${drift.hasDrift}`);
|
|
2185
|
-
* }
|
|
2186
|
-
* ```
|
|
2187
|
-
*/
|
|
2188
|
-
compareSchemas(reference, target, options = {}) {
|
|
2189
|
-
const { includeIndexes = true, includeConstraints = true } = options;
|
|
2190
|
-
const tableDrifts = [];
|
|
2191
|
-
let totalIssues = 0;
|
|
2192
|
-
const refTableMap = new Map(reference.tables.map((t) => [t.name, t]));
|
|
2193
|
-
const targetTableMap = new Map(target.tables.map((t) => [t.name, t]));
|
|
2194
|
-
for (const refTable of reference.tables) {
|
|
2195
|
-
const targetTable = targetTableMap.get(refTable.name);
|
|
2196
|
-
if (!targetTable) {
|
|
2197
|
-
tableDrifts.push({
|
|
2198
|
-
table: refTable.name,
|
|
2199
|
-
status: "missing",
|
|
2200
|
-
columns: refTable.columns.map((c) => ({
|
|
2201
|
-
column: c.name,
|
|
2202
|
-
type: "missing",
|
|
2203
|
-
expected: c.dataType,
|
|
2204
|
-
description: `Column "${c.name}" (${c.dataType}) is missing`
|
|
2205
|
-
})),
|
|
2206
|
-
indexes: [],
|
|
2207
|
-
constraints: []
|
|
2208
|
-
});
|
|
2209
|
-
totalIssues += refTable.columns.length;
|
|
2210
|
-
continue;
|
|
2211
|
-
}
|
|
2212
|
-
const columnDrifts = compareColumns(refTable.columns, targetTable.columns);
|
|
2213
|
-
const indexDrifts = includeIndexes ? compareIndexes(refTable.indexes, targetTable.indexes) : [];
|
|
2214
|
-
const constraintDrifts = includeConstraints ? compareConstraints(refTable.constraints, targetTable.constraints) : [];
|
|
2215
|
-
const issues = columnDrifts.length + indexDrifts.length + constraintDrifts.length;
|
|
2216
|
-
totalIssues += issues;
|
|
2217
|
-
if (issues > 0) {
|
|
2218
|
-
tableDrifts.push({
|
|
2219
|
-
table: refTable.name,
|
|
2220
|
-
status: "drifted",
|
|
2221
|
-
columns: columnDrifts,
|
|
2222
|
-
indexes: indexDrifts,
|
|
2223
|
-
constraints: constraintDrifts
|
|
2224
|
-
});
|
|
2225
|
-
}
|
|
2226
|
-
}
|
|
2227
|
-
for (const targetTable of target.tables) {
|
|
2228
|
-
if (!refTableMap.has(targetTable.name)) {
|
|
2229
|
-
tableDrifts.push({
|
|
2230
|
-
table: targetTable.name,
|
|
2231
|
-
status: "extra",
|
|
2232
|
-
columns: targetTable.columns.map((c) => ({
|
|
2233
|
-
column: c.name,
|
|
2234
|
-
type: "extra",
|
|
2235
|
-
actual: c.dataType,
|
|
2236
|
-
description: `Extra column "${c.name}" (${c.dataType}) not in reference`
|
|
2237
|
-
})),
|
|
2238
|
-
indexes: [],
|
|
2239
|
-
constraints: []
|
|
2240
|
-
});
|
|
2241
|
-
totalIssues += targetTable.columns.length;
|
|
2242
|
-
}
|
|
2243
|
-
}
|
|
2244
|
-
return {
|
|
2245
|
-
tenantId: target.tenantId,
|
|
2246
|
-
schemaName: target.schemaName,
|
|
2247
|
-
hasDrift: totalIssues > 0,
|
|
2248
|
-
tables: tableDrifts,
|
|
2249
|
-
issueCount: totalIssues
|
|
2250
|
-
};
|
|
2251
|
-
}
|
|
2252
|
-
/**
|
|
2253
|
-
* Introspect all tables in a schema
|
|
2254
|
-
*/
|
|
2255
|
-
async introspectTables(pool, schemaName, options) {
|
|
2256
|
-
const { includeIndexes = true, includeConstraints = true, excludeTables = [] } = options;
|
|
2257
|
-
const tablesResult = await pool.query(
|
|
2258
|
-
`SELECT table_name
|
|
58
|
+
ORDER BY tc.constraint_name, kcu.ordinal_position`,[e,t]),a=new Map;for(let r of n.rows){let s=a.get(r.constraint_name);if(s)r.column_name&&!s.columns.includes(r.column_name)&&s.columns.push(r.column_name),r.foreign_column_name&&s.foreignColumns&&!s.foreignColumns.includes(r.foreign_column_name)&&s.foreignColumns.push(r.foreign_column_name);else {let i={name:r.constraint_name,type:r.constraint_type,columns:r.column_name?[r.column_name]:[]};r.foreign_table_name&&(i.foreignTable=r.foreign_table_name),r.foreign_column_name&&(i.foreignColumns=[r.foreign_column_name]),r.check_clause&&(i.checkExpression=r.check_clause),a.set(r.constraint_name,i);}}return Array.from(a.values())}function se(c,e){let t=[],n=new Map(c.map(r=>[r.name,r])),a=new Map(e.map(r=>[r.name,r]));for(let r of c){let s=a.get(r.name);if(!s){t.push({constraint:r.name,type:"missing",expected:`${r.type} on (${r.columns.join(", ")})`,description:`Constraint "${r.name}" (${r.type}) is missing`});continue}let i=r.columns.sort().join(","),o=s.columns.sort().join(",");(r.type!==s.type||i!==o)&&t.push({constraint:r.name,type:"definition_mismatch",expected:`${r.type} on (${r.columns.join(", ")})`,actual:`${s.type} on (${s.columns.join(", ")})`,description:`Constraint "${r.name}" definition differs`});}for(let r of e)n.has(r.name)||t.push({constraint:r.name,type:"extra",actual:`${r.type} on (${r.columns.join(", ")})`,description:`Extra constraint "${r.name}" (${r.type}) not in reference`});return t}var Ee="__drizzle_migrations",$=class{constructor(e,t,n){this.tenantConfig=e;this.schemaManager=t;this.driftConfig=n;this.migrationsTable=n.migrationsTable??Ee;}migrationsTable;getSchemaName(e){return this.tenantConfig.isolation.schemaNameTemplate(e)}async createPool(e){return this.schemaManager.createPool(e)}async detectDrift(e={}){let t=Date.now(),{concurrency:n=10,includeIndexes:a=true,includeConstraints:r=true,excludeTables:s=[this.migrationsTable],onProgress:i}=e,o=e.tenantIds??await this.driftConfig.tenantDiscovery();if(o.length===0)return {referenceTenant:"",total:0,noDrift:0,withDrift:0,error:0,details:[],timestamp:new Date().toISOString(),durationMs:Date.now()-t};let m=e.referenceTenant??o[0];i?.(m,"starting"),i?.(m,"introspecting");let l=await this.introspectSchema(m,{includeIndexes:a,includeConstraints:r,excludeTables:s});if(!l)return {referenceTenant:m,total:o.length,noDrift:0,withDrift:0,error:o.length,details:o.map(d=>({tenantId:d,schemaName:this.getSchemaName(d),hasDrift:false,tables:[],issueCount:0,error:d===m?"Failed to introspect reference tenant":"Reference tenant introspection failed"})),timestamp:new Date().toISOString(),durationMs:Date.now()-t};i?.(m,"completed");let u=o.filter(d=>d!==m),h=[];h.push({tenantId:m,schemaName:l.schemaName,hasDrift:false,tables:[],issueCount:0});for(let d=0;d<u.length;d+=n){let g=u.slice(d,d+n),w=await Promise.all(g.map(async y=>{try{i?.(y,"starting"),i?.(y,"introspecting");let b=await this.introspectSchema(y,{includeIndexes:a,includeConstraints:r,excludeTables:s});if(!b)return i?.(y,"failed"),{tenantId:y,schemaName:this.getSchemaName(y),hasDrift:!1,tables:[],issueCount:0,error:"Failed to introspect schema"};i?.(y,"comparing");let S=this.compareSchemas(l,b,{includeIndexes:a,includeConstraints:r});return i?.(y,"completed"),S}catch(b){return i?.(y,"failed"),{tenantId:y,schemaName:this.getSchemaName(y),hasDrift:false,tables:[],issueCount:0,error:b.message}}}));h.push(...w);}return {referenceTenant:m,total:h.length,noDrift:h.filter(d=>!d.hasDrift&&!d.error).length,withDrift:h.filter(d=>d.hasDrift&&!d.error).length,error:h.filter(d=>!!d.error).length,details:h,timestamp:new Date().toISOString(),durationMs:Date.now()-t}}async compareTenant(e,t,n={}){let{includeIndexes:a=true,includeConstraints:r=true,excludeTables:s=[this.migrationsTable]}=n,i=await this.introspectSchema(t,{includeIndexes:a,includeConstraints:r,excludeTables:s});if(!i)return {tenantId:e,schemaName:this.getSchemaName(e),hasDrift:false,tables:[],issueCount:0,error:"Failed to introspect reference tenant"};let o=await this.introspectSchema(e,{includeIndexes:a,includeConstraints:r,excludeTables:s});return o?this.compareSchemas(i,o,{includeIndexes:a,includeConstraints:r}):{tenantId:e,schemaName:this.getSchemaName(e),hasDrift:false,tables:[],issueCount:0,error:"Failed to introspect tenant schema"}}async introspectSchema(e,t={}){let n=this.getSchemaName(e),a=await this.createPool(n);try{let r=await this.introspectTables(a,n,t);return {tenantId:e,schemaName:n,tables:r,introspectedAt:new Date}}catch{return null}finally{await a.end();}}compareSchemas(e,t,n={}){let{includeIndexes:a=true,includeConstraints:r=true}=n,s=[],i=0,o=new Map(e.tables.map(l=>[l.name,l])),m=new Map(t.tables.map(l=>[l.name,l]));for(let l of e.tables){let u=m.get(l.name);if(!u){s.push({table:l.name,status:"missing",columns:l.columns.map(y=>({column:y.name,type:"missing",expected:y.dataType,description:`Column "${y.name}" (${y.dataType}) is missing`})),indexes:[],constraints:[]}),i+=l.columns.length;continue}let h=te(l.columns,u.columns),d=a?re(l.indexes,u.indexes):[],g=r?se(l.constraints,u.constraints):[],w=h.length+d.length+g.length;i+=w,w>0&&s.push({table:l.name,status:"drifted",columns:h,indexes:d,constraints:g});}for(let l of t.tables)o.has(l.name)||(s.push({table:l.name,status:"extra",columns:l.columns.map(u=>({column:u.name,type:"extra",actual:u.dataType,description:`Extra column "${u.name}" (${u.dataType}) not in reference`})),indexes:[],constraints:[]}),i+=l.columns.length);return {tenantId:t.tenantId,schemaName:t.schemaName,hasDrift:i>0,tables:s,issueCount:i}}async introspectTables(e,t,n){let{includeIndexes:a=true,includeConstraints:r=true,excludeTables:s=[]}=n,i=await e.query(`SELECT table_name
|
|
2259
59
|
FROM information_schema.tables
|
|
2260
60
|
WHERE table_schema = $1
|
|
2261
61
|
AND table_type = 'BASE TABLE'
|
|
2262
|
-
ORDER BY table_name`,
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
if (excludeTables.includes(row.table_name)) {
|
|
2268
|
-
continue;
|
|
2269
|
-
}
|
|
2270
|
-
const columns = await introspectColumns(pool, schemaName, row.table_name);
|
|
2271
|
-
const indexes = includeIndexes ? await introspectIndexes(pool, schemaName, row.table_name) : [];
|
|
2272
|
-
const constraints = includeConstraints ? await introspectConstraints(pool, schemaName, row.table_name) : [];
|
|
2273
|
-
tables.push({
|
|
2274
|
-
name: row.table_name,
|
|
2275
|
-
columns,
|
|
2276
|
-
indexes,
|
|
2277
|
-
constraints
|
|
2278
|
-
});
|
|
2279
|
-
}
|
|
2280
|
-
return tables;
|
|
2281
|
-
}
|
|
2282
|
-
};
|
|
2283
|
-
var Seeder = class {
|
|
2284
|
-
constructor(config, deps) {
|
|
2285
|
-
this.config = config;
|
|
2286
|
-
this.deps = deps;
|
|
2287
|
-
}
|
|
2288
|
-
/**
|
|
2289
|
-
* Seed a single tenant with initial data
|
|
2290
|
-
*
|
|
2291
|
-
* Creates a database connection for the tenant, executes the seed function,
|
|
2292
|
-
* and properly cleans up the connection afterward.
|
|
2293
|
-
*
|
|
2294
|
-
* @param tenantId - The tenant identifier
|
|
2295
|
-
* @param seedFn - Function that seeds the database
|
|
2296
|
-
* @returns Result of the seeding operation
|
|
2297
|
-
*
|
|
2298
|
-
* @example
|
|
2299
|
-
* ```typescript
|
|
2300
|
-
* const result = await seeder.seedTenant('tenant-123', async (db, tenantId) => {
|
|
2301
|
-
* await db.insert(users).values([
|
|
2302
|
-
* { name: 'Admin', email: `admin@${tenantId}.com` },
|
|
2303
|
-
* ]);
|
|
2304
|
-
* });
|
|
2305
|
-
*
|
|
2306
|
-
* if (result.success) {
|
|
2307
|
-
* console.log(`Seeded ${result.tenantId} in ${result.durationMs}ms`);
|
|
2308
|
-
* }
|
|
2309
|
-
* ```
|
|
2310
|
-
*/
|
|
2311
|
-
async seedTenant(tenantId, seedFn) {
|
|
2312
|
-
const startTime = Date.now();
|
|
2313
|
-
const schemaName = this.deps.schemaNameTemplate(tenantId);
|
|
2314
|
-
const pool = await this.deps.createPool(schemaName);
|
|
2315
|
-
try {
|
|
2316
|
-
const db = drizzle(pool, {
|
|
2317
|
-
schema: this.deps.tenantSchema
|
|
2318
|
-
});
|
|
2319
|
-
await seedFn(db, tenantId);
|
|
2320
|
-
return {
|
|
2321
|
-
tenantId,
|
|
2322
|
-
schemaName,
|
|
2323
|
-
success: true,
|
|
2324
|
-
durationMs: Date.now() - startTime
|
|
2325
|
-
};
|
|
2326
|
-
} catch (error) {
|
|
2327
|
-
return {
|
|
2328
|
-
tenantId,
|
|
2329
|
-
schemaName,
|
|
2330
|
-
success: false,
|
|
2331
|
-
error: error.message,
|
|
2332
|
-
durationMs: Date.now() - startTime
|
|
2333
|
-
};
|
|
2334
|
-
} finally {
|
|
2335
|
-
await pool.end();
|
|
2336
|
-
}
|
|
2337
|
-
}
|
|
2338
|
-
/**
|
|
2339
|
-
* Seed all tenants with initial data in parallel
|
|
2340
|
-
*
|
|
2341
|
-
* Discovers all tenants and seeds them in batches with configurable concurrency.
|
|
2342
|
-
* Supports progress callbacks and abort-on-error behavior.
|
|
2343
|
-
*
|
|
2344
|
-
* @param seedFn - Function that seeds each database
|
|
2345
|
-
* @param options - Seeding options
|
|
2346
|
-
* @returns Aggregate results of all seeding operations
|
|
2347
|
-
*
|
|
2348
|
-
* @example
|
|
2349
|
-
* ```typescript
|
|
2350
|
-
* const results = await seeder.seedAll(
|
|
2351
|
-
* async (db, tenantId) => {
|
|
2352
|
-
* await db.insert(settings).values({ key: 'initialized', value: 'true' });
|
|
2353
|
-
* },
|
|
2354
|
-
* {
|
|
2355
|
-
* concurrency: 5,
|
|
2356
|
-
* onProgress: (id, status) => console.log(`${id}: ${status}`),
|
|
2357
|
-
* }
|
|
2358
|
-
* );
|
|
2359
|
-
*
|
|
2360
|
-
* console.log(`Succeeded: ${results.succeeded}/${results.total}`);
|
|
2361
|
-
* ```
|
|
2362
|
-
*/
|
|
2363
|
-
async seedAll(seedFn, options = {}) {
|
|
2364
|
-
const { concurrency = 10, onProgress, onError } = options;
|
|
2365
|
-
const tenantIds = await this.config.tenantDiscovery();
|
|
2366
|
-
const results = [];
|
|
2367
|
-
let aborted = false;
|
|
2368
|
-
for (let i = 0; i < tenantIds.length && !aborted; i += concurrency) {
|
|
2369
|
-
const batch = tenantIds.slice(i, i + concurrency);
|
|
2370
|
-
const batchResults = await Promise.all(
|
|
2371
|
-
batch.map(async (tenantId) => {
|
|
2372
|
-
if (aborted) {
|
|
2373
|
-
return this.createSkippedResult(tenantId);
|
|
2374
|
-
}
|
|
2375
|
-
try {
|
|
2376
|
-
onProgress?.(tenantId, "starting");
|
|
2377
|
-
onProgress?.(tenantId, "seeding");
|
|
2378
|
-
const result = await this.seedTenant(tenantId, seedFn);
|
|
2379
|
-
onProgress?.(tenantId, result.success ? "completed" : "failed");
|
|
2380
|
-
return result;
|
|
2381
|
-
} catch (error) {
|
|
2382
|
-
onProgress?.(tenantId, "failed");
|
|
2383
|
-
const action = onError?.(tenantId, error);
|
|
2384
|
-
if (action === "abort") {
|
|
2385
|
-
aborted = true;
|
|
2386
|
-
}
|
|
2387
|
-
return this.createErrorResult(tenantId, error);
|
|
2388
|
-
}
|
|
2389
|
-
})
|
|
2390
|
-
);
|
|
2391
|
-
results.push(...batchResults);
|
|
2392
|
-
}
|
|
2393
|
-
if (aborted) {
|
|
2394
|
-
const remaining = tenantIds.slice(results.length);
|
|
2395
|
-
for (const tenantId of remaining) {
|
|
2396
|
-
results.push(this.createSkippedResult(tenantId));
|
|
2397
|
-
}
|
|
2398
|
-
}
|
|
2399
|
-
return this.aggregateResults(results);
|
|
2400
|
-
}
|
|
2401
|
-
/**
|
|
2402
|
-
* Seed specific tenants with initial data
|
|
2403
|
-
*
|
|
2404
|
-
* Seeds only the specified tenants in batches with configurable concurrency.
|
|
2405
|
-
*
|
|
2406
|
-
* @param tenantIds - List of tenant IDs to seed
|
|
2407
|
-
* @param seedFn - Function that seeds each database
|
|
2408
|
-
* @param options - Seeding options
|
|
2409
|
-
* @returns Aggregate results of seeding operations
|
|
2410
|
-
*
|
|
2411
|
-
* @example
|
|
2412
|
-
* ```typescript
|
|
2413
|
-
* const results = await seeder.seedTenants(
|
|
2414
|
-
* ['tenant-1', 'tenant-2', 'tenant-3'],
|
|
2415
|
-
* async (db) => {
|
|
2416
|
-
* await db.insert(config).values({ setup: true });
|
|
2417
|
-
* },
|
|
2418
|
-
* { concurrency: 2 }
|
|
2419
|
-
* );
|
|
2420
|
-
* ```
|
|
2421
|
-
*/
|
|
2422
|
-
async seedTenants(tenantIds, seedFn, options = {}) {
|
|
2423
|
-
const { concurrency = 10, onProgress, onError } = options;
|
|
2424
|
-
const results = [];
|
|
2425
|
-
for (let i = 0; i < tenantIds.length; i += concurrency) {
|
|
2426
|
-
const batch = tenantIds.slice(i, i + concurrency);
|
|
2427
|
-
const batchResults = await Promise.all(
|
|
2428
|
-
batch.map(async (tenantId) => {
|
|
2429
|
-
try {
|
|
2430
|
-
onProgress?.(tenantId, "starting");
|
|
2431
|
-
onProgress?.(tenantId, "seeding");
|
|
2432
|
-
const result = await this.seedTenant(tenantId, seedFn);
|
|
2433
|
-
onProgress?.(tenantId, result.success ? "completed" : "failed");
|
|
2434
|
-
return result;
|
|
2435
|
-
} catch (error) {
|
|
2436
|
-
onProgress?.(tenantId, "failed");
|
|
2437
|
-
onError?.(tenantId, error);
|
|
2438
|
-
return this.createErrorResult(tenantId, error);
|
|
2439
|
-
}
|
|
2440
|
-
})
|
|
2441
|
-
);
|
|
2442
|
-
results.push(...batchResults);
|
|
2443
|
-
}
|
|
2444
|
-
return this.aggregateResults(results);
|
|
2445
|
-
}
|
|
2446
|
-
/**
|
|
2447
|
-
* Create a skipped result for aborted seeding
|
|
2448
|
-
*/
|
|
2449
|
-
createSkippedResult(tenantId) {
|
|
2450
|
-
return {
|
|
2451
|
-
tenantId,
|
|
2452
|
-
schemaName: this.deps.schemaNameTemplate(tenantId),
|
|
2453
|
-
success: false,
|
|
2454
|
-
error: "Skipped due to abort",
|
|
2455
|
-
durationMs: 0
|
|
2456
|
-
};
|
|
2457
|
-
}
|
|
2458
|
-
/**
|
|
2459
|
-
* Create an error result for failed seeding
|
|
2460
|
-
*/
|
|
2461
|
-
createErrorResult(tenantId, error) {
|
|
2462
|
-
return {
|
|
2463
|
-
tenantId,
|
|
2464
|
-
schemaName: this.deps.schemaNameTemplate(tenantId),
|
|
2465
|
-
success: false,
|
|
2466
|
-
error: error.message,
|
|
2467
|
-
durationMs: 0
|
|
2468
|
-
};
|
|
2469
|
-
}
|
|
2470
|
-
/**
|
|
2471
|
-
* Aggregate individual results into a summary
|
|
2472
|
-
*/
|
|
2473
|
-
aggregateResults(results) {
|
|
2474
|
-
return {
|
|
2475
|
-
total: results.length,
|
|
2476
|
-
succeeded: results.filter((r) => r.success).length,
|
|
2477
|
-
failed: results.filter(
|
|
2478
|
-
(r) => !r.success && r.error !== "Skipped due to abort"
|
|
2479
|
-
).length,
|
|
2480
|
-
skipped: results.filter((r) => r.error === "Skipped due to abort").length,
|
|
2481
|
-
details: results
|
|
2482
|
-
};
|
|
2483
|
-
}
|
|
2484
|
-
};
|
|
2485
|
-
|
|
2486
|
-
// src/migrator/sync/sync-manager.ts
|
|
2487
|
-
var SyncManager = class {
|
|
2488
|
-
constructor(config, deps) {
|
|
2489
|
-
this.config = config;
|
|
2490
|
-
this.deps = deps;
|
|
2491
|
-
}
|
|
2492
|
-
/**
|
|
2493
|
-
* Get sync status for all tenants
|
|
2494
|
-
*
|
|
2495
|
-
* Detects divergences between migrations on disk and tracking in database.
|
|
2496
|
-
* A tenant is "in sync" when all disk migrations are tracked and no orphan records exist.
|
|
2497
|
-
*
|
|
2498
|
-
* @returns Aggregate sync status for all tenants
|
|
2499
|
-
*
|
|
2500
|
-
* @example
|
|
2501
|
-
* ```typescript
|
|
2502
|
-
* const status = await syncManager.getSyncStatus();
|
|
2503
|
-
* console.log(`Total: ${status.total}, In sync: ${status.inSync}, Out of sync: ${status.outOfSync}`);
|
|
2504
|
-
*
|
|
2505
|
-
* for (const tenant of status.details.filter(d => !d.inSync)) {
|
|
2506
|
-
* console.log(`${tenant.tenantId}: missing=${tenant.missing.length}, orphans=${tenant.orphans.length}`);
|
|
2507
|
-
* }
|
|
2508
|
-
* ```
|
|
2509
|
-
*/
|
|
2510
|
-
async getSyncStatus() {
|
|
2511
|
-
const tenantIds = await this.config.tenantDiscovery();
|
|
2512
|
-
const migrations = await this.deps.loadMigrations();
|
|
2513
|
-
const statuses = [];
|
|
2514
|
-
for (const tenantId of tenantIds) {
|
|
2515
|
-
statuses.push(await this.getTenantSyncStatus(tenantId, migrations));
|
|
2516
|
-
}
|
|
2517
|
-
return {
|
|
2518
|
-
total: statuses.length,
|
|
2519
|
-
inSync: statuses.filter((s) => s.inSync && !s.error).length,
|
|
2520
|
-
outOfSync: statuses.filter((s) => !s.inSync && !s.error).length,
|
|
2521
|
-
error: statuses.filter((s) => !!s.error).length,
|
|
2522
|
-
details: statuses
|
|
2523
|
-
};
|
|
2524
|
-
}
|
|
2525
|
-
/**
|
|
2526
|
-
* Get sync status for a specific tenant
|
|
2527
|
-
*
|
|
2528
|
-
* Compares migrations on disk with records in the database.
|
|
2529
|
-
* Identifies missing migrations (on disk but not tracked) and
|
|
2530
|
-
* orphan records (tracked but not on disk).
|
|
2531
|
-
*
|
|
2532
|
-
* @param tenantId - The tenant identifier
|
|
2533
|
-
* @param migrations - Optional pre-loaded migrations (avoids reloading from disk)
|
|
2534
|
-
* @returns Sync status for the tenant
|
|
2535
|
-
*
|
|
2536
|
-
* @example
|
|
2537
|
-
* ```typescript
|
|
2538
|
-
* const status = await syncManager.getTenantSyncStatus('tenant-123');
|
|
2539
|
-
* if (status.missing.length > 0) {
|
|
2540
|
-
* console.log(`Missing: ${status.missing.join(', ')}`);
|
|
2541
|
-
* }
|
|
2542
|
-
* if (status.orphans.length > 0) {
|
|
2543
|
-
* console.log(`Orphans: ${status.orphans.join(', ')}`);
|
|
2544
|
-
* }
|
|
2545
|
-
* ```
|
|
2546
|
-
*/
|
|
2547
|
-
async getTenantSyncStatus(tenantId, migrations) {
|
|
2548
|
-
const schemaName = this.deps.schemaNameTemplate(tenantId);
|
|
2549
|
-
const pool = await this.deps.createPool(schemaName);
|
|
2550
|
-
try {
|
|
2551
|
-
const allMigrations = migrations ?? await this.deps.loadMigrations();
|
|
2552
|
-
const migrationNames = new Set(allMigrations.map((m) => m.name));
|
|
2553
|
-
const migrationHashes = new Set(allMigrations.map((m) => m.hash));
|
|
2554
|
-
const tableExists = await this.deps.migrationsTableExists(pool, schemaName);
|
|
2555
|
-
if (!tableExists) {
|
|
2556
|
-
return {
|
|
2557
|
-
tenantId,
|
|
2558
|
-
schemaName,
|
|
2559
|
-
missing: allMigrations.map((m) => m.name),
|
|
2560
|
-
orphans: [],
|
|
2561
|
-
inSync: allMigrations.length === 0,
|
|
2562
|
-
format: null
|
|
2563
|
-
};
|
|
2564
|
-
}
|
|
2565
|
-
const format = await this.deps.getOrDetectFormat(pool, schemaName);
|
|
2566
|
-
const applied = await this.getAppliedMigrations(pool, schemaName, format);
|
|
2567
|
-
const appliedIdentifiers = new Set(applied.map((m) => m.identifier));
|
|
2568
|
-
const missing = allMigrations.filter((m) => !this.isMigrationApplied(m, appliedIdentifiers, format)).map((m) => m.name);
|
|
2569
|
-
const orphans = applied.filter((m) => {
|
|
2570
|
-
if (format.columns.identifier === "name") {
|
|
2571
|
-
return !migrationNames.has(m.identifier);
|
|
2572
|
-
}
|
|
2573
|
-
return !migrationHashes.has(m.identifier) && !migrationNames.has(m.identifier);
|
|
2574
|
-
}).map((m) => m.identifier);
|
|
2575
|
-
return {
|
|
2576
|
-
tenantId,
|
|
2577
|
-
schemaName,
|
|
2578
|
-
missing,
|
|
2579
|
-
orphans,
|
|
2580
|
-
inSync: missing.length === 0 && orphans.length === 0,
|
|
2581
|
-
format: format.format
|
|
2582
|
-
};
|
|
2583
|
-
} catch (error) {
|
|
2584
|
-
return {
|
|
2585
|
-
tenantId,
|
|
2586
|
-
schemaName,
|
|
2587
|
-
missing: [],
|
|
2588
|
-
orphans: [],
|
|
2589
|
-
inSync: false,
|
|
2590
|
-
format: null,
|
|
2591
|
-
error: error.message
|
|
2592
|
-
};
|
|
2593
|
-
} finally {
|
|
2594
|
-
await pool.end();
|
|
2595
|
-
}
|
|
2596
|
-
}
|
|
2597
|
-
/**
|
|
2598
|
-
* Mark missing migrations as applied for a tenant
|
|
2599
|
-
*
|
|
2600
|
-
* Records migrations that exist on disk but are not tracked in the database.
|
|
2601
|
-
* Useful for syncing tracking state with already-applied migrations.
|
|
2602
|
-
*
|
|
2603
|
-
* @param tenantId - The tenant identifier
|
|
2604
|
-
* @returns Result of the mark operation
|
|
2605
|
-
*
|
|
2606
|
-
* @example
|
|
2607
|
-
* ```typescript
|
|
2608
|
-
* const result = await syncManager.markMissing('tenant-123');
|
|
2609
|
-
* if (result.success) {
|
|
2610
|
-
* console.log(`Marked ${result.markedMigrations.length} migrations as applied`);
|
|
2611
|
-
* }
|
|
2612
|
-
* ```
|
|
2613
|
-
*/
|
|
2614
|
-
async markMissing(tenantId) {
|
|
2615
|
-
const startTime = Date.now();
|
|
2616
|
-
const schemaName = this.deps.schemaNameTemplate(tenantId);
|
|
2617
|
-
const markedMigrations = [];
|
|
2618
|
-
const pool = await this.deps.createPool(schemaName);
|
|
2619
|
-
try {
|
|
2620
|
-
const syncStatus = await this.getTenantSyncStatus(tenantId);
|
|
2621
|
-
if (syncStatus.error) {
|
|
2622
|
-
return {
|
|
2623
|
-
tenantId,
|
|
2624
|
-
schemaName,
|
|
2625
|
-
success: false,
|
|
2626
|
-
markedMigrations: [],
|
|
2627
|
-
removedOrphans: [],
|
|
2628
|
-
error: syncStatus.error,
|
|
2629
|
-
durationMs: Date.now() - startTime
|
|
2630
|
-
};
|
|
2631
|
-
}
|
|
2632
|
-
if (syncStatus.missing.length === 0) {
|
|
2633
|
-
return {
|
|
2634
|
-
tenantId,
|
|
2635
|
-
schemaName,
|
|
2636
|
-
success: true,
|
|
2637
|
-
markedMigrations: [],
|
|
2638
|
-
removedOrphans: [],
|
|
2639
|
-
durationMs: Date.now() - startTime
|
|
2640
|
-
};
|
|
2641
|
-
}
|
|
2642
|
-
const format = await this.deps.getOrDetectFormat(pool, schemaName);
|
|
2643
|
-
await this.deps.ensureMigrationsTable(pool, schemaName, format);
|
|
2644
|
-
const allMigrations = await this.deps.loadMigrations();
|
|
2645
|
-
const missingSet = new Set(syncStatus.missing);
|
|
2646
|
-
for (const migration of allMigrations) {
|
|
2647
|
-
if (missingSet.has(migration.name)) {
|
|
2648
|
-
await this.recordMigration(pool, schemaName, migration, format);
|
|
2649
|
-
markedMigrations.push(migration.name);
|
|
2650
|
-
}
|
|
2651
|
-
}
|
|
2652
|
-
return {
|
|
2653
|
-
tenantId,
|
|
2654
|
-
schemaName,
|
|
2655
|
-
success: true,
|
|
2656
|
-
markedMigrations,
|
|
2657
|
-
removedOrphans: [],
|
|
2658
|
-
durationMs: Date.now() - startTime
|
|
2659
|
-
};
|
|
2660
|
-
} catch (error) {
|
|
2661
|
-
return {
|
|
2662
|
-
tenantId,
|
|
2663
|
-
schemaName,
|
|
2664
|
-
success: false,
|
|
2665
|
-
markedMigrations,
|
|
2666
|
-
removedOrphans: [],
|
|
2667
|
-
error: error.message,
|
|
2668
|
-
durationMs: Date.now() - startTime
|
|
2669
|
-
};
|
|
2670
|
-
} finally {
|
|
2671
|
-
await pool.end();
|
|
2672
|
-
}
|
|
2673
|
-
}
|
|
2674
|
-
/**
|
|
2675
|
-
* Mark missing migrations as applied for all tenants
|
|
2676
|
-
*
|
|
2677
|
-
* Processes all tenants in parallel with configurable concurrency.
|
|
2678
|
-
* Supports progress callbacks and abort-on-error behavior.
|
|
2679
|
-
*
|
|
2680
|
-
* @param options - Sync options
|
|
2681
|
-
* @returns Aggregate results of all mark operations
|
|
2682
|
-
*
|
|
2683
|
-
* @example
|
|
2684
|
-
* ```typescript
|
|
2685
|
-
* const results = await syncManager.markAllMissing({
|
|
2686
|
-
* concurrency: 10,
|
|
2687
|
-
* onProgress: (id, status) => console.log(`${id}: ${status}`),
|
|
2688
|
-
* });
|
|
2689
|
-
* console.log(`Succeeded: ${results.succeeded}/${results.total}`);
|
|
2690
|
-
* ```
|
|
2691
|
-
*/
|
|
2692
|
-
async markAllMissing(options = {}) {
|
|
2693
|
-
const { concurrency = 10, onProgress, onError } = options;
|
|
2694
|
-
const tenantIds = await this.config.tenantDiscovery();
|
|
2695
|
-
const results = [];
|
|
2696
|
-
let aborted = false;
|
|
2697
|
-
for (let i = 0; i < tenantIds.length && !aborted; i += concurrency) {
|
|
2698
|
-
const batch = tenantIds.slice(i, i + concurrency);
|
|
2699
|
-
const batchResults = await Promise.all(
|
|
2700
|
-
batch.map(async (tenantId) => {
|
|
2701
|
-
if (aborted) {
|
|
2702
|
-
return this.createSkippedSyncResult(tenantId);
|
|
2703
|
-
}
|
|
2704
|
-
try {
|
|
2705
|
-
onProgress?.(tenantId, "starting");
|
|
2706
|
-
const result = await this.markMissing(tenantId);
|
|
2707
|
-
onProgress?.(tenantId, result.success ? "completed" : "failed");
|
|
2708
|
-
return result;
|
|
2709
|
-
} catch (error) {
|
|
2710
|
-
onProgress?.(tenantId, "failed");
|
|
2711
|
-
const action = onError?.(tenantId, error);
|
|
2712
|
-
if (action === "abort") {
|
|
2713
|
-
aborted = true;
|
|
2714
|
-
}
|
|
2715
|
-
return this.createErrorSyncResult(tenantId, error);
|
|
2716
|
-
}
|
|
2717
|
-
})
|
|
2718
|
-
);
|
|
2719
|
-
results.push(...batchResults);
|
|
2720
|
-
}
|
|
2721
|
-
return this.aggregateSyncResults(results);
|
|
2722
|
-
}
|
|
2723
|
-
/**
|
|
2724
|
-
* Remove orphan migration records for a tenant
|
|
2725
|
-
*
|
|
2726
|
-
* Deletes records from the migrations table that don't have
|
|
2727
|
-
* corresponding files on disk.
|
|
2728
|
-
*
|
|
2729
|
-
* @param tenantId - The tenant identifier
|
|
2730
|
-
* @returns Result of the clean operation
|
|
2731
|
-
*
|
|
2732
|
-
* @example
|
|
2733
|
-
* ```typescript
|
|
2734
|
-
* const result = await syncManager.cleanOrphans('tenant-123');
|
|
2735
|
-
* if (result.success) {
|
|
2736
|
-
* console.log(`Removed ${result.removedOrphans.length} orphan records`);
|
|
2737
|
-
* }
|
|
2738
|
-
* ```
|
|
2739
|
-
*/
|
|
2740
|
-
async cleanOrphans(tenantId) {
|
|
2741
|
-
const startTime = Date.now();
|
|
2742
|
-
const schemaName = this.deps.schemaNameTemplate(tenantId);
|
|
2743
|
-
const removedOrphans = [];
|
|
2744
|
-
const pool = await this.deps.createPool(schemaName);
|
|
2745
|
-
try {
|
|
2746
|
-
const syncStatus = await this.getTenantSyncStatus(tenantId);
|
|
2747
|
-
if (syncStatus.error) {
|
|
2748
|
-
return {
|
|
2749
|
-
tenantId,
|
|
2750
|
-
schemaName,
|
|
2751
|
-
success: false,
|
|
2752
|
-
markedMigrations: [],
|
|
2753
|
-
removedOrphans: [],
|
|
2754
|
-
error: syncStatus.error,
|
|
2755
|
-
durationMs: Date.now() - startTime
|
|
2756
|
-
};
|
|
2757
|
-
}
|
|
2758
|
-
if (syncStatus.orphans.length === 0) {
|
|
2759
|
-
return {
|
|
2760
|
-
tenantId,
|
|
2761
|
-
schemaName,
|
|
2762
|
-
success: true,
|
|
2763
|
-
markedMigrations: [],
|
|
2764
|
-
removedOrphans: [],
|
|
2765
|
-
durationMs: Date.now() - startTime
|
|
2766
|
-
};
|
|
2767
|
-
}
|
|
2768
|
-
const format = await this.deps.getOrDetectFormat(pool, schemaName);
|
|
2769
|
-
const identifierColumn = format.columns.identifier;
|
|
2770
|
-
for (const orphan of syncStatus.orphans) {
|
|
2771
|
-
await pool.query(
|
|
2772
|
-
`DELETE FROM "${schemaName}"."${format.tableName}" WHERE "${identifierColumn}" = $1`,
|
|
2773
|
-
[orphan]
|
|
2774
|
-
);
|
|
2775
|
-
removedOrphans.push(orphan);
|
|
2776
|
-
}
|
|
2777
|
-
return {
|
|
2778
|
-
tenantId,
|
|
2779
|
-
schemaName,
|
|
2780
|
-
success: true,
|
|
2781
|
-
markedMigrations: [],
|
|
2782
|
-
removedOrphans,
|
|
2783
|
-
durationMs: Date.now() - startTime
|
|
2784
|
-
};
|
|
2785
|
-
} catch (error) {
|
|
2786
|
-
return {
|
|
2787
|
-
tenantId,
|
|
2788
|
-
schemaName,
|
|
2789
|
-
success: false,
|
|
2790
|
-
markedMigrations: [],
|
|
2791
|
-
removedOrphans,
|
|
2792
|
-
error: error.message,
|
|
2793
|
-
durationMs: Date.now() - startTime
|
|
2794
|
-
};
|
|
2795
|
-
} finally {
|
|
2796
|
-
await pool.end();
|
|
2797
|
-
}
|
|
2798
|
-
}
|
|
2799
|
-
/**
|
|
2800
|
-
* Remove orphan migration records for all tenants
|
|
2801
|
-
*
|
|
2802
|
-
* Processes all tenants in parallel with configurable concurrency.
|
|
2803
|
-
* Supports progress callbacks and abort-on-error behavior.
|
|
2804
|
-
*
|
|
2805
|
-
* @param options - Sync options
|
|
2806
|
-
* @returns Aggregate results of all clean operations
|
|
2807
|
-
*
|
|
2808
|
-
* @example
|
|
2809
|
-
* ```typescript
|
|
2810
|
-
* const results = await syncManager.cleanAllOrphans({
|
|
2811
|
-
* concurrency: 10,
|
|
2812
|
-
* onProgress: (id, status) => console.log(`${id}: ${status}`),
|
|
2813
|
-
* });
|
|
2814
|
-
* console.log(`Succeeded: ${results.succeeded}/${results.total}`);
|
|
2815
|
-
* ```
|
|
2816
|
-
*/
|
|
2817
|
-
async cleanAllOrphans(options = {}) {
|
|
2818
|
-
const { concurrency = 10, onProgress, onError } = options;
|
|
2819
|
-
const tenantIds = await this.config.tenantDiscovery();
|
|
2820
|
-
const results = [];
|
|
2821
|
-
let aborted = false;
|
|
2822
|
-
for (let i = 0; i < tenantIds.length && !aborted; i += concurrency) {
|
|
2823
|
-
const batch = tenantIds.slice(i, i + concurrency);
|
|
2824
|
-
const batchResults = await Promise.all(
|
|
2825
|
-
batch.map(async (tenantId) => {
|
|
2826
|
-
if (aborted) {
|
|
2827
|
-
return this.createSkippedSyncResult(tenantId);
|
|
2828
|
-
}
|
|
2829
|
-
try {
|
|
2830
|
-
onProgress?.(tenantId, "starting");
|
|
2831
|
-
const result = await this.cleanOrphans(tenantId);
|
|
2832
|
-
onProgress?.(tenantId, result.success ? "completed" : "failed");
|
|
2833
|
-
return result;
|
|
2834
|
-
} catch (error) {
|
|
2835
|
-
onProgress?.(tenantId, "failed");
|
|
2836
|
-
const action = onError?.(tenantId, error);
|
|
2837
|
-
if (action === "abort") {
|
|
2838
|
-
aborted = true;
|
|
2839
|
-
}
|
|
2840
|
-
return this.createErrorSyncResult(tenantId, error);
|
|
2841
|
-
}
|
|
2842
|
-
})
|
|
2843
|
-
);
|
|
2844
|
-
results.push(...batchResults);
|
|
2845
|
-
}
|
|
2846
|
-
return this.aggregateSyncResults(results);
|
|
2847
|
-
}
|
|
2848
|
-
// ============================================================================
|
|
2849
|
-
// Private Helper Methods
|
|
2850
|
-
// ============================================================================
|
|
2851
|
-
/**
|
|
2852
|
-
* Get applied migrations for a schema
|
|
2853
|
-
*/
|
|
2854
|
-
async getAppliedMigrations(pool, schemaName, format) {
|
|
2855
|
-
const identifierColumn = format.columns.identifier;
|
|
2856
|
-
const timestampColumn = format.columns.timestamp;
|
|
2857
|
-
const result = await pool.query(
|
|
2858
|
-
`SELECT id, "${identifierColumn}" as identifier, "${timestampColumn}" as applied_at
|
|
2859
|
-
FROM "${schemaName}"."${format.tableName}"
|
|
2860
|
-
ORDER BY id`
|
|
2861
|
-
);
|
|
2862
|
-
return result.rows.map((row) => {
|
|
2863
|
-
const appliedAt = format.columns.timestampType === "bigint" ? new Date(Number(row.applied_at)) : new Date(row.applied_at);
|
|
2864
|
-
return {
|
|
2865
|
-
identifier: row.identifier,
|
|
2866
|
-
appliedAt
|
|
2867
|
-
};
|
|
2868
|
-
});
|
|
2869
|
-
}
|
|
2870
|
-
/**
|
|
2871
|
-
* Check if a migration has been applied
|
|
2872
|
-
*/
|
|
2873
|
-
isMigrationApplied(migration, appliedIdentifiers, format) {
|
|
2874
|
-
if (format.columns.identifier === "name") {
|
|
2875
|
-
return appliedIdentifiers.has(migration.name);
|
|
2876
|
-
}
|
|
2877
|
-
return appliedIdentifiers.has(migration.hash) || appliedIdentifiers.has(migration.name);
|
|
2878
|
-
}
|
|
2879
|
-
/**
|
|
2880
|
-
* Record a migration as applied (without executing SQL)
|
|
2881
|
-
*/
|
|
2882
|
-
async recordMigration(pool, schemaName, migration, format) {
|
|
2883
|
-
const { identifier, timestamp, timestampType } = format.columns;
|
|
2884
|
-
const identifierValue = identifier === "name" ? migration.name : migration.hash;
|
|
2885
|
-
const timestampValue = timestampType === "bigint" ? Date.now() : /* @__PURE__ */ new Date();
|
|
2886
|
-
await pool.query(
|
|
2887
|
-
`INSERT INTO "${schemaName}"."${format.tableName}" ("${identifier}", "${timestamp}") VALUES ($1, $2)`,
|
|
2888
|
-
[identifierValue, timestampValue]
|
|
2889
|
-
);
|
|
2890
|
-
}
|
|
2891
|
-
/**
|
|
2892
|
-
* Create a skipped sync result for aborted operations
|
|
2893
|
-
*/
|
|
2894
|
-
createSkippedSyncResult(tenantId) {
|
|
2895
|
-
return {
|
|
2896
|
-
tenantId,
|
|
2897
|
-
schemaName: this.deps.schemaNameTemplate(tenantId),
|
|
2898
|
-
success: false,
|
|
2899
|
-
markedMigrations: [],
|
|
2900
|
-
removedOrphans: [],
|
|
2901
|
-
error: "Skipped due to abort",
|
|
2902
|
-
durationMs: 0
|
|
2903
|
-
};
|
|
2904
|
-
}
|
|
2905
|
-
/**
|
|
2906
|
-
* Create an error sync result for failed operations
|
|
2907
|
-
*/
|
|
2908
|
-
createErrorSyncResult(tenantId, error) {
|
|
2909
|
-
return {
|
|
2910
|
-
tenantId,
|
|
2911
|
-
schemaName: this.deps.schemaNameTemplate(tenantId),
|
|
2912
|
-
success: false,
|
|
2913
|
-
markedMigrations: [],
|
|
2914
|
-
removedOrphans: [],
|
|
2915
|
-
error: error.message,
|
|
2916
|
-
durationMs: 0
|
|
2917
|
-
};
|
|
2918
|
-
}
|
|
2919
|
-
/**
|
|
2920
|
-
* Aggregate individual sync results into a summary
|
|
2921
|
-
*/
|
|
2922
|
-
aggregateSyncResults(results) {
|
|
2923
|
-
return {
|
|
2924
|
-
total: results.length,
|
|
2925
|
-
succeeded: results.filter((r) => r.success).length,
|
|
2926
|
-
failed: results.filter((r) => !r.success).length,
|
|
2927
|
-
details: results
|
|
2928
|
-
};
|
|
2929
|
-
}
|
|
2930
|
-
};
|
|
2931
|
-
|
|
2932
|
-
// src/migrator/executor/migration-executor.ts
|
|
2933
|
-
var MigrationExecutor = class {
|
|
2934
|
-
constructor(config, deps) {
|
|
2935
|
-
this.config = config;
|
|
2936
|
-
this.deps = deps;
|
|
2937
|
-
}
|
|
2938
|
-
/**
|
|
2939
|
-
* Migrate a single tenant
|
|
2940
|
-
*
|
|
2941
|
-
* Applies all pending migrations to the tenant's schema.
|
|
2942
|
-
* Creates the migrations table if it doesn't exist.
|
|
2943
|
-
*
|
|
2944
|
-
* @param tenantId - The tenant identifier
|
|
2945
|
-
* @param migrations - Optional pre-loaded migrations (avoids reloading from disk)
|
|
2946
|
-
* @param options - Migration options (dryRun, onProgress)
|
|
2947
|
-
* @returns Migration result with applied migrations and duration
|
|
2948
|
-
*
|
|
2949
|
-
* @example
|
|
2950
|
-
* ```typescript
|
|
2951
|
-
* const result = await executor.migrateTenant('tenant-123', undefined, {
|
|
2952
|
-
* dryRun: false,
|
|
2953
|
-
* onProgress: (id, status, name) => console.log(`${id}: ${status} ${name}`),
|
|
2954
|
-
* });
|
|
2955
|
-
*
|
|
2956
|
-
* if (result.success) {
|
|
2957
|
-
* console.log(`Applied ${result.appliedMigrations.length} migrations`);
|
|
2958
|
-
* }
|
|
2959
|
-
* ```
|
|
2960
|
-
*/
|
|
2961
|
-
async migrateTenant(tenantId, migrations, options = {}) {
|
|
2962
|
-
const startTime = Date.now();
|
|
2963
|
-
const schemaName = this.deps.schemaNameTemplate(tenantId);
|
|
2964
|
-
const appliedMigrations = [];
|
|
2965
|
-
const pool = await this.deps.createPool(schemaName);
|
|
2966
|
-
try {
|
|
2967
|
-
await this.config.hooks?.beforeTenant?.(tenantId);
|
|
2968
|
-
const format = await this.deps.getOrDetectFormat(pool, schemaName);
|
|
2969
|
-
await this.deps.ensureMigrationsTable(pool, schemaName, format);
|
|
2970
|
-
const allMigrations = migrations ?? await this.deps.loadMigrations();
|
|
2971
|
-
const applied = await this.getAppliedMigrations(pool, schemaName, format);
|
|
2972
|
-
const appliedSet = new Set(applied.map((m) => m.identifier));
|
|
2973
|
-
const pending = allMigrations.filter(
|
|
2974
|
-
(m) => !this.isMigrationApplied(m, appliedSet, format)
|
|
2975
|
-
);
|
|
2976
|
-
if (options.dryRun) {
|
|
2977
|
-
return {
|
|
2978
|
-
tenantId,
|
|
2979
|
-
schemaName,
|
|
2980
|
-
success: true,
|
|
2981
|
-
appliedMigrations: pending.map((m) => m.name),
|
|
2982
|
-
durationMs: Date.now() - startTime,
|
|
2983
|
-
format: format.format
|
|
2984
|
-
};
|
|
2985
|
-
}
|
|
2986
|
-
for (const migration of pending) {
|
|
2987
|
-
const migrationStart = Date.now();
|
|
2988
|
-
options.onProgress?.(tenantId, "migrating", migration.name);
|
|
2989
|
-
await this.config.hooks?.beforeMigration?.(tenantId, migration.name);
|
|
2990
|
-
await this.applyMigration(pool, schemaName, migration, format);
|
|
2991
|
-
await this.config.hooks?.afterMigration?.(
|
|
2992
|
-
tenantId,
|
|
2993
|
-
migration.name,
|
|
2994
|
-
Date.now() - migrationStart
|
|
2995
|
-
);
|
|
2996
|
-
appliedMigrations.push(migration.name);
|
|
2997
|
-
}
|
|
2998
|
-
const result = {
|
|
2999
|
-
tenantId,
|
|
3000
|
-
schemaName,
|
|
3001
|
-
success: true,
|
|
3002
|
-
appliedMigrations,
|
|
3003
|
-
durationMs: Date.now() - startTime,
|
|
3004
|
-
format: format.format
|
|
3005
|
-
};
|
|
3006
|
-
await this.config.hooks?.afterTenant?.(tenantId, result);
|
|
3007
|
-
return result;
|
|
3008
|
-
} catch (error) {
|
|
3009
|
-
const result = {
|
|
3010
|
-
tenantId,
|
|
3011
|
-
schemaName,
|
|
3012
|
-
success: false,
|
|
3013
|
-
appliedMigrations,
|
|
3014
|
-
error: error.message,
|
|
3015
|
-
durationMs: Date.now() - startTime
|
|
3016
|
-
};
|
|
3017
|
-
await this.config.hooks?.afterTenant?.(tenantId, result);
|
|
3018
|
-
return result;
|
|
3019
|
-
} finally {
|
|
3020
|
-
await pool.end();
|
|
3021
|
-
}
|
|
3022
|
-
}
|
|
3023
|
-
/**
|
|
3024
|
-
* Mark migrations as applied without executing SQL
|
|
3025
|
-
*
|
|
3026
|
-
* Useful for syncing tracking state with already-applied migrations
|
|
3027
|
-
* or when migrations were applied manually.
|
|
3028
|
-
*
|
|
3029
|
-
* @param tenantId - The tenant identifier
|
|
3030
|
-
* @param options - Options with progress callback
|
|
3031
|
-
* @returns Result with list of marked migrations
|
|
3032
|
-
*
|
|
3033
|
-
* @example
|
|
3034
|
-
* ```typescript
|
|
3035
|
-
* const result = await executor.markAsApplied('tenant-123', {
|
|
3036
|
-
* onProgress: (id, status, name) => console.log(`${id}: marking ${name}`),
|
|
3037
|
-
* });
|
|
3038
|
-
*
|
|
3039
|
-
* console.log(`Marked ${result.appliedMigrations.length} migrations`);
|
|
3040
|
-
* ```
|
|
3041
|
-
*/
|
|
3042
|
-
async markAsApplied(tenantId, options = {}) {
|
|
3043
|
-
const startTime = Date.now();
|
|
3044
|
-
const schemaName = this.deps.schemaNameTemplate(tenantId);
|
|
3045
|
-
const markedMigrations = [];
|
|
3046
|
-
const pool = await this.deps.createPool(schemaName);
|
|
3047
|
-
try {
|
|
3048
|
-
await this.config.hooks?.beforeTenant?.(tenantId);
|
|
3049
|
-
const format = await this.deps.getOrDetectFormat(pool, schemaName);
|
|
3050
|
-
await this.deps.ensureMigrationsTable(pool, schemaName, format);
|
|
3051
|
-
const allMigrations = await this.deps.loadMigrations();
|
|
3052
|
-
const applied = await this.getAppliedMigrations(pool, schemaName, format);
|
|
3053
|
-
const appliedSet = new Set(applied.map((m) => m.identifier));
|
|
3054
|
-
const pending = allMigrations.filter(
|
|
3055
|
-
(m) => !this.isMigrationApplied(m, appliedSet, format)
|
|
3056
|
-
);
|
|
3057
|
-
for (const migration of pending) {
|
|
3058
|
-
const migrationStart = Date.now();
|
|
3059
|
-
options.onProgress?.(tenantId, "migrating", migration.name);
|
|
3060
|
-
await this.config.hooks?.beforeMigration?.(tenantId, migration.name);
|
|
3061
|
-
await this.recordMigration(pool, schemaName, migration, format);
|
|
3062
|
-
await this.config.hooks?.afterMigration?.(
|
|
3063
|
-
tenantId,
|
|
3064
|
-
migration.name,
|
|
3065
|
-
Date.now() - migrationStart
|
|
3066
|
-
);
|
|
3067
|
-
markedMigrations.push(migration.name);
|
|
3068
|
-
}
|
|
3069
|
-
const result = {
|
|
3070
|
-
tenantId,
|
|
3071
|
-
schemaName,
|
|
3072
|
-
success: true,
|
|
3073
|
-
appliedMigrations: markedMigrations,
|
|
3074
|
-
durationMs: Date.now() - startTime,
|
|
3075
|
-
format: format.format
|
|
3076
|
-
};
|
|
3077
|
-
await this.config.hooks?.afterTenant?.(tenantId, result);
|
|
3078
|
-
return result;
|
|
3079
|
-
} catch (error) {
|
|
3080
|
-
const result = {
|
|
3081
|
-
tenantId,
|
|
3082
|
-
schemaName,
|
|
3083
|
-
success: false,
|
|
3084
|
-
appliedMigrations: markedMigrations,
|
|
3085
|
-
error: error.message,
|
|
3086
|
-
durationMs: Date.now() - startTime
|
|
3087
|
-
};
|
|
3088
|
-
await this.config.hooks?.afterTenant?.(tenantId, result);
|
|
3089
|
-
return result;
|
|
3090
|
-
} finally {
|
|
3091
|
-
await pool.end();
|
|
3092
|
-
}
|
|
3093
|
-
}
|
|
3094
|
-
/**
|
|
3095
|
-
* Get migration status for a specific tenant
|
|
3096
|
-
*
|
|
3097
|
-
* Returns information about applied and pending migrations.
|
|
3098
|
-
*
|
|
3099
|
-
* @param tenantId - The tenant identifier
|
|
3100
|
-
* @param migrations - Optional pre-loaded migrations
|
|
3101
|
-
* @returns Migration status with counts and pending list
|
|
3102
|
-
*
|
|
3103
|
-
* @example
|
|
3104
|
-
* ```typescript
|
|
3105
|
-
* const status = await executor.getTenantStatus('tenant-123');
|
|
3106
|
-
* if (status.status === 'behind') {
|
|
3107
|
-
* console.log(`Pending: ${status.pendingMigrations.join(', ')}`);
|
|
3108
|
-
* }
|
|
3109
|
-
* ```
|
|
3110
|
-
*/
|
|
3111
|
-
async getTenantStatus(tenantId, migrations) {
|
|
3112
|
-
const schemaName = this.deps.schemaNameTemplate(tenantId);
|
|
3113
|
-
const pool = await this.deps.createPool(schemaName);
|
|
3114
|
-
try {
|
|
3115
|
-
const allMigrations = migrations ?? await this.deps.loadMigrations();
|
|
3116
|
-
const tableExists = await this.deps.migrationsTableExists(pool, schemaName);
|
|
3117
|
-
if (!tableExists) {
|
|
3118
|
-
return {
|
|
3119
|
-
tenantId,
|
|
3120
|
-
schemaName,
|
|
3121
|
-
appliedCount: 0,
|
|
3122
|
-
pendingCount: allMigrations.length,
|
|
3123
|
-
pendingMigrations: allMigrations.map((m) => m.name),
|
|
3124
|
-
status: allMigrations.length > 0 ? "behind" : "ok",
|
|
3125
|
-
format: null
|
|
3126
|
-
};
|
|
3127
|
-
}
|
|
3128
|
-
const format = await this.deps.getOrDetectFormat(pool, schemaName);
|
|
3129
|
-
const applied = await this.getAppliedMigrations(pool, schemaName, format);
|
|
3130
|
-
const appliedSet = new Set(applied.map((m) => m.identifier));
|
|
3131
|
-
const pending = allMigrations.filter(
|
|
3132
|
-
(m) => !this.isMigrationApplied(m, appliedSet, format)
|
|
3133
|
-
);
|
|
3134
|
-
return {
|
|
3135
|
-
tenantId,
|
|
3136
|
-
schemaName,
|
|
3137
|
-
appliedCount: applied.length,
|
|
3138
|
-
pendingCount: pending.length,
|
|
3139
|
-
pendingMigrations: pending.map((m) => m.name),
|
|
3140
|
-
status: pending.length > 0 ? "behind" : "ok",
|
|
3141
|
-
format: format.format
|
|
3142
|
-
};
|
|
3143
|
-
} catch (error) {
|
|
3144
|
-
return {
|
|
3145
|
-
tenantId,
|
|
3146
|
-
schemaName,
|
|
3147
|
-
appliedCount: 0,
|
|
3148
|
-
pendingCount: 0,
|
|
3149
|
-
pendingMigrations: [],
|
|
3150
|
-
status: "error",
|
|
3151
|
-
error: error.message,
|
|
3152
|
-
format: null
|
|
3153
|
-
};
|
|
3154
|
-
} finally {
|
|
3155
|
-
await pool.end();
|
|
3156
|
-
}
|
|
3157
|
-
}
|
|
3158
|
-
// ============================================================================
|
|
3159
|
-
// IMigrationExecutor Interface Methods
|
|
3160
|
-
// ============================================================================
|
|
3161
|
-
/**
|
|
3162
|
-
* Execute a single migration on a schema
|
|
3163
|
-
*/
|
|
3164
|
-
async executeMigration(pool, schemaName, migration, format, options) {
|
|
3165
|
-
if (options?.markOnly) {
|
|
3166
|
-
options.onProgress?.("recording");
|
|
3167
|
-
await this.recordMigration(pool, schemaName, migration, format);
|
|
3168
|
-
} else {
|
|
3169
|
-
options?.onProgress?.("applying");
|
|
3170
|
-
await this.applyMigration(pool, schemaName, migration, format);
|
|
3171
|
-
}
|
|
3172
|
-
}
|
|
3173
|
-
/**
|
|
3174
|
-
* Execute multiple migrations on a schema
|
|
3175
|
-
*/
|
|
3176
|
-
async executeMigrations(pool, schemaName, migrations, format, options) {
|
|
3177
|
-
const appliedNames = [];
|
|
3178
|
-
for (const migration of migrations) {
|
|
3179
|
-
await this.executeMigration(pool, schemaName, migration, format, options);
|
|
3180
|
-
appliedNames.push(migration.name);
|
|
3181
|
-
}
|
|
3182
|
-
return appliedNames;
|
|
3183
|
-
}
|
|
3184
|
-
/**
|
|
3185
|
-
* Record a migration as applied without executing SQL
|
|
3186
|
-
*/
|
|
3187
|
-
async recordMigration(pool, schemaName, migration, format) {
|
|
3188
|
-
const { identifier, timestamp, timestampType } = format.columns;
|
|
3189
|
-
const identifierValue = identifier === "name" ? migration.name : migration.hash;
|
|
3190
|
-
const timestampValue = timestampType === "bigint" ? Date.now() : /* @__PURE__ */ new Date();
|
|
3191
|
-
await pool.query(
|
|
3192
|
-
`INSERT INTO "${schemaName}"."${format.tableName}" ("${identifier}", "${timestamp}") VALUES ($1, $2)`,
|
|
3193
|
-
[identifierValue, timestampValue]
|
|
3194
|
-
);
|
|
3195
|
-
}
|
|
3196
|
-
/**
|
|
3197
|
-
* Get list of applied migrations for a tenant
|
|
3198
|
-
*/
|
|
3199
|
-
async getAppliedMigrations(pool, schemaName, format) {
|
|
3200
|
-
const identifierColumn = format.columns.identifier;
|
|
3201
|
-
const timestampColumn = format.columns.timestamp;
|
|
3202
|
-
const result = await pool.query(
|
|
3203
|
-
`SELECT id, "${identifierColumn}" as identifier, "${timestampColumn}" as applied_at
|
|
3204
|
-
FROM "${schemaName}"."${format.tableName}"
|
|
3205
|
-
ORDER BY id`
|
|
3206
|
-
);
|
|
3207
|
-
return result.rows.map((row) => {
|
|
3208
|
-
const appliedAt = format.columns.timestampType === "bigint" ? new Date(Number(row.applied_at)) : new Date(row.applied_at);
|
|
3209
|
-
return {
|
|
3210
|
-
identifier: row.identifier,
|
|
3211
|
-
// Set name or hash based on format
|
|
3212
|
-
...format.columns.identifier === "name" ? { name: row.identifier } : { hash: row.identifier },
|
|
3213
|
-
appliedAt
|
|
3214
|
-
};
|
|
3215
|
-
});
|
|
3216
|
-
}
|
|
3217
|
-
/**
|
|
3218
|
-
* Get pending migrations (not yet applied)
|
|
3219
|
-
*/
|
|
3220
|
-
async getPendingMigrations(pool, schemaName, allMigrations, format) {
|
|
3221
|
-
const applied = await this.getAppliedMigrations(pool, schemaName, format);
|
|
3222
|
-
const appliedSet = new Set(applied.map((m) => m.identifier));
|
|
3223
|
-
return allMigrations.filter(
|
|
3224
|
-
(m) => !this.isMigrationApplied(m, appliedSet, format)
|
|
3225
|
-
);
|
|
3226
|
-
}
|
|
3227
|
-
// ============================================================================
|
|
3228
|
-
// Private Helper Methods
|
|
3229
|
-
// ============================================================================
|
|
3230
|
-
/**
|
|
3231
|
-
* Check if a migration has been applied
|
|
3232
|
-
*/
|
|
3233
|
-
isMigrationApplied(migration, appliedIdentifiers, format) {
|
|
3234
|
-
if (format.columns.identifier === "name") {
|
|
3235
|
-
return appliedIdentifiers.has(migration.name);
|
|
3236
|
-
}
|
|
3237
|
-
return appliedIdentifiers.has(migration.hash) || appliedIdentifiers.has(migration.name);
|
|
3238
|
-
}
|
|
3239
|
-
/**
|
|
3240
|
-
* Apply a migration to a schema (execute SQL + record)
|
|
3241
|
-
*/
|
|
3242
|
-
async applyMigration(pool, schemaName, migration, format) {
|
|
3243
|
-
const client = await pool.connect();
|
|
3244
|
-
try {
|
|
3245
|
-
await client.query("BEGIN");
|
|
3246
|
-
await client.query(migration.sql);
|
|
3247
|
-
const { identifier, timestamp, timestampType } = format.columns;
|
|
3248
|
-
const identifierValue = identifier === "name" ? migration.name : migration.hash;
|
|
3249
|
-
const timestampValue = timestampType === "bigint" ? Date.now() : /* @__PURE__ */ new Date();
|
|
3250
|
-
await client.query(
|
|
3251
|
-
`INSERT INTO "${schemaName}"."${format.tableName}" ("${identifier}", "${timestamp}") VALUES ($1, $2)`,
|
|
3252
|
-
[identifierValue, timestampValue]
|
|
3253
|
-
);
|
|
3254
|
-
await client.query("COMMIT");
|
|
3255
|
-
} catch (error) {
|
|
3256
|
-
await client.query("ROLLBACK");
|
|
3257
|
-
throw error;
|
|
3258
|
-
} finally {
|
|
3259
|
-
client.release();
|
|
3260
|
-
}
|
|
3261
|
-
}
|
|
3262
|
-
};
|
|
3263
|
-
|
|
3264
|
-
// src/migrator/executor/batch-executor.ts
|
|
3265
|
-
var BatchExecutor = class {
|
|
3266
|
-
constructor(config, executor, loadMigrations) {
|
|
3267
|
-
this.config = config;
|
|
3268
|
-
this.executor = executor;
|
|
3269
|
-
this.loadMigrations = loadMigrations;
|
|
3270
|
-
}
|
|
3271
|
-
/**
|
|
3272
|
-
* Migrate all tenants in parallel
|
|
3273
|
-
*
|
|
3274
|
-
* Processes tenants in batches with configurable concurrency.
|
|
3275
|
-
* Supports progress callbacks, error handling, and abort behavior.
|
|
3276
|
-
*
|
|
3277
|
-
* @param options - Migration options (concurrency, dryRun, callbacks)
|
|
3278
|
-
* @returns Aggregate results for all tenants
|
|
3279
|
-
*
|
|
3280
|
-
* @example
|
|
3281
|
-
* ```typescript
|
|
3282
|
-
* const results = await batchExecutor.migrateAll({
|
|
3283
|
-
* concurrency: 10,
|
|
3284
|
-
* dryRun: false,
|
|
3285
|
-
* onProgress: (id, status) => console.log(`${id}: ${status}`),
|
|
3286
|
-
* onError: (id, error) => {
|
|
3287
|
-
* console.error(`${id} failed: ${error.message}`);
|
|
3288
|
-
* return 'continue'; // or 'abort' to stop all
|
|
3289
|
-
* },
|
|
3290
|
-
* });
|
|
3291
|
-
*
|
|
3292
|
-
* console.log(`Succeeded: ${results.succeeded}/${results.total}`);
|
|
3293
|
-
* ```
|
|
3294
|
-
*/
|
|
3295
|
-
async migrateAll(options = {}) {
|
|
3296
|
-
const {
|
|
3297
|
-
concurrency = 10,
|
|
3298
|
-
onProgress,
|
|
3299
|
-
onError,
|
|
3300
|
-
dryRun = false
|
|
3301
|
-
} = options;
|
|
3302
|
-
const tenantIds = await this.config.tenantDiscovery();
|
|
3303
|
-
const migrations = await this.loadMigrations();
|
|
3304
|
-
const results = [];
|
|
3305
|
-
let aborted = false;
|
|
3306
|
-
for (let i = 0; i < tenantIds.length && !aborted; i += concurrency) {
|
|
3307
|
-
const batch = tenantIds.slice(i, i + concurrency);
|
|
3308
|
-
const batchResults = await Promise.all(
|
|
3309
|
-
batch.map(async (tenantId) => {
|
|
3310
|
-
if (aborted) {
|
|
3311
|
-
return this.createSkippedResult(tenantId);
|
|
3312
|
-
}
|
|
3313
|
-
try {
|
|
3314
|
-
onProgress?.(tenantId, "starting");
|
|
3315
|
-
const result = await this.executor.migrateTenant(tenantId, migrations, { dryRun, onProgress });
|
|
3316
|
-
onProgress?.(tenantId, result.success ? "completed" : "failed");
|
|
3317
|
-
return result;
|
|
3318
|
-
} catch (error) {
|
|
3319
|
-
onProgress?.(tenantId, "failed");
|
|
3320
|
-
const action = onError?.(tenantId, error);
|
|
3321
|
-
if (action === "abort") {
|
|
3322
|
-
aborted = true;
|
|
3323
|
-
}
|
|
3324
|
-
return this.createErrorResult(tenantId, error);
|
|
3325
|
-
}
|
|
3326
|
-
})
|
|
3327
|
-
);
|
|
3328
|
-
results.push(...batchResults);
|
|
3329
|
-
}
|
|
3330
|
-
if (aborted) {
|
|
3331
|
-
const remaining = tenantIds.slice(results.length);
|
|
3332
|
-
for (const tenantId of remaining) {
|
|
3333
|
-
results.push(this.createSkippedResult(tenantId));
|
|
3334
|
-
}
|
|
3335
|
-
}
|
|
3336
|
-
return this.aggregateResults(results);
|
|
3337
|
-
}
|
|
3338
|
-
/**
|
|
3339
|
-
* Migrate specific tenants in parallel
|
|
3340
|
-
*
|
|
3341
|
-
* Same as migrateAll but for a subset of tenants.
|
|
3342
|
-
*
|
|
3343
|
-
* @param tenantIds - List of tenant IDs to migrate
|
|
3344
|
-
* @param options - Migration options
|
|
3345
|
-
* @returns Aggregate results for specified tenants
|
|
3346
|
-
*
|
|
3347
|
-
* @example
|
|
3348
|
-
* ```typescript
|
|
3349
|
-
* const results = await batchExecutor.migrateTenants(
|
|
3350
|
-
* ['tenant-1', 'tenant-2', 'tenant-3'],
|
|
3351
|
-
* { concurrency: 5 }
|
|
3352
|
-
* );
|
|
3353
|
-
* ```
|
|
3354
|
-
*/
|
|
3355
|
-
async migrateTenants(tenantIds, options = {}) {
|
|
3356
|
-
const migrations = await this.loadMigrations();
|
|
3357
|
-
const results = [];
|
|
3358
|
-
const { concurrency = 10, onProgress, onError, dryRun = false } = options;
|
|
3359
|
-
for (let i = 0; i < tenantIds.length; i += concurrency) {
|
|
3360
|
-
const batch = tenantIds.slice(i, i + concurrency);
|
|
3361
|
-
const batchResults = await Promise.all(
|
|
3362
|
-
batch.map(async (tenantId) => {
|
|
3363
|
-
try {
|
|
3364
|
-
onProgress?.(tenantId, "starting");
|
|
3365
|
-
const result = await this.executor.migrateTenant(tenantId, migrations, { dryRun, onProgress });
|
|
3366
|
-
onProgress?.(tenantId, result.success ? "completed" : "failed");
|
|
3367
|
-
return result;
|
|
3368
|
-
} catch (error) {
|
|
3369
|
-
onProgress?.(tenantId, "failed");
|
|
3370
|
-
onError?.(tenantId, error);
|
|
3371
|
-
return this.createErrorResult(tenantId, error);
|
|
3372
|
-
}
|
|
3373
|
-
})
|
|
3374
|
-
);
|
|
3375
|
-
results.push(...batchResults);
|
|
3376
|
-
}
|
|
3377
|
-
return this.aggregateResults(results);
|
|
3378
|
-
}
|
|
3379
|
-
/**
|
|
3380
|
-
* Mark all tenants as applied without executing SQL
|
|
3381
|
-
*
|
|
3382
|
-
* Useful for syncing tracking state with already-applied migrations.
|
|
3383
|
-
* Processes tenants in parallel with configurable concurrency.
|
|
3384
|
-
*
|
|
3385
|
-
* @param options - Migration options
|
|
3386
|
-
* @returns Aggregate results for all tenants
|
|
3387
|
-
*
|
|
3388
|
-
* @example
|
|
3389
|
-
* ```typescript
|
|
3390
|
-
* const results = await batchExecutor.markAllAsApplied({
|
|
3391
|
-
* concurrency: 10,
|
|
3392
|
-
* onProgress: (id, status) => console.log(`${id}: ${status}`),
|
|
3393
|
-
* });
|
|
3394
|
-
* ```
|
|
3395
|
-
*/
|
|
3396
|
-
async markAllAsApplied(options = {}) {
|
|
3397
|
-
const {
|
|
3398
|
-
concurrency = 10,
|
|
3399
|
-
onProgress,
|
|
3400
|
-
onError
|
|
3401
|
-
} = options;
|
|
3402
|
-
const tenantIds = await this.config.tenantDiscovery();
|
|
3403
|
-
const results = [];
|
|
3404
|
-
let aborted = false;
|
|
3405
|
-
for (let i = 0; i < tenantIds.length && !aborted; i += concurrency) {
|
|
3406
|
-
const batch = tenantIds.slice(i, i + concurrency);
|
|
3407
|
-
const batchResults = await Promise.all(
|
|
3408
|
-
batch.map(async (tenantId) => {
|
|
3409
|
-
if (aborted) {
|
|
3410
|
-
return this.createSkippedResult(tenantId);
|
|
3411
|
-
}
|
|
3412
|
-
try {
|
|
3413
|
-
onProgress?.(tenantId, "starting");
|
|
3414
|
-
const result = await this.executor.markAsApplied(tenantId, { onProgress });
|
|
3415
|
-
onProgress?.(tenantId, result.success ? "completed" : "failed");
|
|
3416
|
-
return result;
|
|
3417
|
-
} catch (error) {
|
|
3418
|
-
onProgress?.(tenantId, "failed");
|
|
3419
|
-
const action = onError?.(tenantId, error);
|
|
3420
|
-
if (action === "abort") {
|
|
3421
|
-
aborted = true;
|
|
3422
|
-
}
|
|
3423
|
-
return this.createErrorResult(tenantId, error);
|
|
3424
|
-
}
|
|
3425
|
-
})
|
|
3426
|
-
);
|
|
3427
|
-
results.push(...batchResults);
|
|
3428
|
-
}
|
|
3429
|
-
if (aborted) {
|
|
3430
|
-
const remaining = tenantIds.slice(results.length);
|
|
3431
|
-
for (const tenantId of remaining) {
|
|
3432
|
-
results.push(this.createSkippedResult(tenantId));
|
|
3433
|
-
}
|
|
3434
|
-
}
|
|
3435
|
-
return this.aggregateResults(results);
|
|
3436
|
-
}
|
|
3437
|
-
/**
|
|
3438
|
-
* Get migration status for all tenants
|
|
3439
|
-
*
|
|
3440
|
-
* Queries each tenant's migration status sequentially.
|
|
3441
|
-
*
|
|
3442
|
-
* @returns List of migration status for all tenants
|
|
3443
|
-
*
|
|
3444
|
-
* @example
|
|
3445
|
-
* ```typescript
|
|
3446
|
-
* const statuses = await batchExecutor.getStatus();
|
|
3447
|
-
* const behind = statuses.filter(s => s.status === 'behind');
|
|
3448
|
-
* console.log(`${behind.length} tenants need migrations`);
|
|
3449
|
-
* ```
|
|
3450
|
-
*/
|
|
3451
|
-
async getStatus() {
|
|
3452
|
-
const tenantIds = await this.config.tenantDiscovery();
|
|
3453
|
-
const migrations = await this.loadMigrations();
|
|
3454
|
-
const statuses = [];
|
|
3455
|
-
for (const tenantId of tenantIds) {
|
|
3456
|
-
statuses.push(await this.executor.getTenantStatus(tenantId, migrations));
|
|
3457
|
-
}
|
|
3458
|
-
return statuses;
|
|
3459
|
-
}
|
|
3460
|
-
// ============================================================================
|
|
3461
|
-
// Private Helper Methods
|
|
3462
|
-
// ============================================================================
|
|
3463
|
-
/**
|
|
3464
|
-
* Create a skipped result for aborted operations
|
|
3465
|
-
*/
|
|
3466
|
-
createSkippedResult(tenantId) {
|
|
3467
|
-
return {
|
|
3468
|
-
tenantId,
|
|
3469
|
-
schemaName: "",
|
|
3470
|
-
// Schema name not available in batch context
|
|
3471
|
-
success: false,
|
|
3472
|
-
appliedMigrations: [],
|
|
3473
|
-
error: "Skipped due to abort",
|
|
3474
|
-
durationMs: 0
|
|
3475
|
-
};
|
|
3476
|
-
}
|
|
3477
|
-
/**
|
|
3478
|
-
* Create an error result for failed operations
|
|
3479
|
-
*/
|
|
3480
|
-
createErrorResult(tenantId, error) {
|
|
3481
|
-
return {
|
|
3482
|
-
tenantId,
|
|
3483
|
-
schemaName: "",
|
|
3484
|
-
// Schema name not available in batch context
|
|
3485
|
-
success: false,
|
|
3486
|
-
appliedMigrations: [],
|
|
3487
|
-
error: error.message,
|
|
3488
|
-
durationMs: 0
|
|
3489
|
-
};
|
|
3490
|
-
}
|
|
3491
|
-
/**
|
|
3492
|
-
* Aggregate individual migration results into a summary
|
|
3493
|
-
*/
|
|
3494
|
-
aggregateResults(results) {
|
|
3495
|
-
return {
|
|
3496
|
-
total: results.length,
|
|
3497
|
-
succeeded: results.filter((r) => r.success).length,
|
|
3498
|
-
failed: results.filter((r) => !r.success && r.error !== "Skipped due to abort").length,
|
|
3499
|
-
skipped: results.filter((r) => r.error === "Skipped due to abort").length,
|
|
3500
|
-
details: results
|
|
3501
|
-
};
|
|
3502
|
-
}
|
|
3503
|
-
};
|
|
3504
|
-
|
|
3505
|
-
// src/migrator/clone/ddl-generator.ts
|
|
3506
|
-
async function listTables(pool, schemaName, excludeTables = []) {
|
|
3507
|
-
const excludePlaceholders = excludeTables.length > 0 ? excludeTables.map((_, i) => `$${i + 2}`).join(", ") : "''::text";
|
|
3508
|
-
const result = await pool.query(
|
|
3509
|
-
`SELECT table_name
|
|
62
|
+
ORDER BY table_name`,[t]),o=[];for(let m of i.rows){if(s.includes(m.table_name))continue;let l=await ee(e,t,m.table_name),u=a?await ne(e,t,m.table_name):[],h=r?await ae(e,t,m.table_name):[];o.push({name:m.table_name,columns:l,indexes:u,constraints:h});}return o}};var A=class{constructor(e,t){this.config=e;this.deps=t;}async seedTenant(e,t){let n=Date.now(),a=this.deps.schemaNameTemplate(e),r=await this.deps.createPool(a);try{let s=drizzle(r,{schema:this.deps.tenantSchema});return await t(s,e),{tenantId:e,schemaName:a,success:!0,durationMs:Date.now()-n}}catch(s){return {tenantId:e,schemaName:a,success:false,error:s.message,durationMs:Date.now()-n}}finally{await r.end();}}async seedAll(e,t={}){let{concurrency:n=10,onProgress:a,onError:r}=t,s=await this.config.tenantDiscovery(),i=[],o=false;for(let m=0;m<s.length&&!o;m+=n){let l=s.slice(m,m+n),u=await Promise.all(l.map(async h=>{if(o)return this.createSkippedResult(h);try{a?.(h,"starting"),a?.(h,"seeding");let d=await this.seedTenant(h,e);return a?.(h,d.success?"completed":"failed"),d}catch(d){return a?.(h,"failed"),r?.(h,d)==="abort"&&(o=true),this.createErrorResult(h,d)}}));i.push(...u);}if(o){let m=s.slice(i.length);for(let l of m)i.push(this.createSkippedResult(l));}return this.aggregateResults(i)}async seedTenants(e,t,n={}){let{concurrency:a=10,onProgress:r,onError:s}=n,i=[];for(let o=0;o<e.length;o+=a){let m=e.slice(o,o+a),l=await Promise.all(m.map(async u=>{try{r?.(u,"starting"),r?.(u,"seeding");let h=await this.seedTenant(u,t);return r?.(u,h.success?"completed":"failed"),h}catch(h){return r?.(u,"failed"),s?.(u,h),this.createErrorResult(u,h)}}));i.push(...l);}return this.aggregateResults(i)}createSkippedResult(e){return {tenantId:e,schemaName:this.deps.schemaNameTemplate(e),success:false,error:"Skipped due to abort",durationMs:0}}createErrorResult(e,t){return {tenantId:e,schemaName:this.deps.schemaNameTemplate(e),success:false,error:t.message,durationMs:0}}aggregateResults(e){return {total:e.length,succeeded:e.filter(t=>t.success).length,failed:e.filter(t=>!t.success&&t.error!=="Skipped due to abort").length,skipped:e.filter(t=>t.error==="Skipped due to abort").length,details:e}}};var Ne="public",v=class{constructor(e,t){this.config=e;this.deps=t;this.schemaName=e.schemaName??Ne;}schemaName;async seed(e){let t=Date.now(),n=await this.deps.createPool();try{this.config.hooks?.onStart?.();let a=drizzle(n,{schema:this.deps.sharedSchema});return await e(a),this.config.hooks?.onComplete?.(),{schemaName:this.schemaName,success:!0,durationMs:Date.now()-t}}catch(a){return this.config.hooks?.onError?.(a),{schemaName:this.schemaName,success:false,error:a.message,durationMs:Date.now()-t}}finally{await n.end();}}};var F=class{constructor(e,t){this.config=e;this.deps=t;}async getSyncStatus(){let e=await this.config.tenantDiscovery(),t=await this.deps.loadMigrations(),n=[];for(let a of e)n.push(await this.getTenantSyncStatus(a,t));return {total:n.length,inSync:n.filter(a=>a.inSync&&!a.error).length,outOfSync:n.filter(a=>!a.inSync&&!a.error).length,error:n.filter(a=>!!a.error).length,details:n}}async getTenantSyncStatus(e,t){let n=this.deps.schemaNameTemplate(e),a=await this.deps.createPool(n);try{let r=t??await this.deps.loadMigrations(),s=new Set(r.map(g=>g.name)),i=new Set(r.map(g=>g.hash));if(!await this.deps.migrationsTableExists(a,n))return {tenantId:e,schemaName:n,missing:r.map(g=>g.name),orphans:[],inSync:r.length===0,format:null};let m=await this.deps.getOrDetectFormat(a,n),l=await this.getAppliedMigrations(a,n,m),u=new Set(l.map(g=>g.identifier)),h=r.filter(g=>!this.isMigrationApplied(g,u,m)).map(g=>g.name),d=l.filter(g=>(m.columns.identifier==="name"||!i.has(g.identifier))&&!s.has(g.identifier)).map(g=>g.identifier);return {tenantId:e,schemaName:n,missing:h,orphans:d,inSync:h.length===0&&d.length===0,format:m.format}}catch(r){return {tenantId:e,schemaName:n,missing:[],orphans:[],inSync:false,format:null,error:r.message}}finally{await a.end();}}async markMissing(e){let t=Date.now(),n=this.deps.schemaNameTemplate(e),a=[],r=await this.deps.createPool(n);try{let s=await this.getTenantSyncStatus(e);if(s.error)return {tenantId:e,schemaName:n,success:!1,markedMigrations:[],removedOrphans:[],error:s.error,durationMs:Date.now()-t};if(s.missing.length===0)return {tenantId:e,schemaName:n,success:!0,markedMigrations:[],removedOrphans:[],durationMs:Date.now()-t};let i=await this.deps.getOrDetectFormat(r,n);await this.deps.ensureMigrationsTable(r,n,i);let o=await this.deps.loadMigrations(),m=new Set(s.missing);for(let l of o)m.has(l.name)&&(await this.recordMigration(r,n,l,i),a.push(l.name));return {tenantId:e,schemaName:n,success:!0,markedMigrations:a,removedOrphans:[],durationMs:Date.now()-t}}catch(s){return {tenantId:e,schemaName:n,success:false,markedMigrations:a,removedOrphans:[],error:s.message,durationMs:Date.now()-t}}finally{await r.end();}}async markAllMissing(e={}){let{concurrency:t=10,onProgress:n,onError:a}=e,r=await this.config.tenantDiscovery(),s=[],i=false;for(let o=0;o<r.length&&!i;o+=t){let m=r.slice(o,o+t),l=await Promise.all(m.map(async u=>{if(i)return this.createSkippedSyncResult(u);try{n?.(u,"starting");let h=await this.markMissing(u);return n?.(u,h.success?"completed":"failed"),h}catch(h){return n?.(u,"failed"),a?.(u,h)==="abort"&&(i=true),this.createErrorSyncResult(u,h)}}));s.push(...l);}return this.aggregateSyncResults(s)}async cleanOrphans(e){let t=Date.now(),n=this.deps.schemaNameTemplate(e),a=[],r=await this.deps.createPool(n);try{let s=await this.getTenantSyncStatus(e);if(s.error)return {tenantId:e,schemaName:n,success:!1,markedMigrations:[],removedOrphans:[],error:s.error,durationMs:Date.now()-t};if(s.orphans.length===0)return {tenantId:e,schemaName:n,success:!0,markedMigrations:[],removedOrphans:[],durationMs:Date.now()-t};let i=await this.deps.getOrDetectFormat(r,n),o=i.columns.identifier;for(let m of s.orphans)await r.query(`DELETE FROM "${n}"."${i.tableName}" WHERE "${o}" = $1`,[m]),a.push(m);return {tenantId:e,schemaName:n,success:!0,markedMigrations:[],removedOrphans:a,durationMs:Date.now()-t}}catch(s){return {tenantId:e,schemaName:n,success:false,markedMigrations:[],removedOrphans:a,error:s.message,durationMs:Date.now()-t}}finally{await r.end();}}async cleanAllOrphans(e={}){let{concurrency:t=10,onProgress:n,onError:a}=e,r=await this.config.tenantDiscovery(),s=[],i=false;for(let o=0;o<r.length&&!i;o+=t){let m=r.slice(o,o+t),l=await Promise.all(m.map(async u=>{if(i)return this.createSkippedSyncResult(u);try{n?.(u,"starting");let h=await this.cleanOrphans(u);return n?.(u,h.success?"completed":"failed"),h}catch(h){return n?.(u,"failed"),a?.(u,h)==="abort"&&(i=true),this.createErrorSyncResult(u,h)}}));s.push(...l);}return this.aggregateSyncResults(s)}async getAppliedMigrations(e,t,n){let a=n.columns.identifier,r=n.columns.timestamp;return (await e.query(`SELECT id, "${a}" as identifier, "${r}" as applied_at
|
|
63
|
+
FROM "${t}"."${n.tableName}"
|
|
64
|
+
ORDER BY id`)).rows.map(i=>{let o=n.columns.timestampType==="bigint"?new Date(Number(i.applied_at)):new Date(i.applied_at);return {identifier:i.identifier,appliedAt:o}})}isMigrationApplied(e,t,n){return n.columns.identifier==="name"?t.has(e.name):t.has(e.hash)||t.has(e.name)}async recordMigration(e,t,n,a){let{identifier:r,timestamp:s,timestampType:i}=a.columns,o=r==="name"?n.name:n.hash,m=i==="bigint"?Date.now():new Date;await e.query(`INSERT INTO "${t}"."${a.tableName}" ("${r}", "${s}") VALUES ($1, $2)`,[o,m]);}createSkippedSyncResult(e){return {tenantId:e,schemaName:this.deps.schemaNameTemplate(e),success:false,markedMigrations:[],removedOrphans:[],error:"Skipped due to abort",durationMs:0}}createErrorSyncResult(e,t){return {tenantId:e,schemaName:this.deps.schemaNameTemplate(e),success:false,markedMigrations:[],removedOrphans:[],error:t.message,durationMs:0}}aggregateSyncResults(e){return {total:e.length,succeeded:e.filter(t=>t.success).length,failed:e.filter(t=>!t.success).length,details:e}}};var L=class{constructor(e,t){this.config=e;this.deps=t;}async migrateTenant(e,t,n={}){let a=Date.now(),r=this.deps.schemaNameTemplate(e),s=[],i=await this.deps.createPool(r);try{await this.config.hooks?.beforeTenant?.(e);let o=await this.deps.getOrDetectFormat(i,r);await this.deps.ensureMigrationsTable(i,r,o);let m=t??await this.deps.loadMigrations(),l=await this.getAppliedMigrations(i,r,o),u=new Set(l.map(g=>g.identifier)),h=m.filter(g=>!this.isMigrationApplied(g,u,o));if(n.dryRun)return {tenantId:e,schemaName:r,success:!0,appliedMigrations:h.map(g=>g.name),durationMs:Date.now()-a,format:o.format};for(let g of h){let w=Date.now();n.onProgress?.(e,"migrating",g.name),await this.config.hooks?.beforeMigration?.(e,g.name),await this.applyMigration(i,r,g,o),await this.config.hooks?.afterMigration?.(e,g.name,Date.now()-w),s.push(g.name);}let d={tenantId:e,schemaName:r,success:!0,appliedMigrations:s,durationMs:Date.now()-a,format:o.format};return await this.config.hooks?.afterTenant?.(e,d),d}catch(o){let m={tenantId:e,schemaName:r,success:false,appliedMigrations:s,error:o.message,durationMs:Date.now()-a};return await this.config.hooks?.afterTenant?.(e,m),m}finally{await i.end();}}async markAsApplied(e,t={}){let n=Date.now(),a=this.deps.schemaNameTemplate(e),r=[],s=await this.deps.createPool(a);try{await this.config.hooks?.beforeTenant?.(e);let i=await this.deps.getOrDetectFormat(s,a);await this.deps.ensureMigrationsTable(s,a,i);let o=await this.deps.loadMigrations(),m=await this.getAppliedMigrations(s,a,i),l=new Set(m.map(d=>d.identifier)),u=o.filter(d=>!this.isMigrationApplied(d,l,i));for(let d of u){let g=Date.now();t.onProgress?.(e,"migrating",d.name),await this.config.hooks?.beforeMigration?.(e,d.name),await this.recordMigration(s,a,d,i),await this.config.hooks?.afterMigration?.(e,d.name,Date.now()-g),r.push(d.name);}let h={tenantId:e,schemaName:a,success:!0,appliedMigrations:r,durationMs:Date.now()-n,format:i.format};return await this.config.hooks?.afterTenant?.(e,h),h}catch(i){let o={tenantId:e,schemaName:a,success:false,appliedMigrations:r,error:i.message,durationMs:Date.now()-n};return await this.config.hooks?.afterTenant?.(e,o),o}finally{await s.end();}}async getTenantStatus(e,t){let n=this.deps.schemaNameTemplate(e),a=await this.deps.createPool(n);try{let r=t??await this.deps.loadMigrations();if(!await this.deps.migrationsTableExists(a,n))return {tenantId:e,schemaName:n,appliedCount:0,pendingCount:r.length,pendingMigrations:r.map(u=>u.name),status:r.length>0?"behind":"ok",format:null};let i=await this.deps.getOrDetectFormat(a,n),o=await this.getAppliedMigrations(a,n,i),m=new Set(o.map(u=>u.identifier)),l=r.filter(u=>!this.isMigrationApplied(u,m,i));return {tenantId:e,schemaName:n,appliedCount:o.length,pendingCount:l.length,pendingMigrations:l.map(u=>u.name),status:l.length>0?"behind":"ok",format:i.format}}catch(r){return {tenantId:e,schemaName:n,appliedCount:0,pendingCount:0,pendingMigrations:[],status:"error",error:r.message,format:null}}finally{await a.end();}}async executeMigration(e,t,n,a,r){r?.markOnly?(r.onProgress?.("recording"),await this.recordMigration(e,t,n,a)):(r?.onProgress?.("applying"),await this.applyMigration(e,t,n,a));}async executeMigrations(e,t,n,a,r){let s=[];for(let i of n)await this.executeMigration(e,t,i,a,r),s.push(i.name);return s}async recordMigration(e,t,n,a){let{identifier:r,timestamp:s,timestampType:i}=a.columns,o=r==="name"?n.name:n.hash,m=i==="bigint"?Date.now():new Date;await e.query(`INSERT INTO "${t}"."${a.tableName}" ("${r}", "${s}") VALUES ($1, $2)`,[o,m]);}async getAppliedMigrations(e,t,n){let a=n.columns.identifier,r=n.columns.timestamp;return (await e.query(`SELECT id, "${a}" as identifier, "${r}" as applied_at
|
|
65
|
+
FROM "${t}"."${n.tableName}"
|
|
66
|
+
ORDER BY id`)).rows.map(i=>{let o=n.columns.timestampType==="bigint"?new Date(Number(i.applied_at)):new Date(i.applied_at);return {identifier:i.identifier,...n.columns.identifier==="name"?{name:i.identifier}:{hash:i.identifier},appliedAt:o}})}async getPendingMigrations(e,t,n,a){let r=await this.getAppliedMigrations(e,t,a),s=new Set(r.map(i=>i.identifier));return n.filter(i=>!this.isMigrationApplied(i,s,a))}isMigrationApplied(e,t,n){return n.columns.identifier==="name"?t.has(e.name):t.has(e.hash)||t.has(e.name)}async applyMigration(e,t,n,a){let r=await e.connect();try{await r.query("BEGIN"),await r.query(n.sql);let{identifier:s,timestamp:i,timestampType:o}=a.columns,m=s==="name"?n.name:n.hash,l=o==="bigint"?Date.now():new Date;await r.query(`INSERT INTO "${t}"."${a.tableName}" ("${s}", "${i}") VALUES ($1, $2)`,[m,l]),await r.query("COMMIT");}catch(s){throw await r.query("ROLLBACK"),s}finally{r.release();}}};var I=class{constructor(e,t,n){this.config=e;this.executor=t;this.loadMigrations=n;}async migrateAll(e={}){let{concurrency:t=10,onProgress:n,onError:a,dryRun:r=false}=e,s=await this.config.tenantDiscovery(),i=await this.loadMigrations(),o=[],m=false;for(let l=0;l<s.length&&!m;l+=t){let u=s.slice(l,l+t),h=await Promise.all(u.map(async d=>{if(m)return this.createSkippedResult(d);try{n?.(d,"starting");let g=await this.executor.migrateTenant(d,i,{dryRun:r,onProgress:n});return n?.(d,g.success?"completed":"failed"),g}catch(g){return n?.(d,"failed"),a?.(d,g)==="abort"&&(m=true),this.createErrorResult(d,g)}}));o.push(...h);}if(m){let l=s.slice(o.length);for(let u of l)o.push(this.createSkippedResult(u));}return this.aggregateResults(o)}async migrateTenants(e,t={}){let n=await this.loadMigrations(),a=[],{concurrency:r=10,onProgress:s,onError:i,dryRun:o=false}=t;for(let m=0;m<e.length;m+=r){let l=e.slice(m,m+r),u=await Promise.all(l.map(async h=>{try{s?.(h,"starting");let d=await this.executor.migrateTenant(h,n,{dryRun:o,onProgress:s});return s?.(h,d.success?"completed":"failed"),d}catch(d){return s?.(h,"failed"),i?.(h,d),this.createErrorResult(h,d)}}));a.push(...u);}return this.aggregateResults(a)}async markAllAsApplied(e={}){let{concurrency:t=10,onProgress:n,onError:a}=e,r=await this.config.tenantDiscovery(),s=[],i=false;for(let o=0;o<r.length&&!i;o+=t){let m=r.slice(o,o+t),l=await Promise.all(m.map(async u=>{if(i)return this.createSkippedResult(u);try{n?.(u,"starting");let h=await this.executor.markAsApplied(u,{onProgress:n});return n?.(u,h.success?"completed":"failed"),h}catch(h){return n?.(u,"failed"),a?.(u,h)==="abort"&&(i=true),this.createErrorResult(u,h)}}));s.push(...l);}if(i){let o=r.slice(s.length);for(let m of o)s.push(this.createSkippedResult(m));}return this.aggregateResults(s)}async getStatus(){let e=await this.config.tenantDiscovery(),t=await this.loadMigrations(),n=[];for(let a of e)n.push(await this.executor.getTenantStatus(a,t));return n}createSkippedResult(e){return {tenantId:e,schemaName:"",success:false,appliedMigrations:[],error:"Skipped due to abort",durationMs:0}}createErrorResult(e,t){return {tenantId:e,schemaName:"",success:false,appliedMigrations:[],error:t.message,durationMs:0}}aggregateResults(e){return {total:e.length,succeeded:e.filter(t=>t.success).length,failed:e.filter(t=>!t.success&&t.error!=="Skipped due to abort").length,skipped:e.filter(t=>t.error==="Skipped due to abort").length,details:e}}};async function ie(c,e,t=[]){let n=t.length>0?t.map((r,s)=>`$${s+2}`).join(", "):"''::text";return (await c.query(`SELECT table_name
|
|
3510
67
|
FROM information_schema.tables
|
|
3511
68
|
WHERE table_schema = $1
|
|
3512
69
|
AND table_type = 'BASE TABLE'
|
|
3513
|
-
AND table_name NOT IN (${
|
|
3514
|
-
ORDER BY table_name`,
|
|
3515
|
-
[schemaName, ...excludeTables]
|
|
3516
|
-
);
|
|
3517
|
-
return result.rows.map((r) => r.table_name);
|
|
3518
|
-
}
|
|
3519
|
-
async function getColumns(pool, schemaName, tableName) {
|
|
3520
|
-
const result = await pool.query(
|
|
3521
|
-
`SELECT
|
|
70
|
+
AND table_name NOT IN (${n})
|
|
71
|
+
ORDER BY table_name`,[e,...t])).rows.map(r=>r.table_name)}async function ke(c,e,t){return (await c.query(`SELECT
|
|
3522
72
|
column_name,
|
|
3523
73
|
data_type,
|
|
3524
74
|
udt_name,
|
|
@@ -3529,68 +79,13 @@ async function getColumns(pool, schemaName, tableName) {
|
|
|
3529
79
|
numeric_scale
|
|
3530
80
|
FROM information_schema.columns
|
|
3531
81
|
WHERE table_schema = $1 AND table_name = $2
|
|
3532
|
-
ORDER BY ordinal_position`,
|
|
3533
|
-
|
|
3534
|
-
)
|
|
3535
|
-
|
|
3536
|
-
columnName: row.column_name,
|
|
3537
|
-
dataType: row.data_type,
|
|
3538
|
-
udtName: row.udt_name,
|
|
3539
|
-
isNullable: row.is_nullable === "YES",
|
|
3540
|
-
columnDefault: row.column_default,
|
|
3541
|
-
characterMaximumLength: row.character_maximum_length,
|
|
3542
|
-
numericPrecision: row.numeric_precision,
|
|
3543
|
-
numericScale: row.numeric_scale
|
|
3544
|
-
}));
|
|
3545
|
-
}
|
|
3546
|
-
async function generateTableDdl(pool, schemaName, tableName) {
|
|
3547
|
-
const columns = await getColumns(pool, schemaName, tableName);
|
|
3548
|
-
const columnDefs = columns.map((col) => {
|
|
3549
|
-
let type = col.udtName;
|
|
3550
|
-
if (col.dataType === "character varying" && col.characterMaximumLength) {
|
|
3551
|
-
type = `varchar(${col.characterMaximumLength})`;
|
|
3552
|
-
} else if (col.dataType === "character" && col.characterMaximumLength) {
|
|
3553
|
-
type = `char(${col.characterMaximumLength})`;
|
|
3554
|
-
} else if (col.dataType === "numeric" && col.numericPrecision) {
|
|
3555
|
-
type = `numeric(${col.numericPrecision}${col.numericScale ? `, ${col.numericScale}` : ""})`;
|
|
3556
|
-
} else if (col.dataType === "ARRAY") {
|
|
3557
|
-
type = col.udtName.replace(/^_/, "") + "[]";
|
|
3558
|
-
}
|
|
3559
|
-
let definition = `"${col.columnName}" ${type}`;
|
|
3560
|
-
if (!col.isNullable) {
|
|
3561
|
-
definition += " NOT NULL";
|
|
3562
|
-
}
|
|
3563
|
-
if (col.columnDefault) {
|
|
3564
|
-
const defaultValue = col.columnDefault.replace(
|
|
3565
|
-
new RegExp(`"?${schemaName}"?\\.`, "g"),
|
|
3566
|
-
""
|
|
3567
|
-
);
|
|
3568
|
-
definition += ` DEFAULT ${defaultValue}`;
|
|
3569
|
-
}
|
|
3570
|
-
return definition;
|
|
3571
|
-
});
|
|
3572
|
-
return `CREATE TABLE IF NOT EXISTS "${tableName}" (
|
|
3573
|
-
${columnDefs.join(",\n ")}
|
|
3574
|
-
)`;
|
|
3575
|
-
}
|
|
3576
|
-
async function generateIndexDdls(pool, sourceSchema, targetSchema, tableName) {
|
|
3577
|
-
const result = await pool.query(
|
|
3578
|
-
`SELECT indexname, indexdef
|
|
82
|
+
ORDER BY ordinal_position`,[e,t])).rows.map(a=>({columnName:a.column_name,dataType:a.data_type,udtName:a.udt_name,isNullable:a.is_nullable==="YES",columnDefault:a.column_default,characterMaximumLength:a.character_maximum_length,numericPrecision:a.numeric_precision,numericScale:a.numeric_scale}))}async function Oe(c,e,t){let a=(await ke(c,e,t)).map(r=>{let s=r.udtName;r.dataType==="character varying"&&r.characterMaximumLength?s=`varchar(${r.characterMaximumLength})`:r.dataType==="character"&&r.characterMaximumLength?s=`char(${r.characterMaximumLength})`:r.dataType==="numeric"&&r.numericPrecision?s=`numeric(${r.numericPrecision}${r.numericScale?`, ${r.numericScale}`:""})`:r.dataType==="ARRAY"&&(s=r.udtName.replace(/^_/,"")+"[]");let i=`"${r.columnName}" ${s}`;if(r.isNullable||(i+=" NOT NULL"),r.columnDefault){let o=r.columnDefault.replace(new RegExp(`"?${e}"?\\.`,"g"),"");i+=` DEFAULT ${o}`;}return i});return `CREATE TABLE IF NOT EXISTS "${t}" (
|
|
83
|
+
${a.join(`,
|
|
84
|
+
`)}
|
|
85
|
+
)`}async function $e(c,e,t,n){return (await c.query(`SELECT indexname, indexdef
|
|
3579
86
|
FROM pg_indexes
|
|
3580
87
|
WHERE schemaname = $1 AND tablename = $2
|
|
3581
|
-
AND indexname NOT LIKE '%_pkey'`,
|
|
3582
|
-
[sourceSchema, tableName]
|
|
3583
|
-
);
|
|
3584
|
-
return result.rows.map(
|
|
3585
|
-
(row) => (
|
|
3586
|
-
// Replace source schema with target schema
|
|
3587
|
-
row.indexdef.replace(new RegExp(`ON "${sourceSchema}"\\."`, "g"), `ON "${targetSchema}"."`).replace(new RegExp(`"${sourceSchema}"\\."`, "g"), `"${targetSchema}"."`)
|
|
3588
|
-
)
|
|
3589
|
-
);
|
|
3590
|
-
}
|
|
3591
|
-
async function generatePrimaryKeyDdl(pool, schemaName, tableName) {
|
|
3592
|
-
const result = await pool.query(
|
|
3593
|
-
`SELECT
|
|
88
|
+
AND indexname NOT LIKE '%_pkey'`,[e,n])).rows.map(r=>r.indexdef.replace(new RegExp(`ON "${e}"\\."`,"g"),`ON "${t}"."`).replace(new RegExp(`"${e}"\\."`,"g"),`"${t}"."`))}async function Ae(c,e,t){let n=await c.query(`SELECT
|
|
3594
89
|
tc.constraint_name,
|
|
3595
90
|
kcu.column_name
|
|
3596
91
|
FROM information_schema.table_constraints tc
|
|
@@ -3600,17 +95,7 @@ async function generatePrimaryKeyDdl(pool, schemaName, tableName) {
|
|
|
3600
95
|
WHERE tc.table_schema = $1
|
|
3601
96
|
AND tc.table_name = $2
|
|
3602
97
|
AND tc.constraint_type = 'PRIMARY KEY'
|
|
3603
|
-
ORDER BY kcu.ordinal_position`,
|
|
3604
|
-
[schemaName, tableName]
|
|
3605
|
-
);
|
|
3606
|
-
if (result.rows.length === 0) return null;
|
|
3607
|
-
const columns = result.rows.map((r) => `"${r.column_name}"`).join(", ");
|
|
3608
|
-
const constraintName = result.rows[0].constraint_name;
|
|
3609
|
-
return `ALTER TABLE "${tableName}" ADD CONSTRAINT "${constraintName}" PRIMARY KEY (${columns})`;
|
|
3610
|
-
}
|
|
3611
|
-
async function generateForeignKeyDdls(pool, sourceSchema, targetSchema, tableName) {
|
|
3612
|
-
const result = await pool.query(
|
|
3613
|
-
`SELECT
|
|
98
|
+
ORDER BY kcu.ordinal_position`,[e,t]);if(n.rows.length===0)return null;let a=n.rows.map(s=>`"${s.column_name}"`).join(", "),r=n.rows[0].constraint_name;return `ALTER TABLE "${t}" ADD CONSTRAINT "${r}" PRIMARY KEY (${a})`}async function ve(c,e,t,n){let a=await c.query(`SELECT
|
|
3614
99
|
tc.constraint_name,
|
|
3615
100
|
kcu.column_name,
|
|
3616
101
|
ccu.table_name as foreign_table_name,
|
|
@@ -3630,43 +115,7 @@ async function generateForeignKeyDdls(pool, sourceSchema, targetSchema, tableNam
|
|
|
3630
115
|
WHERE tc.table_schema = $1
|
|
3631
116
|
AND tc.table_name = $2
|
|
3632
117
|
AND tc.constraint_type = 'FOREIGN KEY'
|
|
3633
|
-
ORDER BY tc.constraint_name, kcu.ordinal_position`,
|
|
3634
|
-
[sourceSchema, tableName]
|
|
3635
|
-
);
|
|
3636
|
-
const fkMap = /* @__PURE__ */ new Map();
|
|
3637
|
-
for (const row of result.rows) {
|
|
3638
|
-
const existing = fkMap.get(row.constraint_name);
|
|
3639
|
-
if (existing) {
|
|
3640
|
-
existing.columns.push(row.column_name);
|
|
3641
|
-
existing.foreignColumns.push(row.foreign_column_name);
|
|
3642
|
-
} else {
|
|
3643
|
-
fkMap.set(row.constraint_name, {
|
|
3644
|
-
columns: [row.column_name],
|
|
3645
|
-
foreignTable: row.foreign_table_name,
|
|
3646
|
-
foreignColumns: [row.foreign_column_name],
|
|
3647
|
-
updateRule: row.update_rule,
|
|
3648
|
-
deleteRule: row.delete_rule
|
|
3649
|
-
});
|
|
3650
|
-
}
|
|
3651
|
-
}
|
|
3652
|
-
return Array.from(fkMap.entries()).map(([name, fk]) => {
|
|
3653
|
-
const columns = fk.columns.map((c) => `"${c}"`).join(", ");
|
|
3654
|
-
const foreignColumns = fk.foreignColumns.map((c) => `"${c}"`).join(", ");
|
|
3655
|
-
let ddl = `ALTER TABLE "${targetSchema}"."${tableName}" `;
|
|
3656
|
-
ddl += `ADD CONSTRAINT "${name}" FOREIGN KEY (${columns}) `;
|
|
3657
|
-
ddl += `REFERENCES "${targetSchema}"."${fk.foreignTable}" (${foreignColumns})`;
|
|
3658
|
-
if (fk.updateRule !== "NO ACTION") {
|
|
3659
|
-
ddl += ` ON UPDATE ${fk.updateRule}`;
|
|
3660
|
-
}
|
|
3661
|
-
if (fk.deleteRule !== "NO ACTION") {
|
|
3662
|
-
ddl += ` ON DELETE ${fk.deleteRule}`;
|
|
3663
|
-
}
|
|
3664
|
-
return ddl;
|
|
3665
|
-
});
|
|
3666
|
-
}
|
|
3667
|
-
async function generateUniqueDdls(pool, schemaName, tableName) {
|
|
3668
|
-
const result = await pool.query(
|
|
3669
|
-
`SELECT
|
|
118
|
+
ORDER BY tc.constraint_name, kcu.ordinal_position`,[e,n]),r=new Map;for(let s of a.rows){let i=r.get(s.constraint_name);i?(i.columns.push(s.column_name),i.foreignColumns.push(s.foreign_column_name)):r.set(s.constraint_name,{columns:[s.column_name],foreignTable:s.foreign_table_name,foreignColumns:[s.foreign_column_name],updateRule:s.update_rule,deleteRule:s.delete_rule});}return Array.from(r.entries()).map(([s,i])=>{let o=i.columns.map(u=>`"${u}"`).join(", "),m=i.foreignColumns.map(u=>`"${u}"`).join(", "),l=`ALTER TABLE "${t}"."${n}" `;return l+=`ADD CONSTRAINT "${s}" FOREIGN KEY (${o}) `,l+=`REFERENCES "${t}"."${i.foreignTable}" (${m})`,i.updateRule!=="NO ACTION"&&(l+=` ON UPDATE ${i.updateRule}`),i.deleteRule!=="NO ACTION"&&(l+=` ON DELETE ${i.deleteRule}`),l})}async function Fe(c,e,t){let n=await c.query(`SELECT
|
|
3670
119
|
tc.constraint_name,
|
|
3671
120
|
kcu.column_name
|
|
3672
121
|
FROM information_schema.table_constraints tc
|
|
@@ -3676,26 +125,7 @@ async function generateUniqueDdls(pool, schemaName, tableName) {
|
|
|
3676
125
|
WHERE tc.table_schema = $1
|
|
3677
126
|
AND tc.table_name = $2
|
|
3678
127
|
AND tc.constraint_type = 'UNIQUE'
|
|
3679
|
-
ORDER BY tc.constraint_name, kcu.ordinal_position`,
|
|
3680
|
-
[schemaName, tableName]
|
|
3681
|
-
);
|
|
3682
|
-
const uniqueMap = /* @__PURE__ */ new Map();
|
|
3683
|
-
for (const row of result.rows) {
|
|
3684
|
-
const existing = uniqueMap.get(row.constraint_name);
|
|
3685
|
-
if (existing) {
|
|
3686
|
-
existing.push(row.column_name);
|
|
3687
|
-
} else {
|
|
3688
|
-
uniqueMap.set(row.constraint_name, [row.column_name]);
|
|
3689
|
-
}
|
|
3690
|
-
}
|
|
3691
|
-
return Array.from(uniqueMap.entries()).map(([name, columns]) => {
|
|
3692
|
-
const cols = columns.map((c) => `"${c}"`).join(", ");
|
|
3693
|
-
return `ALTER TABLE "${tableName}" ADD CONSTRAINT "${name}" UNIQUE (${cols})`;
|
|
3694
|
-
});
|
|
3695
|
-
}
|
|
3696
|
-
async function generateCheckDdls(pool, schemaName, tableName) {
|
|
3697
|
-
const result = await pool.query(
|
|
3698
|
-
`SELECT
|
|
128
|
+
ORDER BY tc.constraint_name, kcu.ordinal_position`,[e,t]),a=new Map;for(let r of n.rows){let s=a.get(r.constraint_name);s?s.push(r.column_name):a.set(r.constraint_name,[r.column_name]);}return Array.from(a.entries()).map(([r,s])=>{let i=s.map(o=>`"${o}"`).join(", ");return `ALTER TABLE "${t}" ADD CONSTRAINT "${r}" UNIQUE (${i})`})}async function Le(c,e,t){return (await c.query(`SELECT
|
|
3699
129
|
tc.constraint_name,
|
|
3700
130
|
cc.check_clause
|
|
3701
131
|
FROM information_schema.table_constraints tc
|
|
@@ -3705,91 +135,12 @@ async function generateCheckDdls(pool, schemaName, tableName) {
|
|
|
3705
135
|
WHERE tc.table_schema = $1
|
|
3706
136
|
AND tc.table_name = $2
|
|
3707
137
|
AND tc.constraint_type = 'CHECK'
|
|
3708
|
-
AND tc.constraint_name NOT LIKE '%_not_null'`,
|
|
3709
|
-
[schemaName, tableName]
|
|
3710
|
-
);
|
|
3711
|
-
return result.rows.map(
|
|
3712
|
-
(row) => `ALTER TABLE "${tableName}" ADD CONSTRAINT "${row.constraint_name}" CHECK (${row.check_clause})`
|
|
3713
|
-
);
|
|
3714
|
-
}
|
|
3715
|
-
async function getRowCount(pool, schemaName, tableName) {
|
|
3716
|
-
const result = await pool.query(
|
|
3717
|
-
`SELECT count(*) FROM "${schemaName}"."${tableName}"`
|
|
3718
|
-
);
|
|
3719
|
-
return parseInt(result.rows[0].count, 10);
|
|
3720
|
-
}
|
|
3721
|
-
async function generateTableCloneInfo(pool, sourceSchema, targetSchema, tableName) {
|
|
3722
|
-
const [createDdl, indexDdls, pkDdl, uniqueDdls, checkDdls, fkDdls, rowCount] = await Promise.all([
|
|
3723
|
-
generateTableDdl(pool, sourceSchema, tableName),
|
|
3724
|
-
generateIndexDdls(pool, sourceSchema, targetSchema, tableName),
|
|
3725
|
-
generatePrimaryKeyDdl(pool, sourceSchema, tableName),
|
|
3726
|
-
generateUniqueDdls(pool, sourceSchema, tableName),
|
|
3727
|
-
generateCheckDdls(pool, sourceSchema, tableName),
|
|
3728
|
-
generateForeignKeyDdls(pool, sourceSchema, targetSchema, tableName),
|
|
3729
|
-
getRowCount(pool, sourceSchema, tableName)
|
|
3730
|
-
]);
|
|
3731
|
-
return {
|
|
3732
|
-
name: tableName,
|
|
3733
|
-
createDdl,
|
|
3734
|
-
indexDdls,
|
|
3735
|
-
constraintDdls: [
|
|
3736
|
-
...pkDdl ? [pkDdl] : [],
|
|
3737
|
-
...uniqueDdls,
|
|
3738
|
-
...checkDdls,
|
|
3739
|
-
...fkDdls
|
|
3740
|
-
],
|
|
3741
|
-
rowCount
|
|
3742
|
-
};
|
|
3743
|
-
}
|
|
3744
|
-
|
|
3745
|
-
// src/migrator/clone/data-copier.ts
|
|
3746
|
-
async function getTableColumns(pool, schemaName, tableName) {
|
|
3747
|
-
const result = await pool.query(
|
|
3748
|
-
`SELECT column_name
|
|
138
|
+
AND tc.constraint_name NOT LIKE '%_not_null'`,[e,t])).rows.map(a=>`ALTER TABLE "${t}" ADD CONSTRAINT "${a.constraint_name}" CHECK (${a.check_clause})`)}async function Ie(c,e,t){let n=await c.query(`SELECT count(*) FROM "${e}"."${t}"`);return parseInt(n.rows[0].count,10)}async function oe(c,e,t,n){let[a,r,s,i,o,m,l]=await Promise.all([Oe(c,e,n),$e(c,e,t,n),Ae(c,e,n),Fe(c,e,n),Le(c,e,n),ve(c,e,t,n),Ie(c,e,n)]);return {name:n,createDdl:a,indexDdls:r,constraintDdls:[...s?[s]:[],...i,...o,...m],rowCount:l}}async function He(c,e,t){return (await c.query(`SELECT column_name
|
|
3749
139
|
FROM information_schema.columns
|
|
3750
140
|
WHERE table_schema = $1 AND table_name = $2
|
|
3751
|
-
ORDER BY ordinal_position`,
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
return result.rows.map((r) => r.column_name);
|
|
3755
|
-
}
|
|
3756
|
-
function formatAnonymizeValue(value) {
|
|
3757
|
-
if (value === null) {
|
|
3758
|
-
return "NULL";
|
|
3759
|
-
}
|
|
3760
|
-
if (typeof value === "string") {
|
|
3761
|
-
return `'${value.replace(/'/g, "''")}'`;
|
|
3762
|
-
}
|
|
3763
|
-
if (typeof value === "boolean") {
|
|
3764
|
-
return value ? "TRUE" : "FALSE";
|
|
3765
|
-
}
|
|
3766
|
-
return String(value);
|
|
3767
|
-
}
|
|
3768
|
-
async function copyTableData(pool, sourceSchema, targetSchema, tableName, anonymizeRules) {
|
|
3769
|
-
const columns = await getTableColumns(pool, sourceSchema, tableName);
|
|
3770
|
-
if (columns.length === 0) {
|
|
3771
|
-
return 0;
|
|
3772
|
-
}
|
|
3773
|
-
const tableRules = anonymizeRules?.[tableName] ?? {};
|
|
3774
|
-
const selectColumns = columns.map((col) => {
|
|
3775
|
-
if (col in tableRules) {
|
|
3776
|
-
const value = tableRules[col];
|
|
3777
|
-
return `${formatAnonymizeValue(value)} as "${col}"`;
|
|
3778
|
-
}
|
|
3779
|
-
return `"${col}"`;
|
|
3780
|
-
});
|
|
3781
|
-
const insertColumns = columns.map((c) => `"${c}"`).join(", ");
|
|
3782
|
-
const selectExpr = selectColumns.join(", ");
|
|
3783
|
-
const result = await pool.query(
|
|
3784
|
-
`INSERT INTO "${targetSchema}"."${tableName}" (${insertColumns})
|
|
3785
|
-
SELECT ${selectExpr}
|
|
3786
|
-
FROM "${sourceSchema}"."${tableName}"`
|
|
3787
|
-
);
|
|
3788
|
-
return result.rowCount ?? 0;
|
|
3789
|
-
}
|
|
3790
|
-
async function getTablesInDependencyOrder(pool, schemaName, tables) {
|
|
3791
|
-
const result = await pool.query(
|
|
3792
|
-
`SELECT DISTINCT
|
|
141
|
+
ORDER BY ordinal_position`,[e,t])).rows.map(a=>a.column_name)}function qe(c){return c===null?"NULL":typeof c=="string"?`'${c.replace(/'/g,"''")}'`:typeof c=="boolean"?c?"TRUE":"FALSE":String(c)}async function je(c,e,t,n,a){let r=await He(c,e,n);if(r.length===0)return 0;let s=a?.[n]??{},i=r.map(u=>{if(u in s){let h=s[u];return `${qe(h)} as "${u}"`}return `"${u}"`}),o=r.map(u=>`"${u}"`).join(", "),m=i.join(", ");return (await c.query(`INSERT INTO "${t}"."${n}" (${o})
|
|
142
|
+
SELECT ${m}
|
|
143
|
+
FROM "${e}"."${n}"`)).rowCount??0}async function ze(c,e,t){let n=await c.query(`SELECT DISTINCT
|
|
3793
144
|
tc.table_name,
|
|
3794
145
|
ccu.table_name as foreign_table_name
|
|
3795
146
|
FROM information_schema.table_constraints tc
|
|
@@ -3798,1068 +149,6 @@ async function getTablesInDependencyOrder(pool, schemaName, tables) {
|
|
|
3798
149
|
AND tc.table_schema = ccu.table_schema
|
|
3799
150
|
WHERE tc.table_schema = $1
|
|
3800
151
|
AND tc.constraint_type = 'FOREIGN KEY'
|
|
3801
|
-
AND tc.table_name != ccu.table_name`,
|
|
3802
|
-
|
|
3803
|
-
);
|
|
3804
|
-
const dependencies = /* @__PURE__ */ new Map();
|
|
3805
|
-
const tableSet = new Set(tables);
|
|
3806
|
-
for (const table of tables) {
|
|
3807
|
-
dependencies.set(table, /* @__PURE__ */ new Set());
|
|
3808
|
-
}
|
|
3809
|
-
for (const row of result.rows) {
|
|
3810
|
-
if (tableSet.has(row.table_name) && tableSet.has(row.foreign_table_name)) {
|
|
3811
|
-
dependencies.get(row.table_name).add(row.foreign_table_name);
|
|
3812
|
-
}
|
|
3813
|
-
}
|
|
3814
|
-
const sorted = [];
|
|
3815
|
-
const inDegree = /* @__PURE__ */ new Map();
|
|
3816
|
-
const queue = [];
|
|
3817
|
-
for (const table of tables) {
|
|
3818
|
-
inDegree.set(table, 0);
|
|
3819
|
-
}
|
|
3820
|
-
for (const [table, deps] of dependencies) {
|
|
3821
|
-
for (const dep of deps) {
|
|
3822
|
-
inDegree.set(dep, (inDegree.get(dep) ?? 0) + 1);
|
|
3823
|
-
}
|
|
3824
|
-
}
|
|
3825
|
-
for (const [table, degree] of inDegree) {
|
|
3826
|
-
if (degree === 0) {
|
|
3827
|
-
queue.push(table);
|
|
3828
|
-
}
|
|
3829
|
-
}
|
|
3830
|
-
while (queue.length > 0) {
|
|
3831
|
-
const table = queue.shift();
|
|
3832
|
-
sorted.push(table);
|
|
3833
|
-
for (const [otherTable, deps] of dependencies) {
|
|
3834
|
-
if (deps.has(table)) {
|
|
3835
|
-
deps.delete(table);
|
|
3836
|
-
const newDegree = (inDegree.get(otherTable) ?? 0) - 1;
|
|
3837
|
-
inDegree.set(otherTable, newDegree);
|
|
3838
|
-
if (newDegree === 0) {
|
|
3839
|
-
queue.push(otherTable);
|
|
3840
|
-
}
|
|
3841
|
-
}
|
|
3842
|
-
}
|
|
3843
|
-
}
|
|
3844
|
-
const remaining = tables.filter((t) => !sorted.includes(t));
|
|
3845
|
-
return [...sorted, ...remaining];
|
|
3846
|
-
}
|
|
3847
|
-
async function copyAllData(pool, sourceSchema, targetSchema, tables, anonymizeRules, onProgress) {
|
|
3848
|
-
let totalRows = 0;
|
|
3849
|
-
const orderedTables = await getTablesInDependencyOrder(pool, sourceSchema, tables);
|
|
3850
|
-
await pool.query("SET session_replication_role = replica");
|
|
3851
|
-
try {
|
|
3852
|
-
for (let i = 0; i < orderedTables.length; i++) {
|
|
3853
|
-
const table = orderedTables[i];
|
|
3854
|
-
onProgress?.("copying_data", {
|
|
3855
|
-
table,
|
|
3856
|
-
progress: i + 1,
|
|
3857
|
-
total: orderedTables.length
|
|
3858
|
-
});
|
|
3859
|
-
const rows = await copyTableData(pool, sourceSchema, targetSchema, table, anonymizeRules);
|
|
3860
|
-
totalRows += rows;
|
|
3861
|
-
}
|
|
3862
|
-
} finally {
|
|
3863
|
-
await pool.query("SET session_replication_role = DEFAULT");
|
|
3864
|
-
}
|
|
3865
|
-
return totalRows;
|
|
3866
|
-
}
|
|
3867
|
-
|
|
3868
|
-
// src/migrator/clone/cloner.ts
|
|
3869
|
-
var DEFAULT_MIGRATIONS_TABLE3 = "__drizzle_migrations";
|
|
3870
|
-
var Cloner = class {
|
|
3871
|
-
constructor(config, deps) {
|
|
3872
|
-
this.deps = deps;
|
|
3873
|
-
this.migrationsTable = config.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE3;
|
|
3874
|
-
}
|
|
3875
|
-
migrationsTable;
|
|
3876
|
-
/**
|
|
3877
|
-
* Clone a tenant to another
|
|
3878
|
-
*
|
|
3879
|
-
* @param sourceTenantId - Source tenant ID
|
|
3880
|
-
* @param targetTenantId - Target tenant ID
|
|
3881
|
-
* @param options - Clone options
|
|
3882
|
-
* @returns Clone result
|
|
3883
|
-
*/
|
|
3884
|
-
async cloneTenant(sourceTenantId, targetTenantId, options = {}) {
|
|
3885
|
-
const startTime = Date.now();
|
|
3886
|
-
const {
|
|
3887
|
-
includeData = false,
|
|
3888
|
-
anonymize,
|
|
3889
|
-
excludeTables = [],
|
|
3890
|
-
onProgress
|
|
3891
|
-
} = options;
|
|
3892
|
-
const sourceSchema = this.deps.schemaNameTemplate(sourceTenantId);
|
|
3893
|
-
const targetSchema = this.deps.schemaNameTemplate(targetTenantId);
|
|
3894
|
-
const allExcludes = [this.migrationsTable, ...excludeTables];
|
|
3895
|
-
let sourcePool = null;
|
|
3896
|
-
let rootPool = null;
|
|
3897
|
-
try {
|
|
3898
|
-
onProgress?.("starting");
|
|
3899
|
-
const sourceExists = await this.deps.schemaExists(sourceTenantId);
|
|
3900
|
-
if (!sourceExists) {
|
|
3901
|
-
return this.createErrorResult(
|
|
3902
|
-
sourceTenantId,
|
|
3903
|
-
targetTenantId,
|
|
3904
|
-
targetSchema,
|
|
3905
|
-
`Source tenant "${sourceTenantId}" does not exist`,
|
|
3906
|
-
startTime
|
|
3907
|
-
);
|
|
3908
|
-
}
|
|
3909
|
-
const targetExists = await this.deps.schemaExists(targetTenantId);
|
|
3910
|
-
if (targetExists) {
|
|
3911
|
-
return this.createErrorResult(
|
|
3912
|
-
sourceTenantId,
|
|
3913
|
-
targetTenantId,
|
|
3914
|
-
targetSchema,
|
|
3915
|
-
`Target tenant "${targetTenantId}" already exists`,
|
|
3916
|
-
startTime
|
|
3917
|
-
);
|
|
3918
|
-
}
|
|
3919
|
-
onProgress?.("introspecting");
|
|
3920
|
-
sourcePool = await this.deps.createPool(sourceSchema);
|
|
3921
|
-
const tables = await listTables(sourcePool, sourceSchema, allExcludes);
|
|
3922
|
-
if (tables.length === 0) {
|
|
3923
|
-
onProgress?.("creating_schema");
|
|
3924
|
-
await this.deps.createSchema(targetTenantId);
|
|
3925
|
-
onProgress?.("completed");
|
|
3926
|
-
return {
|
|
3927
|
-
sourceTenant: sourceTenantId,
|
|
3928
|
-
targetTenant: targetTenantId,
|
|
3929
|
-
targetSchema,
|
|
3930
|
-
success: true,
|
|
3931
|
-
tables: [],
|
|
3932
|
-
durationMs: Date.now() - startTime
|
|
3933
|
-
};
|
|
3934
|
-
}
|
|
3935
|
-
const tableInfos = await Promise.all(
|
|
3936
|
-
tables.map((t) => generateTableCloneInfo(sourcePool, sourceSchema, targetSchema, t))
|
|
3937
|
-
);
|
|
3938
|
-
await sourcePool.end();
|
|
3939
|
-
sourcePool = null;
|
|
3940
|
-
onProgress?.("creating_schema");
|
|
3941
|
-
await this.deps.createSchema(targetTenantId);
|
|
3942
|
-
rootPool = await this.deps.createRootPool();
|
|
3943
|
-
onProgress?.("creating_tables");
|
|
3944
|
-
for (const info of tableInfos) {
|
|
3945
|
-
await rootPool.query(`SET search_path TO "${targetSchema}"; ${info.createDdl}`);
|
|
3946
|
-
}
|
|
3947
|
-
onProgress?.("creating_constraints");
|
|
3948
|
-
for (const info of tableInfos) {
|
|
3949
|
-
for (const constraint of info.constraintDdls.filter((c) => !c.includes("FOREIGN KEY"))) {
|
|
3950
|
-
try {
|
|
3951
|
-
await rootPool.query(`SET search_path TO "${targetSchema}"; ${constraint}`);
|
|
3952
|
-
} catch (error) {
|
|
3953
|
-
}
|
|
3954
|
-
}
|
|
3955
|
-
}
|
|
3956
|
-
onProgress?.("creating_indexes");
|
|
3957
|
-
for (const info of tableInfos) {
|
|
3958
|
-
for (const index of info.indexDdls) {
|
|
3959
|
-
try {
|
|
3960
|
-
await rootPool.query(index);
|
|
3961
|
-
} catch (error) {
|
|
3962
|
-
}
|
|
3963
|
-
}
|
|
3964
|
-
}
|
|
3965
|
-
let rowsCopied = 0;
|
|
3966
|
-
if (includeData) {
|
|
3967
|
-
onProgress?.("copying_data");
|
|
3968
|
-
rowsCopied = await copyAllData(
|
|
3969
|
-
rootPool,
|
|
3970
|
-
sourceSchema,
|
|
3971
|
-
targetSchema,
|
|
3972
|
-
tables,
|
|
3973
|
-
anonymize?.enabled ? anonymize.rules : void 0,
|
|
3974
|
-
onProgress
|
|
3975
|
-
);
|
|
3976
|
-
}
|
|
3977
|
-
for (const info of tableInfos) {
|
|
3978
|
-
for (const fk of info.constraintDdls.filter((c) => c.includes("FOREIGN KEY"))) {
|
|
3979
|
-
try {
|
|
3980
|
-
await rootPool.query(fk);
|
|
3981
|
-
} catch (error) {
|
|
3982
|
-
}
|
|
3983
|
-
}
|
|
3984
|
-
}
|
|
3985
|
-
onProgress?.("completed");
|
|
3986
|
-
const result = {
|
|
3987
|
-
sourceTenant: sourceTenantId,
|
|
3988
|
-
targetTenant: targetTenantId,
|
|
3989
|
-
targetSchema,
|
|
3990
|
-
success: true,
|
|
3991
|
-
tables,
|
|
3992
|
-
durationMs: Date.now() - startTime
|
|
3993
|
-
};
|
|
3994
|
-
if (includeData) {
|
|
3995
|
-
result.rowsCopied = rowsCopied;
|
|
3996
|
-
}
|
|
3997
|
-
return result;
|
|
3998
|
-
} catch (error) {
|
|
3999
|
-
options.onError?.(error);
|
|
4000
|
-
onProgress?.("failed");
|
|
4001
|
-
return this.createErrorResult(
|
|
4002
|
-
sourceTenantId,
|
|
4003
|
-
targetTenantId,
|
|
4004
|
-
targetSchema,
|
|
4005
|
-
error.message,
|
|
4006
|
-
startTime
|
|
4007
|
-
);
|
|
4008
|
-
} finally {
|
|
4009
|
-
if (sourcePool) {
|
|
4010
|
-
await sourcePool.end().catch(() => {
|
|
4011
|
-
});
|
|
4012
|
-
}
|
|
4013
|
-
if (rootPool) {
|
|
4014
|
-
await rootPool.end().catch(() => {
|
|
4015
|
-
});
|
|
4016
|
-
}
|
|
4017
|
-
}
|
|
4018
|
-
}
|
|
4019
|
-
createErrorResult(source, target, schema, error, startTime) {
|
|
4020
|
-
return {
|
|
4021
|
-
sourceTenant: source,
|
|
4022
|
-
targetTenant: target,
|
|
4023
|
-
targetSchema: schema,
|
|
4024
|
-
success: false,
|
|
4025
|
-
error,
|
|
4026
|
-
tables: [],
|
|
4027
|
-
durationMs: Date.now() - startTime
|
|
4028
|
-
};
|
|
4029
|
-
}
|
|
4030
|
-
};
|
|
4031
|
-
|
|
4032
|
-
// src/migrator/migrator.ts
|
|
4033
|
-
var DEFAULT_MIGRATIONS_TABLE4 = "__drizzle_migrations";
|
|
4034
|
-
var Migrator = class {
|
|
4035
|
-
constructor(tenantConfig, migratorConfig) {
|
|
4036
|
-
this.migratorConfig = migratorConfig;
|
|
4037
|
-
this.migrationsTable = migratorConfig.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE4;
|
|
4038
|
-
this.schemaManager = new SchemaManager(tenantConfig, this.migrationsTable);
|
|
4039
|
-
this.driftDetector = new DriftDetector(tenantConfig, this.schemaManager, {
|
|
4040
|
-
migrationsTable: this.migrationsTable,
|
|
4041
|
-
tenantDiscovery: migratorConfig.tenantDiscovery
|
|
4042
|
-
});
|
|
4043
|
-
this.seeder = new Seeder(
|
|
4044
|
-
{ tenantDiscovery: migratorConfig.tenantDiscovery },
|
|
4045
|
-
{
|
|
4046
|
-
createPool: this.schemaManager.createPool.bind(this.schemaManager),
|
|
4047
|
-
schemaNameTemplate: tenantConfig.isolation.schemaNameTemplate,
|
|
4048
|
-
tenantSchema: tenantConfig.schemas.tenant
|
|
4049
|
-
}
|
|
4050
|
-
);
|
|
4051
|
-
this.syncManager = new SyncManager(
|
|
4052
|
-
{
|
|
4053
|
-
tenantDiscovery: migratorConfig.tenantDiscovery,
|
|
4054
|
-
migrationsFolder: migratorConfig.migrationsFolder,
|
|
4055
|
-
migrationsTable: this.migrationsTable
|
|
4056
|
-
},
|
|
4057
|
-
{
|
|
4058
|
-
createPool: this.schemaManager.createPool.bind(this.schemaManager),
|
|
4059
|
-
schemaNameTemplate: tenantConfig.isolation.schemaNameTemplate,
|
|
4060
|
-
migrationsTableExists: this.schemaManager.migrationsTableExists.bind(this.schemaManager),
|
|
4061
|
-
ensureMigrationsTable: this.schemaManager.ensureMigrationsTable.bind(this.schemaManager),
|
|
4062
|
-
getOrDetectFormat: this.getOrDetectFormat.bind(this),
|
|
4063
|
-
loadMigrations: this.loadMigrations.bind(this)
|
|
4064
|
-
}
|
|
4065
|
-
);
|
|
4066
|
-
this.migrationExecutor = new MigrationExecutor(
|
|
4067
|
-
{ hooks: migratorConfig.hooks },
|
|
4068
|
-
{
|
|
4069
|
-
createPool: this.schemaManager.createPool.bind(this.schemaManager),
|
|
4070
|
-
schemaNameTemplate: tenantConfig.isolation.schemaNameTemplate,
|
|
4071
|
-
migrationsTableExists: this.schemaManager.migrationsTableExists.bind(this.schemaManager),
|
|
4072
|
-
ensureMigrationsTable: this.schemaManager.ensureMigrationsTable.bind(this.schemaManager),
|
|
4073
|
-
getOrDetectFormat: this.getOrDetectFormat.bind(this),
|
|
4074
|
-
loadMigrations: this.loadMigrations.bind(this)
|
|
4075
|
-
}
|
|
4076
|
-
);
|
|
4077
|
-
this.batchExecutor = new BatchExecutor(
|
|
4078
|
-
{ tenantDiscovery: migratorConfig.tenantDiscovery },
|
|
4079
|
-
this.migrationExecutor,
|
|
4080
|
-
this.loadMigrations.bind(this)
|
|
4081
|
-
);
|
|
4082
|
-
this.cloner = new Cloner(
|
|
4083
|
-
{ migrationsTable: this.migrationsTable },
|
|
4084
|
-
{
|
|
4085
|
-
createPool: this.schemaManager.createPool.bind(this.schemaManager),
|
|
4086
|
-
createRootPool: this.schemaManager.createRootPool.bind(this.schemaManager),
|
|
4087
|
-
schemaNameTemplate: tenantConfig.isolation.schemaNameTemplate,
|
|
4088
|
-
schemaExists: this.schemaManager.schemaExists.bind(this.schemaManager),
|
|
4089
|
-
createSchema: this.schemaManager.createSchema.bind(this.schemaManager)
|
|
4090
|
-
}
|
|
4091
|
-
);
|
|
4092
|
-
}
|
|
4093
|
-
migrationsTable;
|
|
4094
|
-
schemaManager;
|
|
4095
|
-
driftDetector;
|
|
4096
|
-
seeder;
|
|
4097
|
-
syncManager;
|
|
4098
|
-
migrationExecutor;
|
|
4099
|
-
batchExecutor;
|
|
4100
|
-
cloner;
|
|
4101
|
-
/**
|
|
4102
|
-
* Migrate all tenants in parallel
|
|
4103
|
-
*
|
|
4104
|
-
* Delegates to BatchExecutor for parallel migration operations.
|
|
4105
|
-
*/
|
|
4106
|
-
async migrateAll(options = {}) {
|
|
4107
|
-
return this.batchExecutor.migrateAll(options);
|
|
4108
|
-
}
|
|
4109
|
-
/**
|
|
4110
|
-
* Migrate a single tenant
|
|
4111
|
-
*
|
|
4112
|
-
* Delegates to MigrationExecutor for single tenant operations.
|
|
4113
|
-
*/
|
|
4114
|
-
async migrateTenant(tenantId, migrations, options = {}) {
|
|
4115
|
-
return this.migrationExecutor.migrateTenant(tenantId, migrations, options);
|
|
4116
|
-
}
|
|
4117
|
-
/**
|
|
4118
|
-
* Migrate specific tenants
|
|
4119
|
-
*
|
|
4120
|
-
* Delegates to BatchExecutor for parallel migration operations.
|
|
4121
|
-
*/
|
|
4122
|
-
async migrateTenants(tenantIds, options = {}) {
|
|
4123
|
-
return this.batchExecutor.migrateTenants(tenantIds, options);
|
|
4124
|
-
}
|
|
4125
|
-
/**
|
|
4126
|
-
* Get migration status for all tenants
|
|
4127
|
-
*
|
|
4128
|
-
* Delegates to BatchExecutor for status operations.
|
|
4129
|
-
*/
|
|
4130
|
-
async getStatus() {
|
|
4131
|
-
return this.batchExecutor.getStatus();
|
|
4132
|
-
}
|
|
4133
|
-
/**
|
|
4134
|
-
* Get migration status for a specific tenant
|
|
4135
|
-
*
|
|
4136
|
-
* Delegates to MigrationExecutor for single tenant operations.
|
|
4137
|
-
*/
|
|
4138
|
-
async getTenantStatus(tenantId, migrations) {
|
|
4139
|
-
return this.migrationExecutor.getTenantStatus(tenantId, migrations);
|
|
4140
|
-
}
|
|
4141
|
-
/**
|
|
4142
|
-
* Create a new tenant schema and optionally apply migrations
|
|
4143
|
-
*/
|
|
4144
|
-
async createTenant(tenantId, options = {}) {
|
|
4145
|
-
const { migrate = true } = options;
|
|
4146
|
-
await this.schemaManager.createSchema(tenantId);
|
|
4147
|
-
if (migrate) {
|
|
4148
|
-
await this.migrateTenant(tenantId);
|
|
4149
|
-
}
|
|
4150
|
-
}
|
|
4151
|
-
/**
|
|
4152
|
-
* Drop a tenant schema
|
|
4153
|
-
*/
|
|
4154
|
-
async dropTenant(tenantId, options = {}) {
|
|
4155
|
-
await this.schemaManager.dropSchema(tenantId, options);
|
|
4156
|
-
}
|
|
4157
|
-
/**
|
|
4158
|
-
* Check if a tenant schema exists
|
|
4159
|
-
*/
|
|
4160
|
-
async tenantExists(tenantId) {
|
|
4161
|
-
return this.schemaManager.schemaExists(tenantId);
|
|
4162
|
-
}
|
|
4163
|
-
/**
|
|
4164
|
-
* Clone a tenant to a new tenant
|
|
4165
|
-
*
|
|
4166
|
-
* By default, clones only schema structure. Use includeData to copy data.
|
|
4167
|
-
*
|
|
4168
|
-
* @example
|
|
4169
|
-
* ```typescript
|
|
4170
|
-
* // Schema-only clone
|
|
4171
|
-
* await migrator.cloneTenant('production', 'dev');
|
|
4172
|
-
*
|
|
4173
|
-
* // Clone with data
|
|
4174
|
-
* await migrator.cloneTenant('production', 'dev', { includeData: true });
|
|
4175
|
-
*
|
|
4176
|
-
* // Clone with anonymization
|
|
4177
|
-
* await migrator.cloneTenant('production', 'dev', {
|
|
4178
|
-
* includeData: true,
|
|
4179
|
-
* anonymize: {
|
|
4180
|
-
* enabled: true,
|
|
4181
|
-
* rules: {
|
|
4182
|
-
* users: { email: null, phone: null },
|
|
4183
|
-
* },
|
|
4184
|
-
* },
|
|
4185
|
-
* });
|
|
4186
|
-
* ```
|
|
4187
|
-
*/
|
|
4188
|
-
async cloneTenant(sourceTenantId, targetTenantId, options = {}) {
|
|
4189
|
-
return this.cloner.cloneTenant(sourceTenantId, targetTenantId, options);
|
|
4190
|
-
}
|
|
4191
|
-
/**
|
|
4192
|
-
* Mark migrations as applied without executing SQL
|
|
4193
|
-
* Useful for syncing tracking state with already-applied migrations
|
|
4194
|
-
*
|
|
4195
|
-
* Delegates to MigrationExecutor for single tenant operations.
|
|
4196
|
-
*/
|
|
4197
|
-
async markAsApplied(tenantId, options = {}) {
|
|
4198
|
-
return this.migrationExecutor.markAsApplied(tenantId, options);
|
|
4199
|
-
}
|
|
4200
|
-
/**
|
|
4201
|
-
* Mark migrations as applied for all tenants without executing SQL
|
|
4202
|
-
* Useful for syncing tracking state with already-applied migrations
|
|
4203
|
-
*
|
|
4204
|
-
* Delegates to BatchExecutor for parallel operations.
|
|
4205
|
-
*/
|
|
4206
|
-
async markAllAsApplied(options = {}) {
|
|
4207
|
-
return this.batchExecutor.markAllAsApplied(options);
|
|
4208
|
-
}
|
|
4209
|
-
// ============================================================================
|
|
4210
|
-
// Sync Methods (delegated to SyncManager)
|
|
4211
|
-
// ============================================================================
|
|
4212
|
-
/**
|
|
4213
|
-
* Get sync status for all tenants
|
|
4214
|
-
* Detects divergences between migrations on disk and tracking in database
|
|
4215
|
-
*/
|
|
4216
|
-
async getSyncStatus() {
|
|
4217
|
-
return this.syncManager.getSyncStatus();
|
|
4218
|
-
}
|
|
4219
|
-
/**
|
|
4220
|
-
* Get sync status for a specific tenant
|
|
4221
|
-
*/
|
|
4222
|
-
async getTenantSyncStatus(tenantId, migrations) {
|
|
4223
|
-
return this.syncManager.getTenantSyncStatus(tenantId, migrations);
|
|
4224
|
-
}
|
|
4225
|
-
/**
|
|
4226
|
-
* Mark missing migrations as applied for a tenant
|
|
4227
|
-
*/
|
|
4228
|
-
async markMissing(tenantId) {
|
|
4229
|
-
return this.syncManager.markMissing(tenantId);
|
|
4230
|
-
}
|
|
4231
|
-
/**
|
|
4232
|
-
* Mark missing migrations as applied for all tenants
|
|
4233
|
-
*/
|
|
4234
|
-
async markAllMissing(options = {}) {
|
|
4235
|
-
return this.syncManager.markAllMissing(options);
|
|
4236
|
-
}
|
|
4237
|
-
/**
|
|
4238
|
-
* Remove orphan migration records for a tenant
|
|
4239
|
-
*/
|
|
4240
|
-
async cleanOrphans(tenantId) {
|
|
4241
|
-
return this.syncManager.cleanOrphans(tenantId);
|
|
4242
|
-
}
|
|
4243
|
-
/**
|
|
4244
|
-
* Remove orphan migration records for all tenants
|
|
4245
|
-
*/
|
|
4246
|
-
async cleanAllOrphans(options = {}) {
|
|
4247
|
-
return this.syncManager.cleanAllOrphans(options);
|
|
4248
|
-
}
|
|
4249
|
-
// ============================================================================
|
|
4250
|
-
// Seeding Methods (delegated to Seeder)
|
|
4251
|
-
// ============================================================================
|
|
4252
|
-
/**
|
|
4253
|
-
* Seed a single tenant with initial data
|
|
4254
|
-
*
|
|
4255
|
-
* @example
|
|
4256
|
-
* ```typescript
|
|
4257
|
-
* const seed: SeedFunction = async (db, tenantId) => {
|
|
4258
|
-
* await db.insert(roles).values([
|
|
4259
|
-
* { name: 'admin', permissions: ['*'] },
|
|
4260
|
-
* { name: 'user', permissions: ['read'] },
|
|
4261
|
-
* ]);
|
|
4262
|
-
* };
|
|
4263
|
-
*
|
|
4264
|
-
* await migrator.seedTenant('tenant-123', seed);
|
|
4265
|
-
* ```
|
|
4266
|
-
*/
|
|
4267
|
-
async seedTenant(tenantId, seedFn) {
|
|
4268
|
-
return this.seeder.seedTenant(tenantId, seedFn);
|
|
4269
|
-
}
|
|
4270
|
-
/**
|
|
4271
|
-
* Seed all tenants with initial data in parallel
|
|
4272
|
-
*
|
|
4273
|
-
* @example
|
|
4274
|
-
* ```typescript
|
|
4275
|
-
* const seed: SeedFunction = async (db, tenantId) => {
|
|
4276
|
-
* await db.insert(roles).values([
|
|
4277
|
-
* { name: 'admin', permissions: ['*'] },
|
|
4278
|
-
* ]);
|
|
4279
|
-
* };
|
|
4280
|
-
*
|
|
4281
|
-
* await migrator.seedAll(seed, { concurrency: 10 });
|
|
4282
|
-
* ```
|
|
4283
|
-
*/
|
|
4284
|
-
async seedAll(seedFn, options = {}) {
|
|
4285
|
-
return this.seeder.seedAll(seedFn, options);
|
|
4286
|
-
}
|
|
4287
|
-
/**
|
|
4288
|
-
* Seed specific tenants with initial data
|
|
4289
|
-
*/
|
|
4290
|
-
async seedTenants(tenantIds, seedFn, options = {}) {
|
|
4291
|
-
return this.seeder.seedTenants(tenantIds, seedFn, options);
|
|
4292
|
-
}
|
|
4293
|
-
/**
|
|
4294
|
-
* Load migration files from the migrations folder
|
|
4295
|
-
*/
|
|
4296
|
-
async loadMigrations() {
|
|
4297
|
-
const files = await readdir(this.migratorConfig.migrationsFolder);
|
|
4298
|
-
const migrations = [];
|
|
4299
|
-
for (const file of files) {
|
|
4300
|
-
if (!file.endsWith(".sql")) continue;
|
|
4301
|
-
const filePath = join(this.migratorConfig.migrationsFolder, file);
|
|
4302
|
-
const content = await readFile(filePath, "utf-8");
|
|
4303
|
-
const match = file.match(/^(\d+)_/);
|
|
4304
|
-
const timestamp = match?.[1] ? parseInt(match[1], 10) : 0;
|
|
4305
|
-
const hash = createHash("sha256").update(content).digest("hex");
|
|
4306
|
-
migrations.push({
|
|
4307
|
-
name: basename(file, ".sql"),
|
|
4308
|
-
path: filePath,
|
|
4309
|
-
sql: content,
|
|
4310
|
-
timestamp,
|
|
4311
|
-
hash
|
|
4312
|
-
});
|
|
4313
|
-
}
|
|
4314
|
-
return migrations.sort((a, b) => a.timestamp - b.timestamp);
|
|
4315
|
-
}
|
|
4316
|
-
/**
|
|
4317
|
-
* Get or detect the format for a schema
|
|
4318
|
-
* Returns the configured format or auto-detects from existing table
|
|
4319
|
-
*
|
|
4320
|
-
* Note: This method is shared with SyncManager and MigrationExecutor via dependency injection.
|
|
4321
|
-
*/
|
|
4322
|
-
async getOrDetectFormat(pool, schemaName) {
|
|
4323
|
-
const configuredFormat = this.migratorConfig.tableFormat ?? "auto";
|
|
4324
|
-
if (configuredFormat !== "auto") {
|
|
4325
|
-
return getFormatConfig(configuredFormat, this.migrationsTable);
|
|
4326
|
-
}
|
|
4327
|
-
const detected = await detectTableFormat(pool, schemaName, this.migrationsTable);
|
|
4328
|
-
if (detected) {
|
|
4329
|
-
return detected;
|
|
4330
|
-
}
|
|
4331
|
-
const defaultFormat = this.migratorConfig.defaultFormat ?? "name";
|
|
4332
|
-
return getFormatConfig(defaultFormat, this.migrationsTable);
|
|
4333
|
-
}
|
|
4334
|
-
// ============================================================================
|
|
4335
|
-
// Schema Drift Detection Methods (delegated to DriftDetector)
|
|
4336
|
-
// ============================================================================
|
|
4337
|
-
/**
|
|
4338
|
-
* Detect schema drift across all tenants
|
|
4339
|
-
* Compares each tenant's schema against a reference tenant (first tenant by default)
|
|
4340
|
-
*
|
|
4341
|
-
* @example
|
|
4342
|
-
* ```typescript
|
|
4343
|
-
* const drift = await migrator.getSchemaDrift();
|
|
4344
|
-
* if (drift.withDrift > 0) {
|
|
4345
|
-
* console.log('Schema drift detected!');
|
|
4346
|
-
* for (const tenant of drift.details) {
|
|
4347
|
-
* if (tenant.hasDrift) {
|
|
4348
|
-
* console.log(`Tenant ${tenant.tenantId} has drift:`);
|
|
4349
|
-
* for (const table of tenant.tables) {
|
|
4350
|
-
* for (const col of table.columns) {
|
|
4351
|
-
* console.log(` - ${table.table}.${col.column}: ${col.description}`);
|
|
4352
|
-
* }
|
|
4353
|
-
* }
|
|
4354
|
-
* }
|
|
4355
|
-
* }
|
|
4356
|
-
* }
|
|
4357
|
-
* ```
|
|
4358
|
-
*/
|
|
4359
|
-
async getSchemaDrift(options = {}) {
|
|
4360
|
-
return this.driftDetector.detectDrift(options);
|
|
4361
|
-
}
|
|
4362
|
-
/**
|
|
4363
|
-
* Get schema drift for a specific tenant compared to a reference
|
|
4364
|
-
*/
|
|
4365
|
-
async getTenantSchemaDrift(tenantId, referenceTenantId, options = {}) {
|
|
4366
|
-
return this.driftDetector.compareTenant(tenantId, referenceTenantId, options);
|
|
4367
|
-
}
|
|
4368
|
-
/**
|
|
4369
|
-
* Introspect the schema of a tenant
|
|
4370
|
-
*/
|
|
4371
|
-
async introspectTenantSchema(tenantId, options = {}) {
|
|
4372
|
-
return this.driftDetector.introspectSchema(tenantId, options);
|
|
4373
|
-
}
|
|
4374
|
-
};
|
|
4375
|
-
function createMigrator(tenantConfig, migratorConfig) {
|
|
4376
|
-
return new Migrator(tenantConfig, migratorConfig);
|
|
4377
|
-
}
|
|
4378
|
-
var CrossSchemaQueryBuilder = class {
|
|
4379
|
-
constructor(context) {
|
|
4380
|
-
this.context = context;
|
|
4381
|
-
}
|
|
4382
|
-
fromTable = null;
|
|
4383
|
-
joins = [];
|
|
4384
|
-
selectFields = {};
|
|
4385
|
-
whereCondition = null;
|
|
4386
|
-
orderByFields = [];
|
|
4387
|
-
limitValue = null;
|
|
4388
|
-
offsetValue = null;
|
|
4389
|
-
/**
|
|
4390
|
-
* Set the main table to query from
|
|
4391
|
-
*/
|
|
4392
|
-
from(source, table) {
|
|
4393
|
-
const schemaName = this.getSchemaName(source);
|
|
4394
|
-
this.fromTable = { table, source, schemaName };
|
|
4395
|
-
return this;
|
|
4396
|
-
}
|
|
4397
|
-
/**
|
|
4398
|
-
* Add an inner join
|
|
4399
|
-
*/
|
|
4400
|
-
innerJoin(source, table, condition) {
|
|
4401
|
-
return this.addJoin(source, table, condition, "inner");
|
|
4402
|
-
}
|
|
4403
|
-
/**
|
|
4404
|
-
* Add a left join
|
|
4405
|
-
*/
|
|
4406
|
-
leftJoin(source, table, condition) {
|
|
4407
|
-
return this.addJoin(source, table, condition, "left");
|
|
4408
|
-
}
|
|
4409
|
-
/**
|
|
4410
|
-
* Add a right join
|
|
4411
|
-
*/
|
|
4412
|
-
rightJoin(source, table, condition) {
|
|
4413
|
-
return this.addJoin(source, table, condition, "right");
|
|
4414
|
-
}
|
|
4415
|
-
/**
|
|
4416
|
-
* Add a full outer join
|
|
4417
|
-
*/
|
|
4418
|
-
fullJoin(source, table, condition) {
|
|
4419
|
-
return this.addJoin(source, table, condition, "full");
|
|
4420
|
-
}
|
|
4421
|
-
/**
|
|
4422
|
-
* Select specific fields
|
|
4423
|
-
*/
|
|
4424
|
-
select(fields) {
|
|
4425
|
-
this.selectFields = fields;
|
|
4426
|
-
return this;
|
|
4427
|
-
}
|
|
4428
|
-
/**
|
|
4429
|
-
* Add a where condition
|
|
4430
|
-
*/
|
|
4431
|
-
where(condition) {
|
|
4432
|
-
this.whereCondition = condition;
|
|
4433
|
-
return this;
|
|
4434
|
-
}
|
|
4435
|
-
/**
|
|
4436
|
-
* Add order by
|
|
4437
|
-
*/
|
|
4438
|
-
orderBy(...fields) {
|
|
4439
|
-
this.orderByFields = fields;
|
|
4440
|
-
return this;
|
|
4441
|
-
}
|
|
4442
|
-
/**
|
|
4443
|
-
* Set limit
|
|
4444
|
-
*/
|
|
4445
|
-
limit(value) {
|
|
4446
|
-
this.limitValue = value;
|
|
4447
|
-
return this;
|
|
4448
|
-
}
|
|
4449
|
-
/**
|
|
4450
|
-
* Set offset
|
|
4451
|
-
*/
|
|
4452
|
-
offset(value) {
|
|
4453
|
-
this.offsetValue = value;
|
|
4454
|
-
return this;
|
|
4455
|
-
}
|
|
4456
|
-
/**
|
|
4457
|
-
* Execute the query and return typed results
|
|
4458
|
-
*/
|
|
4459
|
-
async execute() {
|
|
4460
|
-
if (!this.fromTable) {
|
|
4461
|
-
throw new Error("[drizzle-multitenant] No table specified. Use .from() first.");
|
|
4462
|
-
}
|
|
4463
|
-
const sqlQuery = this.buildSql();
|
|
4464
|
-
const result = await this.context.tenantDb.execute(sqlQuery);
|
|
4465
|
-
return result.rows;
|
|
4466
|
-
}
|
|
4467
|
-
/**
|
|
4468
|
-
* Build the SQL query
|
|
4469
|
-
*/
|
|
4470
|
-
buildSql() {
|
|
4471
|
-
if (!this.fromTable) {
|
|
4472
|
-
throw new Error("[drizzle-multitenant] No table specified");
|
|
4473
|
-
}
|
|
4474
|
-
const parts = [];
|
|
4475
|
-
const selectParts = Object.entries(this.selectFields).map(([alias, column]) => {
|
|
4476
|
-
const columnName = column.name;
|
|
4477
|
-
return sql`${sql.raw(`"${columnName}"`)} as ${sql.raw(`"${alias}"`)}`;
|
|
4478
|
-
});
|
|
4479
|
-
if (selectParts.length === 0) {
|
|
4480
|
-
parts.push(sql`SELECT *`);
|
|
4481
|
-
} else {
|
|
4482
|
-
parts.push(sql`SELECT ${sql.join(selectParts, sql`, `)}`);
|
|
4483
|
-
}
|
|
4484
|
-
const fromTableRef = this.getFullTableName(this.fromTable.schemaName, this.fromTable.table);
|
|
4485
|
-
parts.push(sql` FROM ${sql.raw(fromTableRef)}`);
|
|
4486
|
-
for (const join2 of this.joins) {
|
|
4487
|
-
const joinTableRef = this.getFullTableName(join2.schemaName, join2.table);
|
|
4488
|
-
const joinType = this.getJoinKeyword(join2.type);
|
|
4489
|
-
parts.push(sql` ${sql.raw(joinType)} ${sql.raw(joinTableRef)} ON ${join2.condition}`);
|
|
4490
|
-
}
|
|
4491
|
-
if (this.whereCondition) {
|
|
4492
|
-
parts.push(sql` WHERE ${this.whereCondition}`);
|
|
4493
|
-
}
|
|
4494
|
-
if (this.orderByFields.length > 0) {
|
|
4495
|
-
parts.push(sql` ORDER BY ${sql.join(this.orderByFields, sql`, `)}`);
|
|
4496
|
-
}
|
|
4497
|
-
if (this.limitValue !== null) {
|
|
4498
|
-
parts.push(sql` LIMIT ${sql.raw(this.limitValue.toString())}`);
|
|
4499
|
-
}
|
|
4500
|
-
if (this.offsetValue !== null) {
|
|
4501
|
-
parts.push(sql` OFFSET ${sql.raw(this.offsetValue.toString())}`);
|
|
4502
|
-
}
|
|
4503
|
-
return sql.join(parts, sql``);
|
|
4504
|
-
}
|
|
4505
|
-
/**
|
|
4506
|
-
* Add a join to the query
|
|
4507
|
-
*/
|
|
4508
|
-
addJoin(source, table, condition, type) {
|
|
4509
|
-
const schemaName = this.getSchemaName(source);
|
|
4510
|
-
this.joins.push({ table, source, schemaName, condition, type });
|
|
4511
|
-
return this;
|
|
4512
|
-
}
|
|
4513
|
-
/**
|
|
4514
|
-
* Get schema name for a source
|
|
4515
|
-
*/
|
|
4516
|
-
getSchemaName(source) {
|
|
4517
|
-
if (source === "tenant") {
|
|
4518
|
-
return this.context.tenantSchema ?? "tenant";
|
|
4519
|
-
}
|
|
4520
|
-
return this.context.sharedSchema ?? "public";
|
|
4521
|
-
}
|
|
4522
|
-
/**
|
|
4523
|
-
* Get fully qualified table name
|
|
4524
|
-
*/
|
|
4525
|
-
getFullTableName(schemaName, table) {
|
|
4526
|
-
const tableName = getTableName(table);
|
|
4527
|
-
return `"${schemaName}"."${tableName}"`;
|
|
4528
|
-
}
|
|
4529
|
-
/**
|
|
4530
|
-
* Get SQL keyword for join type
|
|
4531
|
-
*/
|
|
4532
|
-
getJoinKeyword(type) {
|
|
4533
|
-
switch (type) {
|
|
4534
|
-
case "inner":
|
|
4535
|
-
return "INNER JOIN";
|
|
4536
|
-
case "left":
|
|
4537
|
-
return "LEFT JOIN";
|
|
4538
|
-
case "right":
|
|
4539
|
-
return "RIGHT JOIN";
|
|
4540
|
-
case "full":
|
|
4541
|
-
return "FULL OUTER JOIN";
|
|
4542
|
-
}
|
|
4543
|
-
}
|
|
4544
|
-
};
|
|
4545
|
-
function createCrossSchemaQuery(context) {
|
|
4546
|
-
return new CrossSchemaQueryBuilder(context);
|
|
4547
|
-
}
|
|
4548
|
-
async function withSharedLookup(config) {
|
|
4549
|
-
const {
|
|
4550
|
-
tenantDb,
|
|
4551
|
-
tenantTable,
|
|
4552
|
-
sharedTable,
|
|
4553
|
-
foreignKey,
|
|
4554
|
-
sharedKey = "id",
|
|
4555
|
-
sharedFields,
|
|
4556
|
-
where: whereCondition
|
|
4557
|
-
} = config;
|
|
4558
|
-
const tenantTableName = getTableName(tenantTable);
|
|
4559
|
-
const sharedTableName = getTableName(sharedTable);
|
|
4560
|
-
const sharedFieldList = sharedFields.map((field) => `s."${String(field)}"`).join(", ");
|
|
4561
|
-
const queryParts = [
|
|
4562
|
-
`SELECT t.*, ${sharedFieldList}`,
|
|
4563
|
-
`FROM "${tenantTableName}" t`,
|
|
4564
|
-
`LEFT JOIN "public"."${sharedTableName}" s ON t."${String(foreignKey)}" = s."${String(sharedKey)}"`
|
|
4565
|
-
];
|
|
4566
|
-
if (whereCondition) {
|
|
4567
|
-
queryParts.push("WHERE");
|
|
4568
|
-
}
|
|
4569
|
-
const sqlQuery = sql.raw(queryParts.join(" "));
|
|
4570
|
-
const result = await tenantDb.execute(sqlQuery);
|
|
4571
|
-
return result.rows;
|
|
4572
|
-
}
|
|
4573
|
-
async function crossSchemaRaw(db, options) {
|
|
4574
|
-
const { tenantSchema, sharedSchema, sql: rawSql } = options;
|
|
4575
|
-
const processedSql = rawSql.replace(/\$tenant\./g, `"${tenantSchema}".`).replace(/\$shared\./g, `"${sharedSchema}".`);
|
|
4576
|
-
const query = sql.raw(processedSql);
|
|
4577
|
-
const result = await db.execute(query);
|
|
4578
|
-
return result.rows;
|
|
4579
|
-
}
|
|
4580
|
-
function buildCrossSchemaSelect(fields, tenantSchema, _sharedSchema) {
|
|
4581
|
-
const columns = Object.entries(fields).map(([alias, column]) => {
|
|
4582
|
-
const columnName = column.name;
|
|
4583
|
-
return `"${columnName}" as "${alias}"`;
|
|
4584
|
-
});
|
|
4585
|
-
const getSchema = () => {
|
|
4586
|
-
return tenantSchema;
|
|
4587
|
-
};
|
|
4588
|
-
return { columns, getSchema };
|
|
4589
|
-
}
|
|
4590
|
-
function extractTablesFromSchema(schema) {
|
|
4591
|
-
const tables = /* @__PURE__ */ new Set();
|
|
4592
|
-
for (const value of Object.values(schema)) {
|
|
4593
|
-
if (value && typeof value === "object" && "_" in value) {
|
|
4594
|
-
const branded = value;
|
|
4595
|
-
if (branded._?.brand === "Table") {
|
|
4596
|
-
tables.add(value);
|
|
4597
|
-
}
|
|
4598
|
-
}
|
|
4599
|
-
}
|
|
4600
|
-
return tables;
|
|
4601
|
-
}
|
|
4602
|
-
function isSharedTable(table, sharedTables) {
|
|
4603
|
-
return sharedTables.has(table);
|
|
4604
|
-
}
|
|
4605
|
-
var WithSharedQueryBuilder = class {
|
|
4606
|
-
constructor(tenantDb, sharedTables, tenantSchemaName, sharedSchemaName = "public") {
|
|
4607
|
-
this.tenantDb = tenantDb;
|
|
4608
|
-
this.sharedTables = sharedTables;
|
|
4609
|
-
this.tenantSchemaName = tenantSchemaName;
|
|
4610
|
-
this.sharedSchemaName = sharedSchemaName;
|
|
4611
|
-
}
|
|
4612
|
-
fromTable = null;
|
|
4613
|
-
joins = [];
|
|
4614
|
-
selectFields = {};
|
|
4615
|
-
whereCondition = null;
|
|
4616
|
-
orderByFields = [];
|
|
4617
|
-
limitValue = null;
|
|
4618
|
-
offsetValue = null;
|
|
4619
|
-
/**
|
|
4620
|
-
* Set the main table to query from
|
|
4621
|
-
* Automatically detects if it's a tenant or shared table
|
|
4622
|
-
*/
|
|
4623
|
-
from(table) {
|
|
4624
|
-
const isShared = isSharedTable(table, this.sharedTables);
|
|
4625
|
-
this.fromTable = {
|
|
4626
|
-
table,
|
|
4627
|
-
isShared,
|
|
4628
|
-
schemaName: isShared ? this.sharedSchemaName : this.tenantSchemaName
|
|
4629
|
-
};
|
|
4630
|
-
return this;
|
|
4631
|
-
}
|
|
4632
|
-
/**
|
|
4633
|
-
* Add a left join with automatic schema detection
|
|
4634
|
-
*/
|
|
4635
|
-
leftJoin(table, condition) {
|
|
4636
|
-
return this.addJoin(table, condition, "left");
|
|
4637
|
-
}
|
|
4638
|
-
/**
|
|
4639
|
-
* Add an inner join with automatic schema detection
|
|
4640
|
-
*/
|
|
4641
|
-
innerJoin(table, condition) {
|
|
4642
|
-
return this.addJoin(table, condition, "inner");
|
|
4643
|
-
}
|
|
4644
|
-
/**
|
|
4645
|
-
* Add a right join with automatic schema detection
|
|
4646
|
-
*/
|
|
4647
|
-
rightJoin(table, condition) {
|
|
4648
|
-
return this.addJoin(table, condition, "right");
|
|
4649
|
-
}
|
|
4650
|
-
/**
|
|
4651
|
-
* Add a full outer join with automatic schema detection
|
|
4652
|
-
*/
|
|
4653
|
-
fullJoin(table, condition) {
|
|
4654
|
-
return this.addJoin(table, condition, "full");
|
|
4655
|
-
}
|
|
4656
|
-
/**
|
|
4657
|
-
* Select specific fields
|
|
4658
|
-
*/
|
|
4659
|
-
select(fields) {
|
|
4660
|
-
this.selectFields = fields;
|
|
4661
|
-
return this;
|
|
4662
|
-
}
|
|
4663
|
-
/**
|
|
4664
|
-
* Add a WHERE condition
|
|
4665
|
-
*/
|
|
4666
|
-
where(condition) {
|
|
4667
|
-
this.whereCondition = condition;
|
|
4668
|
-
return this;
|
|
4669
|
-
}
|
|
4670
|
-
/**
|
|
4671
|
-
* Add ORDER BY
|
|
4672
|
-
*/
|
|
4673
|
-
orderBy(...fields) {
|
|
4674
|
-
this.orderByFields = fields;
|
|
4675
|
-
return this;
|
|
4676
|
-
}
|
|
4677
|
-
/**
|
|
4678
|
-
* Set LIMIT
|
|
4679
|
-
*/
|
|
4680
|
-
limit(value) {
|
|
4681
|
-
this.limitValue = value;
|
|
4682
|
-
return this;
|
|
4683
|
-
}
|
|
4684
|
-
/**
|
|
4685
|
-
* Set OFFSET
|
|
4686
|
-
*/
|
|
4687
|
-
offset(value) {
|
|
4688
|
-
this.offsetValue = value;
|
|
4689
|
-
return this;
|
|
4690
|
-
}
|
|
4691
|
-
/**
|
|
4692
|
-
* Execute the query and return typed results
|
|
4693
|
-
*/
|
|
4694
|
-
async execute() {
|
|
4695
|
-
if (!this.fromTable) {
|
|
4696
|
-
throw new Error("[drizzle-multitenant] No table specified. Use .from() first.");
|
|
4697
|
-
}
|
|
4698
|
-
const sqlQuery = this.buildSql();
|
|
4699
|
-
const result = await this.tenantDb.execute(sqlQuery);
|
|
4700
|
-
return result.rows;
|
|
4701
|
-
}
|
|
4702
|
-
/**
|
|
4703
|
-
* Add a join to the query
|
|
4704
|
-
*/
|
|
4705
|
-
addJoin(table, condition, type) {
|
|
4706
|
-
const isShared = isSharedTable(table, this.sharedTables);
|
|
4707
|
-
this.joins.push({
|
|
4708
|
-
table,
|
|
4709
|
-
isShared,
|
|
4710
|
-
schemaName: isShared ? this.sharedSchemaName : this.tenantSchemaName,
|
|
4711
|
-
condition,
|
|
4712
|
-
type
|
|
4713
|
-
});
|
|
4714
|
-
return this;
|
|
4715
|
-
}
|
|
4716
|
-
/**
|
|
4717
|
-
* Build the SQL query
|
|
4718
|
-
*/
|
|
4719
|
-
buildSql() {
|
|
4720
|
-
if (!this.fromTable) {
|
|
4721
|
-
throw new Error("[drizzle-multitenant] No table specified");
|
|
4722
|
-
}
|
|
4723
|
-
const parts = [];
|
|
4724
|
-
const selectParts = Object.entries(this.selectFields).map(([alias, column]) => {
|
|
4725
|
-
const columnName = column.name;
|
|
4726
|
-
const tableName = this.getTableAliasForColumn(column);
|
|
4727
|
-
if (tableName) {
|
|
4728
|
-
return sql`${sql.raw(`"${tableName}"."${columnName}"`)} as ${sql.raw(`"${alias}"`)}`;
|
|
4729
|
-
}
|
|
4730
|
-
return sql`${sql.raw(`"${columnName}"`)} as ${sql.raw(`"${alias}"`)}`;
|
|
4731
|
-
});
|
|
4732
|
-
if (selectParts.length === 0) {
|
|
4733
|
-
parts.push(sql`SELECT *`);
|
|
4734
|
-
} else {
|
|
4735
|
-
parts.push(sql`SELECT ${sql.join(selectParts, sql`, `)}`);
|
|
4736
|
-
}
|
|
4737
|
-
const fromTableName = getTableName(this.fromTable.table);
|
|
4738
|
-
const fromTableRef = `"${this.fromTable.schemaName}"."${fromTableName}"`;
|
|
4739
|
-
parts.push(sql` FROM ${sql.raw(fromTableRef)} "${sql.raw(fromTableName)}"`);
|
|
4740
|
-
for (const join2 of this.joins) {
|
|
4741
|
-
const joinTableName = getTableName(join2.table);
|
|
4742
|
-
const joinTableRef = `"${join2.schemaName}"."${joinTableName}"`;
|
|
4743
|
-
const joinKeyword = this.getJoinKeyword(join2.type);
|
|
4744
|
-
parts.push(
|
|
4745
|
-
sql` ${sql.raw(joinKeyword)} ${sql.raw(joinTableRef)} "${sql.raw(joinTableName)}" ON ${join2.condition}`
|
|
4746
|
-
);
|
|
4747
|
-
}
|
|
4748
|
-
if (this.whereCondition) {
|
|
4749
|
-
parts.push(sql` WHERE ${this.whereCondition}`);
|
|
4750
|
-
}
|
|
4751
|
-
if (this.orderByFields.length > 0) {
|
|
4752
|
-
parts.push(sql` ORDER BY ${sql.join(this.orderByFields, sql`, `)}`);
|
|
4753
|
-
}
|
|
4754
|
-
if (this.limitValue !== null) {
|
|
4755
|
-
parts.push(sql` LIMIT ${sql.raw(this.limitValue.toString())}`);
|
|
4756
|
-
}
|
|
4757
|
-
if (this.offsetValue !== null) {
|
|
4758
|
-
parts.push(sql` OFFSET ${sql.raw(this.offsetValue.toString())}`);
|
|
4759
|
-
}
|
|
4760
|
-
return sql.join(parts, sql``);
|
|
4761
|
-
}
|
|
4762
|
-
/**
|
|
4763
|
-
* Get table alias for a column (used in SELECT)
|
|
4764
|
-
*/
|
|
4765
|
-
getTableAliasForColumn(column) {
|
|
4766
|
-
const columnTable = column.table;
|
|
4767
|
-
if (columnTable) {
|
|
4768
|
-
return getTableName(columnTable);
|
|
4769
|
-
}
|
|
4770
|
-
return null;
|
|
4771
|
-
}
|
|
4772
|
-
/**
|
|
4773
|
-
* Get SQL keyword for join type
|
|
4774
|
-
*/
|
|
4775
|
-
getJoinKeyword(type) {
|
|
4776
|
-
switch (type) {
|
|
4777
|
-
case "inner":
|
|
4778
|
-
return "INNER JOIN";
|
|
4779
|
-
case "left":
|
|
4780
|
-
return "LEFT JOIN";
|
|
4781
|
-
case "right":
|
|
4782
|
-
return "RIGHT JOIN";
|
|
4783
|
-
case "full":
|
|
4784
|
-
return "FULL OUTER JOIN";
|
|
4785
|
-
}
|
|
4786
|
-
}
|
|
4787
|
-
};
|
|
4788
|
-
function withShared(tenantDb, _sharedDb, schemas, options) {
|
|
4789
|
-
const sharedTables = extractTablesFromSchema(schemas.shared);
|
|
4790
|
-
return new WithSharedQueryBuilder(
|
|
4791
|
-
tenantDb,
|
|
4792
|
-
sharedTables,
|
|
4793
|
-
options?.tenantSchema ?? "tenant",
|
|
4794
|
-
options?.sharedSchema ?? "public"
|
|
4795
|
-
);
|
|
4796
|
-
}
|
|
4797
|
-
|
|
4798
|
-
// src/retry.ts
|
|
4799
|
-
function isRetryableError2(error) {
|
|
4800
|
-
const message = error.message.toLowerCase();
|
|
4801
|
-
if (message.includes("econnrefused") || message.includes("econnreset") || message.includes("etimedout") || message.includes("enotfound") || message.includes("connection refused") || message.includes("connection reset") || message.includes("connection terminated") || message.includes("connection timed out") || message.includes("timeout expired") || message.includes("socket hang up")) {
|
|
4802
|
-
return true;
|
|
4803
|
-
}
|
|
4804
|
-
if (message.includes("too many connections") || message.includes("sorry, too many clients") || message.includes("the database system is starting up") || message.includes("the database system is shutting down") || message.includes("server closed the connection unexpectedly") || message.includes("could not connect to server")) {
|
|
4805
|
-
return true;
|
|
4806
|
-
}
|
|
4807
|
-
if (message.includes("ssl connection") || message.includes("ssl handshake")) {
|
|
4808
|
-
return true;
|
|
4809
|
-
}
|
|
4810
|
-
return false;
|
|
4811
|
-
}
|
|
4812
|
-
function calculateDelay(attempt, config) {
|
|
4813
|
-
const exponentialDelay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt);
|
|
4814
|
-
const cappedDelay = Math.min(exponentialDelay, config.maxDelayMs);
|
|
4815
|
-
if (config.jitter) {
|
|
4816
|
-
const jitterFactor = 1 + Math.random() * 0.25;
|
|
4817
|
-
return Math.floor(cappedDelay * jitterFactor);
|
|
4818
|
-
}
|
|
4819
|
-
return Math.floor(cappedDelay);
|
|
4820
|
-
}
|
|
4821
|
-
function sleep2(ms) {
|
|
4822
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4823
|
-
}
|
|
4824
|
-
async function withRetry(operation, config) {
|
|
4825
|
-
const retryConfig = {
|
|
4826
|
-
maxAttempts: config?.maxAttempts ?? DEFAULT_CONFIG.retry.maxAttempts,
|
|
4827
|
-
initialDelayMs: config?.initialDelayMs ?? DEFAULT_CONFIG.retry.initialDelayMs,
|
|
4828
|
-
maxDelayMs: config?.maxDelayMs ?? DEFAULT_CONFIG.retry.maxDelayMs,
|
|
4829
|
-
backoffMultiplier: config?.backoffMultiplier ?? DEFAULT_CONFIG.retry.backoffMultiplier,
|
|
4830
|
-
jitter: config?.jitter ?? DEFAULT_CONFIG.retry.jitter,
|
|
4831
|
-
isRetryable: config?.isRetryable ?? isRetryableError2,
|
|
4832
|
-
onRetry: config?.onRetry
|
|
4833
|
-
};
|
|
4834
|
-
const startTime = Date.now();
|
|
4835
|
-
let lastError = null;
|
|
4836
|
-
for (let attempt = 0; attempt < retryConfig.maxAttempts; attempt++) {
|
|
4837
|
-
try {
|
|
4838
|
-
const result = await operation();
|
|
4839
|
-
return {
|
|
4840
|
-
result,
|
|
4841
|
-
attempts: attempt + 1,
|
|
4842
|
-
totalTimeMs: Date.now() - startTime
|
|
4843
|
-
};
|
|
4844
|
-
} catch (error) {
|
|
4845
|
-
lastError = error;
|
|
4846
|
-
const isLastAttempt = attempt >= retryConfig.maxAttempts - 1;
|
|
4847
|
-
if (isLastAttempt || !retryConfig.isRetryable(lastError)) {
|
|
4848
|
-
throw lastError;
|
|
4849
|
-
}
|
|
4850
|
-
const delay = calculateDelay(attempt, retryConfig);
|
|
4851
|
-
retryConfig.onRetry?.(attempt + 1, lastError, delay);
|
|
4852
|
-
await sleep2(delay);
|
|
4853
|
-
}
|
|
4854
|
-
}
|
|
4855
|
-
throw lastError ?? new Error("Retry failed with no error");
|
|
4856
|
-
}
|
|
4857
|
-
function createRetrier(config) {
|
|
4858
|
-
return (operation) => {
|
|
4859
|
-
return withRetry(operation, config);
|
|
4860
|
-
};
|
|
4861
|
-
}
|
|
4862
|
-
|
|
4863
|
-
export { CrossSchemaQueryBuilder, DEFAULT_CONFIG, Migrator, WithSharedQueryBuilder, buildCrossSchemaSelect, calculateDelay, createCrossSchemaQuery, createMigrator, createRetrier, createTenantContext, createTenantManager, crossSchemaRaw, defineConfig, isRetryableError2 as isRetryableError, withRetry, withShared, withSharedLookup };
|
|
4864
|
-
//# sourceMappingURL=index.js.map
|
|
4865
|
-
//# sourceMappingURL=index.js.map
|
|
152
|
+
AND tc.table_name != ccu.table_name`,[e]),a=new Map,r=new Set(t);for(let l of t)a.set(l,new Set);for(let l of n.rows)r.has(l.table_name)&&r.has(l.foreign_table_name)&&a.get(l.table_name).add(l.foreign_table_name);let s=[],i=new Map,o=[];for(let l of t)i.set(l,0);for(let[l,u]of a)for(let h of u)i.set(h,(i.get(h)??0)+1);for(let[l,u]of i)u===0&&o.push(l);for(;o.length>0;){let l=o.shift();s.push(l);for(let[u,h]of a)if(h.has(l)){h.delete(l);let d=(i.get(u)??0)-1;i.set(u,d),d===0&&o.push(u);}}let m=t.filter(l=>!s.includes(l));return [...s,...m]}async function ce(c,e,t,n,a,r){let s=0,i=await ze(c,e,n);await c.query("SET session_replication_role = replica");try{for(let o=0;o<i.length;o++){let m=i[o];r?.("copying_data",{table:m,progress:o+1,total:i.length});let l=await je(c,e,t,m,a);s+=l;}}finally{await c.query("SET session_replication_role = DEFAULT");}return s}var Be="__drizzle_migrations",H=class{constructor(e,t){this.deps=t;this.migrationsTable=e.migrationsTable??Be;}migrationsTable;async cloneTenant(e,t,n={}){let a=Date.now(),{includeData:r=false,anonymize:s,excludeTables:i=[],onProgress:o}=n,m=this.deps.schemaNameTemplate(e),l=this.deps.schemaNameTemplate(t),u=[this.migrationsTable,...i],h=null,d=null;try{if(o?.("starting"),!await this.deps.schemaExists(e))return this.createErrorResult(e,t,l,`Source tenant "${e}" does not exist`,a);if(await this.deps.schemaExists(t))return this.createErrorResult(e,t,l,`Target tenant "${t}" already exists`,a);o?.("introspecting"),h=await this.deps.createPool(m);let y=await ie(h,m,u);if(y.length===0)return o?.("creating_schema"),await this.deps.createSchema(t),o?.("completed"),{sourceTenant:e,targetTenant:t,targetSchema:l,success:!0,tables:[],durationMs:Date.now()-a};let b=await Promise.all(y.map(M=>oe(h,m,l,M)));await h.end(),h=null,o?.("creating_schema"),await this.deps.createSchema(t),d=await this.deps.createRootPool(),o?.("creating_tables");for(let M of b)await d.query(`SET search_path TO "${l}"; ${M.createDdl}`);o?.("creating_constraints");for(let M of b)for(let D of M.constraintDdls.filter(C=>!C.includes("FOREIGN KEY")))try{await d.query(`SET search_path TO "${l}"; ${D}`);}catch{}o?.("creating_indexes");for(let M of b)for(let D of M.indexDdls)try{await d.query(D);}catch{}let S=0;r&&(o?.("copying_data"),S=await ce(d,m,l,y,s?.enabled?s.rules:void 0,o));for(let M of b)for(let D of M.constraintDdls.filter(C=>C.includes("FOREIGN KEY")))try{await d.query(D);}catch{}o?.("completed");let N={sourceTenant:e,targetTenant:t,targetSchema:l,success:!0,tables:y,durationMs:Date.now()-a};return r&&(N.rowsCopied=S),N}catch(g){return n.onError?.(g),o?.("failed"),this.createErrorResult(e,t,l,g.message,a)}finally{h&&await h.end().catch(()=>{}),d&&await d.end().catch(()=>{});}}createErrorResult(e,t,n,a,r){return {sourceTenant:e,targetTenant:t,targetSchema:n,success:false,error:a,tables:[],durationMs:Date.now()-r}}};var We="public",q=class{constructor(e,t){this.config=e;this.deps=t;this.schemaName=e.schemaName??We;}schemaName;async migrate(e={}){let t=Date.now(),n=[],a=await this.deps.createPool();try{e.onProgress?.("starting"),await this.config.hooks?.beforeMigration?.();let r=await this.deps.getOrDetectFormat(a,this.schemaName);await this.deps.ensureMigrationsTable(a,this.schemaName,r);let s=await this.deps.loadMigrations(),i=await this.getAppliedMigrations(a,r),o=new Set(i.map(l=>l.identifier)),m=s.filter(l=>!this.isMigrationApplied(l,o,r));if(e.dryRun)return {schemaName:this.schemaName,success:!0,appliedMigrations:m.map(l=>l.name),durationMs:Date.now()-t,format:r.format};for(let l of m){let u=Date.now();e.onProgress?.("migrating",l.name),await this.applyMigration(a,l,r),await this.config.hooks?.afterMigration?.(l.name,Date.now()-u),n.push(l.name);}return e.onProgress?.("completed"),{schemaName:this.schemaName,success:!0,appliedMigrations:n,durationMs:Date.now()-t,format:r.format}}catch(r){return e.onProgress?.("failed"),{schemaName:this.schemaName,success:false,appliedMigrations:n,error:r.message,durationMs:Date.now()-t}}finally{await a.end();}}async markAsApplied(e={}){let t=Date.now(),n=[],a=await this.deps.createPool();try{e.onProgress?.("starting");let r=await this.deps.getOrDetectFormat(a,this.schemaName);await this.deps.ensureMigrationsTable(a,this.schemaName,r);let s=await this.deps.loadMigrations(),i=await this.getAppliedMigrations(a,r),o=new Set(i.map(l=>l.identifier)),m=s.filter(l=>!this.isMigrationApplied(l,o,r));for(let l of m)e.onProgress?.("migrating",l.name),await this.recordMigration(a,l,r),n.push(l.name);return e.onProgress?.("completed"),{schemaName:this.schemaName,success:!0,appliedMigrations:n,durationMs:Date.now()-t,format:r.format}}catch(r){return e.onProgress?.("failed"),{schemaName:this.schemaName,success:false,appliedMigrations:n,error:r.message,durationMs:Date.now()-t}}finally{await a.end();}}async getStatus(){let e=await this.deps.createPool();try{let t=await this.deps.loadMigrations();if(!await this.deps.migrationsTableExists(e,this.schemaName))return {schemaName:this.schemaName,appliedCount:0,pendingCount:t.length,pendingMigrations:t.map(o=>o.name),status:t.length>0?"behind":"ok",format:null};let a=await this.deps.getOrDetectFormat(e,this.schemaName),r=await this.getAppliedMigrations(e,a),s=new Set(r.map(o=>o.identifier)),i=t.filter(o=>!this.isMigrationApplied(o,s,a));return {schemaName:this.schemaName,appliedCount:r.length,pendingCount:i.length,pendingMigrations:i.map(o=>o.name),status:i.length>0?"behind":"ok",format:a.format}}catch(t){return {schemaName:this.schemaName,appliedCount:0,pendingCount:0,pendingMigrations:[],status:"error",error:t.message,format:null}}finally{await e.end();}}async getAppliedMigrations(e,t){let n=t.columns.identifier,a=t.columns.timestamp;return (await e.query(`SELECT id, "${n}" as identifier, "${a}" as applied_at
|
|
153
|
+
FROM "${this.schemaName}"."${t.tableName}"
|
|
154
|
+
ORDER BY id`)).rows.map(s=>{let i=t.columns.timestampType==="bigint"?new Date(Number(s.applied_at)):new Date(s.applied_at);return {identifier:s.identifier,...t.columns.identifier==="name"?{name:s.identifier}:{hash:s.identifier},appliedAt:i}})}isMigrationApplied(e,t,n){return n.columns.identifier==="name"?t.has(e.name):t.has(e.hash)||t.has(e.name)}async applyMigration(e,t,n){let a=await e.connect();try{await a.query("BEGIN"),await a.query(t.sql);let{identifier:r,timestamp:s,timestampType:i}=n.columns,o=r==="name"?t.name:t.hash,m=i==="bigint"?Date.now():new Date;await a.query(`INSERT INTO "${this.schemaName}"."${n.tableName}" ("${r}", "${s}") VALUES ($1, $2)`,[o,m]),await a.query("COMMIT");}catch(r){throw await a.query("ROLLBACK"),r}finally{a.release();}}async recordMigration(e,t,n){let{identifier:a,timestamp:r,timestampType:s}=n.columns,i=a==="name"?t.name:t.hash,o=s==="bigint"?Date.now():new Date;await e.query(`INSERT INTO "${this.schemaName}"."${n.tableName}" ("${a}", "${r}") VALUES ($1, $2)`,[i,o]);}};var Ue="__drizzle_migrations",ge="__drizzle_shared_migrations",j=class{constructor(e,t){this.migratorConfig=t;if(this.migrationsTable=t.migrationsTable??Ue,this.schemaManager=new O(e,this.migrationsTable),this.driftDetector=new $(e,this.schemaManager,{migrationsTable:this.migrationsTable,tenantDiscovery:t.tenantDiscovery}),this.seeder=new A({tenantDiscovery:t.tenantDiscovery},{createPool:this.schemaManager.createPool.bind(this.schemaManager),schemaNameTemplate:e.isolation.schemaNameTemplate,tenantSchema:e.schemas.tenant}),this.syncManager=new F({tenantDiscovery:t.tenantDiscovery,migrationsFolder:t.migrationsFolder,migrationsTable:this.migrationsTable},{createPool:this.schemaManager.createPool.bind(this.schemaManager),schemaNameTemplate:e.isolation.schemaNameTemplate,migrationsTableExists:this.schemaManager.migrationsTableExists.bind(this.schemaManager),ensureMigrationsTable:this.schemaManager.ensureMigrationsTable.bind(this.schemaManager),getOrDetectFormat:this.getOrDetectFormat.bind(this),loadMigrations:this.loadMigrations.bind(this)}),this.migrationExecutor=new L({hooks:t.hooks},{createPool:this.schemaManager.createPool.bind(this.schemaManager),schemaNameTemplate:e.isolation.schemaNameTemplate,migrationsTableExists:this.schemaManager.migrationsTableExists.bind(this.schemaManager),ensureMigrationsTable:this.schemaManager.ensureMigrationsTable.bind(this.schemaManager),getOrDetectFormat:this.getOrDetectFormat.bind(this),loadMigrations:this.loadMigrations.bind(this)}),this.batchExecutor=new I({tenantDiscovery:t.tenantDiscovery},this.migrationExecutor,this.loadMigrations.bind(this)),this.cloner=new H({migrationsTable:this.migrationsTable},{createPool:this.schemaManager.createPool.bind(this.schemaManager),createRootPool:this.schemaManager.createRootPool.bind(this.schemaManager),schemaNameTemplate:e.isolation.schemaNameTemplate,schemaExists:this.schemaManager.schemaExists.bind(this.schemaManager),createSchema:this.schemaManager.createSchema.bind(this.schemaManager)}),t.sharedMigrationsFolder&&existsSync(t.sharedMigrationsFolder)){let n=t.sharedMigrationsTable??ge,a=t.sharedHooks,r={schemaName:"public",migrationsTable:n};(a?.beforeMigration||a?.afterApply)&&(r.hooks={},a.beforeMigration&&(r.hooks.beforeMigration=a.beforeMigration),a.afterApply&&(r.hooks.afterMigration=a.afterApply)),this.sharedMigrationExecutor=new q(r,{createPool:this.schemaManager.createRootPool.bind(this.schemaManager),migrationsTableExists:this.schemaManager.migrationsTableExists.bind(this.schemaManager),ensureMigrationsTable:this.schemaManager.ensureMigrationsTable.bind(this.schemaManager),getOrDetectFormat:this.getOrDetectSharedFormat.bind(this),loadMigrations:this.loadSharedMigrations.bind(this)});}else this.sharedMigrationExecutor=null;e.schemas.shared?this.sharedSeeder=new v({schemaName:"public"},{createPool:this.schemaManager.createRootPool.bind(this.schemaManager),sharedSchema:e.schemas.shared}):this.sharedSeeder=null;}migrationsTable;schemaManager;driftDetector;seeder;syncManager;migrationExecutor;batchExecutor;cloner;sharedMigrationExecutor;sharedSeeder;async migrateAll(e={}){return this.batchExecutor.migrateAll(e)}async migrateTenant(e,t,n={}){return this.migrationExecutor.migrateTenant(e,t,n)}async migrateTenants(e,t={}){return this.batchExecutor.migrateTenants(e,t)}async getStatus(){return this.batchExecutor.getStatus()}async getTenantStatus(e,t){return this.migrationExecutor.getTenantStatus(e,t)}async createTenant(e,t={}){let{migrate:n=true}=t;await this.schemaManager.createSchema(e),n&&await this.migrateTenant(e);}async dropTenant(e,t={}){await this.schemaManager.dropSchema(e,t);}async tenantExists(e){return this.schemaManager.schemaExists(e)}async cloneTenant(e,t,n={}){return this.cloner.cloneTenant(e,t,n)}async markAsApplied(e,t={}){return this.migrationExecutor.markAsApplied(e,t)}async markAllAsApplied(e={}){return this.batchExecutor.markAllAsApplied(e)}async getSyncStatus(){return this.syncManager.getSyncStatus()}async getTenantSyncStatus(e,t){return this.syncManager.getTenantSyncStatus(e,t)}async markMissing(e){return this.syncManager.markMissing(e)}async markAllMissing(e={}){return this.syncManager.markAllMissing(e)}async cleanOrphans(e){return this.syncManager.cleanOrphans(e)}async cleanAllOrphans(e={}){return this.syncManager.cleanAllOrphans(e)}async seedTenant(e,t){return this.seeder.seedTenant(e,t)}async seedAll(e,t={}){return this.seeder.seedAll(e,t)}async seedTenants(e,t,n={}){return this.seeder.seedTenants(e,t,n)}hasSharedSeeding(){return this.sharedSeeder!==null}async seedShared(e){return this.sharedSeeder?this.sharedSeeder.seed(e):{schemaName:"public",success:false,error:"Shared schema not configured. Set schemas.shared in tenant config.",durationMs:0}}async seedAllWithShared(e,t,n={}){let a=await this.seedShared(e),r=await this.seedAll(t,n);return {shared:a,tenants:r}}async loadMigrations(){let e=await readdir(this.migratorConfig.migrationsFolder),t=[];for(let n of e){if(!n.endsWith(".sql"))continue;let a=join(this.migratorConfig.migrationsFolder,n),r=await readFile(a,"utf-8"),s=n.match(/^(\d+)_/),i=s?.[1]?parseInt(s[1],10):0,o=createHash("sha256").update(r).digest("hex");t.push({name:basename(n,".sql"),path:a,sql:r,timestamp:i,hash:o});}return t.sort((n,a)=>n.timestamp-a.timestamp)}async getOrDetectFormat(e,t){let n=this.migratorConfig.tableFormat??"auto";if(n!=="auto")return _(n,this.migrationsTable);let a=await Y(e,t,this.migrationsTable);if(a)return a;let r=this.migratorConfig.defaultFormat??"name";return _(r,this.migrationsTable)}async loadSharedMigrations(){if(!this.migratorConfig.sharedMigrationsFolder)return [];let e=await readdir(this.migratorConfig.sharedMigrationsFolder),t=[];for(let n of e){if(!n.endsWith(".sql"))continue;let a=join(this.migratorConfig.sharedMigrationsFolder,n),r=await readFile(a,"utf-8"),s=n.match(/^(\d+)_/),i=s?.[1]?parseInt(s[1],10):0,o=createHash("sha256").update(r).digest("hex");t.push({name:basename(n,".sql"),path:a,sql:r,timestamp:i,hash:o});}return t.sort((n,a)=>n.timestamp-a.timestamp)}async getOrDetectSharedFormat(e,t){let n=this.migratorConfig.sharedMigrationsTable??ge,a=this.migratorConfig.tableFormat??"auto";if(a!=="auto")return _(a,n);let r=await Y(e,t,n);if(r)return r;let s=this.migratorConfig.defaultFormat??"name";return _(s,n)}hasSharedMigrations(){return this.sharedMigrationExecutor!==null}async migrateShared(e={}){return this.sharedMigrationExecutor?this.sharedMigrationExecutor.migrate(e):{schemaName:"public",success:false,appliedMigrations:[],error:"Shared migrations not configured. Set sharedMigrationsFolder in migrator config.",durationMs:0}}async getSharedStatus(){return this.sharedMigrationExecutor?this.sharedMigrationExecutor.getStatus():{schemaName:"public",appliedCount:0,pendingCount:0,pendingMigrations:[],status:"error",error:"Shared migrations not configured. Set sharedMigrationsFolder in migrator config.",format:null}}async markSharedAsApplied(e={}){return this.sharedMigrationExecutor?this.sharedMigrationExecutor.markAsApplied(e):{schemaName:"public",success:false,appliedMigrations:[],error:"Shared migrations not configured. Set sharedMigrationsFolder in migrator config.",durationMs:0}}async migrateAllWithShared(e={}){let{sharedOptions:t,...n}=e,a=await this.migrateShared(t??{}),r=await this.migrateAll(n);return {shared:a,tenants:r}}async getSchemaDrift(e={}){return this.driftDetector.detectDrift(e)}async getTenantSchemaDrift(e,t,n={}){return this.driftDetector.compareTenant(e,t,n)}async introspectTenantSchema(e,t={}){return this.driftDetector.introspectSchema(e,t)}};function Qe(c,e){return new j(c,e)}var z=class{constructor(e){this.context=e;}fromTable=null;joins=[];selectFields={};whereCondition=null;orderByFields=[];limitValue=null;offsetValue=null;from(e,t){let n=this.getSchemaName(e);return this.fromTable={table:t,source:e,schemaName:n},this}innerJoin(e,t,n){return this.addJoin(e,t,n,"inner")}leftJoin(e,t,n){return this.addJoin(e,t,n,"left")}rightJoin(e,t,n){return this.addJoin(e,t,n,"right")}fullJoin(e,t,n){return this.addJoin(e,t,n,"full")}select(e){return this.selectFields=e,this}where(e){return this.whereCondition=e,this}orderBy(...e){return this.orderByFields=e,this}limit(e){return this.limitValue=e,this}offset(e){return this.offsetValue=e,this}async execute(){if(!this.fromTable)throw new Error("[drizzle-multitenant] No table specified. Use .from() first.");let e=this.buildSql();return (await this.context.tenantDb.execute(e)).rows}buildSql(){if(!this.fromTable)throw new Error("[drizzle-multitenant] No table specified");let e=[],t=Object.entries(this.selectFields).map(([a,r])=>{let s=r.name;return sql`${sql.raw(`"${s}"`)} as ${sql.raw(`"${a}"`)}`});t.length===0?e.push(sql`SELECT *`):e.push(sql`SELECT ${sql.join(t,sql`, `)}`);let n=this.getFullTableName(this.fromTable.schemaName,this.fromTable.table);e.push(sql` FROM ${sql.raw(n)}`);for(let a of this.joins){let r=this.getFullTableName(a.schemaName,a.table),s=this.getJoinKeyword(a.type);e.push(sql` ${sql.raw(s)} ${sql.raw(r)} ON ${a.condition}`);}return this.whereCondition&&e.push(sql` WHERE ${this.whereCondition}`),this.orderByFields.length>0&&e.push(sql` ORDER BY ${sql.join(this.orderByFields,sql`, `)}`),this.limitValue!==null&&e.push(sql` LIMIT ${sql.raw(this.limitValue.toString())}`),this.offsetValue!==null&&e.push(sql` OFFSET ${sql.raw(this.offsetValue.toString())}`),sql.join(e,sql``)}addJoin(e,t,n,a){let r=this.getSchemaName(e);return this.joins.push({table:t,source:e,schemaName:r,condition:n,type:a}),this}getSchemaName(e){return e==="tenant"?this.context.tenantSchema??"tenant":this.context.sharedSchema??"public"}getFullTableName(e,t){let n=getTableName(t);return `"${e}"."${n}"`}getJoinKeyword(e){switch(e){case "inner":return "INNER JOIN";case "left":return "LEFT JOIN";case "right":return "RIGHT JOIN";case "full":return "FULL OUTER JOIN"}}};function Ye(c){return new z(c)}async function Ve(c){let{tenantDb:e,tenantTable:t,sharedTable:n,foreignKey:a,sharedKey:r="id",sharedFields:s,where:i}=c,o=getTableName(t),m=getTableName(n),u=[`SELECT t.*, ${s.map(g=>`s."${String(g)}"`).join(", ")}`,`FROM "${o}" t`,`LEFT JOIN "public"."${m}" s ON t."${String(a)}" = s."${String(r)}"`];i&&u.push("WHERE");let h=sql.raw(u.join(" "));return (await e.execute(h)).rows}async function Ke(c,e){let{tenantSchema:t,sharedSchema:n,sql:a}=e,r=a.replace(/\$tenant\./g,`"${t}".`).replace(/\$shared\./g,`"${n}".`),s=sql.raw(r);return (await c.execute(s)).rows}function Ge(c,e,t){return {columns:Object.entries(c).map(([r,s])=>`"${s.name}" as "${r}"`),getSchema:()=>e}}function Xe(c){let e=new Set;for(let t of Object.values(c))t&&typeof t=="object"&&"_"in t&&t._?.brand==="Table"&&e.add(t);return e}function pe(c,e){return e.has(c)}var B=class{constructor(e,t,n,a="public"){this.tenantDb=e;this.sharedTables=t;this.tenantSchemaName=n;this.sharedSchemaName=a;}fromTable=null;joins=[];selectFields={};whereCondition=null;orderByFields=[];limitValue=null;offsetValue=null;from(e){let t=pe(e,this.sharedTables);return this.fromTable={table:e,isShared:t,schemaName:t?this.sharedSchemaName:this.tenantSchemaName},this}leftJoin(e,t){return this.addJoin(e,t,"left")}innerJoin(e,t){return this.addJoin(e,t,"inner")}rightJoin(e,t){return this.addJoin(e,t,"right")}fullJoin(e,t){return this.addJoin(e,t,"full")}select(e){return this.selectFields=e,this}where(e){return this.whereCondition=e,this}orderBy(...e){return this.orderByFields=e,this}limit(e){return this.limitValue=e,this}offset(e){return this.offsetValue=e,this}async execute(){if(!this.fromTable)throw new Error("[drizzle-multitenant] No table specified. Use .from() first.");let e=this.buildSql();return (await this.tenantDb.execute(e)).rows}addJoin(e,t,n){let a=pe(e,this.sharedTables);return this.joins.push({table:e,isShared:a,schemaName:a?this.sharedSchemaName:this.tenantSchemaName,condition:t,type:n}),this}buildSql(){if(!this.fromTable)throw new Error("[drizzle-multitenant] No table specified");let e=[],t=Object.entries(this.selectFields).map(([r,s])=>{let i=s.name,o=this.getTableAliasForColumn(s);return o?sql`${sql.raw(`"${o}"."${i}"`)} as ${sql.raw(`"${r}"`)}`:sql`${sql.raw(`"${i}"`)} as ${sql.raw(`"${r}"`)}`});t.length===0?e.push(sql`SELECT *`):e.push(sql`SELECT ${sql.join(t,sql`, `)}`);let n=getTableName(this.fromTable.table),a=`"${this.fromTable.schemaName}"."${n}"`;e.push(sql` FROM ${sql.raw(a)} "${sql.raw(n)}"`);for(let r of this.joins){let s=getTableName(r.table),i=`"${r.schemaName}"."${s}"`,o=this.getJoinKeyword(r.type);e.push(sql` ${sql.raw(o)} ${sql.raw(i)} "${sql.raw(s)}" ON ${r.condition}`);}return this.whereCondition&&e.push(sql` WHERE ${this.whereCondition}`),this.orderByFields.length>0&&e.push(sql` ORDER BY ${sql.join(this.orderByFields,sql`, `)}`),this.limitValue!==null&&e.push(sql` LIMIT ${sql.raw(this.limitValue.toString())}`),this.offsetValue!==null&&e.push(sql` OFFSET ${sql.raw(this.offsetValue.toString())}`),sql.join(e,sql``)}getTableAliasForColumn(e){let t=e.table;return t?getTableName(t):null}getJoinKeyword(e){switch(e){case "inner":return "INNER JOIN";case "left":return "LEFT JOIN";case "right":return "RIGHT JOIN";case "full":return "FULL OUTER JOIN"}}};function Ze(c,e,t,n){let a=Xe(t.shared);return new B(c,a,n?.tenantSchema??"tenant",n?.sharedSchema??"public")}function fe(c){let e=c.message.toLowerCase();return !!(e.includes("econnrefused")||e.includes("econnreset")||e.includes("etimedout")||e.includes("enotfound")||e.includes("connection refused")||e.includes("connection reset")||e.includes("connection terminated")||e.includes("connection timed out")||e.includes("timeout expired")||e.includes("socket hang up")||e.includes("too many connections")||e.includes("sorry, too many clients")||e.includes("the database system is starting up")||e.includes("the database system is shutting down")||e.includes("server closed the connection unexpectedly")||e.includes("could not connect to server")||e.includes("ssl connection")||e.includes("ssl handshake"))}function ye(c,e){let t=e.initialDelayMs*Math.pow(e.backoffMultiplier,c),n=Math.min(t,e.maxDelayMs);if(e.jitter){let a=1+Math.random()*.25;return Math.floor(n*a)}return Math.floor(n)}function et(c){return new Promise(e=>setTimeout(e,c))}async function Te(c,e){let t={maxAttempts:e?.maxAttempts??T.retry.maxAttempts,initialDelayMs:e?.initialDelayMs??T.retry.initialDelayMs,maxDelayMs:e?.maxDelayMs??T.retry.maxDelayMs,backoffMultiplier:e?.backoffMultiplier??T.retry.backoffMultiplier,jitter:e?.jitter??T.retry.jitter,isRetryable:e?.isRetryable??fe,onRetry:e?.onRetry},n=Date.now(),a=null;for(let r=0;r<t.maxAttempts;r++)try{return {result:await c(),attempts:r+1,totalTimeMs:Date.now()-n}}catch(s){if(a=s,r>=t.maxAttempts-1||!t.isRetryable(a))throw a;let o=ye(r,t);t.onRetry?.(r+1,a,o),await et(o);}throw a??new Error("Retry failed with no error")}function tt(c){return e=>Te(e,c)}export{z as CrossSchemaQueryBuilder,T as DEFAULT_CONFIG,j as Migrator,B as WithSharedQueryBuilder,Ge as buildCrossSchemaSelect,ye as calculateDelay,Ye as createCrossSchemaQuery,Qe as createMigrator,tt as createRetrier,De as createTenantContext,Re as createTenantManager,Ke as crossSchemaRaw,Se as defineConfig,fe as isRetryableError,Te as withRetry,Ze as withShared,Ve as withSharedLookup};
|