@gl-life/gl-life-database 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/API.md +1486 -0
  2. package/LICENSE +190 -0
  3. package/README.md +480 -0
  4. package/dist/cache/index.d.ts +4 -0
  5. package/dist/cache/index.d.ts.map +1 -0
  6. package/dist/cache/invalidation.d.ts +156 -0
  7. package/dist/cache/invalidation.d.ts.map +1 -0
  8. package/dist/cache/kv-cache.d.ts +79 -0
  9. package/dist/cache/kv-cache.d.ts.map +1 -0
  10. package/dist/cache/memory-cache.d.ts +68 -0
  11. package/dist/cache/memory-cache.d.ts.map +1 -0
  12. package/dist/cloudforge/d1-adapter.d.ts +67 -0
  13. package/dist/cloudforge/d1-adapter.d.ts.map +1 -0
  14. package/dist/cloudforge/do-storage.d.ts +51 -0
  15. package/dist/cloudforge/do-storage.d.ts.map +1 -0
  16. package/dist/cloudforge/index.d.ts +4 -0
  17. package/dist/cloudforge/index.d.ts.map +1 -0
  18. package/dist/cloudforge/r2-backup.d.ts +38 -0
  19. package/dist/cloudforge/r2-backup.d.ts.map +1 -0
  20. package/dist/connection/index.d.ts +2 -0
  21. package/dist/connection/index.d.ts.map +1 -0
  22. package/dist/connection/manager.d.ts +54 -0
  23. package/dist/connection/manager.d.ts.map +1 -0
  24. package/dist/index.cjs +4762 -0
  25. package/dist/index.cjs.map +1 -0
  26. package/dist/index.d.ts +18 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +4701 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/migration/index.d.ts +4 -0
  31. package/dist/migration/index.d.ts.map +1 -0
  32. package/dist/migration/loader.d.ts +88 -0
  33. package/dist/migration/loader.d.ts.map +1 -0
  34. package/dist/migration/runner.d.ts +91 -0
  35. package/dist/migration/runner.d.ts.map +1 -0
  36. package/dist/migration/seeder.d.ts +95 -0
  37. package/dist/migration/seeder.d.ts.map +1 -0
  38. package/dist/query/builder.d.ts +47 -0
  39. package/dist/query/builder.d.ts.map +1 -0
  40. package/dist/query/index.d.ts +3 -0
  41. package/dist/query/index.d.ts.map +1 -0
  42. package/dist/query/raw.d.ts +92 -0
  43. package/dist/query/raw.d.ts.map +1 -0
  44. package/dist/tenant/context.d.ts +52 -0
  45. package/dist/tenant/context.d.ts.map +1 -0
  46. package/dist/tenant/index.d.ts +4 -0
  47. package/dist/tenant/index.d.ts.map +1 -0
  48. package/dist/tenant/query-wrapper.d.ts +96 -0
  49. package/dist/tenant/query-wrapper.d.ts.map +1 -0
  50. package/dist/tenant/schema-manager.d.ts +185 -0
  51. package/dist/tenant/schema-manager.d.ts.map +1 -0
  52. package/dist/transaction/index.d.ts +2 -0
  53. package/dist/transaction/index.d.ts.map +1 -0
  54. package/dist/transaction/transaction.d.ts +51 -0
  55. package/dist/transaction/transaction.d.ts.map +1 -0
  56. package/dist/types/cache.d.ts +214 -0
  57. package/dist/types/cache.d.ts.map +1 -0
  58. package/dist/types/cloudforge.d.ts +753 -0
  59. package/dist/types/cloudforge.d.ts.map +1 -0
  60. package/dist/types/connection.d.ts +91 -0
  61. package/dist/types/connection.d.ts.map +1 -0
  62. package/dist/types/index.d.ts +10 -0
  63. package/dist/types/index.d.ts.map +1 -0
  64. package/dist/types/migration.d.ts +225 -0
  65. package/dist/types/migration.d.ts.map +1 -0
  66. package/dist/types/plugin.d.ts +432 -0
  67. package/dist/types/plugin.d.ts.map +1 -0
  68. package/dist/types/query-builder.d.ts +217 -0
  69. package/dist/types/query-builder.d.ts.map +1 -0
  70. package/dist/types/seed.d.ts +187 -0
  71. package/dist/types/seed.d.ts.map +1 -0
  72. package/dist/types/tenant.d.ts +140 -0
  73. package/dist/types/tenant.d.ts.map +1 -0
  74. package/dist/types/transaction.d.ts +144 -0
  75. package/dist/types/transaction.d.ts.map +1 -0
  76. package/package.json +78 -0
