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