forge-sql-orm 2.0.29 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1090 -81
- package/dist/ForgeSQLORM.js +1090 -69
- package/dist/ForgeSQLORM.js.map +1 -1
- package/dist/ForgeSQLORM.mjs +1073 -69
- package/dist/ForgeSQLORM.mjs.map +1 -1
- package/dist/core/ForgeSQLAnalyseOperations.d.ts +1 -1
- package/dist/core/ForgeSQLAnalyseOperations.d.ts.map +1 -1
- package/dist/core/ForgeSQLCacheOperations.d.ts +119 -0
- package/dist/core/ForgeSQLCacheOperations.d.ts.map +1 -0
- package/dist/core/ForgeSQLCrudOperations.d.ts +38 -22
- package/dist/core/ForgeSQLCrudOperations.d.ts.map +1 -1
- package/dist/core/ForgeSQLORM.d.ts +104 -13
- package/dist/core/ForgeSQLORM.d.ts.map +1 -1
- package/dist/core/ForgeSQLQueryBuilder.d.ts +243 -15
- package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/drizzle/extensions/additionalActions.d.ts +42 -0
- package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -0
- package/dist/utils/cacheContextUtils.d.ts +123 -0
- package/dist/utils/cacheContextUtils.d.ts.map +1 -0
- package/dist/utils/cacheUtils.d.ts +56 -0
- package/dist/utils/cacheUtils.d.ts.map +1 -0
- package/dist/utils/sqlUtils.d.ts +8 -0
- package/dist/utils/sqlUtils.d.ts.map +1 -1
- package/dist/webtriggers/clearCacheSchedulerTrigger.d.ts +46 -0
- package/dist/webtriggers/clearCacheSchedulerTrigger.d.ts.map +1 -0
- package/dist/webtriggers/index.d.ts +1 -0
- package/dist/webtriggers/index.d.ts.map +1 -1
- package/package.json +15 -12
- package/src/core/ForgeSQLAnalyseOperations.ts +1 -1
- package/src/core/ForgeSQLCacheOperations.ts +195 -0
- package/src/core/ForgeSQLCrudOperations.ts +49 -40
- package/src/core/ForgeSQLORM.ts +443 -34
- package/src/core/ForgeSQLQueryBuilder.ts +291 -20
- package/src/index.ts +1 -1
- package/src/lib/drizzle/extensions/additionalActions.ts +548 -0
- package/src/lib/drizzle/extensions/types.d.ts +68 -10
- package/src/utils/cacheContextUtils.ts +210 -0
- package/src/utils/cacheUtils.ts +403 -0
- package/src/utils/sqlUtils.ts +29 -12
- package/src/webtriggers/clearCacheSchedulerTrigger.ts +79 -0
- package/src/webtriggers/index.ts +1 -0
- package/dist/lib/drizzle/extensions/selectAliased.d.ts +0 -9
- package/dist/lib/drizzle/extensions/selectAliased.d.ts.map +0 -1
- package/src/lib/drizzle/extensions/selectAliased.ts +0 -72
package/dist/ForgeSQLORM.js
CHANGED
|
@@ -3,24 +3,45 @@ Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toString
|
|
|
3
3
|
const drizzleOrm = require("drizzle-orm");
|
|
4
4
|
const luxon = require("luxon");
|
|
5
5
|
const sql = require("drizzle-orm/sql/sql");
|
|
6
|
+
const node_async_hooks = require("node:async_hooks");
|
|
7
|
+
const table = require("drizzle-orm/table");
|
|
8
|
+
const crypto = require("crypto");
|
|
9
|
+
const kvs = require("@forge/kvs");
|
|
6
10
|
const sql$1 = require("@forge/sql");
|
|
7
11
|
const mysqlProxy = require("drizzle-orm/mysql-proxy");
|
|
8
12
|
const mysqlCore = require("drizzle-orm/mysql-core");
|
|
9
|
-
|
|
13
|
+
function _interopNamespaceDefault(e) {
|
|
14
|
+
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
15
|
+
if (e) {
|
|
16
|
+
for (const k in e) {
|
|
17
|
+
if (k !== "default") {
|
|
18
|
+
const d = Object.getOwnPropertyDescriptor(e, k);
|
|
19
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
get: () => e[k]
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
n.default = e;
|
|
27
|
+
return Object.freeze(n);
|
|
28
|
+
}
|
|
29
|
+
const crypto__namespace = /* @__PURE__ */ _interopNamespaceDefault(crypto);
|
|
10
30
|
const parseDateTime = (value, format) => {
|
|
11
31
|
let result;
|
|
12
32
|
if (value instanceof Date) {
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
const dt = luxon.DateTime.fromFormat(value, format);
|
|
16
|
-
if (dt.isValid) {
|
|
17
|
-
result = dt.toJSDate();
|
|
33
|
+
result = value;
|
|
18
34
|
} else {
|
|
19
|
-
const
|
|
20
|
-
if (
|
|
21
|
-
result =
|
|
35
|
+
const dt = luxon.DateTime.fromFormat(value, format);
|
|
36
|
+
if (dt.isValid) {
|
|
37
|
+
result = dt.toJSDate();
|
|
22
38
|
} else {
|
|
23
|
-
|
|
39
|
+
const isoDt = luxon.DateTime.fromISO(value);
|
|
40
|
+
if (isoDt.isValid) {
|
|
41
|
+
result = isoDt.toJSDate();
|
|
42
|
+
} else {
|
|
43
|
+
result = new Date(value);
|
|
44
|
+
}
|
|
24
45
|
}
|
|
25
46
|
}
|
|
26
47
|
if (isNaN(result.getTime())) {
|
|
@@ -28,6 +49,14 @@ const parseDateTime = (value, format) => {
|
|
|
28
49
|
}
|
|
29
50
|
return result;
|
|
30
51
|
};
|
|
52
|
+
function formatDateTime(value, format) {
|
|
53
|
+
const fromJSDate = luxon.DateTime.fromJSDate(value);
|
|
54
|
+
if (fromJSDate.isValid) {
|
|
55
|
+
return fromJSDate.toFormat(format);
|
|
56
|
+
} else {
|
|
57
|
+
throw new Error("Invalid Date");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
31
60
|
function getPrimaryKeys(table2) {
|
|
32
61
|
const { columns, primaryKeys } = getTableMetadata(table2);
|
|
33
62
|
const columnPrimaryKeys = Object.entries(columns).filter(([, column]) => column.primary);
|
|
@@ -290,6 +319,275 @@ function formatLimitOffset(limitOrOffset) {
|
|
|
290
319
|
function nextVal(sequenceName) {
|
|
291
320
|
return drizzleOrm.sql.raw(`NEXTVAL(${sequenceName})`);
|
|
292
321
|
}
|
|
322
|
+
const CACHE_CONSTANTS = {
|
|
323
|
+
BATCH_SIZE: 25,
|
|
324
|
+
MAX_RETRY_ATTEMPTS: 3,
|
|
325
|
+
INITIAL_RETRY_DELAY: 1e3,
|
|
326
|
+
RETRY_DELAY_MULTIPLIER: 2,
|
|
327
|
+
DEFAULT_ENTITY_QUERY_NAME: "sql",
|
|
328
|
+
DEFAULT_EXPIRATION_NAME: "expiration",
|
|
329
|
+
DEFAULT_DATA_NAME: "data",
|
|
330
|
+
HASH_LENGTH: 32
|
|
331
|
+
};
|
|
332
|
+
function getCurrentTime() {
|
|
333
|
+
const dt = luxon.DateTime.now();
|
|
334
|
+
return Math.floor(dt.toSeconds());
|
|
335
|
+
}
|
|
336
|
+
function nowPlusSeconds(secondsToAdd) {
|
|
337
|
+
const dt = luxon.DateTime.now().plus({ seconds: secondsToAdd });
|
|
338
|
+
return Math.floor(dt.toSeconds());
|
|
339
|
+
}
|
|
340
|
+
function hashKey(query) {
|
|
341
|
+
const h = crypto__namespace.createHash("sha256");
|
|
342
|
+
h.update(query.sql.toLowerCase());
|
|
343
|
+
h.update(JSON.stringify(query.params));
|
|
344
|
+
return "CachedQuery_" + h.digest("hex").slice(0, CACHE_CONSTANTS.HASH_LENGTH);
|
|
345
|
+
}
|
|
346
|
+
async function deleteCacheEntriesInBatches(results, cacheEntityName) {
|
|
347
|
+
for (let i = 0; i < results.length; i += CACHE_CONSTANTS.BATCH_SIZE) {
|
|
348
|
+
const batch = results.slice(i, i + CACHE_CONSTANTS.BATCH_SIZE);
|
|
349
|
+
let transactionBuilder = kvs.kvs.transact();
|
|
350
|
+
batch.forEach((result) => {
|
|
351
|
+
transactionBuilder = transactionBuilder.delete(result.key, { entityName: cacheEntityName });
|
|
352
|
+
});
|
|
353
|
+
await transactionBuilder.execute();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
async function clearCursorCache(tables, cursor, options) {
|
|
357
|
+
const cacheEntityName = options.cacheEntityName;
|
|
358
|
+
if (!cacheEntityName) {
|
|
359
|
+
throw new Error("cacheEntityName is not configured");
|
|
360
|
+
}
|
|
361
|
+
const entityQueryName = options.cacheEntityQueryName ?? CACHE_CONSTANTS.DEFAULT_ENTITY_QUERY_NAME;
|
|
362
|
+
let filters = new kvs.Filter();
|
|
363
|
+
for (const table2 of tables) {
|
|
364
|
+
const wrapIfNeeded = options.cacheWrapTable ? `\`${table2}\`` : table2;
|
|
365
|
+
filters.or(entityQueryName, kvs.FilterConditions.contains(wrapIfNeeded?.toLowerCase()));
|
|
366
|
+
}
|
|
367
|
+
let entityQueryBuilder = kvs.kvs.entity(cacheEntityName).query().index(entityQueryName).filters(filters);
|
|
368
|
+
if (cursor) {
|
|
369
|
+
entityQueryBuilder = entityQueryBuilder.cursor(cursor);
|
|
370
|
+
}
|
|
371
|
+
const listResult = await entityQueryBuilder.limit(100).getMany();
|
|
372
|
+
if (options.logRawSqlQuery) {
|
|
373
|
+
console.warn(`clear cache Records: ${JSON.stringify(listResult.results.map((r) => r.key))}`);
|
|
374
|
+
}
|
|
375
|
+
await deleteCacheEntriesInBatches(listResult.results, cacheEntityName);
|
|
376
|
+
if (listResult.nextCursor) {
|
|
377
|
+
return listResult.results.length + await clearCursorCache(tables, listResult.nextCursor, options);
|
|
378
|
+
} else {
|
|
379
|
+
return listResult.results.length;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
async function clearExpirationCursorCache(cursor, options) {
|
|
383
|
+
const cacheEntityName = options.cacheEntityName;
|
|
384
|
+
if (!cacheEntityName) {
|
|
385
|
+
throw new Error("cacheEntityName is not configured");
|
|
386
|
+
}
|
|
387
|
+
const entityExpirationName = options.cacheEntityExpirationName ?? CACHE_CONSTANTS.DEFAULT_EXPIRATION_NAME;
|
|
388
|
+
let entityQueryBuilder = kvs.kvs.entity(cacheEntityName).query().index(entityExpirationName).where(kvs.WhereConditions.lessThan(Math.floor(luxon.DateTime.now().toSeconds())));
|
|
389
|
+
if (cursor) {
|
|
390
|
+
entityQueryBuilder = entityQueryBuilder.cursor(cursor);
|
|
391
|
+
}
|
|
392
|
+
const listResult = await entityQueryBuilder.limit(100).getMany();
|
|
393
|
+
if (options.logRawSqlQuery) {
|
|
394
|
+
console.warn(`clear expired Records: ${JSON.stringify(listResult.results.map((r) => r.key))}`);
|
|
395
|
+
}
|
|
396
|
+
await deleteCacheEntriesInBatches(listResult.results, cacheEntityName);
|
|
397
|
+
if (listResult.nextCursor) {
|
|
398
|
+
return listResult.results.length + await clearExpirationCursorCache(listResult.nextCursor, options);
|
|
399
|
+
} else {
|
|
400
|
+
return listResult.results.length;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async function executeWithRetry(operation, operationName) {
|
|
404
|
+
let attempt = 0;
|
|
405
|
+
let delay = CACHE_CONSTANTS.INITIAL_RETRY_DELAY;
|
|
406
|
+
while (attempt < CACHE_CONSTANTS.MAX_RETRY_ATTEMPTS) {
|
|
407
|
+
try {
|
|
408
|
+
return await operation();
|
|
409
|
+
} catch (err) {
|
|
410
|
+
console.warn(`Error during ${operationName}: ${err.message}, retry ${attempt}`, err);
|
|
411
|
+
attempt++;
|
|
412
|
+
if (attempt >= CACHE_CONSTANTS.MAX_RETRY_ATTEMPTS) {
|
|
413
|
+
console.error(`Error during ${operationName}: ${err.message}`, err);
|
|
414
|
+
throw err;
|
|
415
|
+
}
|
|
416
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
417
|
+
delay *= CACHE_CONSTANTS.RETRY_DELAY_MULTIPLIER;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
throw new Error(`Maximum retry attempts exceeded for ${operationName}`);
|
|
421
|
+
}
|
|
422
|
+
async function clearCache(schema, options) {
|
|
423
|
+
const tableName = table.getTableName(schema);
|
|
424
|
+
if (cacheApplicationContext.getStore()) {
|
|
425
|
+
cacheApplicationContext.getStore()?.tables.add(tableName);
|
|
426
|
+
} else {
|
|
427
|
+
await clearTablesCache([tableName], options);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async function clearTablesCache(tables, options) {
|
|
431
|
+
if (!options.cacheEntityName) {
|
|
432
|
+
throw new Error("cacheEntityName is not configured");
|
|
433
|
+
}
|
|
434
|
+
const startTime = luxon.DateTime.now();
|
|
435
|
+
let totalRecords = 0;
|
|
436
|
+
try {
|
|
437
|
+
totalRecords = await executeWithRetry(
|
|
438
|
+
() => clearCursorCache(tables, "", options),
|
|
439
|
+
"clearing cache"
|
|
440
|
+
);
|
|
441
|
+
} finally {
|
|
442
|
+
if (options.logRawSqlQuery) {
|
|
443
|
+
const duration = luxon.DateTime.now().toSeconds() - startTime.toSeconds();
|
|
444
|
+
console.info(`Cleared ${totalRecords} cache records in ${duration} seconds`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
async function clearExpiredCache(options) {
|
|
449
|
+
if (!options.cacheEntityName) {
|
|
450
|
+
throw new Error("cacheEntityName is not configured");
|
|
451
|
+
}
|
|
452
|
+
const startTime = luxon.DateTime.now();
|
|
453
|
+
let totalRecords = 0;
|
|
454
|
+
try {
|
|
455
|
+
totalRecords = await executeWithRetry(
|
|
456
|
+
() => clearExpirationCursorCache("", options),
|
|
457
|
+
"clearing expired cache"
|
|
458
|
+
);
|
|
459
|
+
} finally {
|
|
460
|
+
const duration = luxon.DateTime.now().toSeconds() - startTime.toSeconds();
|
|
461
|
+
console.info(`Cleared ${totalRecords} expired cache records in ${duration} seconds`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
async function getFromCache(query, options) {
|
|
465
|
+
if (!options.cacheEntityName) {
|
|
466
|
+
throw new Error("cacheEntityName is not configured");
|
|
467
|
+
}
|
|
468
|
+
const entityQueryName = options.cacheEntityQueryName ?? CACHE_CONSTANTS.DEFAULT_ENTITY_QUERY_NAME;
|
|
469
|
+
const expirationName = options.cacheEntityExpirationName ?? CACHE_CONSTANTS.DEFAULT_EXPIRATION_NAME;
|
|
470
|
+
const dataName = options.cacheEntityDataName ?? CACHE_CONSTANTS.DEFAULT_DATA_NAME;
|
|
471
|
+
const sqlQuery = query.toSQL();
|
|
472
|
+
const key = hashKey(sqlQuery);
|
|
473
|
+
if (await isTableContainsTableInCacheContext(sqlQuery.sql, options)) {
|
|
474
|
+
if (options.logRawSqlQuery) {
|
|
475
|
+
console.warn(`Context contains value to clear. Skip getting from cache`);
|
|
476
|
+
}
|
|
477
|
+
return void 0;
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
const cacheResult = await kvs.kvs.entity(options.cacheEntityName).get(key);
|
|
481
|
+
if (cacheResult && cacheResult[expirationName] >= getCurrentTime() && sqlQuery.sql.toLowerCase() === cacheResult[entityQueryName]) {
|
|
482
|
+
if (options.logRawSqlQuery) {
|
|
483
|
+
console.warn(`Get value from cache, cacheKey: ${key}`);
|
|
484
|
+
}
|
|
485
|
+
const results = cacheResult[dataName];
|
|
486
|
+
return JSON.parse(results);
|
|
487
|
+
}
|
|
488
|
+
} catch (error) {
|
|
489
|
+
console.error(`Error getting from cache: ${error.message}`, error);
|
|
490
|
+
}
|
|
491
|
+
return void 0;
|
|
492
|
+
}
|
|
493
|
+
async function setCacheResult(query, options, results, cacheTtl) {
|
|
494
|
+
if (!options.cacheEntityName) {
|
|
495
|
+
throw new Error("cacheEntityName is not configured");
|
|
496
|
+
}
|
|
497
|
+
try {
|
|
498
|
+
const entityQueryName = options.cacheEntityQueryName ?? CACHE_CONSTANTS.DEFAULT_ENTITY_QUERY_NAME;
|
|
499
|
+
const expirationName = options.cacheEntityExpirationName ?? CACHE_CONSTANTS.DEFAULT_EXPIRATION_NAME;
|
|
500
|
+
const dataName = options.cacheEntityDataName ?? CACHE_CONSTANTS.DEFAULT_DATA_NAME;
|
|
501
|
+
const sqlQuery = query.toSQL();
|
|
502
|
+
if (await isTableContainsTableInCacheContext(sqlQuery.sql, options)) {
|
|
503
|
+
if (options.logRawSqlQuery) {
|
|
504
|
+
console.warn(`Context contains value to clear. Skip setting from cache`);
|
|
505
|
+
}
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const key = hashKey(sqlQuery);
|
|
509
|
+
await kvs.kvs.transact().set(
|
|
510
|
+
key,
|
|
511
|
+
{
|
|
512
|
+
[entityQueryName]: sqlQuery.sql.toLowerCase(),
|
|
513
|
+
[expirationName]: nowPlusSeconds(cacheTtl),
|
|
514
|
+
[dataName]: JSON.stringify(results)
|
|
515
|
+
},
|
|
516
|
+
{ entityName: options.cacheEntityName }
|
|
517
|
+
).execute();
|
|
518
|
+
if (options.logRawSqlQuery) {
|
|
519
|
+
console.warn(`Store value to cache, cacheKey: ${key}`);
|
|
520
|
+
}
|
|
521
|
+
} catch (error) {
|
|
522
|
+
console.error(`Error setting cache: ${error.message}`, error);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
const cacheApplicationContext = new node_async_hooks.AsyncLocalStorage();
|
|
526
|
+
const localCacheApplicationContext = new node_async_hooks.AsyncLocalStorage();
|
|
527
|
+
async function saveTableIfInsideCacheContext(table$1) {
|
|
528
|
+
const context = cacheApplicationContext.getStore();
|
|
529
|
+
if (context) {
|
|
530
|
+
const tableName = table.getTableName(table$1).toLowerCase();
|
|
531
|
+
context.tables.add(tableName);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
async function saveQueryLocalCacheQuery(query, rows) {
|
|
535
|
+
const context = localCacheApplicationContext.getStore();
|
|
536
|
+
if (context) {
|
|
537
|
+
if (!context.cache) {
|
|
538
|
+
context.cache = {};
|
|
539
|
+
}
|
|
540
|
+
const sql2 = query;
|
|
541
|
+
const key = hashKey(sql2.toSQL());
|
|
542
|
+
context.cache[key] = {
|
|
543
|
+
sql: sql2.toSQL().sql.toLowerCase(),
|
|
544
|
+
data: rows
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
async function getQueryLocalCacheQuery(query) {
|
|
549
|
+
const context = localCacheApplicationContext.getStore();
|
|
550
|
+
if (context) {
|
|
551
|
+
if (!context.cache) {
|
|
552
|
+
context.cache = {};
|
|
553
|
+
}
|
|
554
|
+
const sql2 = query;
|
|
555
|
+
const key = hashKey(sql2.toSQL());
|
|
556
|
+
if (context.cache[key] && context.cache[key].sql === sql2.toSQL().sql.toLowerCase()) {
|
|
557
|
+
return context.cache[key].data;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return void 0;
|
|
561
|
+
}
|
|
562
|
+
async function evictLocalCacheQuery(table$1, options) {
|
|
563
|
+
const context = localCacheApplicationContext.getStore();
|
|
564
|
+
if (context) {
|
|
565
|
+
if (!context.cache) {
|
|
566
|
+
context.cache = {};
|
|
567
|
+
}
|
|
568
|
+
const tableName = table.getTableName(table$1);
|
|
569
|
+
const searchString = options.cacheWrapTable ? `\`${tableName}\`` : tableName;
|
|
570
|
+
const keyToEvicts = [];
|
|
571
|
+
Object.keys(context.cache).forEach((key) => {
|
|
572
|
+
if (context.cache[key].sql.includes(searchString)) {
|
|
573
|
+
keyToEvicts.push(key);
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
keyToEvicts.forEach((key) => delete context.cache[key]);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
async function isTableContainsTableInCacheContext(sql2, options) {
|
|
580
|
+
const context = cacheApplicationContext.getStore();
|
|
581
|
+
if (!context) {
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
const tables = Array.from(context.tables);
|
|
585
|
+
const lowerSql = sql2.toLowerCase();
|
|
586
|
+
return tables.some((table2) => {
|
|
587
|
+
const tablePattern = options.cacheWrapTable ? `\`${table2}\`` : table2;
|
|
588
|
+
return lowerSql.includes(tablePattern);
|
|
589
|
+
});
|
|
590
|
+
}
|
|
293
591
|
class ForgeSQLCrudOperations {
|
|
294
592
|
forgeOperations;
|
|
295
593
|
options;
|
|
@@ -306,12 +604,17 @@ class ForgeSQLCrudOperations {
|
|
|
306
604
|
* Inserts records into the database with optional versioning support.
|
|
307
605
|
* If a version field exists in the schema, versioning is applied.
|
|
308
606
|
*
|
|
607
|
+
* This method automatically handles:
|
|
608
|
+
* - Version field initialization for optimistic locking
|
|
609
|
+
* - Batch insertion for multiple records
|
|
610
|
+
* - Duplicate key handling with optional updates
|
|
611
|
+
*
|
|
309
612
|
* @template T - The type of the table schema
|
|
310
|
-
* @param
|
|
311
|
-
* @param
|
|
312
|
-
* @param
|
|
313
|
-
* @returns
|
|
314
|
-
* @throws
|
|
613
|
+
* @param schema - The entity schema
|
|
614
|
+
* @param models - Array of entities to insert
|
|
615
|
+
* @param updateIfExists - Whether to update existing records (default: false)
|
|
616
|
+
* @returns Promise that resolves to the number of inserted rows
|
|
617
|
+
* @throws Error if the insert operation fails
|
|
315
618
|
*/
|
|
316
619
|
async insert(schema, models, updateIfExists = false) {
|
|
317
620
|
if (!models?.length) return 0;
|
|
@@ -320,25 +623,32 @@ class ForgeSQLCrudOperations {
|
|
|
320
623
|
const preparedModels = models.map(
|
|
321
624
|
(model) => this.prepareModelWithVersion(model, versionMetadata, columns)
|
|
322
625
|
);
|
|
323
|
-
const queryBuilder = this.forgeOperations.
|
|
626
|
+
const queryBuilder = this.forgeOperations.insert(schema).values(preparedModels);
|
|
324
627
|
const finalQuery = updateIfExists ? queryBuilder.onDuplicateKeyUpdate({
|
|
325
628
|
set: Object.fromEntries(
|
|
326
629
|
Object.keys(preparedModels[0]).map((key) => [key, schema[key]])
|
|
327
630
|
)
|
|
328
631
|
}) : queryBuilder;
|
|
329
632
|
const result = await finalQuery;
|
|
633
|
+
await saveTableIfInsideCacheContext(schema);
|
|
330
634
|
return result[0].insertId;
|
|
331
635
|
}
|
|
332
636
|
/**
|
|
333
637
|
* Deletes a record by its primary key with optional version check.
|
|
334
638
|
* If versioning is enabled, ensures the record hasn't been modified since last read.
|
|
335
639
|
*
|
|
640
|
+
* This method automatically handles:
|
|
641
|
+
* - Single primary key validation
|
|
642
|
+
* - Optimistic locking checks if versioning is enabled
|
|
643
|
+
* - Version field validation before deletion
|
|
644
|
+
*
|
|
336
645
|
* @template T - The type of the table schema
|
|
337
|
-
* @param
|
|
338
|
-
* @param
|
|
339
|
-
* @returns
|
|
340
|
-
* @throws
|
|
341
|
-
* @throws
|
|
646
|
+
* @param id - The ID of the record to delete
|
|
647
|
+
* @param schema - The entity schema
|
|
648
|
+
* @returns Promise that resolves to the number of affected rows
|
|
649
|
+
* @throws Error if the delete operation fails
|
|
650
|
+
* @throws Error if multiple primary keys are found
|
|
651
|
+
* @throws Error if optimistic locking check fails
|
|
342
652
|
*/
|
|
343
653
|
async deleteById(id, schema) {
|
|
344
654
|
const { tableName, columns } = getTableMetadata(schema);
|
|
@@ -359,8 +669,12 @@ class ForgeSQLCrudOperations {
|
|
|
359
669
|
conditions.push(drizzleOrm.eq(versionField, oldModel[versionMetadata.fieldName]));
|
|
360
670
|
}
|
|
361
671
|
}
|
|
362
|
-
const queryBuilder = this.forgeOperations.
|
|
672
|
+
const queryBuilder = this.forgeOperations.delete(schema).where(drizzleOrm.and(...conditions));
|
|
363
673
|
const result = await queryBuilder;
|
|
674
|
+
if (versionMetadata && result[0].affectedRows === 0) {
|
|
675
|
+
throw new Error(`Optimistic locking failed: record with primary key ${id} has been modified`);
|
|
676
|
+
}
|
|
677
|
+
await saveTableIfInsideCacheContext(schema);
|
|
364
678
|
return result[0].affectedRows;
|
|
365
679
|
}
|
|
366
680
|
/**
|
|
@@ -370,13 +684,19 @@ class ForgeSQLCrudOperations {
|
|
|
370
684
|
* - Checks for concurrent modifications
|
|
371
685
|
* - Increments the version on successful update
|
|
372
686
|
*
|
|
687
|
+
* This method automatically handles:
|
|
688
|
+
* - Primary key validation
|
|
689
|
+
* - Version field retrieval and validation
|
|
690
|
+
* - Optimistic locking conflict detection
|
|
691
|
+
* - Version field incrementation
|
|
692
|
+
*
|
|
373
693
|
* @template T - The type of the table schema
|
|
374
|
-
* @param
|
|
375
|
-
* @param
|
|
376
|
-
* @returns
|
|
377
|
-
* @throws
|
|
378
|
-
* @throws
|
|
379
|
-
* @throws
|
|
694
|
+
* @param entity - The entity with updated values (must include primary key)
|
|
695
|
+
* @param schema - The entity schema
|
|
696
|
+
* @returns Promise that resolves to the number of affected rows
|
|
697
|
+
* @throws Error if the primary key is not provided
|
|
698
|
+
* @throws Error if optimistic locking check fails
|
|
699
|
+
* @throws Error if multiple primary keys are found
|
|
380
700
|
*/
|
|
381
701
|
async updateById(entity, schema) {
|
|
382
702
|
const { tableName, columns } = getTableMetadata(schema);
|
|
@@ -406,13 +726,14 @@ class ForgeSQLCrudOperations {
|
|
|
406
726
|
conditions.push(drizzleOrm.eq(versionField, currentVersion));
|
|
407
727
|
}
|
|
408
728
|
}
|
|
409
|
-
const queryBuilder = this.forgeOperations.
|
|
729
|
+
const queryBuilder = this.forgeOperations.update(schema).set(updateData).where(drizzleOrm.and(...conditions));
|
|
410
730
|
const result = await queryBuilder;
|
|
411
731
|
if (versionMetadata && result[0].affectedRows === 0) {
|
|
412
732
|
throw new Error(
|
|
413
733
|
`Optimistic locking failed: record with primary key ${entity[primaryKeyName]} has been modified`
|
|
414
734
|
);
|
|
415
735
|
}
|
|
736
|
+
await saveTableIfInsideCacheContext(schema);
|
|
416
737
|
return result[0].affectedRows;
|
|
417
738
|
}
|
|
418
739
|
/**
|
|
@@ -431,8 +752,9 @@ class ForgeSQLCrudOperations {
|
|
|
431
752
|
if (!where) {
|
|
432
753
|
throw new Error("WHERE conditions must be provided");
|
|
433
754
|
}
|
|
434
|
-
const queryBuilder = this.forgeOperations.
|
|
755
|
+
const queryBuilder = this.forgeOperations.update(schema).set(updateData).where(where);
|
|
435
756
|
const result = await queryBuilder;
|
|
757
|
+
await saveTableIfInsideCacheContext(schema);
|
|
436
758
|
return result[0].affectedRows;
|
|
437
759
|
}
|
|
438
760
|
// Helper methods
|
|
@@ -576,7 +898,7 @@ class ForgeSQLCrudOperations {
|
|
|
576
898
|
const [versionFieldName, versionFieldColumn] = versionField;
|
|
577
899
|
const primaryKeys = this.getPrimaryKeys(schema);
|
|
578
900
|
const [primaryKeyName, primaryKeyColumn] = primaryKeys[0];
|
|
579
|
-
const resultQuery = this.forgeOperations.
|
|
901
|
+
const resultQuery = this.forgeOperations.select({
|
|
580
902
|
[primaryKeyName]: primaryKeyColumn,
|
|
581
903
|
[versionFieldName]: versionFieldColumn
|
|
582
904
|
}).from(schema).where(drizzleOrm.eq(primaryKeyColumn, primaryKeyValues[primaryKeyName]));
|
|
@@ -721,7 +1043,125 @@ function createForgeDriverProxy(options, logRawSqlQuery) {
|
|
|
721
1043
|
return forgeDriver(modifiedQuery, params, method);
|
|
722
1044
|
};
|
|
723
1045
|
}
|
|
724
|
-
function
|
|
1046
|
+
function shouldClearCacheOnError(error) {
|
|
1047
|
+
if (error?.code === "VALIDATION_ERROR" || error?.code === "CONSTRAINT_ERROR" || error?.message && /validation/i.exec(error.message)) {
|
|
1048
|
+
return false;
|
|
1049
|
+
}
|
|
1050
|
+
if (error?.code === "DEADLOCK" || error?.code === "LOCK_WAIT_TIMEOUT" || error?.code === "CONNECTION_ERROR" || error?.message && /timeout/i.exec(error.message) || error?.message && /connection/i.exec(error.message)) {
|
|
1051
|
+
return true;
|
|
1052
|
+
}
|
|
1053
|
+
return true;
|
|
1054
|
+
}
|
|
1055
|
+
async function handleSuccessfulExecution(rows, onfulfilled, table2, options, isCached) {
|
|
1056
|
+
try {
|
|
1057
|
+
await evictLocalCacheQuery(table2, options);
|
|
1058
|
+
await saveTableIfInsideCacheContext(table2);
|
|
1059
|
+
if (isCached && !cacheApplicationContext.getStore()) {
|
|
1060
|
+
await clearCache(table2, options);
|
|
1061
|
+
}
|
|
1062
|
+
const result = onfulfilled?.(rows);
|
|
1063
|
+
return result;
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
if (shouldClearCacheOnError(error)) {
|
|
1066
|
+
await evictLocalCacheQuery(table2, options);
|
|
1067
|
+
if (isCached) {
|
|
1068
|
+
await clearCache(table2, options).catch(() => {
|
|
1069
|
+
console.warn("Ignore cache clear errors");
|
|
1070
|
+
});
|
|
1071
|
+
} else {
|
|
1072
|
+
await saveTableIfInsideCacheContext(table2);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
throw error;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
function handleFunctionCall(value, target, args, table2, options, isCached) {
|
|
1079
|
+
const result = value.apply(target, args);
|
|
1080
|
+
if (typeof result === "object" && result !== null && "execute" in result) {
|
|
1081
|
+
return wrapCacheEvictBuilder(result, table2, options, isCached);
|
|
1082
|
+
}
|
|
1083
|
+
return result;
|
|
1084
|
+
}
|
|
1085
|
+
const wrapCacheEvictBuilder = (rawBuilder, table2, options, isCached) => {
|
|
1086
|
+
return new Proxy(rawBuilder, {
|
|
1087
|
+
get(target, prop, receiver) {
|
|
1088
|
+
if (prop === "then") {
|
|
1089
|
+
return (onfulfilled, onrejected) => target.execute().then(
|
|
1090
|
+
(rows) => handleSuccessfulExecution(rows, onfulfilled, table2, options, isCached),
|
|
1091
|
+
onrejected
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
const value = Reflect.get(target, prop, receiver);
|
|
1095
|
+
if (typeof value === "function") {
|
|
1096
|
+
return (...args) => handleFunctionCall(value, target, args, table2, options, isCached);
|
|
1097
|
+
}
|
|
1098
|
+
return value;
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1101
|
+
};
|
|
1102
|
+
function insertAndEvictCacheBuilder(db, table2, options, isCached) {
|
|
1103
|
+
const builder = db.insert(table2);
|
|
1104
|
+
return wrapCacheEvictBuilder(
|
|
1105
|
+
builder,
|
|
1106
|
+
table2,
|
|
1107
|
+
options,
|
|
1108
|
+
isCached
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
function updateAndEvictCacheBuilder(db, table2, options, isCached) {
|
|
1112
|
+
const builder = db.update(table2);
|
|
1113
|
+
return wrapCacheEvictBuilder(
|
|
1114
|
+
builder,
|
|
1115
|
+
table2,
|
|
1116
|
+
options,
|
|
1117
|
+
isCached
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
function deleteAndEvictCacheBuilder(db, table2, options, isCached) {
|
|
1121
|
+
const builder = db.delete(table2);
|
|
1122
|
+
return wrapCacheEvictBuilder(
|
|
1123
|
+
builder,
|
|
1124
|
+
table2,
|
|
1125
|
+
options,
|
|
1126
|
+
isCached
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
async function handleCachedQuery(target, options, cacheTtl, selections, aliasMap, onfulfilled, onrejected) {
|
|
1130
|
+
try {
|
|
1131
|
+
const localCached = await getQueryLocalCacheQuery(target);
|
|
1132
|
+
if (localCached) {
|
|
1133
|
+
return onfulfilled?.(localCached);
|
|
1134
|
+
}
|
|
1135
|
+
const cacheResult = await getFromCache(target, options);
|
|
1136
|
+
if (cacheResult) {
|
|
1137
|
+
return onfulfilled?.(cacheResult);
|
|
1138
|
+
}
|
|
1139
|
+
const rows = await target.execute();
|
|
1140
|
+
const transformed = applyFromDriverTransform(rows, selections, aliasMap);
|
|
1141
|
+
await saveQueryLocalCacheQuery(target, transformed);
|
|
1142
|
+
await setCacheResult(target, options, transformed, cacheTtl).catch((cacheError) => {
|
|
1143
|
+
console.warn("Cache set error:", cacheError);
|
|
1144
|
+
});
|
|
1145
|
+
return onfulfilled?.(transformed);
|
|
1146
|
+
} catch (error) {
|
|
1147
|
+
return onrejected?.(error);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
async function handleNonCachedQuery(target, selections, aliasMap, onfulfilled, onrejected) {
|
|
1151
|
+
try {
|
|
1152
|
+
const localCached = await getQueryLocalCacheQuery(target);
|
|
1153
|
+
if (localCached) {
|
|
1154
|
+
return onfulfilled?.(localCached);
|
|
1155
|
+
}
|
|
1156
|
+
const rows = await target.execute();
|
|
1157
|
+
const transformed = applyFromDriverTransform(rows, selections, aliasMap);
|
|
1158
|
+
await saveQueryLocalCacheQuery(target, transformed);
|
|
1159
|
+
return onfulfilled?.(transformed);
|
|
1160
|
+
} catch (error) {
|
|
1161
|
+
return onrejected?.(error);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
function createAliasedSelectBuilder(db, fields, selectFn, useCache, options, cacheTtl) {
|
|
725
1165
|
const { selections, aliasMap } = mapSelectFieldsWithAlias(fields);
|
|
726
1166
|
const builder = selectFn(selections);
|
|
727
1167
|
const wrapBuilder = (rawBuilder) => {
|
|
@@ -734,10 +1174,22 @@ function createAliasedSelectBuilder(db, fields, selectFn) {
|
|
|
734
1174
|
};
|
|
735
1175
|
}
|
|
736
1176
|
if (prop === "then") {
|
|
737
|
-
return (onfulfilled, onrejected) =>
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
1177
|
+
return (onfulfilled, onrejected) => {
|
|
1178
|
+
if (useCache) {
|
|
1179
|
+
const ttl = cacheTtl ?? options.cacheTTL ?? 120;
|
|
1180
|
+
return handleCachedQuery(
|
|
1181
|
+
target,
|
|
1182
|
+
options,
|
|
1183
|
+
ttl,
|
|
1184
|
+
selections,
|
|
1185
|
+
aliasMap,
|
|
1186
|
+
onfulfilled,
|
|
1187
|
+
onrejected
|
|
1188
|
+
);
|
|
1189
|
+
} else {
|
|
1190
|
+
return handleNonCachedQuery(target, selections, aliasMap, onfulfilled, onrejected);
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
741
1193
|
}
|
|
742
1194
|
const value = Reflect.get(target, prop, receiver);
|
|
743
1195
|
if (typeof value === "function") {
|
|
@@ -755,12 +1207,72 @@ function createAliasedSelectBuilder(db, fields, selectFn) {
|
|
|
755
1207
|
};
|
|
756
1208
|
return wrapBuilder(builder);
|
|
757
1209
|
}
|
|
758
|
-
|
|
1210
|
+
const DEFAULT_OPTIONS = {
|
|
1211
|
+
logRawSqlQuery: false,
|
|
1212
|
+
disableOptimisticLocking: false,
|
|
1213
|
+
cacheTTL: 120,
|
|
1214
|
+
cacheWrapTable: true,
|
|
1215
|
+
cacheEntityQueryName: "sql",
|
|
1216
|
+
cacheEntityExpirationName: "expiration",
|
|
1217
|
+
cacheEntityDataName: "data"
|
|
1218
|
+
};
|
|
1219
|
+
function patchDbWithSelectAliased(db, options) {
|
|
1220
|
+
const newOptions = { ...DEFAULT_OPTIONS, ...options };
|
|
759
1221
|
db.selectAliased = function(fields) {
|
|
760
|
-
return createAliasedSelectBuilder(
|
|
1222
|
+
return createAliasedSelectBuilder(
|
|
1223
|
+
db,
|
|
1224
|
+
fields,
|
|
1225
|
+
(selections) => db.select(selections),
|
|
1226
|
+
false,
|
|
1227
|
+
newOptions
|
|
1228
|
+
);
|
|
761
1229
|
};
|
|
762
1230
|
db.selectAliasedDistinct = function(fields) {
|
|
763
|
-
return createAliasedSelectBuilder(
|
|
1231
|
+
return createAliasedSelectBuilder(
|
|
1232
|
+
db,
|
|
1233
|
+
fields,
|
|
1234
|
+
(selections) => db.selectDistinct(selections),
|
|
1235
|
+
false,
|
|
1236
|
+
newOptions
|
|
1237
|
+
);
|
|
1238
|
+
};
|
|
1239
|
+
db.selectAliasedCacheable = function(fields, cacheTtl) {
|
|
1240
|
+
return createAliasedSelectBuilder(
|
|
1241
|
+
db,
|
|
1242
|
+
fields,
|
|
1243
|
+
(selections) => db.select(selections),
|
|
1244
|
+
true,
|
|
1245
|
+
newOptions,
|
|
1246
|
+
cacheTtl
|
|
1247
|
+
);
|
|
1248
|
+
};
|
|
1249
|
+
db.selectAliasedDistinctCacheable = function(fields, cacheTtl) {
|
|
1250
|
+
return createAliasedSelectBuilder(
|
|
1251
|
+
db,
|
|
1252
|
+
fields,
|
|
1253
|
+
(selections) => db.selectDistinct(selections),
|
|
1254
|
+
true,
|
|
1255
|
+
newOptions,
|
|
1256
|
+
cacheTtl
|
|
1257
|
+
);
|
|
1258
|
+
};
|
|
1259
|
+
db.insertWithCacheContext = function(table2) {
|
|
1260
|
+
return insertAndEvictCacheBuilder(db, table2, newOptions, false);
|
|
1261
|
+
};
|
|
1262
|
+
db.insertAndEvictCache = function(table2) {
|
|
1263
|
+
return insertAndEvictCacheBuilder(db, table2, newOptions, true);
|
|
1264
|
+
};
|
|
1265
|
+
db.updateWithCacheContext = function(table2) {
|
|
1266
|
+
return updateAndEvictCacheBuilder(db, table2, newOptions, false);
|
|
1267
|
+
};
|
|
1268
|
+
db.updateAndEvictCache = function(table2) {
|
|
1269
|
+
return updateAndEvictCacheBuilder(db, table2, newOptions, true);
|
|
1270
|
+
};
|
|
1271
|
+
db.deleteWithCacheContext = function(table2) {
|
|
1272
|
+
return deleteAndEvictCacheBuilder(db, table2, newOptions, false);
|
|
1273
|
+
};
|
|
1274
|
+
db.deleteAndEvictCache = function(table2) {
|
|
1275
|
+
return deleteAndEvictCacheBuilder(db, table2, newOptions, true);
|
|
764
1276
|
};
|
|
765
1277
|
return db;
|
|
766
1278
|
}
|
|
@@ -912,14 +1424,14 @@ class ForgeSQLAnalyseOperation {
|
|
|
912
1424
|
* @returns {string} The SQL query for cluster statement history
|
|
913
1425
|
*/
|
|
914
1426
|
buildClusterStatementQuery(tables, from, to) {
|
|
915
|
-
const
|
|
1427
|
+
const formatDateTime2 = (date) => luxon.DateTime.fromJSDate(date).toFormat("yyyy-LL-dd'T'HH:mm:ss.SSS");
|
|
916
1428
|
const tableConditions = tables.map((table2) => `TABLE_NAMES LIKE CONCAT(SCHEMA_NAME, '.', '%', '${table2}', '%')`).join(" OR ");
|
|
917
1429
|
const timeConditions = [];
|
|
918
1430
|
if (from) {
|
|
919
|
-
timeConditions.push(`SUMMARY_BEGIN_TIME >= '${
|
|
1431
|
+
timeConditions.push(`SUMMARY_BEGIN_TIME >= '${formatDateTime2(from)}'`);
|
|
920
1432
|
}
|
|
921
1433
|
if (to) {
|
|
922
|
-
timeConditions.push(`SUMMARY_END_TIME <= '${
|
|
1434
|
+
timeConditions.push(`SUMMARY_END_TIME <= '${formatDateTime2(to)}'`);
|
|
923
1435
|
}
|
|
924
1436
|
let whereClauses;
|
|
925
1437
|
if (tableConditions?.length) {
|
|
@@ -992,12 +1504,167 @@ class ForgeSQLAnalyseOperation {
|
|
|
992
1504
|
return this.analyzeQueriesHistoryRaw(tableNames, fromDate, toDate);
|
|
993
1505
|
}
|
|
994
1506
|
}
|
|
1507
|
+
class ForgeSQLCacheOperations {
|
|
1508
|
+
options;
|
|
1509
|
+
forgeOperations;
|
|
1510
|
+
/**
|
|
1511
|
+
* Creates a new instance of ForgeSQLCacheOperations.
|
|
1512
|
+
*
|
|
1513
|
+
* @param options - Configuration options for the ORM
|
|
1514
|
+
* @param forgeOperations - The ForgeSQL operations instance
|
|
1515
|
+
*/
|
|
1516
|
+
constructor(options, forgeOperations) {
|
|
1517
|
+
this.options = options;
|
|
1518
|
+
this.forgeOperations = forgeOperations;
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Evicts cache for multiple tables using Drizzle table objects.
|
|
1522
|
+
*
|
|
1523
|
+
* @param tables - Array of Drizzle table objects to clear cache for
|
|
1524
|
+
* @returns Promise that resolves when cache eviction is complete
|
|
1525
|
+
* @throws Error if cacheEntityName is not configured
|
|
1526
|
+
*/
|
|
1527
|
+
async evictCacheEntities(tables) {
|
|
1528
|
+
if (!this.options.cacheEntityName) {
|
|
1529
|
+
throw new Error("cacheEntityName is not configured");
|
|
1530
|
+
}
|
|
1531
|
+
await this.evictCache(tables.map((t) => table.getTableName(t)));
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Evicts cache for multiple tables by their names.
|
|
1535
|
+
*
|
|
1536
|
+
* @param tables - Array of table names to clear cache for
|
|
1537
|
+
* @returns Promise that resolves when cache eviction is complete
|
|
1538
|
+
* @throws Error if cacheEntityName is not configured
|
|
1539
|
+
*/
|
|
1540
|
+
async evictCache(tables) {
|
|
1541
|
+
if (!this.options.cacheEntityName) {
|
|
1542
|
+
throw new Error("cacheEntityName is not configured");
|
|
1543
|
+
}
|
|
1544
|
+
await clearTablesCache(tables, this.options);
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Inserts records with optimistic locking/versioning and automatically evicts cache.
|
|
1548
|
+
*
|
|
1549
|
+
* This method uses `modifyWithVersioning().insert()` internally, providing:
|
|
1550
|
+
* - Automatic version field initialization
|
|
1551
|
+
* - Optimistic locking support
|
|
1552
|
+
* - Cache eviction after successful operation
|
|
1553
|
+
*
|
|
1554
|
+
* @param schema - The table schema
|
|
1555
|
+
* @param models - Array of entities to insert
|
|
1556
|
+
* @param updateIfExists - Whether to update existing records
|
|
1557
|
+
* @returns Promise that resolves to the number of inserted rows
|
|
1558
|
+
* @throws Error if cacheEntityName is not configured
|
|
1559
|
+
* @throws Error if optimistic locking check fails
|
|
1560
|
+
*/
|
|
1561
|
+
async insert(schema, models, updateIfExists) {
|
|
1562
|
+
this.validateCacheConfiguration();
|
|
1563
|
+
const number = await this.forgeOperations.modifyWithVersioning().insert(schema, models, updateIfExists);
|
|
1564
|
+
await clearCache(schema, this.options);
|
|
1565
|
+
return number;
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Deletes a record by ID with optimistic locking/versioning and automatically evicts cache.
|
|
1569
|
+
*
|
|
1570
|
+
* This method uses `modifyWithVersioning().deleteById()` internally, providing:
|
|
1571
|
+
* - Optimistic locking checks before deletion
|
|
1572
|
+
* - Version field validation
|
|
1573
|
+
* - Cache eviction after successful operation
|
|
1574
|
+
*
|
|
1575
|
+
* @param id - The ID of the record to delete
|
|
1576
|
+
* @param schema - The table schema
|
|
1577
|
+
* @returns Promise that resolves to the number of affected rows
|
|
1578
|
+
* @throws Error if cacheEntityName is not configured
|
|
1579
|
+
* @throws Error if optimistic locking check fails
|
|
1580
|
+
*/
|
|
1581
|
+
async deleteById(id, schema) {
|
|
1582
|
+
this.validateCacheConfiguration();
|
|
1583
|
+
const number = await this.forgeOperations.modifyWithVersioning().deleteById(id, schema);
|
|
1584
|
+
await clearCache(schema, this.options);
|
|
1585
|
+
return number;
|
|
1586
|
+
}
|
|
1587
|
+
/**
|
|
1588
|
+
* Updates a record by ID with optimistic locking/versioning and automatically evicts cache.
|
|
1589
|
+
*
|
|
1590
|
+
* This method uses `modifyWithVersioning().updateById()` internally, providing:
|
|
1591
|
+
* - Optimistic locking checks before update
|
|
1592
|
+
* - Version field incrementation
|
|
1593
|
+
* - Cache eviction after successful operation
|
|
1594
|
+
*
|
|
1595
|
+
* @param entity - The entity with updated values (must include primary key)
|
|
1596
|
+
* @param schema - The table schema
|
|
1597
|
+
* @returns Promise that resolves to the number of affected rows
|
|
1598
|
+
* @throws Error if cacheEntityName is not configured
|
|
1599
|
+
* @throws Error if optimistic locking check fails
|
|
1600
|
+
*/
|
|
1601
|
+
async updateById(entity, schema) {
|
|
1602
|
+
this.validateCacheConfiguration();
|
|
1603
|
+
const number = await this.forgeOperations.modifyWithVersioning().updateById(entity, schema);
|
|
1604
|
+
await clearCache(schema, this.options);
|
|
1605
|
+
return number;
|
|
1606
|
+
}
|
|
1607
|
+
/**
|
|
1608
|
+
* Updates fields based on conditions with optimistic locking/versioning and automatically evicts cache.
|
|
1609
|
+
*
|
|
1610
|
+
* This method uses `modifyWithVersioning().updateFields()` internally, providing:
|
|
1611
|
+
* - Optimistic locking support (if version field is configured)
|
|
1612
|
+
* - Version field validation and incrementation
|
|
1613
|
+
* - Cache eviction after successful operation
|
|
1614
|
+
*
|
|
1615
|
+
* @param updateData - The data to update
|
|
1616
|
+
* @param schema - The table schema
|
|
1617
|
+
* @param where - Optional WHERE conditions
|
|
1618
|
+
* @returns Promise that resolves to the number of affected rows
|
|
1619
|
+
* @throws Error if cacheEntityName is not configured
|
|
1620
|
+
* @throws Error if optimistic locking check fails
|
|
1621
|
+
*/
|
|
1622
|
+
async updateFields(updateData, schema, where) {
|
|
1623
|
+
this.validateCacheConfiguration();
|
|
1624
|
+
const number = await this.forgeOperations.modifyWithVersioning().updateFields(updateData, schema, where);
|
|
1625
|
+
await clearCache(schema, this.options);
|
|
1626
|
+
return number;
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* Executes a query with caching support.
|
|
1630
|
+
* First checks cache, if not found executes query and stores result in cache.
|
|
1631
|
+
*
|
|
1632
|
+
* @param query - The Drizzle query to execute
|
|
1633
|
+
* @param cacheTtl - Optional cache TTL override
|
|
1634
|
+
* @returns Promise that resolves to the query results
|
|
1635
|
+
* @throws Error if cacheEntityName is not configured
|
|
1636
|
+
*/
|
|
1637
|
+
async executeQuery(query, cacheTtl) {
|
|
1638
|
+
this.validateCacheConfiguration();
|
|
1639
|
+
const sqlQuery = query;
|
|
1640
|
+
const cacheResult = await getFromCache(sqlQuery, this.options);
|
|
1641
|
+
if (cacheResult) {
|
|
1642
|
+
return cacheResult;
|
|
1643
|
+
}
|
|
1644
|
+
const results = await query;
|
|
1645
|
+
await setCacheResult(sqlQuery, this.options, results, cacheTtl ?? this.options.cacheTTL ?? 60);
|
|
1646
|
+
return results;
|
|
1647
|
+
}
|
|
1648
|
+
/**
|
|
1649
|
+
* Validates that cache configuration is properly set up.
|
|
1650
|
+
*
|
|
1651
|
+
* @throws Error if cacheEntityName is not configured
|
|
1652
|
+
* @private
|
|
1653
|
+
*/
|
|
1654
|
+
validateCacheConfiguration() {
|
|
1655
|
+
if (!this.options.cacheEntityName) {
|
|
1656
|
+
throw new Error("cacheEntityName is not configured");
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
995
1660
|
class ForgeSQLORMImpl {
|
|
996
1661
|
static instance = null;
|
|
997
1662
|
drizzle;
|
|
998
1663
|
crudOperations;
|
|
999
1664
|
fetchOperations;
|
|
1000
1665
|
analyzeOperations;
|
|
1666
|
+
cacheOperations;
|
|
1667
|
+
options;
|
|
1001
1668
|
/**
|
|
1002
1669
|
* Private constructor to enforce singleton behavior.
|
|
1003
1670
|
* @param options - Options for configuring ForgeSQL ORM behavior.
|
|
@@ -1006,28 +1673,185 @@ class ForgeSQLORMImpl {
|
|
|
1006
1673
|
try {
|
|
1007
1674
|
const newOptions = options ?? {
|
|
1008
1675
|
logRawSqlQuery: false,
|
|
1009
|
-
disableOptimisticLocking: false
|
|
1676
|
+
disableOptimisticLocking: false,
|
|
1677
|
+
cacheWrapTable: true,
|
|
1678
|
+
cacheTTL: 120,
|
|
1679
|
+
cacheEntityQueryName: "sql",
|
|
1680
|
+
cacheEntityExpirationName: "expiration",
|
|
1681
|
+
cacheEntityDataName: "data"
|
|
1010
1682
|
};
|
|
1683
|
+
this.options = newOptions;
|
|
1011
1684
|
if (newOptions.logRawSqlQuery) {
|
|
1012
1685
|
console.debug("Initializing ForgeSQLORM...");
|
|
1013
1686
|
}
|
|
1014
1687
|
const proxiedDriver = createForgeDriverProxy(newOptions.hints, newOptions.logRawSqlQuery);
|
|
1015
1688
|
this.drizzle = patchDbWithSelectAliased(
|
|
1016
|
-
mysqlProxy.drizzle(proxiedDriver, { logger: newOptions.logRawSqlQuery })
|
|
1689
|
+
mysqlProxy.drizzle(proxiedDriver, { logger: newOptions.logRawSqlQuery }),
|
|
1690
|
+
newOptions
|
|
1017
1691
|
);
|
|
1018
1692
|
this.crudOperations = new ForgeSQLCrudOperations(this, newOptions);
|
|
1019
1693
|
this.fetchOperations = new ForgeSQLSelectOperations(newOptions);
|
|
1020
1694
|
this.analyzeOperations = new ForgeSQLAnalyseOperation(this);
|
|
1695
|
+
this.cacheOperations = new ForgeSQLCacheOperations(newOptions, this);
|
|
1021
1696
|
} catch (error) {
|
|
1022
1697
|
console.error("ForgeSQLORM initialization failed:", error);
|
|
1023
1698
|
throw error;
|
|
1024
1699
|
}
|
|
1025
1700
|
}
|
|
1701
|
+
/**
|
|
1702
|
+
* Executes operations within a cache context that collects cache eviction events.
|
|
1703
|
+
* All clearCache calls within the context are collected and executed in batch at the end.
|
|
1704
|
+
* Queries executed within this context will bypass cache for tables that were marked for clearing.
|
|
1705
|
+
*
|
|
1706
|
+
* This is useful for:
|
|
1707
|
+
* - Batch operations that affect multiple tables
|
|
1708
|
+
* - Transaction-like operations where you want to clear cache only at the end
|
|
1709
|
+
* - Performance optimization by reducing cache clear operations
|
|
1710
|
+
*
|
|
1711
|
+
* @param cacheContext - Function containing operations that may trigger cache evictions
|
|
1712
|
+
* @returns Promise that resolves when all operations and cache clearing are complete
|
|
1713
|
+
*
|
|
1714
|
+
* @example
|
|
1715
|
+
* ```typescript
|
|
1716
|
+
* await forgeSQL.executeWithCacheContext(async () => {
|
|
1717
|
+
* await forgeSQL.modifyWithVersioning().insert(users, userData);
|
|
1718
|
+
* await forgeSQL.modifyWithVersioning().insert(orders, orderData);
|
|
1719
|
+
* // Cache for both users and orders tables will be cleared at the end
|
|
1720
|
+
* });
|
|
1721
|
+
* ```
|
|
1722
|
+
*/
|
|
1723
|
+
executeWithCacheContext(cacheContext) {
|
|
1724
|
+
return this.executeWithCacheContextAndReturnValue(cacheContext);
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* Executes operations within a cache context and returns a value.
|
|
1728
|
+
* All clearCache calls within the context are collected and executed in batch at the end.
|
|
1729
|
+
* Queries executed within this context will bypass cache for tables that were marked for clearing.
|
|
1730
|
+
*
|
|
1731
|
+
* @param cacheContext - Function containing operations that may trigger cache evictions
|
|
1732
|
+
* @returns Promise that resolves to the return value of the cacheContext function
|
|
1733
|
+
*
|
|
1734
|
+
* @example
|
|
1735
|
+
* ```typescript
|
|
1736
|
+
* const result = await forgeSQL.executeWithCacheContextAndReturnValue(async () => {
|
|
1737
|
+
* await forgeSQL.modifyWithVersioning().insert(users, userData);
|
|
1738
|
+
* return await forgeSQL.fetch().executeQueryOnlyOne(selectUserQuery);
|
|
1739
|
+
* });
|
|
1740
|
+
* ```
|
|
1741
|
+
*/
|
|
1742
|
+
async executeWithCacheContextAndReturnValue(cacheContext) {
|
|
1743
|
+
return await this.executeWithLocalCacheContextAndReturnValue(
|
|
1744
|
+
async () => await cacheApplicationContext.run({ tables: /* @__PURE__ */ new Set() }, async () => {
|
|
1745
|
+
try {
|
|
1746
|
+
return await cacheContext();
|
|
1747
|
+
} finally {
|
|
1748
|
+
await clearTablesCache(
|
|
1749
|
+
Array.from(cacheApplicationContext.getStore()?.tables ?? []),
|
|
1750
|
+
this.options
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1753
|
+
})
|
|
1754
|
+
);
|
|
1755
|
+
}
|
|
1756
|
+
/**
|
|
1757
|
+
* Executes operations within a local cache context and returns a value.
|
|
1758
|
+
* This provides in-memory caching for select queries within a single request scope.
|
|
1759
|
+
*
|
|
1760
|
+
* @param cacheContext - Function containing operations that will benefit from local caching
|
|
1761
|
+
* @returns Promise that resolves to the return value of the cacheContext function
|
|
1762
|
+
*/
|
|
1763
|
+
async executeWithLocalCacheContextAndReturnValue(cacheContext) {
|
|
1764
|
+
return await localCacheApplicationContext.run({ cache: {} }, async () => {
|
|
1765
|
+
return await cacheContext();
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* Executes operations within a local cache context.
|
|
1770
|
+
* This provides in-memory caching for select queries within a single request scope.
|
|
1771
|
+
*
|
|
1772
|
+
* @param cacheContext - Function containing operations that will benefit from local caching
|
|
1773
|
+
* @returns Promise that resolves when all operations are complete
|
|
1774
|
+
*/
|
|
1775
|
+
executeWithLocalContext(cacheContext) {
|
|
1776
|
+
return this.executeWithLocalCacheContextAndReturnValue(cacheContext);
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Creates an insert query builder.
|
|
1780
|
+
*
|
|
1781
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
1782
|
+
* For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
|
|
1783
|
+
*
|
|
1784
|
+
* @param table - The table to insert into
|
|
1785
|
+
* @returns Insert query builder (no versioning, no cache management)
|
|
1786
|
+
*/
|
|
1787
|
+
insert(table2) {
|
|
1788
|
+
return this.drizzle.insertWithCacheContext(table2);
|
|
1789
|
+
}
|
|
1790
|
+
/**
|
|
1791
|
+
* Creates an insert query builder that automatically evicts cache after execution.
|
|
1792
|
+
*
|
|
1793
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
1794
|
+
* For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
|
|
1795
|
+
*
|
|
1796
|
+
* @param table - The table to insert into
|
|
1797
|
+
* @returns Insert query builder with automatic cache eviction (no versioning)
|
|
1798
|
+
*/
|
|
1799
|
+
insertAndEvictCache(table2) {
|
|
1800
|
+
return this.drizzle.insertAndEvictCache(table2);
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Creates an update query builder that automatically evicts cache after execution.
|
|
1804
|
+
*
|
|
1805
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
1806
|
+
* For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
|
|
1807
|
+
*
|
|
1808
|
+
* @param table - The table to update
|
|
1809
|
+
* @returns Update query builder with automatic cache eviction (no versioning)
|
|
1810
|
+
*/
|
|
1811
|
+
updateAndEvictCache(table2) {
|
|
1812
|
+
return this.drizzle.updateAndEvictCache(table2);
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Creates an update query builder.
|
|
1816
|
+
*
|
|
1817
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
1818
|
+
* For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
|
|
1819
|
+
*
|
|
1820
|
+
* @param table - The table to update
|
|
1821
|
+
* @returns Update query builder (no versioning, no cache management)
|
|
1822
|
+
*/
|
|
1823
|
+
update(table2) {
|
|
1824
|
+
return this.drizzle.updateWithCacheContext(table2);
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Creates a delete query builder.
|
|
1828
|
+
*
|
|
1829
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
1830
|
+
* For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
|
|
1831
|
+
*
|
|
1832
|
+
* @param table - The table to delete from
|
|
1833
|
+
* @returns Delete query builder (no versioning, no cache management)
|
|
1834
|
+
*/
|
|
1835
|
+
delete(table2) {
|
|
1836
|
+
return this.drizzle.deleteWithCacheContext(table2);
|
|
1837
|
+
}
|
|
1838
|
+
/**
|
|
1839
|
+
* Creates a delete query builder that automatically evicts cache after execution.
|
|
1840
|
+
*
|
|
1841
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
1842
|
+
* For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
|
|
1843
|
+
*
|
|
1844
|
+
* @param table - The table to delete from
|
|
1845
|
+
* @returns Delete query builder with automatic cache eviction (no versioning)
|
|
1846
|
+
*/
|
|
1847
|
+
deleteAndEvictCache(table2) {
|
|
1848
|
+
return this.drizzle.deleteAndEvictCache(table2);
|
|
1849
|
+
}
|
|
1026
1850
|
/**
|
|
1027
1851
|
* Create the modify operations instance.
|
|
1028
1852
|
* @returns modify operations.
|
|
1029
1853
|
*/
|
|
1030
|
-
|
|
1854
|
+
modifyWithVersioning() {
|
|
1031
1855
|
return this.crudOperations;
|
|
1032
1856
|
}
|
|
1033
1857
|
/**
|
|
@@ -1039,13 +1863,6 @@ class ForgeSQLORMImpl {
|
|
|
1039
1863
|
ForgeSQLORMImpl.instance ??= new ForgeSQLORMImpl(options);
|
|
1040
1864
|
return ForgeSQLORMImpl.instance;
|
|
1041
1865
|
}
|
|
1042
|
-
/**
|
|
1043
|
-
* Retrieves the CRUD operations instance.
|
|
1044
|
-
* @returns CRUD operations.
|
|
1045
|
-
*/
|
|
1046
|
-
crud() {
|
|
1047
|
-
return this.modify();
|
|
1048
|
-
}
|
|
1049
1866
|
/**
|
|
1050
1867
|
* Retrieves the fetch operations instance.
|
|
1051
1868
|
* @returns Fetch operations.
|
|
@@ -1053,9 +1870,26 @@ class ForgeSQLORMImpl {
|
|
|
1053
1870
|
fetch() {
|
|
1054
1871
|
return this.fetchOperations;
|
|
1055
1872
|
}
|
|
1873
|
+
/**
|
|
1874
|
+
* Provides query analysis capabilities including EXPLAIN ANALYZE and slow query analysis.
|
|
1875
|
+
* @returns {SchemaAnalyzeForgeSql} Interface for analyzing query performance
|
|
1876
|
+
*/
|
|
1056
1877
|
analyze() {
|
|
1057
1878
|
return this.analyzeOperations;
|
|
1058
1879
|
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Provides schema-level SQL operations with optimistic locking/versioning and automatic cache eviction.
|
|
1882
|
+
*
|
|
1883
|
+
* This method returns operations that use `modifyWithVersioning()` internally, providing:
|
|
1884
|
+
* - Optimistic locking support
|
|
1885
|
+
* - Automatic version field management
|
|
1886
|
+
* - Cache eviction after successful operations
|
|
1887
|
+
*
|
|
1888
|
+
* @returns {ForgeSQLCacheOperations} Interface for executing versioned SQL operations with cache management
|
|
1889
|
+
*/
|
|
1890
|
+
modifyWithVersioningAndEvictCache() {
|
|
1891
|
+
return this.cacheOperations;
|
|
1892
|
+
}
|
|
1059
1893
|
/**
|
|
1060
1894
|
* Returns a Drizzle query builder instance.
|
|
1061
1895
|
*
|
|
@@ -1112,12 +1946,162 @@ class ForgeSQLORMImpl {
|
|
|
1112
1946
|
}
|
|
1113
1947
|
return this.drizzle.selectAliasedDistinct(fields);
|
|
1114
1948
|
}
|
|
1949
|
+
/**
|
|
1950
|
+
* Creates a cacheable select query with unique field aliases to prevent field name collisions in joins.
|
|
1951
|
+
* This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
|
|
1952
|
+
*
|
|
1953
|
+
* @template TSelection - The type of the selected fields
|
|
1954
|
+
* @param {TSelection} fields - Object containing the fields to select, with table schemas as values
|
|
1955
|
+
* @param {number} cacheTTL - cache ttl optional default is 60 sec.
|
|
1956
|
+
* @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A select query builder with unique field aliases
|
|
1957
|
+
* @throws {Error} If fields parameter is empty
|
|
1958
|
+
* @example
|
|
1959
|
+
* ```typescript
|
|
1960
|
+
* await forgeSQL
|
|
1961
|
+
* .selectCacheable({user: users, order: orders},60)
|
|
1962
|
+
* .from(orders)
|
|
1963
|
+
* .innerJoin(users, eq(orders.userId, users.id));
|
|
1964
|
+
* ```
|
|
1965
|
+
*/
|
|
1966
|
+
selectCacheable(fields, cacheTTL) {
|
|
1967
|
+
if (!fields) {
|
|
1968
|
+
throw new Error("fields is empty");
|
|
1969
|
+
}
|
|
1970
|
+
return this.drizzle.selectAliasedCacheable(fields, cacheTTL);
|
|
1971
|
+
}
|
|
1972
|
+
/**
|
|
1973
|
+
* Creates a cacheable distinct select query with unique field aliases to prevent field name collisions in joins.
|
|
1974
|
+
* This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
|
|
1975
|
+
*
|
|
1976
|
+
* @template TSelection - The type of the selected fields
|
|
1977
|
+
* @param {TSelection} fields - Object containing the fields to select, with table schemas as values
|
|
1978
|
+
* @param {number} cacheTTL - cache ttl optional default is 60 sec.
|
|
1979
|
+
* @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A distinct select query builder with unique field aliases
|
|
1980
|
+
* @throws {Error} If fields parameter is empty
|
|
1981
|
+
* @example
|
|
1982
|
+
* ```typescript
|
|
1983
|
+
* await forgeSQL
|
|
1984
|
+
* .selectDistinctCacheable({user: users, order: orders}, 60)
|
|
1985
|
+
* .from(orders)
|
|
1986
|
+
* .innerJoin(users, eq(orders.userId, users.id));
|
|
1987
|
+
* ```
|
|
1988
|
+
*/
|
|
1989
|
+
selectDistinctCacheable(fields, cacheTTL) {
|
|
1990
|
+
if (!fields) {
|
|
1991
|
+
throw new Error("fields is empty");
|
|
1992
|
+
}
|
|
1993
|
+
return this.drizzle.selectAliasedDistinctCacheable(fields, cacheTTL);
|
|
1994
|
+
}
|
|
1115
1995
|
}
|
|
1116
1996
|
class ForgeSQLORM {
|
|
1117
1997
|
ormInstance;
|
|
1118
1998
|
constructor(options) {
|
|
1119
1999
|
this.ormInstance = ForgeSQLORMImpl.getInstance(options);
|
|
1120
2000
|
}
|
|
2001
|
+
selectCacheable(fields, cacheTTL) {
|
|
2002
|
+
return this.ormInstance.selectCacheable(fields, cacheTTL);
|
|
2003
|
+
}
|
|
2004
|
+
selectDistinctCacheable(fields, cacheTTL) {
|
|
2005
|
+
return this.ormInstance.selectDistinctCacheable(fields, cacheTTL);
|
|
2006
|
+
}
|
|
2007
|
+
executeWithCacheContext(cacheContext) {
|
|
2008
|
+
return this.ormInstance.executeWithCacheContext(cacheContext);
|
|
2009
|
+
}
|
|
2010
|
+
executeWithCacheContextAndReturnValue(cacheContext) {
|
|
2011
|
+
return this.ormInstance.executeWithCacheContextAndReturnValue(cacheContext);
|
|
2012
|
+
}
|
|
2013
|
+
/**
|
|
2014
|
+
* Executes operations within a local cache context.
|
|
2015
|
+
* This provides in-memory caching for select queries within a single request scope.
|
|
2016
|
+
*
|
|
2017
|
+
* @param cacheContext - Function containing operations that will benefit from local caching
|
|
2018
|
+
* @returns Promise that resolves when all operations are complete
|
|
2019
|
+
*/
|
|
2020
|
+
executeWithLocalContext(cacheContext) {
|
|
2021
|
+
return this.ormInstance.executeWithLocalContext(cacheContext);
|
|
2022
|
+
}
|
|
2023
|
+
/**
|
|
2024
|
+
* Executes operations within a local cache context and returns a value.
|
|
2025
|
+
* This provides in-memory caching for select queries within a single request scope.
|
|
2026
|
+
*
|
|
2027
|
+
* @param cacheContext - Function containing operations that will benefit from local caching
|
|
2028
|
+
* @returns Promise that resolves to the return value of the cacheContext function
|
|
2029
|
+
*/
|
|
2030
|
+
executeWithLocalCacheContextAndReturnValue(cacheContext) {
|
|
2031
|
+
return this.ormInstance.executeWithLocalCacheContextAndReturnValue(cacheContext);
|
|
2032
|
+
}
|
|
2033
|
+
/**
|
|
2034
|
+
* Creates an insert query builder.
|
|
2035
|
+
*
|
|
2036
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
2037
|
+
* For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
|
|
2038
|
+
*
|
|
2039
|
+
* @param table - The table to insert into
|
|
2040
|
+
* @returns Insert query builder (no versioning, no cache management)
|
|
2041
|
+
*/
|
|
2042
|
+
insert(table2) {
|
|
2043
|
+
return this.ormInstance.insert(table2);
|
|
2044
|
+
}
|
|
2045
|
+
/**
|
|
2046
|
+
* Creates an insert query builder that automatically evicts cache after execution.
|
|
2047
|
+
*
|
|
2048
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
2049
|
+
* For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
|
|
2050
|
+
*
|
|
2051
|
+
* @param table - The table to insert into
|
|
2052
|
+
* @returns Insert query builder with automatic cache eviction (no versioning)
|
|
2053
|
+
*/
|
|
2054
|
+
insertAndEvictCache(table2) {
|
|
2055
|
+
return this.ormInstance.insertAndEvictCache(table2);
|
|
2056
|
+
}
|
|
2057
|
+
/**
|
|
2058
|
+
* Creates an update query builder.
|
|
2059
|
+
*
|
|
2060
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
2061
|
+
* For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
|
|
2062
|
+
*
|
|
2063
|
+
* @param table - The table to update
|
|
2064
|
+
* @returns Update query builder (no versioning, no cache management)
|
|
2065
|
+
*/
|
|
2066
|
+
update(table2) {
|
|
2067
|
+
return this.ormInstance.update(table2);
|
|
2068
|
+
}
|
|
2069
|
+
/**
|
|
2070
|
+
* Creates an update query builder that automatically evicts cache after execution.
|
|
2071
|
+
*
|
|
2072
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
2073
|
+
* For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
|
|
2074
|
+
*
|
|
2075
|
+
* @param table - The table to update
|
|
2076
|
+
* @returns Update query builder with automatic cache eviction (no versioning)
|
|
2077
|
+
*/
|
|
2078
|
+
updateAndEvictCache(table2) {
|
|
2079
|
+
return this.ormInstance.updateAndEvictCache(table2);
|
|
2080
|
+
}
|
|
2081
|
+
/**
|
|
2082
|
+
* Creates a delete query builder.
|
|
2083
|
+
*
|
|
2084
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
2085
|
+
* For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
|
|
2086
|
+
*
|
|
2087
|
+
* @param table - The table to delete from
|
|
2088
|
+
* @returns Delete query builder (no versioning, no cache management)
|
|
2089
|
+
*/
|
|
2090
|
+
delete(table2) {
|
|
2091
|
+
return this.ormInstance.delete(table2);
|
|
2092
|
+
}
|
|
2093
|
+
/**
|
|
2094
|
+
* Creates a delete query builder that automatically evicts cache after execution.
|
|
2095
|
+
*
|
|
2096
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
2097
|
+
* For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
|
|
2098
|
+
*
|
|
2099
|
+
* @param table - The table to delete from
|
|
2100
|
+
* @returns Delete query builder with automatic cache eviction (no versioning)
|
|
2101
|
+
*/
|
|
2102
|
+
deleteAndEvictCache(table2) {
|
|
2103
|
+
return this.ormInstance.deleteAndEvictCache(table2);
|
|
2104
|
+
}
|
|
1121
2105
|
/**
|
|
1122
2106
|
* Creates a select query with unique field aliases to prevent field name collisions in joins.
|
|
1123
2107
|
* This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
|
|
@@ -1156,19 +2140,12 @@ class ForgeSQLORM {
|
|
|
1156
2140
|
selectDistinct(fields) {
|
|
1157
2141
|
return this.ormInstance.selectDistinct(fields);
|
|
1158
2142
|
}
|
|
1159
|
-
/**
|
|
1160
|
-
* Proxies the `crud` method from `ForgeSQLORMImpl`.
|
|
1161
|
-
* @returns CRUD operations.
|
|
1162
|
-
*/
|
|
1163
|
-
crud() {
|
|
1164
|
-
return this.ormInstance.modify();
|
|
1165
|
-
}
|
|
1166
2143
|
/**
|
|
1167
2144
|
* Proxies the `modify` method from `ForgeSQLORMImpl`.
|
|
1168
2145
|
* @returns Modify operations.
|
|
1169
2146
|
*/
|
|
1170
|
-
|
|
1171
|
-
return this.ormInstance.
|
|
2147
|
+
modifyWithVersioning() {
|
|
2148
|
+
return this.ormInstance.modifyWithVersioning();
|
|
1172
2149
|
}
|
|
1173
2150
|
/**
|
|
1174
2151
|
* Proxies the `fetch` method from `ForgeSQLORMImpl`.
|
|
@@ -1184,13 +2161,16 @@ class ForgeSQLORM {
|
|
|
1184
2161
|
analyze() {
|
|
1185
2162
|
return this.ormInstance.analyze();
|
|
1186
2163
|
}
|
|
2164
|
+
/**
|
|
2165
|
+
* Provides schema-level SQL cacheable operations with type safety.
|
|
2166
|
+
* @returns {ForgeSQLCacheOperations} Interface for executing schema-bound SQL queries
|
|
2167
|
+
*/
|
|
2168
|
+
modifyWithVersioningAndEvictCache() {
|
|
2169
|
+
return this.ormInstance.modifyWithVersioningAndEvictCache();
|
|
2170
|
+
}
|
|
1187
2171
|
/**
|
|
1188
2172
|
* Returns a Drizzle query builder instance.
|
|
1189
2173
|
*
|
|
1190
|
-
* ⚠️ IMPORTANT: This method should be used ONLY for query building purposes.
|
|
1191
|
-
* The returned instance should NOT be used for direct database connections or query execution.
|
|
1192
|
-
* All database operations should be performed through Forge SQL's executeRawSQL or executeRawUpdateSQL methods.
|
|
1193
|
-
*
|
|
1194
2174
|
* @returns A Drizzle query builder instance for query construction only.
|
|
1195
2175
|
*/
|
|
1196
2176
|
getDrizzleQueryBuilder() {
|
|
@@ -1202,7 +2182,7 @@ const forgeDateTimeString = mysqlCore.customType({
|
|
|
1202
2182
|
return "datetime";
|
|
1203
2183
|
},
|
|
1204
2184
|
toDriver(value) {
|
|
1205
|
-
return
|
|
2185
|
+
return formatDateTime(value, "yyyy-LL-dd' 'HH:mm:ss.SSS");
|
|
1206
2186
|
},
|
|
1207
2187
|
fromDriver(value) {
|
|
1208
2188
|
const format = "yyyy-LL-dd'T'HH:mm:ss.SSS";
|
|
@@ -1214,7 +2194,7 @@ const forgeTimestampString = mysqlCore.customType({
|
|
|
1214
2194
|
return "timestamp";
|
|
1215
2195
|
},
|
|
1216
2196
|
toDriver(value) {
|
|
1217
|
-
return
|
|
2197
|
+
return formatDateTime(value, "yyyy-LL-dd' 'HH:mm:ss.SSS");
|
|
1218
2198
|
},
|
|
1219
2199
|
fromDriver(value) {
|
|
1220
2200
|
const format = "yyyy-LL-dd'T'HH:mm:ss.SSS";
|
|
@@ -1226,7 +2206,7 @@ const forgeDateString = mysqlCore.customType({
|
|
|
1226
2206
|
return "date";
|
|
1227
2207
|
},
|
|
1228
2208
|
toDriver(value) {
|
|
1229
|
-
return
|
|
2209
|
+
return formatDateTime(value, "yyyy-LL-dd");
|
|
1230
2210
|
},
|
|
1231
2211
|
fromDriver(value) {
|
|
1232
2212
|
const format = "yyyy-LL-dd";
|
|
@@ -1238,7 +2218,7 @@ const forgeTimeString = mysqlCore.customType({
|
|
|
1238
2218
|
return "time";
|
|
1239
2219
|
},
|
|
1240
2220
|
toDriver(value) {
|
|
1241
|
-
return
|
|
2221
|
+
return formatDateTime(value, "HH:mm:ss.SSS");
|
|
1242
2222
|
},
|
|
1243
2223
|
fromDriver(value) {
|
|
1244
2224
|
return parseDateTime(value, "HH:mm:ss.SSS");
|
|
@@ -1355,6 +2335,45 @@ async function dropTableSchemaMigrations() {
|
|
|
1355
2335
|
return getHttpResponse(500, errorMessage);
|
|
1356
2336
|
}
|
|
1357
2337
|
}
|
|
2338
|
+
const clearCacheSchedulerTrigger = async (options) => {
|
|
2339
|
+
try {
|
|
2340
|
+
const newOptions = options ?? {
|
|
2341
|
+
logRawSqlQuery: false,
|
|
2342
|
+
disableOptimisticLocking: false,
|
|
2343
|
+
cacheTTL: 120,
|
|
2344
|
+
cacheEntityName: "cache",
|
|
2345
|
+
cacheEntityQueryName: "sql",
|
|
2346
|
+
cacheEntityExpirationName: "expiration",
|
|
2347
|
+
cacheEntityDataName: "data"
|
|
2348
|
+
};
|
|
2349
|
+
if (!newOptions.cacheEntityName) {
|
|
2350
|
+
throw new Error("cacheEntityName is not configured");
|
|
2351
|
+
}
|
|
2352
|
+
await clearExpiredCache(newOptions);
|
|
2353
|
+
return {
|
|
2354
|
+
headers: { "Content-Type": ["application/json"] },
|
|
2355
|
+
statusCode: 200,
|
|
2356
|
+
statusText: "OK",
|
|
2357
|
+
body: JSON.stringify({
|
|
2358
|
+
success: true,
|
|
2359
|
+
message: "Cache cleanup completed successfully",
|
|
2360
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2361
|
+
})
|
|
2362
|
+
};
|
|
2363
|
+
} catch (error) {
|
|
2364
|
+
console.error("Error during cache cleanup: ", JSON.stringify(error));
|
|
2365
|
+
return {
|
|
2366
|
+
headers: { "Content-Type": ["application/json"] },
|
|
2367
|
+
statusCode: 500,
|
|
2368
|
+
statusText: "Internal Server Error",
|
|
2369
|
+
body: JSON.stringify({
|
|
2370
|
+
success: false,
|
|
2371
|
+
error: error instanceof Error ? error.message : "Unknown error during cache cleanup",
|
|
2372
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2373
|
+
})
|
|
2374
|
+
};
|
|
2375
|
+
}
|
|
2376
|
+
};
|
|
1358
2377
|
const getHttpResponse = (statusCode, body) => {
|
|
1359
2378
|
let statusText = "";
|
|
1360
2379
|
if (statusCode === 200) {
|
|
@@ -1373,6 +2392,7 @@ exports.ForgeSQLCrudOperations = ForgeSQLCrudOperations;
|
|
|
1373
2392
|
exports.ForgeSQLSelectOperations = ForgeSQLSelectOperations;
|
|
1374
2393
|
exports.applyFromDriverTransform = applyFromDriverTransform;
|
|
1375
2394
|
exports.applySchemaMigrations = applySchemaMigrations;
|
|
2395
|
+
exports.clearCacheSchedulerTrigger = clearCacheSchedulerTrigger;
|
|
1376
2396
|
exports.default = ForgeSQLORM;
|
|
1377
2397
|
exports.dropSchemaMigrations = dropSchemaMigrations;
|
|
1378
2398
|
exports.dropTableSchemaMigrations = dropTableSchemaMigrations;
|
|
@@ -1383,6 +2403,7 @@ exports.forgeDriver = forgeDriver;
|
|
|
1383
2403
|
exports.forgeSystemTables = forgeSystemTables;
|
|
1384
2404
|
exports.forgeTimeString = forgeTimeString;
|
|
1385
2405
|
exports.forgeTimestampString = forgeTimestampString;
|
|
2406
|
+
exports.formatDateTime = formatDateTime;
|
|
1386
2407
|
exports.formatLimitOffset = formatLimitOffset;
|
|
1387
2408
|
exports.generateDropTableStatements = generateDropTableStatements;
|
|
1388
2409
|
exports.getHttpResponse = getHttpResponse;
|