package/dist/index.js ADDED
@@ -0,0 +1,4701 @@
1
+ import { Result, Option } from '@gl-life/gl-life-core';
2
+ export { Config, EventBus, Logger, Option, Result } from '@gl-life/gl-life-core';
3
+ import crypto, { randomBytes } from 'crypto';
4
+ import { AsyncLocalStorage } from 'async_hooks';
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import { gzipSync, gunzipSync } from 'zlib';
8
+
9
+ // src/index.ts
10
+ var InternalConnection = class {
11
+ id;
12
+ config;
13
+ connected = false;
14
+ lastPing = null;
15
+ constructor(id, config) {
16
+ this.id = id;
17
+ this.config = config;
18
+ }
19
+ get isConnected() {
20
+ return this.connected;
21
+ }
22
+ async connect() {
23
+ try {
24
+ this.connected = true;
25
+ this.lastPing = /* @__PURE__ */ new Date();
26
+ return Result.ok(void 0);
27
+ } catch (error) {
28
+ return Result.err({
29
+ type: "CONNECTION_FAILED",
30
+ message: `Failed to connect: ${error}`,
31
+ cause: error
32
+ });
33
+ }
34
+ }
35
+ async disconnect() {
36
+ try {
37
+ this.connected = false;
38
+ this.lastPing = null;
39
+ return Result.ok(void 0);
40
+ } catch (error) {
41
+ return Result.err({
42
+ type: "CONNECTION_FAILED",
43
+ message: `Failed to disconnect: ${error}`,
44
+ cause: error
45
+ });
46
+ }
47
+ }
48
+ async execute(sql, params) {
49
+ if (!this.connected) {
50
+ return Result.err({
51
+ type: "CONNECTION_CLOSED",
52
+ message: "Connection is not active"
53
+ });
54
+ }
55
+ try {
56
+ return Result.ok({});
57
+ } catch (error) {
58
+ return Result.err({
59
+ type: "UNKNOWN",
60
+ message: `Query execution failed: ${error}`,
61
+ cause: error
62
+ });
63
+ }
64
+ }
65
+ async ping() {
66
+ if (!this.connected) {
67
+ return Result.ok(false);
68
+ }
69
+ try {
70
+ this.lastPing = /* @__PURE__ */ new Date();
71
+ return Result.ok(true);
72
+ } catch (error) {
73
+ return Result.err({
74
+ type: "UNKNOWN",
75
+ message: `Ping failed: ${error}`,
76
+ cause: error
77
+ });
78
+ }
79
+ }
80
+ };
81
+ var DatabaseConnectionManager = class {
82
+ config;
83
+ logger;
84
+ pool = [];
85
+ available = [];
86
+ active = /* @__PURE__ */ new Set();
87
+ initialized = false;
88
+ destroyed = false;
89
+ connectionIdCounter = 0;
90
+ constructor(config, logger) {
91
+ if (!config.name || config.name.trim() === "") {
92
+ throw new Error("Database name is required");
93
+ }
94
+ if (config.maxConnections !== void 0 && config.maxConnections < 0) {
95
+ throw new Error("maxConnections must be >= 0");
96
+ }
97
+ this.config = {
98
+ maxConnections: 10,
99
+ timeout: 3e4,
100
+ autoReconnect: false,
101
+ maxRetries: 3,
102
+ retryDelay: 1e3,
103
+ ...config
104
+ };
105
+ this.logger = logger;
106
+ }
107
+ /**
108
+ * Get manager configuration
109
+ */
110
+ getConfig() {
111
+ return { ...this.config };
112
+ }
113
+ /**
114
+ * Get number of active connections
115
+ */
116
+ getActiveCount() {
117
+ return this.active.size;
118
+ }
119
+ /**
120
+ * Initialize the connection manager
121
+ */
122
+ async initialize() {
123
+ if (this.initialized) {
124
+ return Result.ok(void 0);
125
+ }
126
+ if (this.destroyed) {
127
+ return Result.err({
128
+ type: "CONNECTION_CLOSED",
129
+ message: "Manager has been destroyed"
130
+ });
131
+ }
132
+ this.logger.info("Initializing DatabaseConnectionManager", {
133
+ name: this.config.name,
134
+ maxConnections: this.config.maxConnections
135
+ });
136
+ try {
137
+ this.initialized = true;
138
+ this.logger.info("DatabaseConnectionManager initialized successfully");
139
+ return Result.ok(void 0);
140
+ } catch (error) {
141
+ this.initialized = false;
142
+ return Result.err({
143
+ type: "CONNECTION_FAILED",
144
+ message: `Initialization failed: ${error}`,
145
+ cause: error
146
+ });
147
+ }
148
+ }
149
+ /**
150
+ * Acquire a connection from the pool
151
+ */
152
+ async acquire() {
153
+ if (this.destroyed) {
154
+ this.logger.error("Cannot acquire connection: manager destroyed");
155
+ return Result.err({
156
+ type: "CONNECTION_CLOSED",
157
+ message: "Manager has been destroyed"
158
+ });
159
+ }
160
+ if (!this.initialized) {
161
+ return Result.err({
162
+ type: "CONNECTION_CLOSED",
163
+ message: "Manager not initialized"
164
+ });
165
+ }
166
+ this.logger.debug("Acquiring connection from pool", {
167
+ available: this.available.length,
168
+ active: this.active.size,
169
+ total: this.pool.length
170
+ });
171
+ if (this.available.length > 0) {
172
+ const conn2 = this.available.pop();
173
+ this.active.add(conn2);
174
+ const pingResult = await conn2.ping();
175
+ if (pingResult.isOk() && pingResult.unwrap()) {
176
+ this.logger.debug("Reusing existing connection", { id: conn2.id });
177
+ return Result.ok(conn2);
178
+ } else {
179
+ if (this.config.autoReconnect) {
180
+ const reconnectResult = await conn2.connect();
181
+ if (reconnectResult.isOk()) {
182
+ this.logger.debug("Reconnected unhealthy connection", { id: conn2.id });
183
+ return Result.ok(conn2);
184
+ }
185
+ }
186
+ this.active.delete(conn2);
187
+ this.pool = this.pool.filter((c) => c.id !== conn2.id);
188
+ }
189
+ }
190
+ const maxConnections = this.config.maxConnections;
191
+ if (this.pool.length >= maxConnections) {
192
+ this.logger.error("Connection pool limit reached", {
193
+ max: maxConnections,
194
+ current: this.pool.length
195
+ });
196
+ return Result.err({
197
+ type: "CONNECTION_FAILED",
198
+ message: `Connection pool limit reached (${maxConnections})`
199
+ });
200
+ }
201
+ const connId = `conn-${++this.connectionIdCounter}`;
202
+ const conn = new InternalConnection(connId, this.config);
203
+ const connectResult = await conn.connect();
204
+ if (connectResult.isErr()) {
205
+ return connectResult;
206
+ }
207
+ this.pool.push(conn);
208
+ this.active.add(conn);
209
+ this.logger.debug("Created new connection", {
210
+ id: conn.id,
211
+ poolSize: this.pool.length
212
+ });
213
+ return Result.ok(conn);
214
+ }
215
+ /**
216
+ * Release a connection back to the pool
217
+ */
218
+ async release(connection) {
219
+ if (!connection || !connection.id) {
220
+ return Result.err({
221
+ type: "DATABASE_NOT_FOUND",
222
+ message: "Invalid connection provided"
223
+ });
224
+ }
225
+ const conn = this.pool.find((c) => c.id === connection.id);
226
+ if (!conn) {
227
+ return Result.err({
228
+ type: "DATABASE_NOT_FOUND",
229
+ message: "Connection not found in pool"
230
+ });
231
+ }
232
+ this.active.delete(conn);
233
+ this.available.push(conn);
234
+ this.logger.debug("Released connection back to pool", {
235
+ id: connection.id,
236
+ available: this.available.length
237
+ });
238
+ return Result.ok(void 0);
239
+ }
240
+ /**
241
+ * Perform health check on all connections
242
+ */
243
+ async healthCheck() {
244
+ if (this.destroyed) {
245
+ return Result.ok(false);
246
+ }
247
+ if (!this.initialized) {
248
+ return Result.ok(false);
249
+ }
250
+ try {
251
+ if (this.pool.length === 0) {
252
+ const testConn = await this.acquire();
253
+ if (testConn.isErr()) {
254
+ return Result.ok(false);
255
+ }
256
+ await this.release(testConn.unwrap());
257
+ return Result.ok(true);
258
+ }
259
+ const checks = this.pool.map((conn) => conn.ping());
260
+ const results = await Promise.all(checks);
261
+ const allHealthy = results.every((r) => r.isOk() && r.unwrap() === true);
262
+ if (!allHealthy && this.config.autoReconnect) {
263
+ for (let i = 0; i < this.pool.length; i++) {
264
+ const result = results[i];
265
+ if (result.isErr() || !result.unwrap()) {
266
+ await this.pool[i].connect();
267
+ }
268
+ }
269
+ }
270
+ return Result.ok(allHealthy);
271
+ } catch (error) {
272
+ return Result.err({
273
+ type: "UNKNOWN",
274
+ message: `Health check failed: ${error}`,
275
+ cause: error
276
+ });
277
+ }
278
+ }
279
+ /**
280
+ * Destroy the connection manager and close all connections
281
+ */
282
+ async destroy() {
283
+ if (this.destroyed) {
284
+ return Result.ok(void 0);
285
+ }
286
+ this.logger.info("Destroying DatabaseConnectionManager", {
287
+ totalConnections: this.pool.length,
288
+ activeConnections: this.active.size
289
+ });
290
+ try {
291
+ const closePromises = this.pool.map((conn) => conn.disconnect());
292
+ await Promise.all(closePromises);
293
+ this.pool = [];
294
+ this.available = [];
295
+ this.active.clear();
296
+ this.initialized = false;
297
+ this.destroyed = true;
298
+ this.logger.info("DatabaseConnectionManager destroyed successfully");
299
+ return Result.ok(void 0);
300
+ } catch (error) {
301
+ return Result.err({
302
+ type: "UNKNOWN",
303
+ message: `Destroy failed: ${error}`,
304
+ cause: error
305
+ });
306
+ }
307
+ }
308
+ };
309
+ var TypeSafeQueryBuilder = class _TypeSafeQueryBuilder {
310
+ tableName;
311
+ metaDataService;
312
+ logger;
313
+ selectedColumns = [];
314
+ whereConditions = [];
315
+ joinClauses = [];
316
+ orderByClauses = [];
317
+ groupByFields = [];
318
+ havingCondition = null;
319
+ limitValue = null;
320
+ offsetValue = null;
321
+ constructor(tableName, metaDataService, logger) {
322
+ if (!tableName || tableName.trim() === "") {
323
+ throw new Error("Table name is required");
324
+ }
325
+ this.tableName = tableName;
326
+ this.metaDataService = metaDataService;
327
+ this.logger = logger;
328
+ }
329
+ table(table) {
330
+ this.tableName = table;
331
+ return this;
332
+ }
333
+ select(...columns) {
334
+ this.selectedColumns.push(...columns);
335
+ return this;
336
+ }
337
+ where(field, operator, value) {
338
+ this.whereConditions.push({ field, operator, value, isOr: false });
339
+ return this;
340
+ }
341
+ orWhere(field, operator, value) {
342
+ this.whereConditions.push({ field, operator, value, isOr: true });
343
+ return this;
344
+ }
345
+ whereIn(field, values) {
346
+ this.whereConditions.push({ field, operator: "IN", value: values, isOr: false });
347
+ return this;
348
+ }
349
+ join(table, leftField, rightField, type = "INNER") {
350
+ this.joinClauses.push({ type, table, left: leftField, right: rightField });
351
+ return this;
352
+ }
353
+ orderBy(field, direction = "ASC") {
354
+ this.orderByClauses.push({ field, direction });
355
+ return this;
356
+ }
357
+ groupBy(...fields) {
358
+ this.groupByFields.push(...fields);
359
+ return this;
360
+ }
361
+ having(condition) {
362
+ this.havingCondition = condition;
363
+ return this;
364
+ }
365
+ limit(limit) {
366
+ this.limitValue = limit;
367
+ return this;
368
+ }
369
+ offset(offset) {
370
+ this.offsetValue = offset;
371
+ return this;
372
+ }
373
+ async insert(data) {
374
+ try {
375
+ this.logger.debug("Insert operation", { table: this.tableName, data });
376
+ return Result.ok(data);
377
+ } catch (error) {
378
+ return Result.err({
379
+ type: "EXECUTION_FAILED",
380
+ message: `Insert failed: ${error}`,
381
+ cause: error
382
+ });
383
+ }
384
+ }
385
+ async insertMany(data) {
386
+ try {
387
+ this.logger.debug("Insert many operation", { table: this.tableName, count: data.length });
388
+ return Result.ok(data);
389
+ } catch (error) {
390
+ return Result.err({
391
+ type: "EXECUTION_FAILED",
392
+ message: `Insert many failed: ${error}`,
393
+ cause: error
394
+ });
395
+ }
396
+ }
397
+ async update(data) {
398
+ try {
399
+ this.logger.debug("Update operation", { table: this.tableName, data });
400
+ return Result.ok(1);
401
+ } catch (error) {
402
+ return Result.err({
403
+ type: "EXECUTION_FAILED",
404
+ message: `Update failed: ${error}`,
405
+ cause: error
406
+ });
407
+ }
408
+ }
409
+ async delete() {
410
+ try {
411
+ this.logger.debug("Delete operation", { table: this.tableName });
412
+ return Result.ok(1);
413
+ } catch (error) {
414
+ return Result.err({
415
+ type: "EXECUTION_FAILED",
416
+ message: `Delete failed: ${error}`,
417
+ cause: error
418
+ });
419
+ }
420
+ }
421
+ async first() {
422
+ try {
423
+ this.logger.debug("First operation", { table: this.tableName });
424
+ return Result.ok(null);
425
+ } catch (error) {
426
+ return Result.err({
427
+ type: "EXECUTION_FAILED",
428
+ message: `First failed: ${error}`,
429
+ cause: error
430
+ });
431
+ }
432
+ }
433
+ async get() {
434
+ try {
435
+ this.logger.debug("Get operation", { table: this.tableName });
436
+ return Result.ok([]);
437
+ } catch (error) {
438
+ return Result.err({
439
+ type: "EXECUTION_FAILED",
440
+ message: `Get failed: ${error}`,
441
+ cause: error
442
+ });
443
+ }
444
+ }
445
+ async count(field = "*") {
446
+ try {
447
+ this.logger.debug("Count operation", { table: this.tableName, field });
448
+ return Result.ok(0);
449
+ } catch (error) {
450
+ return Result.err({
451
+ type: "EXECUTION_FAILED",
452
+ message: `Count failed: ${error}`,
453
+ cause: error
454
+ });
455
+ }
456
+ }
457
+ toSQL() {
458
+ const columns = this.selectedColumns.length > 0 ? this.selectedColumns.join(", ") : "*";
459
+ let sql = `SELECT ${columns} FROM ${this.tableName}`;
460
+ if (this.joinClauses.length > 0) {
461
+ sql += " " + this.joinClauses.map((j) => `${j.type} JOIN ${j.table} ON ${j.left} = ${j.right}`).join(" ");
462
+ }
463
+ if (this.whereConditions.length > 0) {
464
+ const whereParts = this.whereConditions.map((w, i) => {
465
+ const prefix = i === 0 ? "WHERE" : w.isOr ? "OR" : "AND";
466
+ return `${prefix} ${w.field} ${w.operator} ?`;
467
+ });
468
+ sql += " " + whereParts.join(" ");
469
+ }
470
+ if (this.groupByFields.length > 0) {
471
+ sql += ` GROUP BY ${this.groupByFields.join(", ")}`;
472
+ }
473
+ if (this.havingCondition) {
474
+ sql += ` HAVING ${this.havingCondition}`;
475
+ }
476
+ if (this.orderByClauses.length > 0) {
477
+ sql += " ORDER BY " + this.orderByClauses.map((o) => `${o.field} ${o.direction}`).join(", ");
478
+ }
479
+ if (this.limitValue !== null) {
480
+ sql += ` LIMIT ${this.limitValue}`;
481
+ }
482
+ if (this.offsetValue !== null) {
483
+ sql += ` OFFSET ${this.offsetValue}`;
484
+ }
485
+ return sql;
486
+ }
487
+ getBindings() {
488
+ return this.whereConditions.map((w) => w.value).filter((v) => v !== void 0);
489
+ }
490
+ clone() {
491
+ const cloned = new _TypeSafeQueryBuilder(this.tableName, this.metaDataService, this.logger);
492
+ cloned.selectedColumns = [...this.selectedColumns];
493
+ cloned.whereConditions = [...this.whereConditions];
494
+ cloned.joinClauses = [...this.joinClauses];
495
+ cloned.orderByClauses = [...this.orderByClauses];
496
+ cloned.groupByFields = [...this.groupByFields];
497
+ cloned.havingCondition = this.havingCondition;
498
+ cloned.limitValue = this.limitValue;
499
+ cloned.offsetValue = this.offsetValue;
500
+ return cloned;
501
+ }
502
+ };
503
+ var RawSQLExecutor = class {
504
+ connection;
505
+ logger;
506
+ constructor(connection, logger) {
507
+ this.connection = connection;
508
+ this.logger = logger;
509
+ }
510
+ /**
511
+ * Execute a raw SQL query with parameters
512
+ *
513
+ * Uses parameterized queries to prevent SQL injection.
514
+ * All parameter values are properly escaped by the database driver.
515
+ *
516
+ * @param sql - SQL query string with ? placeholders
517
+ * @param params - Parameter values to bind to placeholders
518
+ * @param options - Execution options
519
+ * @returns Result containing array of rows or error
520
+ */
521
+ async execute(sql, params = [], options) {
522
+ const validationResult = this.validateSQL(sql, params);
523
+ if (validationResult.isErr()) {
524
+ return validationResult;
525
+ }
526
+ try {
527
+ this.logger.debug("Executing raw SQL", {
528
+ sql: sql.substring(0, 100),
529
+ paramCount: params.length
530
+ });
531
+ const result = await this.connection.execute(sql, params);
532
+ if (result.isErr()) {
533
+ return Result.err({
534
+ type: "EXECUTION_FAILED",
535
+ message: "Query execution failed",
536
+ cause: result
537
+ });
538
+ }
539
+ const data = result.unwrap();
540
+ let rows;
541
+ if (data && typeof data === "object" && "rows" in data) {
542
+ rows = data.rows;
543
+ } else if (Array.isArray(data)) {
544
+ rows = data;
545
+ } else {
546
+ rows = [];
547
+ }
548
+ this.logger.debug("Query executed successfully", {
549
+ rowCount: rows.length
550
+ });
551
+ return Result.ok(rows);
552
+ } catch (error) {
553
+ this.logger.error("Raw SQL execution failed", {
554
+ error,
555
+ sql: sql.substring(0, 100)
556
+ });
557
+ return Result.err({
558
+ type: "EXECUTION_FAILED",
559
+ message: `Query execution failed: ${error instanceof Error ? error.message : String(error)}`,
560
+ cause: error
561
+ });
562
+ }
563
+ }
564
+ /**
565
+ * Execute a query and return the first row or null
566
+ *
567
+ * @param sql - SQL query string
568
+ * @param params - Parameter values
569
+ * @param options - Execution options
570
+ * @returns Result containing single row or null
571
+ */
572
+ async executeOne(sql, params = [], options) {
573
+ const result = await this.execute(sql, params, options);
574
+ if (result.isErr()) {
575
+ return result;
576
+ }
577
+ const rows = result.unwrap();
578
+ return Result.ok(rows.length > 0 ? rows[0] : null);
579
+ }
580
+ /**
581
+ * Execute multiple queries in batch
582
+ *
583
+ * All queries execute sequentially. If any query fails, execution stops
584
+ * and an error is returned.
585
+ *
586
+ * @param queries - Array of SQL queries with parameters
587
+ * @returns Result containing array of results or error
588
+ */
589
+ async executeBatch(queries) {
590
+ const results = [];
591
+ try {
592
+ this.logger.debug("Executing batch queries", { count: queries.length });
593
+ for (const query of queries) {
594
+ const result = await this.execute(query.sql, query.params || []);
595
+ if (result.isErr()) {
596
+ return Result.err({
597
+ type: "EXECUTION_FAILED",
598
+ message: "Batch execution failed",
599
+ cause: result
600
+ });
601
+ }
602
+ results.push(result.unwrap());
603
+ }
604
+ this.logger.debug("Batch execution completed", {
605
+ queryCount: queries.length,
606
+ totalRows: results.reduce((sum, rows) => sum + rows.length, 0)
607
+ });
608
+ return Result.ok(results);
609
+ } catch (error) {
610
+ this.logger.error("Batch execution failed", { error });
611
+ return Result.err({
612
+ type: "EXECUTION_FAILED",
613
+ message: `Batch execution failed: ${error instanceof Error ? error.message : String(error)}`,
614
+ cause: error
615
+ });
616
+ }
617
+ }
618
+ /**
619
+ * Validate SQL query and parameters
620
+ *
621
+ * Checks for:
622
+ * - Empty/whitespace-only queries
623
+ * - Parameter count matching placeholder count
624
+ *
625
+ * @param sql - SQL query string
626
+ * @param params - Parameter values
627
+ * @returns Result indicating validation success or error
628
+ */
629
+ validateSQL(sql, params) {
630
+ if (!sql || sql.trim().length === 0) {
631
+ return Result.err({
632
+ type: "INVALID_SQL",
633
+ message: "SQL query cannot be empty"
634
+ });
635
+ }
636
+ const placeholderCount = (sql.match(/\?/g) || []).length;
637
+ if (placeholderCount !== params.length) {
638
+ return Result.err({
639
+ type: "PARAMETER_MISMATCH",
640
+ message: `Parameter count mismatch: expected ${placeholderCount} but got ${params.length}`
641
+ });
642
+ }
643
+ return Result.ok(void 0);
644
+ }
645
+ };
646
+ var DatabaseTransaction = class {
647
+ _id;
648
+ db;
649
+ _logger;
650
+ _config;
651
+ _state = "PENDING";
652
+ startTime = null;
653
+ timeoutHandle = null;
654
+ constructor(db, logger, config = {}) {
655
+ this._id = this.generateTransactionId();
656
+ this.db = db;
657
+ this._logger = logger;
658
+ this._config = {
659
+ isolationLevel: config.isolationLevel || "READ COMMITTED",
660
+ timeout: config.timeout || 3e4,
661
+ autoRollback: config.autoRollback !== void 0 ? config.autoRollback : true
662
+ };
663
+ }
664
+ generateTransactionId() {
665
+ return `tx_${randomBytes(16).toString("hex")}`;
666
+ }
667
+ get id() {
668
+ return this._id;
669
+ }
670
+ get state() {
671
+ return this._state;
672
+ }
673
+ get config() {
674
+ return this._config;
675
+ }
676
+ get isActive() {
677
+ return this._state === "ACTIVE";
678
+ }
679
+ async begin() {
680
+ if (this._state !== "PENDING") {
681
+ return Result.err({
682
+ type: "ALREADY_STARTED",
683
+ message: `Transaction ${this._id} is already ${this._state}`
684
+ });
685
+ }
686
+ try {
687
+ this._logger.debug("Beginning transaction", {
688
+ id: this._id,
689
+ isolationLevel: this._config.isolationLevel
690
+ });
691
+ await this.db.execute("BEGIN", []);
692
+ this._state = "ACTIVE";
693
+ this.startTime = Date.now();
694
+ if (this._config.timeout && this._config.timeout > 0) {
695
+ this.timeoutHandle = setTimeout(() => {
696
+ this.handleTimeout();
697
+ }, this._config.timeout);
698
+ }
699
+ return Result.ok(void 0);
700
+ } catch (error) {
701
+ this._logger.error("Failed to begin transaction", { error, id: this._id });
702
+ return Result.err({
703
+ type: "BEGIN_FAILED",
704
+ message: `Failed to begin transaction: ${error instanceof Error ? error.message : String(error)}`,
705
+ cause: error
706
+ });
707
+ }
708
+ }
709
+ async commit() {
710
+ if (this._state === "PENDING") {
711
+ return Result.err({
712
+ type: "NOT_STARTED",
713
+ message: `Transaction ${this._id} has not been started`
714
+ });
715
+ }
716
+ if (this._state === "COMMITTED" || this._state === "ROLLED_BACK") {
717
+ return Result.err({
718
+ type: "ALREADY_COMPLETED",
719
+ message: `Transaction ${this._id} is already ${this._state}`
720
+ });
721
+ }
722
+ try {
723
+ this._logger.debug("Committing transaction", { id: this._id });
724
+ await this.db.execute("COMMIT", []);
725
+ this._state = "COMMITTED";
726
+ this.clearTimeout();
727
+ const duration = this.startTime ? Date.now() - this.startTime : 0;
728
+ this._logger.info("Transaction committed", { id: this._id, duration });
729
+ return Result.ok(void 0);
730
+ } catch (error) {
731
+ this._logger.error("Failed to commit transaction", { error, id: this._id });
732
+ return Result.err({
733
+ type: "COMMIT_FAILED",
734
+ message: `Failed to commit transaction: ${error instanceof Error ? error.message : String(error)}`,
735
+ cause: error
736
+ });
737
+ }
738
+ }
739
+ async rollback() {
740
+ if (this._state === "PENDING") {
741
+ return Result.err({
742
+ type: "NOT_STARTED",
743
+ message: `Transaction ${this._id} has not been started`
744
+ });
745
+ }
746
+ if (this._state === "COMMITTED" || this._state === "ROLLED_BACK") {
747
+ return Result.err({
748
+ type: "ALREADY_COMPLETED",
749
+ message: `Transaction ${this._id} is already ${this._state}`
750
+ });
751
+ }
752
+ try {
753
+ this._logger.debug("Rolling back transaction", { id: this._id });
754
+ await this.db.execute("ROLLBACK", []);
755
+ this._state = "ROLLED_BACK";
756
+ this.clearTimeout();
757
+ const duration = this.startTime ? Date.now() - this.startTime : 0;
758
+ this._logger.info("Transaction rolled back", { id: this._id, duration });
759
+ return Result.ok(void 0);
760
+ } catch (error) {
761
+ this._logger.error("Failed to rollback transaction", { error, id: this._id });
762
+ return Result.err({
763
+ type: "ROLLBACK_FAILED",
764
+ message: `Failed to rollback transaction: ${error instanceof Error ? error.message : String(error)}`,
765
+ cause: error
766
+ });
767
+ }
768
+ }
769
+ async savepoint(name) {
770
+ if (!this.isActive) {
771
+ return Result.err({
772
+ type: "NOT_STARTED",
773
+ message: `Cannot create savepoint: transaction ${this._id} is not active`
774
+ });
775
+ }
776
+ try {
777
+ this._logger.debug("Creating savepoint", { id: this._id, savepoint: name });
778
+ await this.db.execute(`SAVEPOINT ${name}`, []);
779
+ return Result.ok(name);
780
+ } catch (error) {
781
+ this._logger.error("Failed to create savepoint", { error, id: this._id, savepoint: name });
782
+ return Result.err({
783
+ type: "UNKNOWN",
784
+ message: `Failed to create savepoint: ${error instanceof Error ? error.message : String(error)}`,
785
+ cause: error
786
+ });
787
+ }
788
+ }
789
+ async rollbackToSavepoint(name) {
790
+ if (!this.isActive) {
791
+ return Result.err({
792
+ type: "NOT_STARTED",
793
+ message: `Cannot rollback to savepoint: transaction ${this._id} is not active`
794
+ });
795
+ }
796
+ try {
797
+ this._logger.debug("Rolling back to savepoint", { id: this._id, savepoint: name });
798
+ await this.db.execute(`ROLLBACK TO SAVEPOINT ${name}`, []);
799
+ return Result.ok(void 0);
800
+ } catch (error) {
801
+ this._logger.error("Failed to rollback to savepoint", { error, id: this._id, savepoint: name });
802
+ return Result.err({
803
+ type: "UNKNOWN",
804
+ message: `Failed to rollback to savepoint: ${error instanceof Error ? error.message : String(error)}`,
805
+ cause: error
806
+ });
807
+ }
808
+ }
809
+ async releaseSavepoint(name) {
810
+ if (!this.isActive) {
811
+ return Result.err({
812
+ type: "NOT_STARTED",
813
+ message: `Cannot release savepoint: transaction ${this._id} is not active`
814
+ });
815
+ }
816
+ try {
817
+ this._logger.debug("Releasing savepoint", { id: this._id, savepoint: name });
818
+ await this.db.execute(`RELEASE SAVEPOINT ${name}`, []);
819
+ return Result.ok(void 0);
820
+ } catch (error) {
821
+ this._logger.error("Failed to release savepoint", { error, id: this._id, savepoint: name });
822
+ return Result.err({
823
+ type: "UNKNOWN",
824
+ message: `Failed to release savepoint: ${error instanceof Error ? error.message : String(error)}`,
825
+ cause: error
826
+ });
827
+ }
828
+ }
829
+ async execute(sql, params = []) {
830
+ if (!this.isActive) {
831
+ return Result.err({
832
+ type: "NOT_STARTED",
833
+ message: `Cannot execute query: transaction ${this._id} is not active`
834
+ });
835
+ }
836
+ try {
837
+ this._logger.debug("Executing query in transaction", {
838
+ id: this._id,
839
+ sql: sql.substring(0, 100)
840
+ });
841
+ const result = await this.db.execute(sql, params);
842
+ return Result.ok(result);
843
+ } catch (error) {
844
+ this._logger.error("Query execution failed in transaction", {
845
+ error,
846
+ id: this._id,
847
+ sql: sql.substring(0, 100)
848
+ });
849
+ return Result.err({
850
+ type: "UNKNOWN",
851
+ message: `Query execution failed: ${error instanceof Error ? error.message : String(error)}`,
852
+ cause: error
853
+ });
854
+ }
855
+ }
856
+ queryBuilder(table) {
857
+ const mockMetaDataService = {
858
+ getAllMappings: async () => [],
859
+ getMapping: async () => null
860
+ };
861
+ return new TypeSafeQueryBuilder(table, mockMetaDataService, this._logger);
862
+ }
863
+ async run(callback) {
864
+ if (this._state === "PENDING") {
865
+ const beginResult = await this.begin();
866
+ if (beginResult.isErr()) {
867
+ return beginResult;
868
+ }
869
+ }
870
+ try {
871
+ const result = await callback(this);
872
+ if (result.isErr()) {
873
+ if (this._config.autoRollback) {
874
+ await this.rollback();
875
+ }
876
+ return Result.err({
877
+ type: "UNKNOWN",
878
+ message: "Transaction callback returned error",
879
+ cause: result
880
+ });
881
+ }
882
+ const commitResult = await this.commit();
883
+ if (commitResult.isErr()) {
884
+ return commitResult;
885
+ }
886
+ return Result.ok(result.unwrap());
887
+ } catch (error) {
888
+ if (this._config.autoRollback) {
889
+ await this.rollback();
890
+ }
891
+ return Result.err({
892
+ type: "UNKNOWN",
893
+ message: `Transaction failed: ${error instanceof Error ? error.message : String(error)}`,
894
+ cause: error
895
+ });
896
+ }
897
+ }
898
+ handleTimeout() {
899
+ if (this.isActive) {
900
+ this._logger.warn("Transaction timeout", {
901
+ id: this._id,
902
+ timeout: this._config.timeout
903
+ });
904
+ this.rollback().then((result) => {
905
+ if (result.isErr()) {
906
+ this._logger.error("Failed to rollback timed out transaction", {
907
+ id: this._id,
908
+ error: "Transaction rollback failed"
909
+ });
910
+ }
911
+ });
912
+ }
913
+ }
914
+ clearTimeout() {
915
+ if (this.timeoutHandle) {
916
+ clearTimeout(this.timeoutHandle);
917
+ this.timeoutHandle = null;
918
+ }
919
+ }
920
+ };
921
+ var TenantContextManager = class _TenantContextManager {
922
+ storage;
923
+ _config;
924
+ logger;
925
+ currentTenant = null;
926
+ constructor(config, logger) {
927
+ this._config = config;
928
+ this.logger = logger;
929
+ this.storage = new AsyncLocalStorage();
930
+ }
931
+ get tenant() {
932
+ return this.getTenant();
933
+ }
934
+ get config() {
935
+ return this._config;
936
+ }
937
+ get hasTenant() {
938
+ return this.currentTenant !== null;
939
+ }
940
+ async setTenant(tenantId) {
941
+ this.validateTenantId(tenantId);
942
+ this.logger.debug("Setting tenant", { tenantId });
943
+ const metadata = {
944
+ id: tenantId,
945
+ name: tenantId,
946
+ createdAt: /* @__PURE__ */ new Date(),
947
+ updatedAt: /* @__PURE__ */ new Date(),
948
+ isActive: true
949
+ };
950
+ this.currentTenant = metadata;
951
+ }
952
+ async setTenantWithMetadata(metadata) {
953
+ this.validateTenantId(metadata.id);
954
+ this.logger.debug("Setting tenant with metadata", {
955
+ tenantId: metadata.id,
956
+ name: metadata.name
957
+ });
958
+ this.currentTenant = metadata;
959
+ }
960
+ async clearTenant() {
961
+ this.logger.debug("Clearing tenant context");
962
+ this.currentTenant = null;
963
+ }
964
+ getTenantId() {
965
+ if (this.currentTenant) {
966
+ return Option.some(this.currentTenant.id);
967
+ }
968
+ return Option.none();
969
+ }
970
+ getTenant() {
971
+ if (this.currentTenant) {
972
+ return Option.some(this.currentTenant);
973
+ }
974
+ return Option.none();
975
+ }
976
+ shouldIsolateTable(table) {
977
+ if (!this._config.enforceIsolation) {
978
+ return false;
979
+ }
980
+ if (this._config.excludedTables?.includes(table)) {
981
+ return false;
982
+ }
983
+ return true;
984
+ }
985
+ applyTenantIsolation(sql, params = []) {
986
+ const tenantId = this.getTenantId();
987
+ if (tenantId.isNone()) {
988
+ throw new Error("No tenant context set. Cannot apply tenant isolation.");
989
+ }
990
+ const tenant = tenantId.unwrap();
991
+ const tableMatch = sql.match(/(?:FROM|INTO|UPDATE)\s+([a-zA-Z_][a-zA-Z0-9_]*)/i);
992
+ if (tableMatch) {
993
+ const tableName = tableMatch[1];
994
+ if (!this.shouldIsolateTable(tableName)) {
995
+ return { sql, params };
996
+ }
997
+ }
998
+ switch (this._config.isolationStrategy) {
999
+ case "ROW":
1000
+ return this.applyRowLevelIsolation(sql, params, tenant);
1001
+ case "SCHEMA":
1002
+ return this.applySchemaIsolation(sql, params, tenant);
1003
+ case "DATABASE":
1004
+ return { sql, params };
1005
+ case "HYBRID":
1006
+ const schemaResult = this.applySchemaIsolation(sql, params, tenant);
1007
+ return this.applyRowLevelIsolation(schemaResult.sql, schemaResult.params, tenant);
1008
+ default:
1009
+ return { sql, params };
1010
+ }
1011
+ }
1012
+ getTenantDatabase() {
1013
+ const tenantId = this.getTenantId();
1014
+ if (tenantId.isNone()) {
1015
+ return Option.none();
1016
+ }
1017
+ if (this._config.isolationStrategy !== "DATABASE" && this._config.isolationStrategy !== "HYBRID") {
1018
+ return Option.none();
1019
+ }
1020
+ const tenant = tenantId.unwrap();
1021
+ const baseName = this._config.databaseName || "app";
1022
+ const dbName = `${baseName}_${tenant}`;
1023
+ return Option.some(dbName);
1024
+ }
1025
+ getTenantSchema() {
1026
+ const tenantId = this.getTenantId();
1027
+ if (tenantId.isNone()) {
1028
+ return Option.none();
1029
+ }
1030
+ if (this._config.isolationStrategy !== "SCHEMA" && this._config.isolationStrategy !== "HYBRID") {
1031
+ return Option.none();
1032
+ }
1033
+ const tenant = tenantId.unwrap();
1034
+ const baseName = this._config.schemaName || "app";
1035
+ const schemaName = `${baseName}_${tenant}`;
1036
+ return Option.some(schemaName);
1037
+ }
1038
+ async withTenant(tenantId, callback) {
1039
+ const previousTenant = this.currentTenant;
1040
+ try {
1041
+ await this.setTenant(tenantId);
1042
+ const result = await callback();
1043
+ return result;
1044
+ } finally {
1045
+ if (previousTenant) {
1046
+ this.currentTenant = previousTenant;
1047
+ } else {
1048
+ this.currentTenant = null;
1049
+ }
1050
+ }
1051
+ }
1052
+ clone() {
1053
+ return new _TenantContextManager(this._config, this.logger);
1054
+ }
1055
+ /**
1056
+ * Validate tenant ID format
1057
+ */
1058
+ validateTenantId(tenantId) {
1059
+ if (!tenantId || typeof tenantId !== "string") {
1060
+ throw new Error("Tenant ID must be a non-empty string");
1061
+ }
1062
+ if (tenantId.trim().length === 0) {
1063
+ throw new Error("Tenant ID cannot be whitespace only");
1064
+ }
1065
+ if (/[;'"`\\]/.test(tenantId)) {
1066
+ throw new Error("Tenant ID contains invalid characters");
1067
+ }
1068
+ }
1069
+ /**
1070
+ * Apply ROW-level tenant isolation to SQL query
1071
+ */
1072
+ applyRowLevelIsolation(sql, params, tenantId) {
1073
+ const tenantColumn = this._config.tenantIdColumn || "tenant_id";
1074
+ const newParams = [...params];
1075
+ const sqlUpper = sql.trim().toUpperCase();
1076
+ if (sqlUpper.startsWith("SELECT")) {
1077
+ if (sql.toUpperCase().includes("WHERE")) {
1078
+ sql = sql.replace(/WHERE/i, `WHERE ${tenantColumn} = ? AND`);
1079
+ newParams.unshift(tenantId);
1080
+ } else {
1081
+ const clauseMatch = sql.match(/(ORDER BY|LIMIT|OFFSET|GROUP BY)/i);
1082
+ if (clauseMatch) {
1083
+ const pos = sql.indexOf(clauseMatch[0]);
1084
+ sql = sql.slice(0, pos) + ` WHERE ${tenantColumn} = ? ` + sql.slice(pos);
1085
+ } else {
1086
+ sql = `${sql} WHERE ${tenantColumn} = ?`;
1087
+ }
1088
+ newParams.push(tenantId);
1089
+ }
1090
+ } else if (sqlUpper.startsWith("INSERT")) {
1091
+ const valuesMatch = sql.match(/\(([^)]+)\)\s+VALUES\s+\(([^)]+)\)/i);
1092
+ if (valuesMatch) {
1093
+ const columns = valuesMatch[1];
1094
+ const placeholders = valuesMatch[2];
1095
+ const newColumns = `${columns}, ${tenantColumn}`;
1096
+ const newPlaceholders = `${placeholders}, ?`;
1097
+ sql = sql.replace(
1098
+ /\(([^)]+)\)\s+VALUES\s+\(([^)]+)\)/i,
1099
+ `(${newColumns}) VALUES (${newPlaceholders})`
1100
+ );
1101
+ newParams.push(tenantId);
1102
+ }
1103
+ } else if (sqlUpper.startsWith("UPDATE")) {
1104
+ if (sql.toUpperCase().includes("WHERE")) {
1105
+ sql = sql.replace(/WHERE/i, `WHERE ${tenantColumn} = ? AND`);
1106
+ newParams.unshift(tenantId);
1107
+ } else {
1108
+ sql = `${sql} WHERE ${tenantColumn} = ?`;
1109
+ newParams.push(tenantId);
1110
+ }
1111
+ } else if (sqlUpper.startsWith("DELETE")) {
1112
+ if (sql.toUpperCase().includes("WHERE")) {
1113
+ sql = sql.replace(/WHERE/i, `WHERE ${tenantColumn} = ? AND`);
1114
+ newParams.unshift(tenantId);
1115
+ } else {
1116
+ sql = `${sql} WHERE ${tenantColumn} = ?`;
1117
+ newParams.push(tenantId);
1118
+ }
1119
+ }
1120
+ return { sql, params: newParams };
1121
+ }
1122
+ /**
1123
+ * Apply SCHEMA-level tenant isolation to SQL query
1124
+ */
1125
+ applySchemaIsolation(sql, params, tenantId) {
1126
+ const schemaName = this.getTenantSchema();
1127
+ if (schemaName.isNone()) {
1128
+ return { sql, params };
1129
+ }
1130
+ const schema = schemaName.unwrap();
1131
+ sql = sql.replace(
1132
+ /(?:FROM|INTO|UPDATE|JOIN)\s+([a-zA-Z_][a-zA-Z0-9_]*)/gi,
1133
+ (match, tableName) => {
1134
+ if (tableName.includes(".") || this._config.excludedTables?.includes(tableName)) {
1135
+ return match;
1136
+ }
1137
+ return match.replace(tableName, `${schema}.${tableName}`);
1138
+ }
1139
+ );
1140
+ return { sql, params };
1141
+ }
1142
+ };
1143
+
1144
+ // src/tenant/query-wrapper.ts
1145
+ var TenantAwareQueryBuilder = class _TenantAwareQueryBuilder {
1146
+ baseBuilder;
1147
+ tenantContext;
1148
+ logger;
1149
+ isAdminMode = false;
1150
+ constructor(baseBuilder, tenantContext, logger) {
1151
+ this.baseBuilder = baseBuilder;
1152
+ this.tenantContext = tenantContext;
1153
+ this.logger = logger;
1154
+ }
1155
+ /**
1156
+ * Switch to admin mode - bypasses tenant filtering
1157
+ *
1158
+ * Use with caution! Only for administrative queries that need
1159
+ * to access data across all tenants.
1160
+ *
1161
+ * @returns QueryBuilder in admin mode
1162
+ */
1163
+ asAdmin() {
1164
+ const adminBuilder = new _TenantAwareQueryBuilder(
1165
+ this.baseBuilder,
1166
+ this.tenantContext,
1167
+ this.logger
1168
+ );
1169
+ adminBuilder.isAdminMode = true;
1170
+ return adminBuilder;
1171
+ }
1172
+ /**
1173
+ * Switch back to tenant mode from admin mode
1174
+ *
1175
+ * Re-enables automatic tenant filtering
1176
+ *
1177
+ * @returns QueryBuilder in tenant mode
1178
+ */
1179
+ asTenant() {
1180
+ const tenantBuilder = new _TenantAwareQueryBuilder(
1181
+ this.baseBuilder,
1182
+ this.tenantContext,
1183
+ this.logger
1184
+ );
1185
+ tenantBuilder.isAdminMode = false;
1186
+ return tenantBuilder;
1187
+ }
1188
+ /**
1189
+ * Validate that tenant context is set (unless admin mode or excluded table)
1190
+ */
1191
+ validateTenantContext() {
1192
+ if (this.isAdminMode) {
1193
+ return;
1194
+ }
1195
+ const tableName = this.getTableName();
1196
+ if (!this.tenantContext.shouldIsolateTable(tableName)) {
1197
+ return;
1198
+ }
1199
+ if (!this.tenantContext.hasTenant) {
1200
+ throw new Error(
1201
+ "No tenant context set. Cannot execute query without tenant context. Use asAdmin() for admin queries or set tenant via tenantContext.setTenant()"
1202
+ );
1203
+ }
1204
+ }
1205
+ /**
1206
+ * Get table name from base builder
1207
+ */
1208
+ getTableName() {
1209
+ const sql = this.baseBuilder.toSQL();
1210
+ const match = sql.match(/FROM\s+([a-zA-Z_][a-zA-Z0-9_]*)/i);
1211
+ return match ? match[1] : "";
1212
+ }
1213
+ /**
1214
+ * Inject tenant filter into query builder
1215
+ *
1216
+ * This method ensures the correct tenant filter is present.
1217
+ * If a manual tenant_id filter exists, it will be overridden.
1218
+ */
1219
+ injectTenantFilter() {
1220
+ if (this.isAdminMode) {
1221
+ return;
1222
+ }
1223
+ const tableName = this.getTableName();
1224
+ if (!this.tenantContext.shouldIsolateTable(tableName)) {
1225
+ return;
1226
+ }
1227
+ const tenantId = this.tenantContext.getTenantId();
1228
+ if (tenantId.isNone()) {
1229
+ throw new Error("No tenant context set. Cannot inject tenant filter.");
1230
+ }
1231
+ const tenant = tenantId.unwrap();
1232
+ const tenantColumn = this.tenantContext.config.tenantIdColumn || "tenant_id";
1233
+ this.baseBuilder.where(tenantColumn, "=", tenant);
1234
+ }
1235
+ /**
1236
+ * Override tenant_id in data object for INSERT operations
1237
+ */
1238
+ enforceTenantInData(data) {
1239
+ if (this.isAdminMode) {
1240
+ return data;
1241
+ }
1242
+ const tableName = this.getTableName();
1243
+ if (!this.tenantContext.shouldIsolateTable(tableName)) {
1244
+ return data;
1245
+ }
1246
+ const tenantId = this.tenantContext.getTenantId();
1247
+ if (tenantId.isNone()) {
1248
+ throw new Error("No tenant context set. Cannot enforce tenant in INSERT.");
1249
+ }
1250
+ const tenant = tenantId.unwrap();
1251
+ const tenantColumn = this.tenantContext.config.tenantIdColumn || "tenant_id";
1252
+ return {
1253
+ ...data,
1254
+ [tenantColumn]: tenant
1255
+ };
1256
+ }
1257
+ // QueryBuilder interface implementation
1258
+ table(table) {
1259
+ this.baseBuilder.table(table);
1260
+ return this;
1261
+ }
1262
+ select(...columns) {
1263
+ this.baseBuilder.select(...columns);
1264
+ return this;
1265
+ }
1266
+ where(field, operator, value) {
1267
+ this.baseBuilder.where(field, operator, value);
1268
+ return this;
1269
+ }
1270
+ orWhere(field, operator, value) {
1271
+ this.baseBuilder.orWhere(field, operator, value);
1272
+ return this;
1273
+ }
1274
+ whereIn(field, values) {
1275
+ this.baseBuilder.whereIn(field, values);
1276
+ return this;
1277
+ }
1278
+ join(table, leftField, rightField, type = "INNER") {
1279
+ this.baseBuilder.join(table, leftField, rightField, type);
1280
+ return this;
1281
+ }
1282
+ orderBy(field, direction = "ASC") {
1283
+ this.baseBuilder.orderBy(field, direction);
1284
+ return this;
1285
+ }
1286
+ groupBy(...fields) {
1287
+ this.baseBuilder.groupBy(...fields);
1288
+ return this;
1289
+ }
1290
+ having(condition) {
1291
+ this.baseBuilder.having(condition);
1292
+ return this;
1293
+ }
1294
+ limit(limit) {
1295
+ this.baseBuilder.limit(limit);
1296
+ return this;
1297
+ }
1298
+ offset(offset) {
1299
+ this.baseBuilder.offset(offset);
1300
+ return this;
1301
+ }
1302
+ async insert(data) {
1303
+ this.validateTenantContext();
1304
+ const dataWithTenant = this.enforceTenantInData(data);
1305
+ this.logger.debug("Tenant-aware INSERT", {
1306
+ table: this.getTableName(),
1307
+ isAdmin: this.isAdminMode
1308
+ });
1309
+ return this.baseBuilder.insert(dataWithTenant);
1310
+ }
1311
+ async insertMany(data) {
1312
+ this.validateTenantContext();
1313
+ const dataWithTenant = data.map((item) => this.enforceTenantInData(item));
1314
+ this.logger.debug("Tenant-aware INSERT MANY", {
1315
+ table: this.getTableName(),
1316
+ count: data.length,
1317
+ isAdmin: this.isAdminMode
1318
+ });
1319
+ return this.baseBuilder.insertMany(dataWithTenant);
1320
+ }
1321
+ async update(data) {
1322
+ this.validateTenantContext();
1323
+ this.injectTenantFilter();
1324
+ this.logger.debug("Tenant-aware UPDATE", {
1325
+ table: this.getTableName(),
1326
+ isAdmin: this.isAdminMode
1327
+ });
1328
+ return this.baseBuilder.update(data);
1329
+ }
1330
+ async delete() {
1331
+ this.validateTenantContext();
1332
+ this.injectTenantFilter();
1333
+ this.logger.debug("Tenant-aware DELETE", {
1334
+ table: this.getTableName(),
1335
+ isAdmin: this.isAdminMode
1336
+ });
1337
+ return this.baseBuilder.delete();
1338
+ }
1339
+ async first() {
1340
+ this.validateTenantContext();
1341
+ this.injectTenantFilter();
1342
+ this.logger.debug("Tenant-aware FIRST", {
1343
+ table: this.getTableName(),
1344
+ isAdmin: this.isAdminMode
1345
+ });
1346
+ return this.baseBuilder.first();
1347
+ }
1348
+ async get() {
1349
+ this.validateTenantContext();
1350
+ this.injectTenantFilter();
1351
+ this.logger.debug("Tenant-aware GET", {
1352
+ table: this.getTableName(),
1353
+ isAdmin: this.isAdminMode
1354
+ });
1355
+ return this.baseBuilder.get();
1356
+ }
1357
+ async count(field = "*") {
1358
+ this.validateTenantContext();
1359
+ this.injectTenantFilter();
1360
+ this.logger.debug("Tenant-aware COUNT", {
1361
+ table: this.getTableName(),
1362
+ field,
1363
+ isAdmin: this.isAdminMode
1364
+ });
1365
+ return this.baseBuilder.count(field);
1366
+ }
1367
+ toSQL() {
1368
+ this.validateTenantContext();
1369
+ this.injectTenantFilter();
1370
+ return this.baseBuilder.toSQL();
1371
+ }
1372
+ getBindings() {
1373
+ return this.baseBuilder.getBindings();
1374
+ }
1375
+ clone() {
1376
+ const clonedBase = this.baseBuilder.clone();
1377
+ const clonedTenantAware = new _TenantAwareQueryBuilder(
1378
+ clonedBase,
1379
+ this.tenantContext,
1380
+ this.logger
1381
+ );
1382
+ clonedTenantAware.isAdminMode = this.isAdminMode;
1383
+ return clonedTenantAware;
1384
+ }
1385
+ };
1386
+ var TenantSchemaManager = class {
1387
+ connection;
1388
+ logger;
1389
+ config;
1390
+ constructor(connection, logger, config) {
1391
+ this.connection = connection;
1392
+ this.logger = logger;
1393
+ this.config = {
1394
+ schemaVersionTable: "schema_versions",
1395
+ ...config
1396
+ };
1397
+ }
1398
+ /**
1399
+ * Create a new tenant schema
1400
+ *
1401
+ * @param tenantId - Tenant identifier
1402
+ * @param options - Schema creation options
1403
+ * @returns Result with void on success or SchemaManagerError
1404
+ */
1405
+ async createTenantSchema(tenantId, options = {}) {
1406
+ const validationResult = this.validateTenantId(tenantId);
1407
+ if (validationResult.isErr()) {
1408
+ return validationResult;
1409
+ }
1410
+ const schemaName = this.getSchemaName(tenantId);
1411
+ try {
1412
+ this.logger.debug("Creating tenant schema", { tenantId, schemaName });
1413
+ if (options.useTransaction) {
1414
+ await this.connection.execute("BEGIN");
1415
+ }
1416
+ const createResult = await this.connection.execute(
1417
+ `CREATE SCHEMA ${schemaName}`,
1418
+ []
1419
+ );
1420
+ if (createResult.isErr()) {
1421
+ if (options.useTransaction) {
1422
+ await this.connection.execute("ROLLBACK");
1423
+ }
1424
+ const error = createResult.variant.error;
1425
+ if (error.message.includes("already exists")) {
1426
+ return Result.err({
1427
+ type: "SCHEMA_EXISTS",
1428
+ message: `Schema for tenant ${tenantId} already exists`
1429
+ });
1430
+ }
1431
+ return Result.err({
1432
+ type: "DATABASE_ERROR",
1433
+ message: `Failed to create schema for tenant ${tenantId}`,
1434
+ cause: error
1435
+ });
1436
+ }
1437
+ if (options.version) {
1438
+ const versionResult = await this.initializeVersionTracking(
1439
+ tenantId,
1440
+ options.version
1441
+ );
1442
+ if (versionResult.isErr()) {
1443
+ if (options.useTransaction) {
1444
+ await this.connection.execute("ROLLBACK");
1445
+ }
1446
+ return versionResult;
1447
+ }
1448
+ }
1449
+ if (options.useTransaction) {
1450
+ await this.connection.execute("COMMIT");
1451
+ }
1452
+ this.logger.info("Tenant schema created", { tenantId, schemaName });
1453
+ return Result.ok(void 0);
1454
+ } catch (error) {
1455
+ if (options.useTransaction) {
1456
+ await this.connection.execute("ROLLBACK");
1457
+ }
1458
+ this.logger.error("Schema creation failed", { error, tenantId });
1459
+ return Result.err({
1460
+ type: "DATABASE_ERROR",
1461
+ message: `Schema creation failed for tenant ${tenantId}: ${error}`,
1462
+ cause: error
1463
+ });
1464
+ }
1465
+ }
1466
+ /**
1467
+ * Drop a tenant schema
1468
+ *
1469
+ * @param tenantId - Tenant identifier
1470
+ * @param options - Drop schema options
1471
+ * @returns Result with void on success or SchemaManagerError
1472
+ */
1473
+ async dropTenantSchema(tenantId, options = {}) {
1474
+ const validationResult = this.validateTenantId(tenantId);
1475
+ if (validationResult.isErr()) {
1476
+ return validationResult;
1477
+ }
1478
+ const schemaName = this.getSchemaName(tenantId);
1479
+ try {
1480
+ this.logger.debug("Dropping tenant schema", { tenantId, schemaName });
1481
+ let sql = `DROP SCHEMA ${options.ifExists ? "IF EXISTS " : ""}${schemaName}`;
1482
+ if (options.cascade) {
1483
+ sql += " CASCADE";
1484
+ }
1485
+ const dropResult = await this.connection.execute(sql, []);
1486
+ if (dropResult.isErr()) {
1487
+ const error = dropResult.variant.error;
1488
+ return Result.err({
1489
+ type: "DATABASE_ERROR",
1490
+ message: `Failed to drop schema for tenant ${tenantId}`,
1491
+ cause: error
1492
+ });
1493
+ }
1494
+ if (options.cleanupVersions) {
1495
+ await this.connection.execute(
1496
+ `DELETE FROM ${this.config.schemaVersionTable} WHERE tenant_id = ?`,
1497
+ [tenantId]
1498
+ );
1499
+ }
1500
+ this.logger.info("Tenant schema dropped", { tenantId, schemaName });
1501
+ return Result.ok(void 0);
1502
+ } catch (error) {
1503
+ this.logger.error("Schema drop failed", { error, tenantId });
1504
+ return Result.err({
1505
+ type: "DATABASE_ERROR",
1506
+ message: `Schema drop failed for tenant ${tenantId}: ${error}`,
1507
+ cause: error
1508
+ });
1509
+ }
1510
+ }
1511
+ /**
1512
+ * Check if tenant schema exists
1513
+ *
1514
+ * @param tenantId - Tenant identifier
1515
+ * @returns Result with boolean indicating existence
1516
+ */
1517
+ async schemaExists(tenantId) {
1518
+ const schemaName = this.getSchemaName(tenantId);
1519
+ try {
1520
+ const result = await this.connection.execute(
1521
+ `SELECT schema_name FROM information_schema.schemata WHERE schema_name = ?`,
1522
+ [schemaName]
1523
+ );
1524
+ if (result.isErr()) {
1525
+ return Result.err({
1526
+ type: "DATABASE_ERROR",
1527
+ message: "Failed to check schema existence",
1528
+ cause: result.variant.error
1529
+ });
1530
+ }
1531
+ const data = result.unwrap();
1532
+ const rows = data.rows || data || [];
1533
+ return Result.ok(rows.length > 0);
1534
+ } catch (error) {
1535
+ return Result.err({
1536
+ type: "DATABASE_ERROR",
1537
+ message: `Failed to check schema existence: ${error}`,
1538
+ cause: error
1539
+ });
1540
+ }
1541
+ }
1542
+ /**
1543
+ * Get current schema version for tenant
1544
+ *
1545
+ * @param tenantId - Tenant identifier
1546
+ * @returns Result with version string or null
1547
+ */
1548
+ async getSchemaVersion(tenantId) {
1549
+ try {
1550
+ const result = await this.connection.execute(
1551
+ `SELECT version FROM ${this.config.schemaVersionTable} WHERE tenant_id = ? ORDER BY applied_at DESC LIMIT 1`,
1552
+ [tenantId]
1553
+ );
1554
+ if (result.isErr()) {
1555
+ return Result.err({
1556
+ type: "DATABASE_ERROR",
1557
+ message: "Failed to get schema version",
1558
+ cause: result.variant.error
1559
+ });
1560
+ }
1561
+ const data = result.unwrap();
1562
+ const rows = data.rows || data || [];
1563
+ if (rows.length === 0) {
1564
+ return Result.ok(null);
1565
+ }
1566
+ return Result.ok(rows[0].version);
1567
+ } catch (error) {
1568
+ return Result.err({
1569
+ type: "DATABASE_ERROR",
1570
+ message: `Failed to get schema version: ${error}`,
1571
+ cause: error
1572
+ });
1573
+ }
1574
+ }
1575
+ /**
1576
+ * Update schema version for tenant
1577
+ *
1578
+ * @param tenantId - Tenant identifier
1579
+ * @param version - New version string
1580
+ * @returns Result with void on success
1581
+ */
1582
+ async updateSchemaVersion(tenantId, version2) {
1583
+ if (!this.isValidVersion(version2)) {
1584
+ return Result.err({
1585
+ type: "INVALID_VERSION",
1586
+ message: `Invalid version format: ${version2}. Expected semver format (e.g., 1.0.0)`
1587
+ });
1588
+ }
1589
+ try {
1590
+ const result = await this.connection.execute(
1591
+ `INSERT INTO ${this.config.schemaVersionTable} (tenant_id, version, applied_at) VALUES (?, ?, ?)`,
1592
+ [tenantId, version2, /* @__PURE__ */ new Date()]
1593
+ );
1594
+ if (result.isErr()) {
1595
+ return Result.err({
1596
+ type: "DATABASE_ERROR",
1597
+ message: "Failed to update schema version",
1598
+ cause: result.variant.error
1599
+ });
1600
+ }
1601
+ return Result.ok(void 0);
1602
+ } catch (error) {
1603
+ return Result.err({
1604
+ type: "DATABASE_ERROR",
1605
+ message: `Failed to update schema version: ${error}`,
1606
+ cause: error
1607
+ });
1608
+ }
1609
+ }
1610
+ /**
1611
+ * Get version history for tenant
1612
+ *
1613
+ * @param tenantId - Tenant identifier
1614
+ * @returns Result with array of schema versions
1615
+ */
1616
+ async getVersionHistory(tenantId) {
1617
+ try {
1618
+ const result = await this.connection.execute(
1619
+ `SELECT version, applied_at, description FROM ${this.config.schemaVersionTable} WHERE tenant_id = ? ORDER BY applied_at ASC`,
1620
+ [tenantId]
1621
+ );
1622
+ if (result.isErr()) {
1623
+ return Result.err({
1624
+ type: "DATABASE_ERROR",
1625
+ message: "Failed to get version history",
1626
+ cause: result.variant.error
1627
+ });
1628
+ }
1629
+ const data = result.unwrap();
1630
+ const rows = data.rows || data || [];
1631
+ return Result.ok(rows);
1632
+ } catch (error) {
1633
+ return Result.err({
1634
+ type: "DATABASE_ERROR",
1635
+ message: `Failed to get version history: ${error}`,
1636
+ cause: error
1637
+ });
1638
+ }
1639
+ }
1640
+ /**
1641
+ * Apply a migration to tenant schema
1642
+ *
1643
+ * @param tenantId - Tenant identifier
1644
+ * @param migration - Migration definition
1645
+ * @returns Result with void on success
1646
+ */
1647
+ async applyMigration(tenantId, migration) {
1648
+ const schemaName = this.getSchemaName(tenantId);
1649
+ try {
1650
+ this.logger.debug("Applying migration", {
1651
+ tenantId,
1652
+ version: migration.version
1653
+ });
1654
+ const currentVersion = await this.getSchemaVersion(tenantId);
1655
+ if (currentVersion.isOk() && currentVersion.unwrap() === migration.version) {
1656
+ this.logger.info("Migration already applied", {
1657
+ tenantId,
1658
+ version: migration.version
1659
+ });
1660
+ return Result.ok(void 0);
1661
+ }
1662
+ await this.connection.execute(`SET search_path TO ${schemaName}`, []);
1663
+ const migrationResult = await this.connection.execute(migration.sql, []);
1664
+ if (migrationResult.isErr()) {
1665
+ return Result.err({
1666
+ type: "MIGRATION_FAILED",
1667
+ message: `Migration ${migration.version} failed for tenant ${tenantId}`,
1668
+ cause: migrationResult.variant.error
1669
+ });
1670
+ }
1671
+ await this.connection.execute(
1672
+ `INSERT INTO ${this.config.schemaVersionTable} (tenant_id, version, applied_at, description) VALUES (?, ?, ?, ?)`,
1673
+ [tenantId, migration.version, /* @__PURE__ */ new Date(), migration.description || ""]
1674
+ );
1675
+ this.logger.info("Migration applied", {
1676
+ tenantId,
1677
+ version: migration.version
1678
+ });
1679
+ return Result.ok(void 0);
1680
+ } catch (error) {
1681
+ this.logger.error("Migration failed", {
1682
+ error,
1683
+ tenantId,
1684
+ version: migration.version
1685
+ });
1686
+ return Result.err({
1687
+ type: "MIGRATION_FAILED",
1688
+ message: `Migration failed: ${error}`,
1689
+ cause: error
1690
+ });
1691
+ }
1692
+ }
1693
+ /**
1694
+ * Get the schema name for a tenant
1695
+ *
1696
+ * @param tenantId - Tenant identifier
1697
+ * @returns Schema name
1698
+ */
1699
+ getSchemaName(tenantId) {
1700
+ if (/[;'"`\\]/.test(tenantId)) {
1701
+ throw new Error(`Invalid tenant ID: contains dangerous characters`);
1702
+ }
1703
+ return `${this.config.baseSchemaName}_tenant_${tenantId.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
1704
+ }
1705
+ /**
1706
+ * Validate tenant ID format
1707
+ */
1708
+ validateTenantId(tenantId) {
1709
+ if (!tenantId || typeof tenantId !== "string") {
1710
+ return Result.err({
1711
+ type: "INVALID_TENANT_ID",
1712
+ message: "Tenant ID must be a non-empty string"
1713
+ });
1714
+ }
1715
+ if (tenantId.trim().length === 0) {
1716
+ return Result.err({
1717
+ type: "INVALID_TENANT_ID",
1718
+ message: "Tenant ID cannot be whitespace only"
1719
+ });
1720
+ }
1721
+ if (/[;'"`\\]/.test(tenantId)) {
1722
+ return Result.err({
1723
+ type: "INVALID_TENANT_ID",
1724
+ message: "Tenant ID contains invalid characters"
1725
+ });
1726
+ }
1727
+ return Result.ok(void 0);
1728
+ }
1729
+ /**
1730
+ * Validate version format (simple semver check)
1731
+ */
1732
+ isValidVersion(version2) {
1733
+ return /^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$/.test(version2);
1734
+ }
1735
+ /**
1736
+ * Initialize version tracking for new tenant
1737
+ */
1738
+ async initializeVersionTracking(tenantId, version2) {
1739
+ if (!this.isValidVersion(version2)) {
1740
+ return Result.err({
1741
+ type: "INVALID_VERSION",
1742
+ message: `Invalid version format: ${version2}`
1743
+ });
1744
+ }
1745
+ const result = await this.connection.execute(
1746
+ `INSERT INTO ${this.config.schemaVersionTable} (tenant_id, version, applied_at) VALUES (?, ?, ?)`,
1747
+ [tenantId, version2, /* @__PURE__ */ new Date()]
1748
+ );
1749
+ if (result.isErr()) {
1750
+ return Result.err({
1751
+ type: "DATABASE_ERROR",
1752
+ message: "Failed to initialize version tracking",
1753
+ cause: result.variant.error
1754
+ });
1755
+ }
1756
+ return Result.ok(void 0);
1757
+ }
1758
+ };
1759
+ var MemoryCache = class {
1760
+ config;
1761
+ logger;
1762
+ store;
1763
+ lruList = [];
1764
+ // Most recent at end
1765
+ stats = {
1766
+ hits: 0,
1767
+ misses: 0,
1768
+ evictions: 0
1769
+ };
1770
+ constructor(config, logger) {
1771
+ this.config = config;
1772
+ this.logger = logger;
1773
+ this.store = /* @__PURE__ */ new Map();
1774
+ }
1775
+ async get(key) {
1776
+ try {
1777
+ const fullKey = this.getFullKey(key);
1778
+ const entry = this.store.get(fullKey);
1779
+ if (!entry) {
1780
+ this.stats.misses++;
1781
+ return Result.ok(Option.none());
1782
+ }
1783
+ if (this.isExpired(entry)) {
1784
+ this.store.delete(fullKey);
1785
+ this.removeLRU(fullKey);
1786
+ this.stats.misses++;
1787
+ return Result.ok(Option.none());
1788
+ }
1789
+ this.touchEntry(fullKey, entry);
1790
+ this.stats.hits++;
1791
+ return Result.ok(Option.some(entry.value));
1792
+ } catch (error) {
1793
+ this.logger.error("Cache get failed", { error, key });
1794
+ return Result.err({
1795
+ type: "GET_FAILED",
1796
+ message: `Failed to get cache entry: ${error}`,
1797
+ cause: error
1798
+ });
1799
+ }
1800
+ }
1801
+ async getWithMetadata(key) {
1802
+ try {
1803
+ const fullKey = this.getFullKey(key);
1804
+ const entry = this.store.get(fullKey);
1805
+ if (!entry) {
1806
+ this.stats.misses++;
1807
+ return Result.ok(Option.none());
1808
+ }
1809
+ if (this.isExpired(entry)) {
1810
+ this.store.delete(fullKey);
1811
+ this.removeLRU(fullKey);
1812
+ this.stats.misses++;
1813
+ return Result.ok(Option.none());
1814
+ }
1815
+ this.touchEntry(fullKey, entry);
1816
+ this.stats.hits++;
1817
+ return Result.ok(Option.some(entry));
1818
+ } catch (error) {
1819
+ this.logger.error("Cache getWithMetadata failed", { error, key });
1820
+ return Result.err({
1821
+ type: "GET_FAILED",
1822
+ message: `Failed to get cache entry with metadata: ${error}`,
1823
+ cause: error
1824
+ });
1825
+ }
1826
+ }
1827
+ async set(key, value, ttl) {
1828
+ try {
1829
+ const fullKey = this.getFullKey(key);
1830
+ const effectiveTTL = ttl !== void 0 ? ttl : this.config.defaultTTL;
1831
+ if (this.config.maxItems && this.store.size >= this.config.maxItems && !this.store.has(fullKey)) {
1832
+ this.evictLRU();
1833
+ }
1834
+ const now = /* @__PURE__ */ new Date();
1835
+ const expiresAt = effectiveTTL > 0 ? new Date(now.getTime() + effectiveTTL * 1e3) : void 0;
1836
+ const metadata = {
1837
+ key: fullKey,
1838
+ createdAt: now,
1839
+ lastAccessedAt: now,
1840
+ accessCount: 0,
1841
+ size: this.estimateSize(value),
1842
+ ttl: effectiveTTL,
1843
+ expiresAt
1844
+ };
1845
+ const entry = {
1846
+ value,
1847
+ metadata
1848
+ };
1849
+ if (this.store.has(fullKey)) {
1850
+ this.removeLRU(fullKey);
1851
+ }
1852
+ this.store.set(fullKey, entry);
1853
+ this.lruList.push(fullKey);
1854
+ this.logger.debug("Cache set", { key, ttl: effectiveTTL });
1855
+ return Result.ok(void 0);
1856
+ } catch (error) {
1857
+ this.logger.error("Cache set failed", { error, key });
1858
+ return Result.err({
1859
+ type: "SET_FAILED",
1860
+ message: `Failed to set cache entry: ${error}`,
1861
+ cause: error
1862
+ });
1863
+ }
1864
+ }
1865
+ async delete(key) {
1866
+ try {
1867
+ const fullKey = this.getFullKey(key);
1868
+ const existed = this.store.delete(fullKey);
1869
+ if (existed) {
1870
+ this.removeLRU(fullKey);
1871
+ this.logger.debug("Cache delete", { key });
1872
+ }
1873
+ return Result.ok(existed);
1874
+ } catch (error) {
1875
+ this.logger.error("Cache delete failed", { error, key });
1876
+ return Result.err({
1877
+ type: "DELETE_FAILED",
1878
+ message: `Failed to delete cache entry: ${error}`,
1879
+ cause: error
1880
+ });
1881
+ }
1882
+ }
1883
+ async deleteMany(keys) {
1884
+ try {
1885
+ let deleted = 0;
1886
+ for (const key of keys) {
1887
+ const result = await this.delete(key);
1888
+ if (result.isOk() && result.unwrap()) {
1889
+ deleted++;
1890
+ }
1891
+ }
1892
+ return Result.ok(deleted);
1893
+ } catch (error) {
1894
+ this.logger.error("Cache deleteMany failed", { error });
1895
+ return Result.err({
1896
+ type: "DELETE_FAILED",
1897
+ message: `Failed to delete multiple cache entries: ${error}`,
1898
+ cause: error
1899
+ });
1900
+ }
1901
+ }
1902
+ async clear() {
1903
+ try {
1904
+ this.store.clear();
1905
+ this.lruList.length = 0;
1906
+ this.logger.debug("Cache cleared");
1907
+ return Result.ok(void 0);
1908
+ } catch (error) {
1909
+ this.logger.error("Cache clear failed", { error });
1910
+ return Result.err({
1911
+ type: "CLEAR_FAILED",
1912
+ message: `Failed to clear cache: ${error}`,
1913
+ cause: error
1914
+ });
1915
+ }
1916
+ }
1917
+ async has(key) {
1918
+ try {
1919
+ const fullKey = this.getFullKey(key);
1920
+ const entry = this.store.get(fullKey);
1921
+ if (!entry) {
1922
+ return Result.ok(false);
1923
+ }
1924
+ if (this.isExpired(entry)) {
1925
+ this.store.delete(fullKey);
1926
+ this.removeLRU(fullKey);
1927
+ return Result.ok(false);
1928
+ }
1929
+ return Result.ok(true);
1930
+ } catch (error) {
1931
+ this.logger.error("Cache has failed", { error, key });
1932
+ return Result.err({
1933
+ type: "GET_FAILED",
1934
+ message: `Failed to check cache key existence: ${error}`,
1935
+ cause: error
1936
+ });
1937
+ }
1938
+ }
1939
+ async invalidate(pattern) {
1940
+ try {
1941
+ const fullPattern = this.config.keyPrefix ? `${this.config.keyPrefix}${pattern}` : pattern;
1942
+ const regex = this.patternToRegex(fullPattern);
1943
+ let invalidated = 0;
1944
+ for (const key of this.store.keys()) {
1945
+ if (regex.test(key)) {
1946
+ this.store.delete(key);
1947
+ this.removeLRU(key);
1948
+ invalidated++;
1949
+ }
1950
+ }
1951
+ this.logger.debug("Cache invalidate", { pattern, count: invalidated });
1952
+ return Result.ok(invalidated);
1953
+ } catch (error) {
1954
+ this.logger.error("Cache invalidate failed", { error, pattern });
1955
+ return Result.err({
1956
+ type: "DELETE_FAILED",
1957
+ message: `Failed to invalidate cache entries: ${error}`,
1958
+ cause: error
1959
+ });
1960
+ }
1961
+ }
1962
+ async invalidateTable(table) {
1963
+ try {
1964
+ const pattern = `*${table}*`;
1965
+ return await this.invalidate(pattern);
1966
+ } catch (error) {
1967
+ this.logger.error("Cache invalidateTable failed", { error, table });
1968
+ return Result.err({
1969
+ type: "DELETE_FAILED",
1970
+ message: `Failed to invalidate table cache: ${error}`,
1971
+ cause: error
1972
+ });
1973
+ }
1974
+ }
1975
+ getStats() {
1976
+ try {
1977
+ const totalAccesses = this.stats.hits + this.stats.misses;
1978
+ const hitRate = totalAccesses > 0 ? this.stats.hits / totalAccesses : 0;
1979
+ let totalSize = 0;
1980
+ for (const entry of this.store.values()) {
1981
+ totalSize += entry.metadata.size;
1982
+ }
1983
+ return Result.ok({
1984
+ itemCount: this.store.size,
1985
+ totalSize,
1986
+ hits: this.stats.hits,
1987
+ misses: this.stats.misses,
1988
+ hitRate,
1989
+ evictions: this.stats.evictions
1990
+ });
1991
+ } catch (error) {
1992
+ return Result.err({
1993
+ type: "UNKNOWN",
1994
+ message: `Failed to get cache stats: ${error}`,
1995
+ cause: error
1996
+ });
1997
+ }
1998
+ }
1999
+ resetStats() {
2000
+ try {
2001
+ this.stats.hits = 0;
2002
+ this.stats.misses = 0;
2003
+ this.stats.evictions = 0;
2004
+ return Result.ok(void 0);
2005
+ } catch (error) {
2006
+ return Result.err({
2007
+ type: "UNKNOWN",
2008
+ message: `Failed to reset cache stats: ${error}`,
2009
+ cause: error
2010
+ });
2011
+ }
2012
+ }
2013
+ generateKey(sql, params) {
2014
+ const data = {
2015
+ sql,
2016
+ params: params || []
2017
+ };
2018
+ const hash = crypto.createHash("sha256").update(JSON.stringify(data)).digest("hex");
2019
+ return hash.substring(0, 16);
2020
+ }
2021
+ /**
2022
+ * Get full cache key with prefix
2023
+ */
2024
+ getFullKey(key) {
2025
+ return this.config.keyPrefix ? `${this.config.keyPrefix}${key}` : key;
2026
+ }
2027
+ /**
2028
+ * Check if entry is expired
2029
+ */
2030
+ isExpired(entry) {
2031
+ if (!entry.metadata.expiresAt) {
2032
+ return false;
2033
+ }
2034
+ return /* @__PURE__ */ new Date() > entry.metadata.expiresAt;
2035
+ }
2036
+ /**
2037
+ * Update entry access metadata and LRU position
2038
+ */
2039
+ touchEntry(key, entry) {
2040
+ entry.metadata.lastAccessedAt = /* @__PURE__ */ new Date();
2041
+ entry.metadata.accessCount++;
2042
+ this.removeLRU(key);
2043
+ this.lruList.push(key);
2044
+ }
2045
+ /**
2046
+ * Remove key from LRU list
2047
+ */
2048
+ removeLRU(key) {
2049
+ const index = this.lruList.indexOf(key);
2050
+ if (index > -1) {
2051
+ this.lruList.splice(index, 1);
2052
+ }
2053
+ }
2054
+ /**
2055
+ * Evict least recently used entry
2056
+ */
2057
+ evictLRU() {
2058
+ if (this.lruList.length === 0) {
2059
+ return;
2060
+ }
2061
+ const lruKey = this.lruList[0];
2062
+ this.store.delete(lruKey);
2063
+ this.lruList.shift();
2064
+ this.stats.evictions++;
2065
+ this.logger.debug("LRU eviction", { key: lruKey });
2066
+ }
2067
+ /**
2068
+ * Estimate size of value in bytes
2069
+ */
2070
+ estimateSize(value) {
2071
+ try {
2072
+ return JSON.stringify(value).length;
2073
+ } catch {
2074
+ return 0;
2075
+ }
2076
+ }
2077
+ /**
2078
+ * Convert glob pattern to regex
2079
+ */
2080
+ patternToRegex(pattern) {
2081
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
2082
+ return new RegExp(`^${escaped}$`);
2083
+ }
2084
+ };
2085
+ var KVCache = class {
2086
+ config;
2087
+ kv;
2088
+ logger;
2089
+ stats = {
2090
+ hits: 0,
2091
+ misses: 0
2092
+ };
2093
+ constructor(kv, config, logger) {
2094
+ this.kv = kv;
2095
+ this.config = config;
2096
+ this.logger = logger;
2097
+ }
2098
+ async get(key) {
2099
+ try {
2100
+ const fullKey = this.getFullKey(key);
2101
+ let value = await this.kv.get(fullKey, { type: "json" });
2102
+ if (value === null) {
2103
+ this.stats.misses++;
2104
+ return Result.ok(Option.none());
2105
+ }
2106
+ if (typeof value === "string") {
2107
+ try {
2108
+ value = JSON.parse(value);
2109
+ } catch {
2110
+ this.stats.hits++;
2111
+ return Result.ok(Option.some(value));
2112
+ }
2113
+ }
2114
+ this.stats.hits++;
2115
+ if (value && typeof value === "object" && "value" in value) {
2116
+ return Result.ok(Option.some(value.value));
2117
+ }
2118
+ return Result.ok(Option.some(value));
2119
+ } catch (error) {
2120
+ this.logger.error("KV cache get failed", { error, key });
2121
+ return Result.err({
2122
+ type: "GET_FAILED",
2123
+ message: `Failed to get cache entry from KV: ${error}`,
2124
+ cause: error
2125
+ });
2126
+ }
2127
+ }
2128
+ async getWithMetadata(key) {
2129
+ try {
2130
+ const fullKey = this.getFullKey(key);
2131
+ let value = await this.kv.get(fullKey, { type: "json" });
2132
+ if (value === null) {
2133
+ this.stats.misses++;
2134
+ return Result.ok(Option.none());
2135
+ }
2136
+ if (typeof value === "string") {
2137
+ try {
2138
+ value = JSON.parse(value);
2139
+ } catch {
2140
+ const now2 = /* @__PURE__ */ new Date();
2141
+ this.stats.hits++;
2142
+ const entry2 = {
2143
+ value,
2144
+ metadata: {
2145
+ key: fullKey,
2146
+ createdAt: now2,
2147
+ lastAccessedAt: now2,
2148
+ accessCount: 1,
2149
+ size: this.estimateSize(value),
2150
+ ttl: this.config.defaultTTL
2151
+ }
2152
+ };
2153
+ return Result.ok(Option.some(entry2));
2154
+ }
2155
+ }
2156
+ this.stats.hits++;
2157
+ if (value && typeof value === "object" && "value" in value && "metadata" in value) {
2158
+ const entry2 = value;
2159
+ entry2.metadata.createdAt = new Date(entry2.metadata.createdAt);
2160
+ entry2.metadata.lastAccessedAt = new Date(entry2.metadata.lastAccessedAt);
2161
+ if (entry2.metadata.expiresAt) {
2162
+ entry2.metadata.expiresAt = new Date(entry2.metadata.expiresAt);
2163
+ }
2164
+ return Result.ok(Option.some(entry2));
2165
+ }
2166
+ const now = /* @__PURE__ */ new Date();
2167
+ const entry = {
2168
+ value,
2169
+ metadata: {
2170
+ key: fullKey,
2171
+ createdAt: now,
2172
+ lastAccessedAt: now,
2173
+ accessCount: 1,
2174
+ size: this.estimateSize(value),
2175
+ ttl: this.config.defaultTTL
2176
+ }
2177
+ };
2178
+ return Result.ok(Option.some(entry));
2179
+ } catch (error) {
2180
+ this.logger.error("KV cache getWithMetadata failed", { error, key });
2181
+ return Result.err({
2182
+ type: "GET_FAILED",
2183
+ message: `Failed to get cache entry with metadata from KV: ${error}`,
2184
+ cause: error
2185
+ });
2186
+ }
2187
+ }
2188
+ async set(key, value, ttl) {
2189
+ try {
2190
+ const fullKey = this.getFullKey(key);
2191
+ const effectiveTTL = ttl !== void 0 ? ttl : this.config.defaultTTL;
2192
+ const now = /* @__PURE__ */ new Date();
2193
+ const expiresAt = effectiveTTL > 0 ? new Date(now.getTime() + effectiveTTL * 1e3) : void 0;
2194
+ const metadata = {
2195
+ key: fullKey,
2196
+ createdAt: now,
2197
+ lastAccessedAt: now,
2198
+ accessCount: 0,
2199
+ size: this.estimateSize(value),
2200
+ ttl: effectiveTTL,
2201
+ expiresAt
2202
+ };
2203
+ const entry = {
2204
+ value,
2205
+ metadata
2206
+ };
2207
+ const options = effectiveTTL > 0 ? { expirationTtl: effectiveTTL } : {};
2208
+ await this.kv.put(fullKey, JSON.stringify(entry), options);
2209
+ this.logger.debug("KV cache set", { key, ttl: effectiveTTL });
2210
+ return Result.ok(void 0);
2211
+ } catch (error) {
2212
+ this.logger.error("KV cache set failed", { error, key });
2213
+ return Result.err({
2214
+ type: "SET_FAILED",
2215
+ message: `Failed to set cache entry in KV: ${error}`,
2216
+ cause: error
2217
+ });
2218
+ }
2219
+ }
2220
+ async delete(key) {
2221
+ try {
2222
+ const fullKey = this.getFullKey(key);
2223
+ const existed = await this.kv.get(fullKey);
2224
+ await this.kv.delete(fullKey);
2225
+ const wasDeleted = existed !== null;
2226
+ if (wasDeleted) {
2227
+ this.logger.debug("KV cache delete", { key });
2228
+ }
2229
+ return Result.ok(wasDeleted);
2230
+ } catch (error) {
2231
+ this.logger.error("KV cache delete failed", { error, key });
2232
+ return Result.err({
2233
+ type: "DELETE_FAILED",
2234
+ message: `Failed to delete cache entry from KV: ${error}`,
2235
+ cause: error
2236
+ });
2237
+ }
2238
+ }
2239
+ async deleteMany(keys) {
2240
+ try {
2241
+ let deleted = 0;
2242
+ for (const key of keys) {
2243
+ const result = await this.delete(key);
2244
+ if (result.isOk() && result.unwrap()) {
2245
+ deleted++;
2246
+ }
2247
+ }
2248
+ return Result.ok(deleted);
2249
+ } catch (error) {
2250
+ this.logger.error("KV cache deleteMany failed", { error });
2251
+ return Result.err({
2252
+ type: "DELETE_FAILED",
2253
+ message: `Failed to delete multiple cache entries from KV: ${error}`,
2254
+ cause: error
2255
+ });
2256
+ }
2257
+ }
2258
+ async clear() {
2259
+ try {
2260
+ const prefix = this.config.keyPrefix || "";
2261
+ let cursor;
2262
+ let totalDeleted = 0;
2263
+ do {
2264
+ const listResult = await this.kv.list({
2265
+ prefix,
2266
+ limit: 1e3,
2267
+ cursor
2268
+ });
2269
+ const deletePromises = listResult.keys.map(({ name }) => this.kv.delete(name));
2270
+ await Promise.all(deletePromises);
2271
+ totalDeleted += listResult.keys.length;
2272
+ cursor = listResult.list_complete ? void 0 : listResult.cursor;
2273
+ } while (cursor);
2274
+ this.logger.debug("KV cache cleared", { deleted: totalDeleted });
2275
+ return Result.ok(void 0);
2276
+ } catch (error) {
2277
+ this.logger.error("KV cache clear failed", { error });
2278
+ return Result.err({
2279
+ type: "CLEAR_FAILED",
2280
+ message: `Failed to clear KV cache: ${error}`,
2281
+ cause: error
2282
+ });
2283
+ }
2284
+ }
2285
+ async has(key) {
2286
+ try {
2287
+ const fullKey = this.getFullKey(key);
2288
+ const value = await this.kv.get(fullKey);
2289
+ return Result.ok(value !== null);
2290
+ } catch (error) {
2291
+ this.logger.error("KV cache has failed", { error, key });
2292
+ return Result.err({
2293
+ type: "GET_FAILED",
2294
+ message: `Failed to check cache key existence in KV: ${error}`,
2295
+ cause: error
2296
+ });
2297
+ }
2298
+ }
2299
+ async invalidate(pattern) {
2300
+ try {
2301
+ const fullPattern = this.config.keyPrefix ? `${this.config.keyPrefix}${pattern}` : pattern;
2302
+ const prefix = fullPattern.split("*")[0].split("?")[0];
2303
+ const regex = this.patternToRegex(fullPattern);
2304
+ let invalidated = 0;
2305
+ let cursor;
2306
+ do {
2307
+ const listResult = await this.kv.list({
2308
+ prefix,
2309
+ limit: 1e3,
2310
+ cursor
2311
+ });
2312
+ const matchingKeys = listResult.keys.map(({ name }) => name).filter((name) => regex.test(name));
2313
+ const deletePromises = matchingKeys.map((name) => this.kv.delete(name));
2314
+ await Promise.all(deletePromises);
2315
+ invalidated += matchingKeys.length;
2316
+ cursor = listResult.list_complete ? void 0 : listResult.cursor;
2317
+ } while (cursor);
2318
+ this.logger.debug("KV cache invalidate", { pattern, count: invalidated });
2319
+ return Result.ok(invalidated);
2320
+ } catch (error) {
2321
+ this.logger.error("KV cache invalidate failed", { error, pattern });
2322
+ return Result.err({
2323
+ type: "DELETE_FAILED",
2324
+ message: `Failed to invalidate cache entries in KV: ${error}`,
2325
+ cause: error
2326
+ });
2327
+ }
2328
+ }
2329
+ async invalidateTable(table) {
2330
+ try {
2331
+ const pattern = `*${table}*`;
2332
+ return await this.invalidate(pattern);
2333
+ } catch (error) {
2334
+ this.logger.error("KV cache invalidateTable failed", { error, table });
2335
+ return Result.err({
2336
+ type: "DELETE_FAILED",
2337
+ message: `Failed to invalidate table cache in KV: ${error}`,
2338
+ cause: error
2339
+ });
2340
+ }
2341
+ }
2342
+ getStats() {
2343
+ try {
2344
+ const totalAccesses = this.stats.hits + this.stats.misses;
2345
+ const hitRate = totalAccesses > 0 ? this.stats.hits / totalAccesses : 0;
2346
+ return Result.ok({
2347
+ itemCount: 0,
2348
+ // Not available for KV
2349
+ totalSize: 0,
2350
+ // Not available for KV
2351
+ hits: this.stats.hits,
2352
+ misses: this.stats.misses,
2353
+ hitRate,
2354
+ evictions: 0
2355
+ // KV handles eviction internally
2356
+ });
2357
+ } catch (error) {
2358
+ return Result.err({
2359
+ type: "UNKNOWN",
2360
+ message: `Failed to get KV cache stats: ${error}`,
2361
+ cause: error
2362
+ });
2363
+ }
2364
+ }
2365
+ resetStats() {
2366
+ try {
2367
+ this.stats.hits = 0;
2368
+ this.stats.misses = 0;
2369
+ return Result.ok(void 0);
2370
+ } catch (error) {
2371
+ return Result.err({
2372
+ type: "UNKNOWN",
2373
+ message: `Failed to reset KV cache stats: ${error}`,
2374
+ cause: error
2375
+ });
2376
+ }
2377
+ }
2378
+ generateKey(sql, params) {
2379
+ const data = {
2380
+ sql,
2381
+ params: params || []
2382
+ };
2383
+ const hash = crypto.createHash("sha256").update(JSON.stringify(data)).digest("hex");
2384
+ return hash.substring(0, 16);
2385
+ }
2386
+ /**
2387
+ * Get full cache key with prefix
2388
+ */
2389
+ getFullKey(key) {
2390
+ return this.config.keyPrefix ? `${this.config.keyPrefix}${key}` : key;
2391
+ }
2392
+ /**
2393
+ * Estimate size of value in bytes
2394
+ */
2395
+ estimateSize(value) {
2396
+ try {
2397
+ return JSON.stringify(value).length;
2398
+ } catch {
2399
+ return 0;
2400
+ }
2401
+ }
2402
+ /**
2403
+ * Convert glob pattern to regex
2404
+ */
2405
+ patternToRegex(pattern) {
2406
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
2407
+ return new RegExp(`^${escaped}$`);
2408
+ }
2409
+ };
2410
+ var CacheInvalidator = class {
2411
+ cache;
2412
+ eventBus;
2413
+ logger;
2414
+ // Tag index: tag -> Set<key>
2415
+ tagIndex = /* @__PURE__ */ new Map();
2416
+ // Entry tags: key -> Set<tag>
2417
+ entryTags = /* @__PURE__ */ new Map();
2418
+ // Statistics
2419
+ stats = {
2420
+ totalInvalidations: 0,
2421
+ tagInvalidations: 0,
2422
+ patternInvalidations: 0,
2423
+ keyInvalidations: 0,
2424
+ expiredInvalidations: 0
2425
+ };
2426
+ constructor(cache, eventBus, logger) {
2427
+ this.cache = cache;
2428
+ this.eventBus = eventBus;
2429
+ this.logger = logger;
2430
+ }
2431
+ /**
2432
+ * Tag a cache entry for group invalidation
2433
+ *
2434
+ * @param key - Cache key
2435
+ * @param tags - Array of tags to associate with this entry
2436
+ * @returns Result with void on success
2437
+ */
2438
+ async tagEntry(key, tags) {
2439
+ try {
2440
+ if (!this.entryTags.has(key)) {
2441
+ this.entryTags.set(key, /* @__PURE__ */ new Set());
2442
+ }
2443
+ const entryTagSet = this.entryTags.get(key);
2444
+ for (const tag of tags) {
2445
+ entryTagSet.add(tag);
2446
+ if (!this.tagIndex.has(tag)) {
2447
+ this.tagIndex.set(tag, /* @__PURE__ */ new Set());
2448
+ }
2449
+ this.tagIndex.get(tag).add(key);
2450
+ }
2451
+ this.logger.debug("Tagged cache entry", { key, tags });
2452
+ return Result.ok(void 0);
2453
+ } catch (error) {
2454
+ this.logger.error("Failed to tag entry", { error, key, tags });
2455
+ return Result.err({
2456
+ type: "UNKNOWN",
2457
+ message: `Failed to tag entry: ${error}`,
2458
+ cause: error
2459
+ });
2460
+ }
2461
+ }
2462
+ /**
2463
+ * Remove a tag from a cache entry
2464
+ *
2465
+ * @param key - Cache key
2466
+ * @param tag - Tag to remove
2467
+ * @returns Result with void on success
2468
+ */
2469
+ async removeTag(key, tag) {
2470
+ try {
2471
+ const entryTagSet = this.entryTags.get(key);
2472
+ if (entryTagSet) {
2473
+ entryTagSet.delete(tag);
2474
+ if (entryTagSet.size === 0) {
2475
+ this.entryTags.delete(key);
2476
+ }
2477
+ }
2478
+ const tagSet = this.tagIndex.get(tag);
2479
+ if (tagSet) {
2480
+ tagSet.delete(key);
2481
+ if (tagSet.size === 0) {
2482
+ this.tagIndex.delete(tag);
2483
+ }
2484
+ }
2485
+ this.logger.debug("Removed tag from entry", { key, tag });
2486
+ return Result.ok(void 0);
2487
+ } catch (error) {
2488
+ this.logger.error("Failed to remove tag", { error, key, tag });
2489
+ return Result.err({
2490
+ type: "UNKNOWN",
2491
+ message: `Failed to remove tag: ${error}`,
2492
+ cause: error
2493
+ });
2494
+ }
2495
+ }
2496
+ /**
2497
+ * Invalidate all entries with a specific tag
2498
+ *
2499
+ * @param tag - Tag to invalidate (supports wildcards)
2500
+ * @returns Result with number of invalidated entries
2501
+ */
2502
+ async invalidateByTag(tag) {
2503
+ try {
2504
+ const keysToInvalidate = /* @__PURE__ */ new Set();
2505
+ if (tag.includes("*") || tag.includes("?")) {
2506
+ const regex = this.patternToRegex(tag);
2507
+ for (const [tagName, keys] of this.tagIndex.entries()) {
2508
+ if (regex.test(tagName)) {
2509
+ for (const key of keys) {
2510
+ keysToInvalidate.add(key);
2511
+ }
2512
+ }
2513
+ }
2514
+ } else {
2515
+ const keys = this.tagIndex.get(tag);
2516
+ if (keys) {
2517
+ for (const key of keys) {
2518
+ keysToInvalidate.add(key);
2519
+ }
2520
+ }
2521
+ }
2522
+ let invalidated = 0;
2523
+ for (const key of keysToInvalidate) {
2524
+ const result = await this.cache.delete(key);
2525
+ if (result.isOk() && result.unwrap()) {
2526
+ invalidated++;
2527
+ await this.cleanupEntryTags(key);
2528
+ }
2529
+ }
2530
+ this.stats.totalInvalidations += invalidated;
2531
+ this.stats.tagInvalidations += invalidated;
2532
+ this.logger.debug("Invalidated by tag", { tag, count: invalidated });
2533
+ await this.eventBus.emit("cache:invalidated", {
2534
+ type: "tag",
2535
+ tag,
2536
+ count: invalidated
2537
+ });
2538
+ return Result.ok(invalidated);
2539
+ } catch (error) {
2540
+ this.logger.error("Failed to invalidate by tag", { error, tag });
2541
+ return Result.err({
2542
+ type: "INVALIDATION_FAILED",
2543
+ message: `Failed to invalidate by tag: ${error}`,
2544
+ cause: error
2545
+ });
2546
+ }
2547
+ }
2548
+ /**
2549
+ * Invalidate entries by multiple tags
2550
+ *
2551
+ * @param tags - Array of tags to invalidate
2552
+ * @returns Result with number of invalidated entries
2553
+ */
2554
+ async invalidateByTags(tags) {
2555
+ try {
2556
+ const keysToInvalidate = /* @__PURE__ */ new Set();
2557
+ for (const tag of tags) {
2558
+ const result = await this.getTaggedKeys(tag);
2559
+ if (result.isOk()) {
2560
+ const keys = result.unwrap();
2561
+ for (const key of keys) {
2562
+ keysToInvalidate.add(key);
2563
+ }
2564
+ }
2565
+ }
2566
+ let invalidated = 0;
2567
+ for (const key of keysToInvalidate) {
2568
+ const result = await this.cache.delete(key);
2569
+ if (result.isOk() && result.unwrap()) {
2570
+ invalidated++;
2571
+ await this.cleanupEntryTags(key);
2572
+ }
2573
+ }
2574
+ this.stats.totalInvalidations += invalidated;
2575
+ this.stats.tagInvalidations += invalidated;
2576
+ this.logger.debug("Invalidated by tags", { tags, count: invalidated });
2577
+ return Result.ok(invalidated);
2578
+ } catch (error) {
2579
+ this.logger.error("Failed to invalidate by tags", { error, tags });
2580
+ return Result.err({
2581
+ type: "INVALIDATION_FAILED",
2582
+ message: `Failed to invalidate by tags: ${error}`,
2583
+ cause: error
2584
+ });
2585
+ }
2586
+ }
2587
+ /**
2588
+ * Invalidate specific cache keys
2589
+ *
2590
+ * @param keys - Array of keys to invalidate
2591
+ * @returns Result with number of invalidated entries
2592
+ */
2593
+ async invalidateByKeys(keys) {
2594
+ try {
2595
+ let invalidated = 0;
2596
+ for (const key of keys) {
2597
+ const result = await this.cache.delete(key);
2598
+ if (result.isOk() && result.unwrap()) {
2599
+ invalidated++;
2600
+ await this.cleanupEntryTags(key);
2601
+ }
2602
+ }
2603
+ this.stats.totalInvalidations += invalidated;
2604
+ this.stats.keyInvalidations += invalidated;
2605
+ this.logger.debug("Invalidated by keys", { count: invalidated });
2606
+ return Result.ok(invalidated);
2607
+ } catch (error) {
2608
+ this.logger.error("Failed to invalidate by keys", { error, keys });
2609
+ return Result.err({
2610
+ type: "INVALIDATION_FAILED",
2611
+ message: `Failed to invalidate by keys: ${error}`,
2612
+ cause: error
2613
+ });
2614
+ }
2615
+ }
2616
+ /**
2617
+ * Invalidate entries matching a pattern
2618
+ *
2619
+ * @param pattern - Pattern to match (supports wildcards)
2620
+ * @returns Result with number of invalidated entries
2621
+ */
2622
+ async invalidateByPattern(pattern) {
2623
+ try {
2624
+ const result = await this.cache.invalidate(pattern);
2625
+ if (result.isOk()) {
2626
+ const count = result.unwrap();
2627
+ this.stats.totalInvalidations += count;
2628
+ this.stats.patternInvalidations += count;
2629
+ }
2630
+ return result;
2631
+ } catch (error) {
2632
+ this.logger.error("Failed to invalidate by pattern", { error, pattern });
2633
+ return Result.err({
2634
+ type: "INVALIDATION_FAILED",
2635
+ message: `Failed to invalidate by pattern: ${error}`,
2636
+ cause: error
2637
+ });
2638
+ }
2639
+ }
2640
+ /**
2641
+ * Scan and invalidate expired entries
2642
+ *
2643
+ * @returns Result with number of invalidated entries
2644
+ */
2645
+ async invalidateExpired() {
2646
+ try {
2647
+ let invalidated = 0;
2648
+ const allKeys = Array.from(this.entryTags.keys());
2649
+ for (const key of allKeys) {
2650
+ const result = await this.cache.get(key);
2651
+ if (result.isOk() && result.unwrap().isNone()) {
2652
+ await this.cleanupEntryTags(key);
2653
+ invalidated++;
2654
+ }
2655
+ }
2656
+ this.stats.totalInvalidations += invalidated;
2657
+ this.stats.expiredInvalidations += invalidated;
2658
+ this.logger.debug("Invalidated expired entries", { count: invalidated });
2659
+ return Result.ok(invalidated);
2660
+ } catch (error) {
2661
+ this.logger.error("Failed to invalidate expired entries", { error });
2662
+ return Result.err({
2663
+ type: "INVALIDATION_FAILED",
2664
+ message: `Failed to invalidate expired entries: ${error}`,
2665
+ cause: error
2666
+ });
2667
+ }
2668
+ }
2669
+ /**
2670
+ * Subscribe to data change events for automatic invalidation
2671
+ *
2672
+ * @returns Unsubscribe function
2673
+ */
2674
+ subscribeToDataChanges() {
2675
+ const unsubscribers = [];
2676
+ unsubscribers.push(
2677
+ this.eventBus.subscribe("*", async (data) => {
2678
+ })
2679
+ );
2680
+ const cacheInvalidateHandler = async (data) => {
2681
+ if (data && data.tag) {
2682
+ await this.invalidateByTag(data.tag);
2683
+ }
2684
+ };
2685
+ const events = [
2686
+ "cache:invalidate:users",
2687
+ "cache:invalidate:posts",
2688
+ "cache:invalidate:comments"
2689
+ ];
2690
+ for (const event of events) {
2691
+ unsubscribers.push(this.eventBus.subscribe(event, cacheInvalidateHandler));
2692
+ }
2693
+ unsubscribers.push(
2694
+ this.eventBus.subscribe("cache:invalidate:batch", async (data) => {
2695
+ if (data && data.tags) {
2696
+ await this.invalidateByTags(data.tags);
2697
+ }
2698
+ })
2699
+ );
2700
+ const dataUpdateHandler = async (data) => {
2701
+ if (data && data.table) {
2702
+ await this.cache.invalidateTable(data.table);
2703
+ }
2704
+ };
2705
+ const dataEvents = ["data:updated:users", "data:updated:posts"];
2706
+ for (const event of dataEvents) {
2707
+ unsubscribers.push(this.eventBus.subscribe(event, dataUpdateHandler));
2708
+ }
2709
+ return () => {
2710
+ for (const unsubscribe of unsubscribers) {
2711
+ unsubscribe();
2712
+ }
2713
+ };
2714
+ }
2715
+ /**
2716
+ * Get all keys tagged with a specific tag
2717
+ *
2718
+ * @param tag - Tag name
2719
+ * @returns Result with array of keys
2720
+ */
2721
+ async getTaggedKeys(tag) {
2722
+ try {
2723
+ const keys = this.tagIndex.get(tag);
2724
+ if (!keys) {
2725
+ return Result.ok([]);
2726
+ }
2727
+ return Result.ok(Array.from(keys));
2728
+ } catch (error) {
2729
+ return Result.err({
2730
+ type: "UNKNOWN",
2731
+ message: `Failed to get tagged keys: ${error}`,
2732
+ cause: error
2733
+ });
2734
+ }
2735
+ }
2736
+ /**
2737
+ * Get all tags for a cache entry
2738
+ *
2739
+ * @param key - Cache key
2740
+ * @returns Result with array of tags
2741
+ */
2742
+ async getEntryTags(key) {
2743
+ try {
2744
+ const tags = this.entryTags.get(key);
2745
+ if (!tags) {
2746
+ return Result.ok([]);
2747
+ }
2748
+ return Result.ok(Array.from(tags));
2749
+ } catch (error) {
2750
+ return Result.err({
2751
+ type: "UNKNOWN",
2752
+ message: `Failed to get entry tags: ${error}`,
2753
+ cause: error
2754
+ });
2755
+ }
2756
+ }
2757
+ /**
2758
+ * Get invalidation statistics
2759
+ *
2760
+ * @returns Result with statistics
2761
+ */
2762
+ getStats() {
2763
+ try {
2764
+ return Result.ok({ ...this.stats });
2765
+ } catch (error) {
2766
+ return Result.err({
2767
+ type: "UNKNOWN",
2768
+ message: `Failed to get stats: ${error}`,
2769
+ cause: error
2770
+ });
2771
+ }
2772
+ }
2773
+ /**
2774
+ * Reset invalidation statistics
2775
+ *
2776
+ * @returns Result with void on success
2777
+ */
2778
+ resetStats() {
2779
+ try {
2780
+ this.stats = {
2781
+ totalInvalidations: 0,
2782
+ tagInvalidations: 0,
2783
+ patternInvalidations: 0,
2784
+ keyInvalidations: 0,
2785
+ expiredInvalidations: 0
2786
+ };
2787
+ return Result.ok(void 0);
2788
+ } catch (error) {
2789
+ return Result.err({
2790
+ type: "UNKNOWN",
2791
+ message: `Failed to reset stats: ${error}`,
2792
+ cause: error
2793
+ });
2794
+ }
2795
+ }
2796
+ /**
2797
+ * Clean up tag mappings for a deleted entry
2798
+ */
2799
+ async cleanupEntryTags(key) {
2800
+ const tags = this.entryTags.get(key);
2801
+ if (tags) {
2802
+ for (const tag of tags) {
2803
+ const tagSet = this.tagIndex.get(tag);
2804
+ if (tagSet) {
2805
+ tagSet.delete(key);
2806
+ if (tagSet.size === 0) {
2807
+ this.tagIndex.delete(tag);
2808
+ }
2809
+ }
2810
+ }
2811
+ this.entryTags.delete(key);
2812
+ }
2813
+ }
2814
+ /**
2815
+ * Convert glob pattern to regex
2816
+ */
2817
+ patternToRegex(pattern) {
2818
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
2819
+ return new RegExp(`^${escaped}$`);
2820
+ }
2821
+ };
2822
+ var MigrationLoader = class {
2823
+ config;
2824
+ logger;
2825
+ filePattern;
2826
+ constructor(config, logger) {
2827
+ this.config = config;
2828
+ this.logger = logger;
2829
+ this.filePattern = config.filePattern || /^(\d+|V\d+)[_-].*\.ts$/;
2830
+ }
2831
+ /**
2832
+ * Load all migrations from the configured directory
2833
+ *
2834
+ * @returns Result with array of migrations ordered by version
2835
+ */
2836
+ async loadMigrations() {
2837
+ try {
2838
+ if (!fs.existsSync(this.config.migrationsPath)) {
2839
+ return Result.err({
2840
+ type: "INVALID_MIGRATION",
2841
+ message: `Migrations directory does not exist: ${this.config.migrationsPath}`
2842
+ });
2843
+ }
2844
+ const files = fs.readdirSync(this.config.migrationsPath);
2845
+ const migrationFiles = files.filter((file) => this.filePattern.test(file));
2846
+ this.logger.debug("Found migration files", {
2847
+ total: files.length,
2848
+ migrations: migrationFiles.length
2849
+ });
2850
+ const migrations = [];
2851
+ for (const file of migrationFiles) {
2852
+ const filePath = path.join(this.config.migrationsPath, file);
2853
+ const result = await this.loadMigrationFile(filePath);
2854
+ if (result.isErr()) {
2855
+ return result;
2856
+ }
2857
+ migrations.push(result.unwrap());
2858
+ }
2859
+ const duplicates = this.findDuplicateVersions(migrations);
2860
+ if (duplicates.length > 0) {
2861
+ return Result.err({
2862
+ type: "INVALID_MIGRATION",
2863
+ message: `Duplicate migration versions found: ${duplicates.join(", ")}`
2864
+ });
2865
+ }
2866
+ migrations.sort((a, b) => a.version - b.version);
2867
+ this.logger.debug("Loaded migrations", { count: migrations.length });
2868
+ return Result.ok(migrations);
2869
+ } catch (error) {
2870
+ this.logger.error("Failed to load migrations", { error });
2871
+ return Result.err({
2872
+ type: "UNKNOWN",
2873
+ message: `Failed to load migrations: ${error}`,
2874
+ cause: error
2875
+ });
2876
+ }
2877
+ }
2878
+ /**
2879
+ * Load a single migration file
2880
+ *
2881
+ * @param filePath - Path to migration file
2882
+ * @returns Result with Migration or MigrationError
2883
+ */
2884
+ async loadMigrationFile(filePath) {
2885
+ try {
2886
+ const content = fs.readFileSync(filePath, "utf-8");
2887
+ const fileUrl = `file://${filePath.replace(/\\/g, "/")}`;
2888
+ const module = await import(fileUrl);
2889
+ const validation = this.validateMigration(module, filePath);
2890
+ if (validation.isErr()) {
2891
+ return validation;
2892
+ }
2893
+ const { version: version2, name, up, down } = module;
2894
+ const id = this.generateMigrationId(version2, name);
2895
+ const checksum = this.computeChecksum(content);
2896
+ const migration = {
2897
+ id,
2898
+ version: version2,
2899
+ name,
2900
+ up,
2901
+ down,
2902
+ checksum
2903
+ };
2904
+ this.logger.debug("Loaded migration", { id, version: version2, name });
2905
+ return Result.ok(migration);
2906
+ } catch (error) {
2907
+ this.logger.error("Failed to load migration file", { error, filePath });
2908
+ return Result.err({
2909
+ type: "INVALID_MIGRATION",
2910
+ message: `Failed to load migration file ${filePath}: ${error}`,
2911
+ cause: error
2912
+ });
2913
+ }
2914
+ }
2915
+ /**
2916
+ * Validate migration module structure
2917
+ *
2918
+ * @param module - Migration module
2919
+ * @param filePath - File path for error messages
2920
+ * @returns Result with void on success or MigrationError
2921
+ */
2922
+ validateMigration(module, filePath) {
2923
+ if (module.version === void 0) {
2924
+ return Result.err({
2925
+ type: "INVALID_MIGRATION",
2926
+ message: `Migration ${filePath} is missing 'version' export`
2927
+ });
2928
+ }
2929
+ if (typeof module.version !== "number") {
2930
+ return Result.err({
2931
+ type: "INVALID_MIGRATION",
2932
+ message: `Migration ${filePath} has invalid version type (must be number)`
2933
+ });
2934
+ }
2935
+ if (module.version < 0) {
2936
+ return Result.err({
2937
+ type: "INVALID_MIGRATION",
2938
+ message: `Migration ${filePath} has negative version number`
2939
+ });
2940
+ }
2941
+ if (!module.name) {
2942
+ return Result.err({
2943
+ type: "INVALID_MIGRATION",
2944
+ message: `Migration ${filePath} is missing 'name' export`
2945
+ });
2946
+ }
2947
+ if (!module.up || typeof module.up !== "function") {
2948
+ return Result.err({
2949
+ type: "INVALID_MIGRATION",
2950
+ message: `Migration ${filePath} is missing 'up' function`
2951
+ });
2952
+ }
2953
+ if (!module.down || typeof module.down !== "function") {
2954
+ return Result.err({
2955
+ type: "INVALID_MIGRATION",
2956
+ message: `Migration ${filePath} is missing 'down' function`
2957
+ });
2958
+ }
2959
+ return Result.ok(void 0);
2960
+ }
2961
+ /**
2962
+ * Find duplicate version numbers in migrations
2963
+ *
2964
+ * @param migrations - Array of migrations
2965
+ * @returns Array of duplicate version numbers
2966
+ */
2967
+ findDuplicateVersions(migrations) {
2968
+ const versionCounts = /* @__PURE__ */ new Map();
2969
+ for (const migration of migrations) {
2970
+ const count = versionCounts.get(migration.version) || 0;
2971
+ versionCounts.set(migration.version, count + 1);
2972
+ }
2973
+ const duplicates = [];
2974
+ for (const [version2, count] of versionCounts.entries()) {
2975
+ if (count > 1) {
2976
+ duplicates.push(version2);
2977
+ }
2978
+ }
2979
+ return duplicates;
2980
+ }
2981
+ /**
2982
+ * Generate unique migration ID from version and name
2983
+ *
2984
+ * @param version - Migration version
2985
+ * @param name - Migration name
2986
+ * @returns Migration ID
2987
+ */
2988
+ generateMigrationId(version2, name) {
2989
+ return `${version2}_${name}`;
2990
+ }
2991
+ /**
2992
+ * Compute SHA-256 checksum of migration content
2993
+ *
2994
+ * @param content - Migration file content
2995
+ * @returns Checksum string
2996
+ */
2997
+ computeChecksum(content) {
2998
+ return crypto.createHash("sha256").update(content).digest("hex");
2999
+ }
3000
+ /**
3001
+ * Get SQL schema for migration tracking table
3002
+ *
3003
+ * @returns SQL CREATE TABLE statement
3004
+ */
3005
+ getMigrationTableSchema() {
3006
+ const tableName = this.config.migrationTable;
3007
+ return `
3008
+ CREATE TABLE IF NOT EXISTS ${tableName} (
3009
+ id TEXT PRIMARY KEY,
3010
+ version INTEGER NOT NULL UNIQUE,
3011
+ name TEXT NOT NULL,
3012
+ checksum TEXT,
3013
+ status TEXT NOT NULL DEFAULT 'PENDING',
3014
+ executed_at TIMESTAMP,
3015
+ execution_time INTEGER,
3016
+ error TEXT
3017
+ );
3018
+
3019
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_version ON ${tableName}(version);
3020
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_status ON ${tableName}(status);
3021
+ `.trim();
3022
+ }
3023
+ };
3024
+ var MigrationRunner = class {
3025
+ connection;
3026
+ loader;
3027
+ config;
3028
+ logger;
3029
+ constructor(connection, loader, config, logger) {
3030
+ this.connection = connection;
3031
+ this.loader = loader;
3032
+ this.config = config;
3033
+ this.logger = logger;
3034
+ }
3035
+ /**
3036
+ * Create migration table if it doesn't exist
3037
+ */
3038
+ async createMigrationTable() {
3039
+ try {
3040
+ const schema = this.loader.getMigrationTableSchema();
3041
+ const result = await this.connection.execute(schema);
3042
+ if (result.isErr()) {
3043
+ return Result.err({
3044
+ type: "UNKNOWN",
3045
+ message: "Failed to create migration table",
3046
+ cause: result.variant.error
3047
+ });
3048
+ }
3049
+ this.logger.debug("Migration table created");
3050
+ return Result.ok(void 0);
3051
+ } catch (error) {
3052
+ this.logger.error("Failed to create migration table", { error });
3053
+ return Result.err({
3054
+ type: "UNKNOWN",
3055
+ message: `Failed to create migration table: ${error}`,
3056
+ cause: error
3057
+ });
3058
+ }
3059
+ }
3060
+ /**
3061
+ * Get all available migrations
3062
+ */
3063
+ async getMigrations() {
3064
+ return this.loader.loadMigrations();
3065
+ }
3066
+ /**
3067
+ * Get migration history from database
3068
+ */
3069
+ async getHistory() {
3070
+ try {
3071
+ const query = `SELECT * FROM ${this.config.migrationTable} ORDER BY version DESC`;
3072
+ const result = await this.connection.query(query);
3073
+ if (result.isErr()) {
3074
+ return Result.err({
3075
+ type: "UNKNOWN",
3076
+ message: "Failed to get migration history",
3077
+ cause: result.variant.error
3078
+ });
3079
+ }
3080
+ const rows = result.unwrap();
3081
+ const history = rows.map((row) => ({
3082
+ id: row.id,
3083
+ version: row.version,
3084
+ name: row.name,
3085
+ status: row.status,
3086
+ executedAt: row.executed_at ? new Date(row.executed_at) : void 0,
3087
+ executionTime: row.execution_time,
3088
+ error: row.error,
3089
+ checksum: row.checksum
3090
+ }));
3091
+ return Result.ok(history);
3092
+ } catch (error) {
3093
+ this.logger.error("Failed to get migration history", { error });
3094
+ return Result.err({
3095
+ type: "UNKNOWN",
3096
+ message: `Failed to get migration history: ${error}`,
3097
+ cause: error
3098
+ });
3099
+ }
3100
+ }
3101
+ /**
3102
+ * Get pending migrations
3103
+ */
3104
+ async getPending() {
3105
+ try {
3106
+ const migrationsResult = await this.getMigrations();
3107
+ if (migrationsResult.isErr()) {
3108
+ return migrationsResult;
3109
+ }
3110
+ const allMigrations = migrationsResult.unwrap();
3111
+ const historyResult = await this.getHistory();
3112
+ if (historyResult.isErr()) {
3113
+ return Result.err(historyResult.variant.error);
3114
+ }
3115
+ const history = historyResult.unwrap();
3116
+ const appliedVersions = new Set(
3117
+ history.filter((h) => h.status === "COMPLETED").map((h) => h.version)
3118
+ );
3119
+ const pending = allMigrations.filter((m) => !appliedVersions.has(m.version));
3120
+ return Result.ok(pending);
3121
+ } catch (error) {
3122
+ this.logger.error("Failed to get pending migrations", { error });
3123
+ return Result.err({
3124
+ type: "UNKNOWN",
3125
+ message: `Failed to get pending migrations: ${error}`,
3126
+ cause: error
3127
+ });
3128
+ }
3129
+ }
3130
+ /**
3131
+ * Run all pending migrations
3132
+ */
3133
+ async migrate() {
3134
+ try {
3135
+ if (this.config.validateChecksums) {
3136
+ const validationResult = await this.validateChecksums();
3137
+ if (validationResult.isErr()) {
3138
+ return Result.err(validationResult.variant.error);
3139
+ }
3140
+ }
3141
+ const pendingResult = await this.getPending();
3142
+ if (pendingResult.isErr()) {
3143
+ return Result.err(pendingResult.variant.error);
3144
+ }
3145
+ const pending = pendingResult.unwrap();
3146
+ const applied = [];
3147
+ for (const migration of pending) {
3148
+ const result = await this.runMigration(migration, "UP");
3149
+ if (result.isErr()) {
3150
+ return Result.err(result.variant.error);
3151
+ }
3152
+ applied.push(result.unwrap());
3153
+ }
3154
+ this.logger.info("Migrations completed", { count: applied.length });
3155
+ return Result.ok(applied);
3156
+ } catch (error) {
3157
+ this.logger.error("Migration failed", { error });
3158
+ return Result.err({
3159
+ type: "UNKNOWN",
3160
+ message: `Migration failed: ${error}`,
3161
+ cause: error
3162
+ });
3163
+ }
3164
+ }
3165
+ /**
3166
+ * Run migrations up to a specific version
3167
+ */
3168
+ async migrateTo(targetVersion) {
3169
+ try {
3170
+ const migrationsResult = await this.getMigrations();
3171
+ if (migrationsResult.isErr()) {
3172
+ return Result.err(migrationsResult.variant.error);
3173
+ }
3174
+ const allMigrations = migrationsResult.unwrap();
3175
+ const targetMigration = allMigrations.find((m) => m.version === targetVersion);
3176
+ if (!targetMigration) {
3177
+ return Result.err({
3178
+ type: "MIGRATION_NOT_FOUND",
3179
+ message: `Migration version ${targetVersion} not found`,
3180
+ migrationId: `${targetVersion}`
3181
+ });
3182
+ }
3183
+ const pendingResult = await this.getPending();
3184
+ if (pendingResult.isErr()) {
3185
+ return Result.err(pendingResult.variant.error);
3186
+ }
3187
+ const pending = pendingResult.unwrap();
3188
+ const toApply = pending.filter((m) => m.version <= targetVersion);
3189
+ const applied = [];
3190
+ for (const migration of toApply) {
3191
+ const result = await this.runMigration(migration, "UP");
3192
+ if (result.isErr()) {
3193
+ return Result.err(result.variant.error);
3194
+ }
3195
+ applied.push(result.unwrap());
3196
+ }
3197
+ this.logger.info("Migrated to version", { version: targetVersion });
3198
+ return Result.ok(applied);
3199
+ } catch (error) {
3200
+ this.logger.error("Migration to version failed", { error, targetVersion });
3201
+ return Result.err({
3202
+ type: "UNKNOWN",
3203
+ message: `Migration to version ${targetVersion} failed: ${error}`,
3204
+ cause: error
3205
+ });
3206
+ }
3207
+ }
3208
+ /**
3209
+ * Rollback the last migration
3210
+ */
3211
+ async rollback() {
3212
+ try {
3213
+ const historyResult = await this.getHistory();
3214
+ if (historyResult.isErr()) {
3215
+ return Result.err(historyResult.variant.error);
3216
+ }
3217
+ const history = historyResult.unwrap();
3218
+ const completed = history.filter((h) => h.status === "COMPLETED");
3219
+ if (completed.length === 0) {
3220
+ return Result.err({
3221
+ type: "NOT_APPLIED",
3222
+ message: "No migrations to rollback",
3223
+ migrationId: ""
3224
+ });
3225
+ }
3226
+ const lastMigration = completed[0];
3227
+ const migrationsResult = await this.getMigrations();
3228
+ if (migrationsResult.isErr()) {
3229
+ return Result.err(migrationsResult.variant.error);
3230
+ }
3231
+ const migrations = migrationsResult.unwrap();
3232
+ const migration = migrations.find((m) => m.id === lastMigration.id);
3233
+ if (!migration) {
3234
+ return Result.err({
3235
+ type: "MIGRATION_NOT_FOUND",
3236
+ message: `Migration ${lastMigration.id} not found`,
3237
+ migrationId: lastMigration.id
3238
+ });
3239
+ }
3240
+ const result = await this.runMigration(migration, "DOWN");
3241
+ if (result.isErr()) {
3242
+ return Result.err(result.variant.error);
3243
+ }
3244
+ this.logger.info("Rolled back migration", { id: migration.id });
3245
+ return Result.ok(result.unwrap());
3246
+ } catch (error) {
3247
+ this.logger.error("Rollback failed", { error });
3248
+ return Result.err({
3249
+ type: "UNKNOWN",
3250
+ message: `Rollback failed: ${error}`,
3251
+ cause: error
3252
+ });
3253
+ }
3254
+ }
3255
+ /**
3256
+ * Rollback to a specific version
3257
+ */
3258
+ async rollbackTo(targetVersion) {
3259
+ try {
3260
+ const historyResult = await this.getHistory();
3261
+ if (historyResult.isErr()) {
3262
+ return Result.err(historyResult.variant.error);
3263
+ }
3264
+ const history = historyResult.unwrap();
3265
+ const completed = history.filter((h) => h.status === "COMPLETED" && h.version > targetVersion).sort((a, b) => b.version - a.version);
3266
+ const migrationsResult = await this.getMigrations();
3267
+ if (migrationsResult.isErr()) {
3268
+ return Result.err(migrationsResult.variant.error);
3269
+ }
3270
+ const allMigrations = migrationsResult.unwrap();
3271
+ const rolledBack = [];
3272
+ for (const historyEntry of completed) {
3273
+ const migration = allMigrations.find((m) => m.id === historyEntry.id);
3274
+ if (!migration) {
3275
+ return Result.err({
3276
+ type: "MIGRATION_NOT_FOUND",
3277
+ message: `Migration ${historyEntry.id} not found`,
3278
+ migrationId: historyEntry.id
3279
+ });
3280
+ }
3281
+ const result = await this.runMigration(migration, "DOWN");
3282
+ if (result.isErr()) {
3283
+ return Result.err(result.variant.error);
3284
+ }
3285
+ rolledBack.push(result.unwrap());
3286
+ }
3287
+ this.logger.info("Rolled back to version", { version: targetVersion });
3288
+ return Result.ok(rolledBack);
3289
+ } catch (error) {
3290
+ this.logger.error("Rollback to version failed", { error, targetVersion });
3291
+ return Result.err({
3292
+ type: "UNKNOWN",
3293
+ message: `Rollback to version ${targetVersion} failed: ${error}`,
3294
+ cause: error
3295
+ });
3296
+ }
3297
+ }
3298
+ /**
3299
+ * Reset database by rolling back all migrations
3300
+ */
3301
+ async reset() {
3302
+ return this.rollbackTo(0);
3303
+ }
3304
+ /**
3305
+ * Refresh database by resetting and re-running all migrations
3306
+ */
3307
+ async refresh() {
3308
+ try {
3309
+ const resetResult = await this.reset();
3310
+ if (resetResult.isErr()) {
3311
+ return Result.err(resetResult.variant.error);
3312
+ }
3313
+ const rolledBack = resetResult.unwrap();
3314
+ const migrateResult = await this.migrate();
3315
+ if (migrateResult.isErr()) {
3316
+ return Result.err(migrateResult.variant.error);
3317
+ }
3318
+ const applied = migrateResult.unwrap();
3319
+ this.logger.info("Database refreshed", {
3320
+ rolledBack: rolledBack.length,
3321
+ applied: applied.length
3322
+ });
3323
+ return Result.ok({ rolledBack, applied });
3324
+ } catch (error) {
3325
+ this.logger.error("Refresh failed", { error });
3326
+ return Result.err({
3327
+ type: "UNKNOWN",
3328
+ message: `Refresh failed: ${error}`,
3329
+ cause: error
3330
+ });
3331
+ }
3332
+ }
3333
+ /**
3334
+ * Get current migration version
3335
+ */
3336
+ async getCurrentVersion() {
3337
+ try {
3338
+ const historyResult = await this.getHistory();
3339
+ if (historyResult.isErr()) {
3340
+ return Result.err(historyResult.variant.error);
3341
+ }
3342
+ const history = historyResult.unwrap();
3343
+ const completed = history.filter((h) => h.status === "COMPLETED");
3344
+ if (completed.length === 0) {
3345
+ return Result.ok(0);
3346
+ }
3347
+ const maxVersion = Math.max(...completed.map((h) => h.version));
3348
+ return Result.ok(maxVersion);
3349
+ } catch (error) {
3350
+ this.logger.error("Failed to get current version", { error });
3351
+ return Result.err({
3352
+ type: "UNKNOWN",
3353
+ message: `Failed to get current version: ${error}`,
3354
+ cause: error
3355
+ });
3356
+ }
3357
+ }
3358
+ /**
3359
+ * Validate migration checksums
3360
+ */
3361
+ async validateChecksums() {
3362
+ try {
3363
+ const migrationsResult = await this.getMigrations();
3364
+ if (migrationsResult.isErr()) {
3365
+ return Result.err(migrationsResult.variant.error);
3366
+ }
3367
+ const migrations = migrationsResult.unwrap();
3368
+ const historyResult = await this.getHistory();
3369
+ if (historyResult.isErr()) {
3370
+ return Result.err(historyResult.variant.error);
3371
+ }
3372
+ const history = historyResult.unwrap();
3373
+ const completedMigrations = history.filter((h) => h.status === "COMPLETED");
3374
+ for (const historyEntry of completedMigrations) {
3375
+ const migration = migrations.find((m) => m.id === historyEntry.id);
3376
+ if (!migration) {
3377
+ continue;
3378
+ }
3379
+ if (migration.checksum && historyEntry.checksum) {
3380
+ if (migration.checksum !== historyEntry.checksum) {
3381
+ return Result.err({
3382
+ type: "CHECKSUM_MISMATCH",
3383
+ message: `Checksum mismatch for migration ${migration.id}`,
3384
+ migrationId: migration.id
3385
+ });
3386
+ }
3387
+ }
3388
+ }
3389
+ return Result.ok(true);
3390
+ } catch (error) {
3391
+ this.logger.error("Checksum validation failed", { error });
3392
+ return Result.err({
3393
+ type: "UNKNOWN",
3394
+ message: `Checksum validation failed: ${error}`,
3395
+ cause: error
3396
+ });
3397
+ }
3398
+ }
3399
+ /**
3400
+ * Load migration from file
3401
+ */
3402
+ async loadMigration(filePath) {
3403
+ if (this.loader.loadMigrationFile) {
3404
+ return this.loader.loadMigrationFile(filePath);
3405
+ }
3406
+ return Result.err({
3407
+ type: "UNKNOWN",
3408
+ message: "Migration loader does not support loading individual files"
3409
+ });
3410
+ }
3411
+ /**
3412
+ * Run a single migration in a transaction
3413
+ */
3414
+ async runMigration(migration, direction) {
3415
+ const startTime = Date.now();
3416
+ const isUp = direction === "UP";
3417
+ const fn = isUp ? migration.up : migration.down;
3418
+ try {
3419
+ await this.updateMigrationStatus(
3420
+ migration,
3421
+ isUp ? "RUNNING" : "RUNNING",
3422
+ void 0,
3423
+ void 0
3424
+ );
3425
+ const runFn = async (conn) => {
3426
+ const context = {
3427
+ execute: async (sql, params) => {
3428
+ const result2 = await conn.execute(sql, params);
3429
+ if (result2.isErr()) {
3430
+ return Result.err({
3431
+ type: "MIGRATION_FAILED",
3432
+ message: `Migration execution failed: ${result2.variant.error}`,
3433
+ migrationId: migration.id,
3434
+ cause: result2.variant.error
3435
+ });
3436
+ }
3437
+ return Result.ok(result2.unwrap());
3438
+ },
3439
+ log: (message) => {
3440
+ this.logger.info(message, { migrationId: migration.id });
3441
+ },
3442
+ metadata: {
3443
+ id: migration.id,
3444
+ version: migration.version,
3445
+ name: migration.name,
3446
+ status: "RUNNING"
3447
+ }
3448
+ };
3449
+ return await fn(context);
3450
+ };
3451
+ let result;
3452
+ if (this.config.transactional) {
3453
+ result = await this.connection.transaction(runFn);
3454
+ } else {
3455
+ result = await runFn(this.connection);
3456
+ }
3457
+ const executionTime = Date.now() - startTime;
3458
+ if (result.isErr()) {
3459
+ await this.updateMigrationStatus(
3460
+ migration,
3461
+ "FAILED",
3462
+ executionTime,
3463
+ result.variant.error.message
3464
+ );
3465
+ return Result.err(result.variant.error);
3466
+ }
3467
+ const finalStatus = isUp ? "COMPLETED" : "ROLLED_BACK";
3468
+ await this.updateMigrationStatus(migration, finalStatus, executionTime, void 0);
3469
+ const metadata = {
3470
+ id: migration.id,
3471
+ version: migration.version,
3472
+ name: migration.name,
3473
+ status: finalStatus,
3474
+ executedAt: /* @__PURE__ */ new Date(),
3475
+ executionTime,
3476
+ checksum: migration.checksum
3477
+ };
3478
+ this.logger.info("Migration executed", {
3479
+ id: migration.id,
3480
+ direction,
3481
+ executionTime
3482
+ });
3483
+ return Result.ok(metadata);
3484
+ } catch (error) {
3485
+ const executionTime = Date.now() - startTime;
3486
+ await this.updateMigrationStatus(
3487
+ migration,
3488
+ "FAILED",
3489
+ executionTime,
3490
+ String(error)
3491
+ );
3492
+ this.logger.error("Migration execution failed", { error, migration: migration.id });
3493
+ return Result.err({
3494
+ type: isUp ? "MIGRATION_FAILED" : "ROLLBACK_FAILED",
3495
+ message: `Migration ${migration.id} failed: ${error}`,
3496
+ migrationId: migration.id,
3497
+ cause: error
3498
+ });
3499
+ }
3500
+ }
3501
+ /**
3502
+ * Update migration status in database
3503
+ */
3504
+ async updateMigrationStatus(migration, status, executionTime, error) {
3505
+ try {
3506
+ const tableName = this.config.migrationTable;
3507
+ if (status === "RUNNING") {
3508
+ const insertSQL = `
3509
+ INSERT OR REPLACE INTO ${tableName} (id, version, name, checksum, status, executed_at, execution_time, error)
3510
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3511
+ `;
3512
+ await this.connection.execute(insertSQL, [
3513
+ {
3514
+ id: migration.id,
3515
+ version: migration.version,
3516
+ name: migration.name,
3517
+ checksum: migration.checksum || null,
3518
+ status,
3519
+ executed_at: (/* @__PURE__ */ new Date()).toISOString(),
3520
+ execution_time: null,
3521
+ error: null
3522
+ }
3523
+ ]);
3524
+ } else {
3525
+ const updateSQL = `
3526
+ UPDATE ${tableName}
3527
+ SET status = ?, execution_time = ?, error = ?, executed_at = ?
3528
+ WHERE id = ?
3529
+ `;
3530
+ await this.connection.execute(updateSQL, [
3531
+ {
3532
+ status,
3533
+ execution_time: executionTime || null,
3534
+ error: error || null,
3535
+ executed_at: (/* @__PURE__ */ new Date()).toISOString(),
3536
+ id: migration.id
3537
+ }
3538
+ ]);
3539
+ }
3540
+ } catch (err) {
3541
+ this.logger.error("Failed to update migration status", {
3542
+ error: err,
3543
+ migration: migration.id
3544
+ });
3545
+ }
3546
+ }
3547
+ };
3548
+ var DataSeeder = class {
3549
+ connection;
3550
+ config;
3551
+ logger;
3552
+ seedsCache = null;
3553
+ constructor(connection, config, logger) {
3554
+ this.connection = connection;
3555
+ this.config = config;
3556
+ this.logger = logger;
3557
+ }
3558
+ /**
3559
+ * Create seed table if it doesn't exist
3560
+ */
3561
+ async createSeedTable() {
3562
+ try {
3563
+ const schema = this.getSeedTableSchema();
3564
+ const result = await this.connection.execute(schema);
3565
+ if (result.isErr()) {
3566
+ return Result.err({
3567
+ type: "UNKNOWN",
3568
+ message: "Failed to create seed table",
3569
+ cause: result.variant.error
3570
+ });
3571
+ }
3572
+ this.logger.debug("Seed table created");
3573
+ return Result.ok(void 0);
3574
+ } catch (error) {
3575
+ this.logger.error("Failed to create seed table", { error });
3576
+ return Result.err({
3577
+ type: "UNKNOWN",
3578
+ message: `Failed to create seed table: ${error}`,
3579
+ cause: error
3580
+ });
3581
+ }
3582
+ }
3583
+ /**
3584
+ * Get all available seeds
3585
+ */
3586
+ async getSeeds() {
3587
+ return this.loadSeedsFromDirectory();
3588
+ }
3589
+ /**
3590
+ * Get seed history from database
3591
+ */
3592
+ async getHistory() {
3593
+ try {
3594
+ const query = `SELECT * FROM ${this.config.seedTable} ORDER BY executed_at DESC`;
3595
+ const result = await this.connection.query(query);
3596
+ if (result.isErr()) {
3597
+ return Result.err({
3598
+ type: "UNKNOWN",
3599
+ message: "Failed to get seed history",
3600
+ cause: result.variant.error
3601
+ });
3602
+ }
3603
+ const rows = result.unwrap();
3604
+ const history = rows.map((row) => ({
3605
+ id: row.id,
3606
+ name: row.name,
3607
+ environment: JSON.parse(row.environment || "[]"),
3608
+ status: row.status,
3609
+ executedAt: row.executed_at ? new Date(row.executed_at) : void 0,
3610
+ executionTime: row.execution_time,
3611
+ error: row.error,
3612
+ checksum: row.checksum
3613
+ }));
3614
+ return Result.ok(history);
3615
+ } catch (error) {
3616
+ this.logger.error("Failed to get seed history", { error });
3617
+ return Result.err({
3618
+ type: "UNKNOWN",
3619
+ message: `Failed to get seed history: ${error}`,
3620
+ cause: error
3621
+ });
3622
+ }
3623
+ }
3624
+ /**
3625
+ * Get pending seeds for current environment
3626
+ */
3627
+ async getPending() {
3628
+ try {
3629
+ const seedsResult = await this.getSeeds();
3630
+ if (seedsResult.isErr()) {
3631
+ return seedsResult;
3632
+ }
3633
+ const allSeeds = seedsResult.unwrap();
3634
+ const environmentSeeds = allSeeds.filter(
3635
+ (seed) => this.matchesEnvironment(seed.environment)
3636
+ );
3637
+ const historyResult = await this.getHistory();
3638
+ if (historyResult.isErr()) {
3639
+ return Result.err(historyResult.variant.error);
3640
+ }
3641
+ const history = historyResult.unwrap();
3642
+ const executedIds = new Set(
3643
+ history.filter((h) => h.status === "COMPLETED").map((h) => h.id)
3644
+ );
3645
+ const pending = environmentSeeds.filter((s) => !executedIds.has(s.id));
3646
+ return Result.ok(pending);
3647
+ } catch (error) {
3648
+ this.logger.error("Failed to get pending seeds", { error });
3649
+ return Result.err({
3650
+ type: "UNKNOWN",
3651
+ message: `Failed to get pending seeds: ${error}`,
3652
+ cause: error
3653
+ });
3654
+ }
3655
+ }
3656
+ /**
3657
+ * Run all pending seeds for current environment
3658
+ */
3659
+ async seed() {
3660
+ try {
3661
+ if (this.config.validateChecksums) {
3662
+ const validationResult = await this.validateChecksums();
3663
+ if (validationResult.isErr()) {
3664
+ return Result.err(validationResult.variant.error);
3665
+ }
3666
+ }
3667
+ const pendingResult = await this.getPending();
3668
+ if (pendingResult.isErr()) {
3669
+ return Result.err(pendingResult.variant.error);
3670
+ }
3671
+ const pending = pendingResult.unwrap();
3672
+ const executed = [];
3673
+ for (const seed of pending) {
3674
+ const result = await this.executeSeed(seed);
3675
+ if (result.isErr()) {
3676
+ return Result.err(result.variant.error);
3677
+ }
3678
+ executed.push(result.unwrap());
3679
+ }
3680
+ this.logger.info("Seeds completed", { count: executed.length });
3681
+ return Result.ok(executed);
3682
+ } catch (error) {
3683
+ this.logger.error("Seeding failed", { error });
3684
+ return Result.err({
3685
+ type: "UNKNOWN",
3686
+ message: `Seeding failed: ${error}`,
3687
+ cause: error
3688
+ });
3689
+ }
3690
+ }
3691
+ /**
3692
+ * Run a specific seed by ID
3693
+ */
3694
+ async runSeed(seedId) {
3695
+ try {
3696
+ const seedsResult = await this.getSeeds();
3697
+ if (seedsResult.isErr()) {
3698
+ return Result.err(seedsResult.variant.error);
3699
+ }
3700
+ const allSeeds = seedsResult.unwrap();
3701
+ const seed = allSeeds.find((s) => s.id === seedId);
3702
+ if (!seed) {
3703
+ return Result.err({
3704
+ type: "SEED_NOT_FOUND",
3705
+ message: `Seed ${seedId} not found`,
3706
+ seedId
3707
+ });
3708
+ }
3709
+ return this.executeSeed(seed);
3710
+ } catch (error) {
3711
+ this.logger.error("Run seed failed", { error, seedId });
3712
+ return Result.err({
3713
+ type: "UNKNOWN",
3714
+ message: `Run seed ${seedId} failed: ${error}`,
3715
+ cause: error
3716
+ });
3717
+ }
3718
+ }
3719
+ /**
3720
+ * Reset all seeds (clear history)
3721
+ */
3722
+ async reset() {
3723
+ try {
3724
+ const sql = `DELETE FROM ${this.config.seedTable}`;
3725
+ const result = await this.connection.execute(sql);
3726
+ if (result.isErr()) {
3727
+ return Result.err({
3728
+ type: "UNKNOWN",
3729
+ message: "Failed to reset seeds",
3730
+ cause: result.variant.error
3731
+ });
3732
+ }
3733
+ this.logger.info("All seeds reset");
3734
+ return Result.ok(void 0);
3735
+ } catch (error) {
3736
+ this.logger.error("Failed to reset seeds", { error });
3737
+ return Result.err({
3738
+ type: "UNKNOWN",
3739
+ message: `Failed to reset seeds: ${error}`,
3740
+ cause: error
3741
+ });
3742
+ }
3743
+ }
3744
+ /**
3745
+ * Validate seed checksums
3746
+ */
3747
+ async validateChecksums() {
3748
+ try {
3749
+ const seedsResult = await this.getSeeds();
3750
+ if (seedsResult.isErr()) {
3751
+ return Result.err(seedsResult.variant.error);
3752
+ }
3753
+ const seeds = seedsResult.unwrap();
3754
+ const historyResult = await this.getHistory();
3755
+ if (historyResult.isErr()) {
3756
+ return Result.err(historyResult.variant.error);
3757
+ }
3758
+ const history = historyResult.unwrap();
3759
+ const completedSeeds = history.filter((h) => h.status === "COMPLETED");
3760
+ for (const historyEntry of completedSeeds) {
3761
+ const seed = seeds.find((s) => s.id === historyEntry.id);
3762
+ if (!seed) {
3763
+ continue;
3764
+ }
3765
+ if (seed.checksum && historyEntry.checksum) {
3766
+ if (seed.checksum !== historyEntry.checksum) {
3767
+ return Result.err({
3768
+ type: "CHECKSUM_MISMATCH",
3769
+ message: `Checksum mismatch for seed ${seed.id}`,
3770
+ seedId: seed.id
3771
+ });
3772
+ }
3773
+ }
3774
+ }
3775
+ return Result.ok(true);
3776
+ } catch (error) {
3777
+ this.logger.error("Checksum validation failed", { error });
3778
+ return Result.err({
3779
+ type: "UNKNOWN",
3780
+ message: `Checksum validation failed: ${error}`,
3781
+ cause: error
3782
+ });
3783
+ }
3784
+ }
3785
+ /**
3786
+ * Load seed from file
3787
+ */
3788
+ async loadSeed(filePath) {
3789
+ return this.loadSeedFile(filePath);
3790
+ }
3791
+ /**
3792
+ * Load seeds from directory
3793
+ */
3794
+ async loadSeedsFromDirectory() {
3795
+ if (this.seedsCache) {
3796
+ return Result.ok(this.seedsCache);
3797
+ }
3798
+ try {
3799
+ if (!fs.existsSync(this.config.seedsPath)) {
3800
+ return Result.err({
3801
+ type: "INVALID_SEED",
3802
+ message: `Seeds directory does not exist: ${this.config.seedsPath}`
3803
+ });
3804
+ }
3805
+ const files = fs.readdirSync(this.config.seedsPath);
3806
+ const filePattern = this.config.filePattern || /^[0-9]+[_-].*\.ts$/;
3807
+ const seedFiles = files.filter((file) => filePattern.test(file));
3808
+ this.logger.debug("Found seed files", {
3809
+ total: files.length,
3810
+ seeds: seedFiles.length
3811
+ });
3812
+ const seeds = [];
3813
+ for (const file of seedFiles) {
3814
+ const filePath = path.join(this.config.seedsPath, file);
3815
+ const result = await this.loadSeedFile(filePath);
3816
+ if (result.isErr()) {
3817
+ return result;
3818
+ }
3819
+ seeds.push(result.unwrap());
3820
+ }
3821
+ seeds.sort((a, b) => a.id.localeCompare(b.id));
3822
+ this.seedsCache = seeds;
3823
+ this.logger.debug("Loaded seeds", { count: seeds.length });
3824
+ return Result.ok(seeds);
3825
+ } catch (error) {
3826
+ this.logger.error("Failed to load seeds", { error });
3827
+ return Result.err({
3828
+ type: "UNKNOWN",
3829
+ message: `Failed to load seeds: ${error}`,
3830
+ cause: error
3831
+ });
3832
+ }
3833
+ }
3834
+ /**
3835
+ * Load a single seed file
3836
+ */
3837
+ async loadSeedFile(filePath) {
3838
+ try {
3839
+ const content = fs.readFileSync(filePath, "utf-8");
3840
+ const fileUrl = `file://${filePath.replace(/\\/g, "/")}`;
3841
+ const module = await import(fileUrl);
3842
+ const validation = this.validateSeed(module, filePath);
3843
+ if (validation.isErr()) {
3844
+ return validation;
3845
+ }
3846
+ const { id, name, environment, run } = module;
3847
+ const checksum = this.computeChecksum(content);
3848
+ const seed = {
3849
+ id,
3850
+ name,
3851
+ environment: Array.isArray(environment) ? environment : [environment],
3852
+ run,
3853
+ checksum
3854
+ };
3855
+ this.logger.debug("Loaded seed", { id, name, environment });
3856
+ return Result.ok(seed);
3857
+ } catch (error) {
3858
+ this.logger.error("Failed to load seed file", { error, filePath });
3859
+ return Result.err({
3860
+ type: "INVALID_SEED",
3861
+ message: `Failed to load seed file ${filePath}: ${error}`,
3862
+ cause: error
3863
+ });
3864
+ }
3865
+ }
3866
+ /**
3867
+ * Validate seed module structure
3868
+ */
3869
+ validateSeed(module, filePath) {
3870
+ if (!module.id) {
3871
+ return Result.err({
3872
+ type: "INVALID_SEED",
3873
+ message: `Seed ${filePath} is missing 'id' export`
3874
+ });
3875
+ }
3876
+ if (!module.name) {
3877
+ return Result.err({
3878
+ type: "INVALID_SEED",
3879
+ message: `Seed ${filePath} is missing 'name' export`
3880
+ });
3881
+ }
3882
+ if (!module.environment) {
3883
+ return Result.err({
3884
+ type: "INVALID_SEED",
3885
+ message: `Seed ${filePath} is missing 'environment' export`
3886
+ });
3887
+ }
3888
+ if (!module.run || typeof module.run !== "function") {
3889
+ return Result.err({
3890
+ type: "INVALID_SEED",
3891
+ message: `Seed ${filePath} is missing 'run' function`
3892
+ });
3893
+ }
3894
+ return Result.ok(void 0);
3895
+ }
3896
+ /**
3897
+ * Check if seed matches current environment
3898
+ */
3899
+ matchesEnvironment(environments) {
3900
+ if (environments.includes("all")) {
3901
+ return true;
3902
+ }
3903
+ return environments.includes(this.config.environment);
3904
+ }
3905
+ /**
3906
+ * Execute a single seed
3907
+ */
3908
+ async executeSeed(seed) {
3909
+ const startTime = Date.now();
3910
+ try {
3911
+ await this.updateSeedStatus(seed, "RUNNING", void 0, void 0);
3912
+ const runFn = async (conn) => {
3913
+ const context = {
3914
+ execute: async (sql, params) => {
3915
+ const result2 = await conn.execute(sql, params);
3916
+ if (result2.isErr()) {
3917
+ return Result.err({
3918
+ type: "SEED_FAILED",
3919
+ message: `Seed execution failed: ${result2.variant.error}`,
3920
+ seedId: seed.id,
3921
+ cause: result2.variant.error
3922
+ });
3923
+ }
3924
+ return Result.ok(result2.unwrap());
3925
+ },
3926
+ log: (message) => {
3927
+ this.logger.info(message, { seedId: seed.id });
3928
+ },
3929
+ metadata: {
3930
+ id: seed.id,
3931
+ name: seed.name,
3932
+ environment: seed.environment,
3933
+ status: "RUNNING"
3934
+ },
3935
+ environment: this.config.environment
3936
+ };
3937
+ return await seed.run(context);
3938
+ };
3939
+ let result;
3940
+ if (this.config.transactional) {
3941
+ result = await this.connection.transaction(runFn);
3942
+ } else {
3943
+ result = await runFn(this.connection);
3944
+ }
3945
+ const executionTime = Date.now() - startTime;
3946
+ if (result.isErr()) {
3947
+ await this.updateSeedStatus(seed, "FAILED", executionTime, result.variant.error.message);
3948
+ return Result.err(result.variant.error);
3949
+ }
3950
+ await this.updateSeedStatus(seed, "COMPLETED", executionTime, void 0);
3951
+ const metadata = {
3952
+ id: seed.id,
3953
+ name: seed.name,
3954
+ environment: seed.environment,
3955
+ status: "COMPLETED",
3956
+ executedAt: /* @__PURE__ */ new Date(),
3957
+ executionTime,
3958
+ checksum: seed.checksum
3959
+ };
3960
+ this.logger.info("Seed executed", {
3961
+ id: seed.id,
3962
+ executionTime
3963
+ });
3964
+ return Result.ok(metadata);
3965
+ } catch (error) {
3966
+ const executionTime = Date.now() - startTime;
3967
+ await this.updateSeedStatus(seed, "FAILED", executionTime, String(error));
3968
+ this.logger.error("Seed execution failed", { error, seed: seed.id });
3969
+ return Result.err({
3970
+ type: "SEED_FAILED",
3971
+ message: `Seed ${seed.id} failed: ${error}`,
3972
+ seedId: seed.id,
3973
+ cause: error
3974
+ });
3975
+ }
3976
+ }
3977
+ /**
3978
+ * Update seed status in database
3979
+ */
3980
+ async updateSeedStatus(seed, status, executionTime, error) {
3981
+ try {
3982
+ const tableName = this.config.seedTable;
3983
+ if (status === "RUNNING") {
3984
+ const insertSQL = `
3985
+ INSERT OR REPLACE INTO ${tableName} (id, name, environment, checksum, status, executed_at, execution_time, error)
3986
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3987
+ `;
3988
+ await this.connection.execute(insertSQL, [
3989
+ seed.id,
3990
+ seed.name,
3991
+ JSON.stringify(seed.environment),
3992
+ seed.checksum || null,
3993
+ status,
3994
+ (/* @__PURE__ */ new Date()).toISOString(),
3995
+ null,
3996
+ null
3997
+ ]);
3998
+ } else {
3999
+ const updateSQL = `
4000
+ UPDATE ${tableName}
4001
+ SET status = ?, execution_time = ?, error = ?, executed_at = ?
4002
+ WHERE id = ?
4003
+ `;
4004
+ await this.connection.execute(updateSQL, [
4005
+ status,
4006
+ executionTime || null,
4007
+ error || null,
4008
+ (/* @__PURE__ */ new Date()).toISOString(),
4009
+ seed.id
4010
+ ]);
4011
+ }
4012
+ } catch (err) {
4013
+ this.logger.error("Failed to update seed status", {
4014
+ error: err,
4015
+ seed: seed.id
4016
+ });
4017
+ }
4018
+ }
4019
+ /**
4020
+ * Compute SHA-256 checksum of seed content
4021
+ */
4022
+ computeChecksum(content) {
4023
+ return crypto.createHash("sha256").update(content).digest("hex");
4024
+ }
4025
+ /**
4026
+ * Get SQL schema for seed tracking table
4027
+ */
4028
+ getSeedTableSchema() {
4029
+ const tableName = this.config.seedTable;
4030
+ return `
4031
+ CREATE TABLE IF NOT EXISTS ${tableName} (
4032
+ id TEXT PRIMARY KEY,
4033
+ name TEXT NOT NULL,
4034
+ environment TEXT NOT NULL,
4035
+ checksum TEXT,
4036
+ status TEXT NOT NULL DEFAULT 'PENDING',
4037
+ executed_at TIMESTAMP,
4038
+ execution_time INTEGER,
4039
+ error TEXT
4040
+ );
4041
+
4042
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_status ON ${tableName}(status);
4043
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_environment ON ${tableName}(environment);
4044
+ `.trim();
4045
+ }
4046
+ };
4047
+ var D1DatabaseAdapter = class {
4048
+ config;
4049
+ logger;
4050
+ db;
4051
+ constructor(config, logger) {
4052
+ this.config = config;
4053
+ this.logger = logger;
4054
+ this.db = config.database;
4055
+ }
4056
+ /**
4057
+ * Execute a SQL query and return all results
4058
+ */
4059
+ async query(sql, params) {
4060
+ try {
4061
+ if (this.config.enableQueryLogging) {
4062
+ this.logger.debug("Executing D1 query", { sql, params });
4063
+ }
4064
+ const stmt = this.prepareStatement(sql, params);
4065
+ const result = await stmt.all();
4066
+ if (!result.success) {
4067
+ return Result.err({
4068
+ type: "QUERY_FAILED",
4069
+ message: result.error || "Query failed",
4070
+ sql
4071
+ });
4072
+ }
4073
+ return Result.ok(result.results || []);
4074
+ } catch (error) {
4075
+ this.logger.error("D1 query failed", { error, sql });
4076
+ return Result.err({
4077
+ type: "QUERY_FAILED",
4078
+ message: `Query failed: ${error}`,
4079
+ sql,
4080
+ cause: error
4081
+ });
4082
+ }
4083
+ }
4084
+ /**
4085
+ * Execute a SQL query and return first result
4086
+ */
4087
+ async queryFirst(sql, params) {
4088
+ try {
4089
+ if (this.config.enableQueryLogging) {
4090
+ this.logger.debug("Executing D1 queryFirst", { sql, params });
4091
+ }
4092
+ const stmt = this.prepareStatement(sql, params);
4093
+ const result = await stmt.first();
4094
+ return Result.ok(result);
4095
+ } catch (error) {
4096
+ this.logger.error("D1 queryFirst failed", { error, sql });
4097
+ return Result.err({
4098
+ type: "QUERY_FAILED",
4099
+ message: `QueryFirst failed: ${error}`,
4100
+ sql,
4101
+ cause: error
4102
+ });
4103
+ }
4104
+ }
4105
+ /**
4106
+ * Execute a SQL statement (INSERT, UPDATE, DELETE)
4107
+ */
4108
+ async execute(sql, params) {
4109
+ try {
4110
+ if (this.config.enableQueryLogging) {
4111
+ this.logger.debug("Executing D1 statement", { sql, params });
4112
+ }
4113
+ const stmt = this.prepareStatement(sql, params);
4114
+ const result = await stmt.run();
4115
+ if (!result.success) {
4116
+ return Result.err({
4117
+ type: "QUERY_FAILED",
4118
+ message: result.error || "Execution failed",
4119
+ sql
4120
+ });
4121
+ }
4122
+ return Result.ok(result);
4123
+ } catch (error) {
4124
+ this.logger.error("D1 execute failed", { error, sql });
4125
+ return Result.err({
4126
+ type: "QUERY_FAILED",
4127
+ message: `Execute failed: ${error}`,
4128
+ sql,
4129
+ cause: error
4130
+ });
4131
+ }
4132
+ }
4133
+ /**
4134
+ * Execute multiple SQL statements in a batch
4135
+ */
4136
+ async batch(statements) {
4137
+ try {
4138
+ const maxBatchSize = this.config.maxBatchSize || 100;
4139
+ if (statements.length > maxBatchSize) {
4140
+ return Result.err({
4141
+ type: "BATCH_FAILED",
4142
+ message: `Batch size ${statements.length} exceeds maximum ${maxBatchSize}`
4143
+ });
4144
+ }
4145
+ if (statements.length === 0) {
4146
+ return Result.ok([]);
4147
+ }
4148
+ if (this.config.enableQueryLogging) {
4149
+ this.logger.debug("Executing D1 batch", {
4150
+ count: statements.length
4151
+ });
4152
+ }
4153
+ const preparedStatements = statements.map(
4154
+ ({ sql, params }) => this.prepareStatement(sql, params)
4155
+ );
4156
+ const results = await this.db.batch(preparedStatements);
4157
+ return Result.ok(results);
4158
+ } catch (error) {
4159
+ this.logger.error("D1 batch failed", { error, count: statements.length });
4160
+ return Result.err({
4161
+ type: "BATCH_FAILED",
4162
+ message: `Batch execution failed: ${error}`,
4163
+ cause: error
4164
+ });
4165
+ }
4166
+ }
4167
+ /**
4168
+ * Execute raw SQL (for migrations and schema changes)
4169
+ */
4170
+ async exec(sql) {
4171
+ try {
4172
+ if (this.config.enableQueryLogging) {
4173
+ this.logger.debug("Executing D1 raw SQL", { sql });
4174
+ }
4175
+ const result = await this.db.exec(sql);
4176
+ return Result.ok(result);
4177
+ } catch (error) {
4178
+ this.logger.error("D1 exec failed", { error, sql });
4179
+ return Result.err({
4180
+ type: "EXEC_FAILED",
4181
+ message: `Exec failed: ${error}`,
4182
+ cause: error
4183
+ });
4184
+ }
4185
+ }
4186
+ /**
4187
+ * Begin a transaction
4188
+ *
4189
+ * Note: D1 doesn't support explicit transactions yet, so this wraps
4190
+ * the callback execution and provides the same interface for compatibility.
4191
+ * Consider using batch() for atomic multi-statement operations.
4192
+ */
4193
+ async transaction(callback) {
4194
+ try {
4195
+ this.logger.debug("Starting D1 transaction (logical)");
4196
+ const result = await callback(this);
4197
+ return result;
4198
+ } catch (error) {
4199
+ this.logger.error("D1 transaction failed", { error });
4200
+ return Result.err({
4201
+ type: "UNKNOWN",
4202
+ message: `Transaction failed: ${error}`,
4203
+ cause: error
4204
+ });
4205
+ }
4206
+ }
4207
+ /**
4208
+ * Prepare a SQL statement with parameters
4209
+ */
4210
+ prepareStatement(sql, params) {
4211
+ const stmt = this.db.prepare(sql);
4212
+ if (params && params.length > 0) {
4213
+ return stmt.bind(...params);
4214
+ }
4215
+ return stmt;
4216
+ }
4217
+ };
4218
+ var DurableObjectStorageAdapter = class _DurableObjectStorageAdapter {
4219
+ config;
4220
+ storage;
4221
+ logger;
4222
+ constructor(storage, config, logger) {
4223
+ this.storage = storage;
4224
+ this.config = config;
4225
+ this.logger = logger;
4226
+ }
4227
+ /**
4228
+ * Get a single value by key
4229
+ */
4230
+ async get(key) {
4231
+ try {
4232
+ if (this.config.enableLogging) {
4233
+ this.logger.debug("DO Storage operation", { operation: "get", key });
4234
+ }
4235
+ const value = await this.storage.get(key);
4236
+ return Result.ok(value === void 0 ? null : value);
4237
+ } catch (error) {
4238
+ this.logger.error("DO Storage get failed", { error, key });
4239
+ return Result.err({
4240
+ type: "STORAGE_FAILED",
4241
+ message: `Failed to get value for key "${key}": ${error}`,
4242
+ key,
4243
+ cause: error
4244
+ });
4245
+ }
4246
+ }
4247
+ /**
4248
+ * Get multiple values by keys
4249
+ */
4250
+ async getMultiple(keys) {
4251
+ try {
4252
+ if (this.config.enableLogging) {
4253
+ this.logger.debug("DO Storage operation", {
4254
+ operation: "getMultiple",
4255
+ keyCount: keys.length
4256
+ });
4257
+ }
4258
+ const result = await this.storage.get(keys);
4259
+ const typedResult = result;
4260
+ return Result.ok(typedResult);
4261
+ } catch (error) {
4262
+ this.logger.error("DO Storage getMultiple failed", { error, keyCount: keys.length });
4263
+ return Result.err({
4264
+ type: "STORAGE_FAILED",
4265
+ message: `Failed to get multiple values: ${error}`,
4266
+ cause: error
4267
+ });
4268
+ }
4269
+ }
4270
+ /**
4271
+ * Put a single key-value pair
4272
+ */
4273
+ async put(key, value) {
4274
+ try {
4275
+ if (this.config.enableLogging) {
4276
+ this.logger.debug("DO Storage operation", { operation: "put", key });
4277
+ }
4278
+ await this.storage.put(key, value);
4279
+ return Result.ok(void 0);
4280
+ } catch (error) {
4281
+ if (error instanceof Error && (error.message.includes("DataCloneError") || error.message.includes("could not be cloned"))) {
4282
+ this.logger.error("DO Storage serialization failed", { error, key });
4283
+ return Result.err({
4284
+ type: "SERIALIZATION_ERROR",
4285
+ message: `Failed to serialize value for key "${key}": ${error.message}`,
4286
+ cause: error
4287
+ });
4288
+ }
4289
+ if (error instanceof Error && (error.message.includes("QuotaExceededError") || error.message.includes("storage quota exceeded"))) {
4290
+ this.logger.error("DO Storage quota exceeded", { error, key });
4291
+ return Result.err({
4292
+ type: "QUOTA_EXCEEDED",
4293
+ message: `Storage quota exceeded when setting key "${key}"`
4294
+ });
4295
+ }
4296
+ this.logger.error("DO Storage put failed", { error, key });
4297
+ return Result.err({
4298
+ type: "STORAGE_FAILED",
4299
+ message: `Failed to put value for key "${key}": ${error}`,
4300
+ key,
4301
+ cause: error
4302
+ });
4303
+ }
4304
+ }
4305
+ /**
4306
+ * Put multiple key-value pairs
4307
+ */
4308
+ async putMultiple(entries) {
4309
+ try {
4310
+ if (this.config.enableLogging) {
4311
+ this.logger.debug("DO Storage operation", {
4312
+ operation: "putMultiple",
4313
+ entryCount: entries.length
4314
+ });
4315
+ }
4316
+ const entriesObj = {};
4317
+ for (const [key, value] of entries) {
4318
+ entriesObj[key] = value;
4319
+ }
4320
+ await this.storage.put(entriesObj);
4321
+ return Result.ok(void 0);
4322
+ } catch (error) {
4323
+ if (error instanceof Error && (error.message.includes("DataCloneError") || error.message.includes("could not be cloned"))) {
4324
+ this.logger.error("DO Storage serialization failed", { error });
4325
+ return Result.err({
4326
+ type: "SERIALIZATION_ERROR",
4327
+ message: `Failed to serialize values: ${error.message}`,
4328
+ cause: error
4329
+ });
4330
+ }
4331
+ if (error instanceof Error && (error.message.includes("QuotaExceededError") || error.message.includes("storage quota exceeded"))) {
4332
+ this.logger.error("DO Storage quota exceeded", { error });
4333
+ return Result.err({
4334
+ type: "QUOTA_EXCEEDED",
4335
+ message: "Storage quota exceeded when setting multiple values"
4336
+ });
4337
+ }
4338
+ this.logger.error("DO Storage putMultiple failed", { error, entryCount: entries.length });
4339
+ return Result.err({
4340
+ type: "STORAGE_FAILED",
4341
+ message: `Failed to put multiple values: ${error}`,
4342
+ cause: error
4343
+ });
4344
+ }
4345
+ }
4346
+ /**
4347
+ * Delete a single key
4348
+ */
4349
+ async delete(key) {
4350
+ try {
4351
+ if (this.config.enableLogging) {
4352
+ this.logger.debug("DO Storage operation", { operation: "delete", key });
4353
+ }
4354
+ const result = await this.storage.delete(key);
4355
+ return Result.ok(result);
4356
+ } catch (error) {
4357
+ this.logger.error("DO Storage delete failed", { error, key });
4358
+ return Result.err({
4359
+ type: "STORAGE_FAILED",
4360
+ message: `Failed to delete key "${key}": ${error}`,
4361
+ key,
4362
+ cause: error
4363
+ });
4364
+ }
4365
+ }
4366
+ /**
4367
+ * Delete multiple keys
4368
+ */
4369
+ async deleteMultiple(keys) {
4370
+ try {
4371
+ if (this.config.enableLogging) {
4372
+ this.logger.debug("DO Storage operation", {
4373
+ operation: "deleteMultiple",
4374
+ keyCount: keys.length
4375
+ });
4376
+ }
4377
+ const result = await this.storage.delete(keys);
4378
+ return Result.ok(result);
4379
+ } catch (error) {
4380
+ this.logger.error("DO Storage deleteMultiple failed", { error, keyCount: keys.length });
4381
+ return Result.err({
4382
+ type: "STORAGE_FAILED",
4383
+ message: `Failed to delete multiple keys: ${error}`,
4384
+ cause: error
4385
+ });
4386
+ }
4387
+ }
4388
+ /**
4389
+ * Delete all keys in storage
4390
+ */
4391
+ async deleteAll() {
4392
+ try {
4393
+ if (this.config.enableLogging) {
4394
+ this.logger.debug("DO Storage operation", { operation: "deleteAll" });
4395
+ }
4396
+ await this.storage.deleteAll();
4397
+ return Result.ok(void 0);
4398
+ } catch (error) {
4399
+ this.logger.error("DO Storage deleteAll failed", { error });
4400
+ return Result.err({
4401
+ type: "STORAGE_FAILED",
4402
+ message: `Failed to delete all keys: ${error}`,
4403
+ cause: error
4404
+ });
4405
+ }
4406
+ }
4407
+ /**
4408
+ * List keys in storage
4409
+ */
4410
+ async list(options) {
4411
+ try {
4412
+ if (this.config.enableLogging) {
4413
+ this.logger.debug("DO Storage operation", { operation: "list", options });
4414
+ }
4415
+ const result = await this.storage.list(options);
4416
+ return Result.ok(result);
4417
+ } catch (error) {
4418
+ this.logger.error("DO Storage list failed", { error, options });
4419
+ return Result.err({
4420
+ type: "STORAGE_FAILED",
4421
+ message: `Failed to list keys: ${error}`,
4422
+ cause: error
4423
+ });
4424
+ }
4425
+ }
4426
+ /**
4427
+ * Execute operations in a transaction
4428
+ */
4429
+ async transaction(callback) {
4430
+ try {
4431
+ if (this.config.enableLogging) {
4432
+ this.logger.debug("DO Storage operation", { operation: "transaction" });
4433
+ }
4434
+ const result = await this.storage.transaction(async (txnStorage) => {
4435
+ const txnAdapter = new _DurableObjectStorageAdapter(
4436
+ txnStorage,
4437
+ this.config,
4438
+ this.logger
4439
+ );
4440
+ return await callback(txnAdapter);
4441
+ });
4442
+ return result;
4443
+ } catch (error) {
4444
+ this.logger.error("DO Storage transaction failed", { error });
4445
+ return Result.err({
4446
+ type: "TRANSACTION_FAILED",
4447
+ message: `Transaction failed: ${error}`,
4448
+ cause: error
4449
+ });
4450
+ }
4451
+ }
4452
+ };
4453
+ var R2BackupAdapter = class {
4454
+ config;
4455
+ bucket;
4456
+ logger;
4457
+ constructor(config, logger) {
4458
+ this.config = config;
4459
+ this.bucket = config.bucket;
4460
+ this.logger = logger;
4461
+ }
4462
+ /**
4463
+ * Create a backup with compression
4464
+ */
4465
+ async backup(data, options) {
4466
+ try {
4467
+ if (this.config.enableLogging) {
4468
+ this.logger.debug("R2 Backup operation", { operation: "backup" });
4469
+ }
4470
+ const buffer = data instanceof ArrayBuffer ? Buffer.from(data) : data;
4471
+ if (!buffer || buffer.length === 0) {
4472
+ return Result.err({
4473
+ type: "COMPRESSION_FAILED",
4474
+ message: "Cannot backup empty or invalid data"
4475
+ });
4476
+ }
4477
+ const compressionLevel = options?.compressionLevel ?? this.config.defaultCompressionLevel ?? 6;
4478
+ let compressedData;
4479
+ try {
4480
+ compressedData = gzipSync(buffer, { level: compressionLevel });
4481
+ } catch (error) {
4482
+ this.logger.error("Compression failed", { error });
4483
+ return Result.err({
4484
+ type: "COMPRESSION_FAILED",
4485
+ message: `Failed to compress data: ${error}`,
4486
+ cause: error
4487
+ });
4488
+ }
4489
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4490
+ const key = `${this.config.name}-backup-${timestamp}.gz`;
4491
+ const customMetadata = {
4492
+ originalSize: buffer.length.toString(),
4493
+ compressionRatio: (compressedData.length / buffer.length).toString(),
4494
+ compressionLevel: compressionLevel.toString(),
4495
+ backupDate: (/* @__PURE__ */ new Date()).toISOString(),
4496
+ ...options?.metadata || {}
4497
+ };
4498
+ try {
4499
+ const r2Object = await this.bucket.put(key, compressedData, {
4500
+ httpMetadata: {
4501
+ contentType: "application/gzip",
4502
+ contentEncoding: "gzip",
4503
+ contentDisposition: `attachment; filename="${key}"`
4504
+ },
4505
+ customMetadata,
4506
+ storageClass: options?.storageClass || "Standard"
4507
+ });
4508
+ const backupInfo = {
4509
+ key: r2Object.key,
4510
+ size: buffer.length,
4511
+ compressedSize: r2Object.size,
4512
+ compressionRatio: r2Object.size / buffer.length,
4513
+ uploadedAt: r2Object.uploaded,
4514
+ etag: r2Object.etag,
4515
+ metadata: {
4516
+ databaseVersion: customMetadata.databaseVersion,
4517
+ backupType: customMetadata.backupType,
4518
+ originalChecksum: customMetadata.originalChecksum
4519
+ }
4520
+ };
4521
+ if (this.config.enableLogging) {
4522
+ this.logger.debug("Backup completed", {
4523
+ key: backupInfo.key,
4524
+ originalSize: backupInfo.size,
4525
+ compressedSize: backupInfo.compressedSize,
4526
+ compressionRatio: backupInfo.compressionRatio
4527
+ });
4528
+ }
4529
+ return Result.ok(backupInfo);
4530
+ } catch (error) {
4531
+ this.logger.error("R2 upload failed", { error, key });
4532
+ return Result.err({
4533
+ type: "UPLOAD_FAILED",
4534
+ message: `Failed to upload backup: ${error}`,
4535
+ key,
4536
+ cause: error
4537
+ });
4538
+ }
4539
+ } catch (error) {
4540
+ this.logger.error("Backup operation failed", { error });
4541
+ return Result.err({
4542
+ type: "UNKNOWN",
4543
+ message: `Backup failed: ${error}`,
4544
+ cause: error
4545
+ });
4546
+ }
4547
+ }
4548
+ /**
4549
+ * Restore from a backup with decompression
4550
+ */
4551
+ async restore(key, options) {
4552
+ try {
4553
+ if (this.config.enableLogging) {
4554
+ this.logger.debug("R2 Backup operation", { operation: "restore", key });
4555
+ }
4556
+ let r2Object;
4557
+ try {
4558
+ r2Object = await this.bucket.get(key);
4559
+ } catch (error) {
4560
+ this.logger.error("R2 download failed", { error, key });
4561
+ return Result.err({
4562
+ type: "DOWNLOAD_FAILED",
4563
+ message: `Failed to download backup: ${error}`,
4564
+ key,
4565
+ cause: error
4566
+ });
4567
+ }
4568
+ if (!r2Object) {
4569
+ return Result.err({
4570
+ type: "BACKUP_NOT_FOUND",
4571
+ message: `Backup not found: ${key}`,
4572
+ key
4573
+ });
4574
+ }
4575
+ const compressedData = await r2Object.arrayBuffer();
4576
+ try {
4577
+ const decompressed = gunzipSync(Buffer.from(compressedData));
4578
+ if (this.config.enableLogging) {
4579
+ this.logger.debug("Restore completed", {
4580
+ key,
4581
+ compressedSize: compressedData.byteLength,
4582
+ originalSize: decompressed.length
4583
+ });
4584
+ }
4585
+ return Result.ok(decompressed.buffer);
4586
+ } catch (error) {
4587
+ this.logger.error("Decompression failed", { error, key });
4588
+ return Result.err({
4589
+ type: "DECOMPRESSION_FAILED",
4590
+ message: `Failed to decompress backup: ${error}`,
4591
+ cause: error
4592
+ });
4593
+ }
4594
+ } catch (error) {
4595
+ this.logger.error("Restore operation failed", { error, key });
4596
+ return Result.err({
4597
+ type: "RESTORE_FAILED",
4598
+ message: `Restore failed: ${error}`,
4599
+ cause: error
4600
+ });
4601
+ }
4602
+ }
4603
+ /**
4604
+ * List all backups
4605
+ */
4606
+ async listBackups(options) {
4607
+ try {
4608
+ if (this.config.enableLogging) {
4609
+ this.logger.debug("R2 Backup operation", { operation: "listBackups" });
4610
+ }
4611
+ const r2Objects = await this.bucket.list({
4612
+ prefix: `${this.config.name}-backup`,
4613
+ limit: options?.limit,
4614
+ cursor: options?.cursor,
4615
+ include: ["customMetadata"]
4616
+ });
4617
+ const backups = r2Objects.objects.map((obj) => {
4618
+ const originalSize = parseInt(obj.customMetadata?.originalSize || "0", 10);
4619
+ const compressionRatio = parseFloat(
4620
+ obj.customMetadata?.compressionRatio || "1.0"
4621
+ );
4622
+ return {
4623
+ key: obj.key,
4624
+ size: originalSize,
4625
+ compressedSize: obj.size,
4626
+ compressionRatio,
4627
+ uploadedAt: obj.uploaded,
4628
+ etag: obj.etag,
4629
+ metadata: {
4630
+ databaseVersion: obj.customMetadata?.databaseVersion,
4631
+ backupType: obj.customMetadata?.backupType,
4632
+ originalChecksum: obj.customMetadata?.originalChecksum
4633
+ }
4634
+ };
4635
+ });
4636
+ return Result.ok(backups);
4637
+ } catch (error) {
4638
+ this.logger.error("List backups failed", { error });
4639
+ return Result.err({
4640
+ type: "UNKNOWN",
4641
+ message: `Failed to list backups: ${error}`,
4642
+ cause: error
4643
+ });
4644
+ }
4645
+ }
4646
+ /**
4647
+ * Delete a single backup
4648
+ */
4649
+ async deleteBackup(key) {
4650
+ try {
4651
+ if (this.config.enableLogging) {
4652
+ this.logger.debug("R2 Backup operation", { operation: "deleteBackup", key });
4653
+ }
4654
+ await this.bucket.delete(key);
4655
+ return Result.ok(void 0);
4656
+ } catch (error) {
4657
+ this.logger.error("Delete backup failed", { error, key });
4658
+ return Result.err({
4659
+ type: "UNKNOWN",
4660
+ message: `Failed to delete backup: ${error}`,
4661
+ cause: error
4662
+ });
4663
+ }
4664
+ }
4665
+ /**
4666
+ * Delete multiple backups (batched for efficiency)
4667
+ */
4668
+ async deleteMultipleBackups(keys) {
4669
+ try {
4670
+ if (this.config.enableLogging) {
4671
+ this.logger.debug("R2 Backup operation", {
4672
+ operation: "deleteMultipleBackups",
4673
+ count: keys.length
4674
+ });
4675
+ }
4676
+ const batchSize = 1e3;
4677
+ const batches = [];
4678
+ for (let i = 0; i < keys.length; i += batchSize) {
4679
+ batches.push(keys.slice(i, i + batchSize));
4680
+ }
4681
+ for (const batch of batches) {
4682
+ await this.bucket.delete(batch);
4683
+ }
4684
+ return Result.ok(void 0);
4685
+ } catch (error) {
4686
+ this.logger.error("Delete multiple backups failed", { error, count: keys.length });
4687
+ return Result.err({
4688
+ type: "UNKNOWN",
4689
+ message: `Failed to delete backups: ${error}`,
4690
+ cause: error
4691
+ });
4692
+ }
4693
+ }
4694
+ };
4695
+
4696
+ // src/index.ts
4697
+ var version = "1.0.0";
4698
+
4699
+ export { CacheInvalidator, D1DatabaseAdapter, DataSeeder, DatabaseConnectionManager, DatabaseTransaction, DurableObjectStorageAdapter, KVCache, MemoryCache, MigrationLoader, MigrationRunner, R2BackupAdapter, RawSQLExecutor, TenantAwareQueryBuilder, TenantContextManager, TenantSchemaManager, TypeSafeQueryBuilder, version };
4700
+ //# sourceMappingURL=index.js.map
4701
+ //# sourceMappingURL=index.js.map