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.mjs
CHANGED
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
import { isTable, sql, eq, and } from "drizzle-orm";
|
|
2
2
|
import { DateTime } from "luxon";
|
|
3
3
|
import { isSQLWrapper } from "drizzle-orm/sql/sql";
|
|
4
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
|
+
import { getTableName } from "drizzle-orm/table";
|
|
6
|
+
import * as crypto from "crypto";
|
|
7
|
+
import { kvs, WhereConditions, Filter, FilterConditions } from "@forge/kvs";
|
|
4
8
|
import { sql as sql$1, migrationRunner } from "@forge/sql";
|
|
5
9
|
import { drizzle } from "drizzle-orm/mysql-proxy";
|
|
6
10
|
import { customType, mysqlTable, timestamp, varchar, bigint } from "drizzle-orm/mysql-core";
|
|
7
|
-
import { getTableName } from "drizzle-orm/table";
|
|
8
11
|
const parseDateTime = (value, format) => {
|
|
9
12
|
let result;
|
|
10
13
|
if (value instanceof Date) {
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
const dt = DateTime.fromFormat(value, format);
|
|
14
|
-
if (dt.isValid) {
|
|
15
|
-
result = dt.toJSDate();
|
|
14
|
+
result = value;
|
|
16
15
|
} else {
|
|
17
|
-
const
|
|
18
|
-
if (
|
|
19
|
-
result =
|
|
16
|
+
const dt = DateTime.fromFormat(value, format);
|
|
17
|
+
if (dt.isValid) {
|
|
18
|
+
result = dt.toJSDate();
|
|
20
19
|
} else {
|
|
21
|
-
|
|
20
|
+
const isoDt = DateTime.fromISO(value);
|
|
21
|
+
if (isoDt.isValid) {
|
|
22
|
+
result = isoDt.toJSDate();
|
|
23
|
+
} else {
|
|
24
|
+
result = new Date(value);
|
|
25
|
+
}
|
|
22
26
|
}
|
|
23
27
|
}
|
|
24
28
|
if (isNaN(result.getTime())) {
|
|
@@ -26,6 +30,14 @@ const parseDateTime = (value, format) => {
|
|
|
26
30
|
}
|
|
27
31
|
return result;
|
|
28
32
|
};
|
|
33
|
+
function formatDateTime(value, format) {
|
|
34
|
+
const fromJSDate = DateTime.fromJSDate(value);
|
|
35
|
+
if (fromJSDate.isValid) {
|
|
36
|
+
return fromJSDate.toFormat(format);
|
|
37
|
+
} else {
|
|
38
|
+
throw new Error("Invalid Date");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
29
41
|
function getPrimaryKeys(table) {
|
|
30
42
|
const { columns, primaryKeys } = getTableMetadata(table);
|
|
31
43
|
const columnPrimaryKeys = Object.entries(columns).filter(([, column]) => column.primary);
|
|
@@ -288,6 +300,275 @@ function formatLimitOffset(limitOrOffset) {
|
|
|
288
300
|
function nextVal(sequenceName) {
|
|
289
301
|
return sql.raw(`NEXTVAL(${sequenceName})`);
|
|
290
302
|
}
|
|
303
|
+
const CACHE_CONSTANTS = {
|
|
304
|
+
BATCH_SIZE: 25,
|
|
305
|
+
MAX_RETRY_ATTEMPTS: 3,
|
|
306
|
+
INITIAL_RETRY_DELAY: 1e3,
|
|
307
|
+
RETRY_DELAY_MULTIPLIER: 2,
|
|
308
|
+
DEFAULT_ENTITY_QUERY_NAME: "sql",
|
|
309
|
+
DEFAULT_EXPIRATION_NAME: "expiration",
|
|
310
|
+
DEFAULT_DATA_NAME: "data",
|
|
311
|
+
HASH_LENGTH: 32
|
|
312
|
+
};
|
|
313
|
+
function getCurrentTime() {
|
|
314
|
+
const dt = DateTime.now();
|
|
315
|
+
return Math.floor(dt.toSeconds());
|
|
316
|
+
}
|
|
317
|
+
function nowPlusSeconds(secondsToAdd) {
|
|
318
|
+
const dt = DateTime.now().plus({ seconds: secondsToAdd });
|
|
319
|
+
return Math.floor(dt.toSeconds());
|
|
320
|
+
}
|
|
321
|
+
function hashKey(query) {
|
|
322
|
+
const h = crypto.createHash("sha256");
|
|
323
|
+
h.update(query.sql.toLowerCase());
|
|
324
|
+
h.update(JSON.stringify(query.params));
|
|
325
|
+
return "CachedQuery_" + h.digest("hex").slice(0, CACHE_CONSTANTS.HASH_LENGTH);
|
|
326
|
+
}
|
|
327
|
+
async function deleteCacheEntriesInBatches(results, cacheEntityName) {
|
|
328
|
+
for (let i = 0; i < results.length; i += CACHE_CONSTANTS.BATCH_SIZE) {
|
|
329
|
+
const batch = results.slice(i, i + CACHE_CONSTANTS.BATCH_SIZE);
|
|
330
|
+
let transactionBuilder = kvs.transact();
|
|
331
|
+
batch.forEach((result) => {
|
|
332
|
+
transactionBuilder = transactionBuilder.delete(result.key, { entityName: cacheEntityName });
|
|
333
|
+
});
|
|
334
|
+
await transactionBuilder.execute();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
async function clearCursorCache(tables, cursor, options) {
|
|
338
|
+
const cacheEntityName = options.cacheEntityName;
|
|
339
|
+
if (!cacheEntityName) {
|
|
340
|
+
throw new Error("cacheEntityName is not configured");
|
|
341
|
+
}
|
|
342
|
+
const entityQueryName = options.cacheEntityQueryName ?? CACHE_CONSTANTS.DEFAULT_ENTITY_QUERY_NAME;
|
|
343
|
+
let filters = new Filter();
|
|
344
|
+
for (const table of tables) {
|
|
345
|
+
const wrapIfNeeded = options.cacheWrapTable ? `\`${table}\`` : table;
|
|
346
|
+
filters.or(entityQueryName, FilterConditions.contains(wrapIfNeeded?.toLowerCase()));
|
|
347
|
+
}
|
|
348
|
+
let entityQueryBuilder = kvs.entity(cacheEntityName).query().index(entityQueryName).filters(filters);
|
|
349
|
+
if (cursor) {
|
|
350
|
+
entityQueryBuilder = entityQueryBuilder.cursor(cursor);
|
|
351
|
+
}
|
|
352
|
+
const listResult = await entityQueryBuilder.limit(100).getMany();
|
|
353
|
+
if (options.logRawSqlQuery) {
|
|
354
|
+
console.warn(`clear cache Records: ${JSON.stringify(listResult.results.map((r) => r.key))}`);
|
|
355
|
+
}
|
|
356
|
+
await deleteCacheEntriesInBatches(listResult.results, cacheEntityName);
|
|
357
|
+
if (listResult.nextCursor) {
|
|
358
|
+
return listResult.results.length + await clearCursorCache(tables, listResult.nextCursor, options);
|
|
359
|
+
} else {
|
|
360
|
+
return listResult.results.length;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async function clearExpirationCursorCache(cursor, options) {
|
|
364
|
+
const cacheEntityName = options.cacheEntityName;
|
|
365
|
+
if (!cacheEntityName) {
|
|
366
|
+
throw new Error("cacheEntityName is not configured");
|
|
367
|
+
}
|
|
368
|
+
const entityExpirationName = options.cacheEntityExpirationName ?? CACHE_CONSTANTS.DEFAULT_EXPIRATION_NAME;
|
|
369
|
+
let entityQueryBuilder = kvs.entity(cacheEntityName).query().index(entityExpirationName).where(WhereConditions.lessThan(Math.floor(DateTime.now().toSeconds())));
|
|
370
|
+
if (cursor) {
|
|
371
|
+
entityQueryBuilder = entityQueryBuilder.cursor(cursor);
|
|
372
|
+
}
|
|
373
|
+
const listResult = await entityQueryBuilder.limit(100).getMany();
|
|
374
|
+
if (options.logRawSqlQuery) {
|
|
375
|
+
console.warn(`clear expired Records: ${JSON.stringify(listResult.results.map((r) => r.key))}`);
|
|
376
|
+
}
|
|
377
|
+
await deleteCacheEntriesInBatches(listResult.results, cacheEntityName);
|
|
378
|
+
if (listResult.nextCursor) {
|
|
379
|
+
return listResult.results.length + await clearExpirationCursorCache(listResult.nextCursor, options);
|
|
380
|
+
} else {
|
|
381
|
+
return listResult.results.length;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
async function executeWithRetry(operation, operationName) {
|
|
385
|
+
let attempt = 0;
|
|
386
|
+
let delay = CACHE_CONSTANTS.INITIAL_RETRY_DELAY;
|
|
387
|
+
while (attempt < CACHE_CONSTANTS.MAX_RETRY_ATTEMPTS) {
|
|
388
|
+
try {
|
|
389
|
+
return await operation();
|
|
390
|
+
} catch (err) {
|
|
391
|
+
console.warn(`Error during ${operationName}: ${err.message}, retry ${attempt}`, err);
|
|
392
|
+
attempt++;
|
|
393
|
+
if (attempt >= CACHE_CONSTANTS.MAX_RETRY_ATTEMPTS) {
|
|
394
|
+
console.error(`Error during ${operationName}: ${err.message}`, err);
|
|
395
|
+
throw err;
|
|
396
|
+
}
|
|
397
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
398
|
+
delay *= CACHE_CONSTANTS.RETRY_DELAY_MULTIPLIER;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
throw new Error(`Maximum retry attempts exceeded for ${operationName}`);
|
|
402
|
+
}
|
|
403
|
+
async function clearCache(schema, options) {
|
|
404
|
+
const tableName = getTableName(schema);
|
|
405
|
+
if (cacheApplicationContext.getStore()) {
|
|
406
|
+
cacheApplicationContext.getStore()?.tables.add(tableName);
|
|
407
|
+
} else {
|
|
408
|
+
await clearTablesCache([tableName], options);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
async function clearTablesCache(tables, options) {
|
|
412
|
+
if (!options.cacheEntityName) {
|
|
413
|
+
throw new Error("cacheEntityName is not configured");
|
|
414
|
+
}
|
|
415
|
+
const startTime = DateTime.now();
|
|
416
|
+
let totalRecords = 0;
|
|
417
|
+
try {
|
|
418
|
+
totalRecords = await executeWithRetry(
|
|
419
|
+
() => clearCursorCache(tables, "", options),
|
|
420
|
+
"clearing cache"
|
|
421
|
+
);
|
|
422
|
+
} finally {
|
|
423
|
+
if (options.logRawSqlQuery) {
|
|
424
|
+
const duration = DateTime.now().toSeconds() - startTime.toSeconds();
|
|
425
|
+
console.info(`Cleared ${totalRecords} cache records in ${duration} seconds`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
async function clearExpiredCache(options) {
|
|
430
|
+
if (!options.cacheEntityName) {
|
|
431
|
+
throw new Error("cacheEntityName is not configured");
|
|
432
|
+
}
|
|
433
|
+
const startTime = DateTime.now();
|
|
434
|
+
let totalRecords = 0;
|
|
435
|
+
try {
|
|
436
|
+
totalRecords = await executeWithRetry(
|
|
437
|
+
() => clearExpirationCursorCache("", options),
|
|
438
|
+
"clearing expired cache"
|
|
439
|
+
);
|
|
440
|
+
} finally {
|
|
441
|
+
const duration = DateTime.now().toSeconds() - startTime.toSeconds();
|
|
442
|
+
console.info(`Cleared ${totalRecords} expired cache records in ${duration} seconds`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
async function getFromCache(query, options) {
|
|
446
|
+
if (!options.cacheEntityName) {
|
|
447
|
+
throw new Error("cacheEntityName is not configured");
|
|
448
|
+
}
|
|
449
|
+
const entityQueryName = options.cacheEntityQueryName ?? CACHE_CONSTANTS.DEFAULT_ENTITY_QUERY_NAME;
|
|
450
|
+
const expirationName = options.cacheEntityExpirationName ?? CACHE_CONSTANTS.DEFAULT_EXPIRATION_NAME;
|
|
451
|
+
const dataName = options.cacheEntityDataName ?? CACHE_CONSTANTS.DEFAULT_DATA_NAME;
|
|
452
|
+
const sqlQuery = query.toSQL();
|
|
453
|
+
const key = hashKey(sqlQuery);
|
|
454
|
+
if (await isTableContainsTableInCacheContext(sqlQuery.sql, options)) {
|
|
455
|
+
if (options.logRawSqlQuery) {
|
|
456
|
+
console.warn(`Context contains value to clear. Skip getting from cache`);
|
|
457
|
+
}
|
|
458
|
+
return void 0;
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
const cacheResult = await kvs.entity(options.cacheEntityName).get(key);
|
|
462
|
+
if (cacheResult && cacheResult[expirationName] >= getCurrentTime() && sqlQuery.sql.toLowerCase() === cacheResult[entityQueryName]) {
|
|
463
|
+
if (options.logRawSqlQuery) {
|
|
464
|
+
console.warn(`Get value from cache, cacheKey: ${key}`);
|
|
465
|
+
}
|
|
466
|
+
const results = cacheResult[dataName];
|
|
467
|
+
return JSON.parse(results);
|
|
468
|
+
}
|
|
469
|
+
} catch (error) {
|
|
470
|
+
console.error(`Error getting from cache: ${error.message}`, error);
|
|
471
|
+
}
|
|
472
|
+
return void 0;
|
|
473
|
+
}
|
|
474
|
+
async function setCacheResult(query, options, results, cacheTtl) {
|
|
475
|
+
if (!options.cacheEntityName) {
|
|
476
|
+
throw new Error("cacheEntityName is not configured");
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
const entityQueryName = options.cacheEntityQueryName ?? CACHE_CONSTANTS.DEFAULT_ENTITY_QUERY_NAME;
|
|
480
|
+
const expirationName = options.cacheEntityExpirationName ?? CACHE_CONSTANTS.DEFAULT_EXPIRATION_NAME;
|
|
481
|
+
const dataName = options.cacheEntityDataName ?? CACHE_CONSTANTS.DEFAULT_DATA_NAME;
|
|
482
|
+
const sqlQuery = query.toSQL();
|
|
483
|
+
if (await isTableContainsTableInCacheContext(sqlQuery.sql, options)) {
|
|
484
|
+
if (options.logRawSqlQuery) {
|
|
485
|
+
console.warn(`Context contains value to clear. Skip setting from cache`);
|
|
486
|
+
}
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const key = hashKey(sqlQuery);
|
|
490
|
+
await kvs.transact().set(
|
|
491
|
+
key,
|
|
492
|
+
{
|
|
493
|
+
[entityQueryName]: sqlQuery.sql.toLowerCase(),
|
|
494
|
+
[expirationName]: nowPlusSeconds(cacheTtl),
|
|
495
|
+
[dataName]: JSON.stringify(results)
|
|
496
|
+
},
|
|
497
|
+
{ entityName: options.cacheEntityName }
|
|
498
|
+
).execute();
|
|
499
|
+
if (options.logRawSqlQuery) {
|
|
500
|
+
console.warn(`Store value to cache, cacheKey: ${key}`);
|
|
501
|
+
}
|
|
502
|
+
} catch (error) {
|
|
503
|
+
console.error(`Error setting cache: ${error.message}`, error);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const cacheApplicationContext = new AsyncLocalStorage();
|
|
507
|
+
const localCacheApplicationContext = new AsyncLocalStorage();
|
|
508
|
+
async function saveTableIfInsideCacheContext(table) {
|
|
509
|
+
const context = cacheApplicationContext.getStore();
|
|
510
|
+
if (context) {
|
|
511
|
+
const tableName = getTableName(table).toLowerCase();
|
|
512
|
+
context.tables.add(tableName);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
async function saveQueryLocalCacheQuery(query, rows) {
|
|
516
|
+
const context = localCacheApplicationContext.getStore();
|
|
517
|
+
if (context) {
|
|
518
|
+
if (!context.cache) {
|
|
519
|
+
context.cache = {};
|
|
520
|
+
}
|
|
521
|
+
const sql2 = query;
|
|
522
|
+
const key = hashKey(sql2.toSQL());
|
|
523
|
+
context.cache[key] = {
|
|
524
|
+
sql: sql2.toSQL().sql.toLowerCase(),
|
|
525
|
+
data: rows
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
async function getQueryLocalCacheQuery(query) {
|
|
530
|
+
const context = localCacheApplicationContext.getStore();
|
|
531
|
+
if (context) {
|
|
532
|
+
if (!context.cache) {
|
|
533
|
+
context.cache = {};
|
|
534
|
+
}
|
|
535
|
+
const sql2 = query;
|
|
536
|
+
const key = hashKey(sql2.toSQL());
|
|
537
|
+
if (context.cache[key] && context.cache[key].sql === sql2.toSQL().sql.toLowerCase()) {
|
|
538
|
+
return context.cache[key].data;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return void 0;
|
|
542
|
+
}
|
|
543
|
+
async function evictLocalCacheQuery(table, options) {
|
|
544
|
+
const context = localCacheApplicationContext.getStore();
|
|
545
|
+
if (context) {
|
|
546
|
+
if (!context.cache) {
|
|
547
|
+
context.cache = {};
|
|
548
|
+
}
|
|
549
|
+
const tableName = getTableName(table);
|
|
550
|
+
const searchString = options.cacheWrapTable ? `\`${tableName}\`` : tableName;
|
|
551
|
+
const keyToEvicts = [];
|
|
552
|
+
Object.keys(context.cache).forEach((key) => {
|
|
553
|
+
if (context.cache[key].sql.includes(searchString)) {
|
|
554
|
+
keyToEvicts.push(key);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
keyToEvicts.forEach((key) => delete context.cache[key]);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
async function isTableContainsTableInCacheContext(sql2, options) {
|
|
561
|
+
const context = cacheApplicationContext.getStore();
|
|
562
|
+
if (!context) {
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
const tables = Array.from(context.tables);
|
|
566
|
+
const lowerSql = sql2.toLowerCase();
|
|
567
|
+
return tables.some((table) => {
|
|
568
|
+
const tablePattern = options.cacheWrapTable ? `\`${table}\`` : table;
|
|
569
|
+
return lowerSql.includes(tablePattern);
|
|
570
|
+
});
|
|
571
|
+
}
|
|
291
572
|
class ForgeSQLCrudOperations {
|
|
292
573
|
forgeOperations;
|
|
293
574
|
options;
|
|
@@ -304,12 +585,17 @@ class ForgeSQLCrudOperations {
|
|
|
304
585
|
* Inserts records into the database with optional versioning support.
|
|
305
586
|
* If a version field exists in the schema, versioning is applied.
|
|
306
587
|
*
|
|
588
|
+
* This method automatically handles:
|
|
589
|
+
* - Version field initialization for optimistic locking
|
|
590
|
+
* - Batch insertion for multiple records
|
|
591
|
+
* - Duplicate key handling with optional updates
|
|
592
|
+
*
|
|
307
593
|
* @template T - The type of the table schema
|
|
308
|
-
* @param
|
|
309
|
-
* @param
|
|
310
|
-
* @param
|
|
311
|
-
* @returns
|
|
312
|
-
* @throws
|
|
594
|
+
* @param schema - The entity schema
|
|
595
|
+
* @param models - Array of entities to insert
|
|
596
|
+
* @param updateIfExists - Whether to update existing records (default: false)
|
|
597
|
+
* @returns Promise that resolves to the number of inserted rows
|
|
598
|
+
* @throws Error if the insert operation fails
|
|
313
599
|
*/
|
|
314
600
|
async insert(schema, models, updateIfExists = false) {
|
|
315
601
|
if (!models?.length) return 0;
|
|
@@ -318,25 +604,32 @@ class ForgeSQLCrudOperations {
|
|
|
318
604
|
const preparedModels = models.map(
|
|
319
605
|
(model) => this.prepareModelWithVersion(model, versionMetadata, columns)
|
|
320
606
|
);
|
|
321
|
-
const queryBuilder = this.forgeOperations.
|
|
607
|
+
const queryBuilder = this.forgeOperations.insert(schema).values(preparedModels);
|
|
322
608
|
const finalQuery = updateIfExists ? queryBuilder.onDuplicateKeyUpdate({
|
|
323
609
|
set: Object.fromEntries(
|
|
324
610
|
Object.keys(preparedModels[0]).map((key) => [key, schema[key]])
|
|
325
611
|
)
|
|
326
612
|
}) : queryBuilder;
|
|
327
613
|
const result = await finalQuery;
|
|
614
|
+
await saveTableIfInsideCacheContext(schema);
|
|
328
615
|
return result[0].insertId;
|
|
329
616
|
}
|
|
330
617
|
/**
|
|
331
618
|
* Deletes a record by its primary key with optional version check.
|
|
332
619
|
* If versioning is enabled, ensures the record hasn't been modified since last read.
|
|
333
620
|
*
|
|
621
|
+
* This method automatically handles:
|
|
622
|
+
* - Single primary key validation
|
|
623
|
+
* - Optimistic locking checks if versioning is enabled
|
|
624
|
+
* - Version field validation before deletion
|
|
625
|
+
*
|
|
334
626
|
* @template T - The type of the table schema
|
|
335
|
-
* @param
|
|
336
|
-
* @param
|
|
337
|
-
* @returns
|
|
338
|
-
* @throws
|
|
339
|
-
* @throws
|
|
627
|
+
* @param id - The ID of the record to delete
|
|
628
|
+
* @param schema - The entity schema
|
|
629
|
+
* @returns Promise that resolves to the number of affected rows
|
|
630
|
+
* @throws Error if the delete operation fails
|
|
631
|
+
* @throws Error if multiple primary keys are found
|
|
632
|
+
* @throws Error if optimistic locking check fails
|
|
340
633
|
*/
|
|
341
634
|
async deleteById(id, schema) {
|
|
342
635
|
const { tableName, columns } = getTableMetadata(schema);
|
|
@@ -357,8 +650,12 @@ class ForgeSQLCrudOperations {
|
|
|
357
650
|
conditions.push(eq(versionField, oldModel[versionMetadata.fieldName]));
|
|
358
651
|
}
|
|
359
652
|
}
|
|
360
|
-
const queryBuilder = this.forgeOperations.
|
|
653
|
+
const queryBuilder = this.forgeOperations.delete(schema).where(and(...conditions));
|
|
361
654
|
const result = await queryBuilder;
|
|
655
|
+
if (versionMetadata && result[0].affectedRows === 0) {
|
|
656
|
+
throw new Error(`Optimistic locking failed: record with primary key ${id} has been modified`);
|
|
657
|
+
}
|
|
658
|
+
await saveTableIfInsideCacheContext(schema);
|
|
362
659
|
return result[0].affectedRows;
|
|
363
660
|
}
|
|
364
661
|
/**
|
|
@@ -368,13 +665,19 @@ class ForgeSQLCrudOperations {
|
|
|
368
665
|
* - Checks for concurrent modifications
|
|
369
666
|
* - Increments the version on successful update
|
|
370
667
|
*
|
|
668
|
+
* This method automatically handles:
|
|
669
|
+
* - Primary key validation
|
|
670
|
+
* - Version field retrieval and validation
|
|
671
|
+
* - Optimistic locking conflict detection
|
|
672
|
+
* - Version field incrementation
|
|
673
|
+
*
|
|
371
674
|
* @template T - The type of the table schema
|
|
372
|
-
* @param
|
|
373
|
-
* @param
|
|
374
|
-
* @returns
|
|
375
|
-
* @throws
|
|
376
|
-
* @throws
|
|
377
|
-
* @throws
|
|
675
|
+
* @param entity - The entity with updated values (must include primary key)
|
|
676
|
+
* @param schema - The entity schema
|
|
677
|
+
* @returns Promise that resolves to the number of affected rows
|
|
678
|
+
* @throws Error if the primary key is not provided
|
|
679
|
+
* @throws Error if optimistic locking check fails
|
|
680
|
+
* @throws Error if multiple primary keys are found
|
|
378
681
|
*/
|
|
379
682
|
async updateById(entity, schema) {
|
|
380
683
|
const { tableName, columns } = getTableMetadata(schema);
|
|
@@ -404,13 +707,14 @@ class ForgeSQLCrudOperations {
|
|
|
404
707
|
conditions.push(eq(versionField, currentVersion));
|
|
405
708
|
}
|
|
406
709
|
}
|
|
407
|
-
const queryBuilder = this.forgeOperations.
|
|
710
|
+
const queryBuilder = this.forgeOperations.update(schema).set(updateData).where(and(...conditions));
|
|
408
711
|
const result = await queryBuilder;
|
|
409
712
|
if (versionMetadata && result[0].affectedRows === 0) {
|
|
410
713
|
throw new Error(
|
|
411
714
|
`Optimistic locking failed: record with primary key ${entity[primaryKeyName]} has been modified`
|
|
412
715
|
);
|
|
413
716
|
}
|
|
717
|
+
await saveTableIfInsideCacheContext(schema);
|
|
414
718
|
return result[0].affectedRows;
|
|
415
719
|
}
|
|
416
720
|
/**
|
|
@@ -429,8 +733,9 @@ class ForgeSQLCrudOperations {
|
|
|
429
733
|
if (!where) {
|
|
430
734
|
throw new Error("WHERE conditions must be provided");
|
|
431
735
|
}
|
|
432
|
-
const queryBuilder = this.forgeOperations.
|
|
736
|
+
const queryBuilder = this.forgeOperations.update(schema).set(updateData).where(where);
|
|
433
737
|
const result = await queryBuilder;
|
|
738
|
+
await saveTableIfInsideCacheContext(schema);
|
|
434
739
|
return result[0].affectedRows;
|
|
435
740
|
}
|
|
436
741
|
// Helper methods
|
|
@@ -574,7 +879,7 @@ class ForgeSQLCrudOperations {
|
|
|
574
879
|
const [versionFieldName, versionFieldColumn] = versionField;
|
|
575
880
|
const primaryKeys = this.getPrimaryKeys(schema);
|
|
576
881
|
const [primaryKeyName, primaryKeyColumn] = primaryKeys[0];
|
|
577
|
-
const resultQuery = this.forgeOperations.
|
|
882
|
+
const resultQuery = this.forgeOperations.select({
|
|
578
883
|
[primaryKeyName]: primaryKeyColumn,
|
|
579
884
|
[versionFieldName]: versionFieldColumn
|
|
580
885
|
}).from(schema).where(eq(primaryKeyColumn, primaryKeyValues[primaryKeyName]));
|
|
@@ -719,7 +1024,125 @@ function createForgeDriverProxy(options, logRawSqlQuery) {
|
|
|
719
1024
|
return forgeDriver(modifiedQuery, params, method);
|
|
720
1025
|
};
|
|
721
1026
|
}
|
|
722
|
-
function
|
|
1027
|
+
function shouldClearCacheOnError(error) {
|
|
1028
|
+
if (error?.code === "VALIDATION_ERROR" || error?.code === "CONSTRAINT_ERROR" || error?.message && /validation/i.exec(error.message)) {
|
|
1029
|
+
return false;
|
|
1030
|
+
}
|
|
1031
|
+
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)) {
|
|
1032
|
+
return true;
|
|
1033
|
+
}
|
|
1034
|
+
return true;
|
|
1035
|
+
}
|
|
1036
|
+
async function handleSuccessfulExecution(rows, onfulfilled, table, options, isCached) {
|
|
1037
|
+
try {
|
|
1038
|
+
await evictLocalCacheQuery(table, options);
|
|
1039
|
+
await saveTableIfInsideCacheContext(table);
|
|
1040
|
+
if (isCached && !cacheApplicationContext.getStore()) {
|
|
1041
|
+
await clearCache(table, options);
|
|
1042
|
+
}
|
|
1043
|
+
const result = onfulfilled?.(rows);
|
|
1044
|
+
return result;
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
if (shouldClearCacheOnError(error)) {
|
|
1047
|
+
await evictLocalCacheQuery(table, options);
|
|
1048
|
+
if (isCached) {
|
|
1049
|
+
await clearCache(table, options).catch(() => {
|
|
1050
|
+
console.warn("Ignore cache clear errors");
|
|
1051
|
+
});
|
|
1052
|
+
} else {
|
|
1053
|
+
await saveTableIfInsideCacheContext(table);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
throw error;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
function handleFunctionCall(value, target, args, table, options, isCached) {
|
|
1060
|
+
const result = value.apply(target, args);
|
|
1061
|
+
if (typeof result === "object" && result !== null && "execute" in result) {
|
|
1062
|
+
return wrapCacheEvictBuilder(result, table, options, isCached);
|
|
1063
|
+
}
|
|
1064
|
+
return result;
|
|
1065
|
+
}
|
|
1066
|
+
const wrapCacheEvictBuilder = (rawBuilder, table, options, isCached) => {
|
|
1067
|
+
return new Proxy(rawBuilder, {
|
|
1068
|
+
get(target, prop, receiver) {
|
|
1069
|
+
if (prop === "then") {
|
|
1070
|
+
return (onfulfilled, onrejected) => target.execute().then(
|
|
1071
|
+
(rows) => handleSuccessfulExecution(rows, onfulfilled, table, options, isCached),
|
|
1072
|
+
onrejected
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
const value = Reflect.get(target, prop, receiver);
|
|
1076
|
+
if (typeof value === "function") {
|
|
1077
|
+
return (...args) => handleFunctionCall(value, target, args, table, options, isCached);
|
|
1078
|
+
}
|
|
1079
|
+
return value;
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
};
|
|
1083
|
+
function insertAndEvictCacheBuilder(db, table, options, isCached) {
|
|
1084
|
+
const builder = db.insert(table);
|
|
1085
|
+
return wrapCacheEvictBuilder(
|
|
1086
|
+
builder,
|
|
1087
|
+
table,
|
|
1088
|
+
options,
|
|
1089
|
+
isCached
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
function updateAndEvictCacheBuilder(db, table, options, isCached) {
|
|
1093
|
+
const builder = db.update(table);
|
|
1094
|
+
return wrapCacheEvictBuilder(
|
|
1095
|
+
builder,
|
|
1096
|
+
table,
|
|
1097
|
+
options,
|
|
1098
|
+
isCached
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
function deleteAndEvictCacheBuilder(db, table, options, isCached) {
|
|
1102
|
+
const builder = db.delete(table);
|
|
1103
|
+
return wrapCacheEvictBuilder(
|
|
1104
|
+
builder,
|
|
1105
|
+
table,
|
|
1106
|
+
options,
|
|
1107
|
+
isCached
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
async function handleCachedQuery(target, options, cacheTtl, selections, aliasMap, onfulfilled, onrejected) {
|
|
1111
|
+
try {
|
|
1112
|
+
const localCached = await getQueryLocalCacheQuery(target);
|
|
1113
|
+
if (localCached) {
|
|
1114
|
+
return onfulfilled?.(localCached);
|
|
1115
|
+
}
|
|
1116
|
+
const cacheResult = await getFromCache(target, options);
|
|
1117
|
+
if (cacheResult) {
|
|
1118
|
+
return onfulfilled?.(cacheResult);
|
|
1119
|
+
}
|
|
1120
|
+
const rows = await target.execute();
|
|
1121
|
+
const transformed = applyFromDriverTransform(rows, selections, aliasMap);
|
|
1122
|
+
await saveQueryLocalCacheQuery(target, transformed);
|
|
1123
|
+
await setCacheResult(target, options, transformed, cacheTtl).catch((cacheError) => {
|
|
1124
|
+
console.warn("Cache set error:", cacheError);
|
|
1125
|
+
});
|
|
1126
|
+
return onfulfilled?.(transformed);
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
return onrejected?.(error);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
async function handleNonCachedQuery(target, selections, aliasMap, onfulfilled, onrejected) {
|
|
1132
|
+
try {
|
|
1133
|
+
const localCached = await getQueryLocalCacheQuery(target);
|
|
1134
|
+
if (localCached) {
|
|
1135
|
+
return onfulfilled?.(localCached);
|
|
1136
|
+
}
|
|
1137
|
+
const rows = await target.execute();
|
|
1138
|
+
const transformed = applyFromDriverTransform(rows, selections, aliasMap);
|
|
1139
|
+
await saveQueryLocalCacheQuery(target, transformed);
|
|
1140
|
+
return onfulfilled?.(transformed);
|
|
1141
|
+
} catch (error) {
|
|
1142
|
+
return onrejected?.(error);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
function createAliasedSelectBuilder(db, fields, selectFn, useCache, options, cacheTtl) {
|
|
723
1146
|
const { selections, aliasMap } = mapSelectFieldsWithAlias(fields);
|
|
724
1147
|
const builder = selectFn(selections);
|
|
725
1148
|
const wrapBuilder = (rawBuilder) => {
|
|
@@ -732,10 +1155,22 @@ function createAliasedSelectBuilder(db, fields, selectFn) {
|
|
|
732
1155
|
};
|
|
733
1156
|
}
|
|
734
1157
|
if (prop === "then") {
|
|
735
|
-
return (onfulfilled, onrejected) =>
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
1158
|
+
return (onfulfilled, onrejected) => {
|
|
1159
|
+
if (useCache) {
|
|
1160
|
+
const ttl = cacheTtl ?? options.cacheTTL ?? 120;
|
|
1161
|
+
return handleCachedQuery(
|
|
1162
|
+
target,
|
|
1163
|
+
options,
|
|
1164
|
+
ttl,
|
|
1165
|
+
selections,
|
|
1166
|
+
aliasMap,
|
|
1167
|
+
onfulfilled,
|
|
1168
|
+
onrejected
|
|
1169
|
+
);
|
|
1170
|
+
} else {
|
|
1171
|
+
return handleNonCachedQuery(target, selections, aliasMap, onfulfilled, onrejected);
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
739
1174
|
}
|
|
740
1175
|
const value = Reflect.get(target, prop, receiver);
|
|
741
1176
|
if (typeof value === "function") {
|
|
@@ -753,12 +1188,72 @@ function createAliasedSelectBuilder(db, fields, selectFn) {
|
|
|
753
1188
|
};
|
|
754
1189
|
return wrapBuilder(builder);
|
|
755
1190
|
}
|
|
756
|
-
|
|
1191
|
+
const DEFAULT_OPTIONS = {
|
|
1192
|
+
logRawSqlQuery: false,
|
|
1193
|
+
disableOptimisticLocking: false,
|
|
1194
|
+
cacheTTL: 120,
|
|
1195
|
+
cacheWrapTable: true,
|
|
1196
|
+
cacheEntityQueryName: "sql",
|
|
1197
|
+
cacheEntityExpirationName: "expiration",
|
|
1198
|
+
cacheEntityDataName: "data"
|
|
1199
|
+
};
|
|
1200
|
+
function patchDbWithSelectAliased(db, options) {
|
|
1201
|
+
const newOptions = { ...DEFAULT_OPTIONS, ...options };
|
|
757
1202
|
db.selectAliased = function(fields) {
|
|
758
|
-
return createAliasedSelectBuilder(
|
|
1203
|
+
return createAliasedSelectBuilder(
|
|
1204
|
+
db,
|
|
1205
|
+
fields,
|
|
1206
|
+
(selections) => db.select(selections),
|
|
1207
|
+
false,
|
|
1208
|
+
newOptions
|
|
1209
|
+
);
|
|
759
1210
|
};
|
|
760
1211
|
db.selectAliasedDistinct = function(fields) {
|
|
761
|
-
return createAliasedSelectBuilder(
|
|
1212
|
+
return createAliasedSelectBuilder(
|
|
1213
|
+
db,
|
|
1214
|
+
fields,
|
|
1215
|
+
(selections) => db.selectDistinct(selections),
|
|
1216
|
+
false,
|
|
1217
|
+
newOptions
|
|
1218
|
+
);
|
|
1219
|
+
};
|
|
1220
|
+
db.selectAliasedCacheable = function(fields, cacheTtl) {
|
|
1221
|
+
return createAliasedSelectBuilder(
|
|
1222
|
+
db,
|
|
1223
|
+
fields,
|
|
1224
|
+
(selections) => db.select(selections),
|
|
1225
|
+
true,
|
|
1226
|
+
newOptions,
|
|
1227
|
+
cacheTtl
|
|
1228
|
+
);
|
|
1229
|
+
};
|
|
1230
|
+
db.selectAliasedDistinctCacheable = function(fields, cacheTtl) {
|
|
1231
|
+
return createAliasedSelectBuilder(
|
|
1232
|
+
db,
|
|
1233
|
+
fields,
|
|
1234
|
+
(selections) => db.selectDistinct(selections),
|
|
1235
|
+
true,
|
|
1236
|
+
newOptions,
|
|
1237
|
+
cacheTtl
|
|
1238
|
+
);
|
|
1239
|
+
};
|
|
1240
|
+
db.insertWithCacheContext = function(table) {
|
|
1241
|
+
return insertAndEvictCacheBuilder(db, table, newOptions, false);
|
|
1242
|
+
};
|
|
1243
|
+
db.insertAndEvictCache = function(table) {
|
|
1244
|
+
return insertAndEvictCacheBuilder(db, table, newOptions, true);
|
|
1245
|
+
};
|
|
1246
|
+
db.updateWithCacheContext = function(table) {
|
|
1247
|
+
return updateAndEvictCacheBuilder(db, table, newOptions, false);
|
|
1248
|
+
};
|
|
1249
|
+
db.updateAndEvictCache = function(table) {
|
|
1250
|
+
return updateAndEvictCacheBuilder(db, table, newOptions, true);
|
|
1251
|
+
};
|
|
1252
|
+
db.deleteWithCacheContext = function(table) {
|
|
1253
|
+
return deleteAndEvictCacheBuilder(db, table, newOptions, false);
|
|
1254
|
+
};
|
|
1255
|
+
db.deleteAndEvictCache = function(table) {
|
|
1256
|
+
return deleteAndEvictCacheBuilder(db, table, newOptions, true);
|
|
762
1257
|
};
|
|
763
1258
|
return db;
|
|
764
1259
|
}
|
|
@@ -910,14 +1405,14 @@ class ForgeSQLAnalyseOperation {
|
|
|
910
1405
|
* @returns {string} The SQL query for cluster statement history
|
|
911
1406
|
*/
|
|
912
1407
|
buildClusterStatementQuery(tables, from, to) {
|
|
913
|
-
const
|
|
1408
|
+
const formatDateTime2 = (date) => DateTime.fromJSDate(date).toFormat("yyyy-LL-dd'T'HH:mm:ss.SSS");
|
|
914
1409
|
const tableConditions = tables.map((table) => `TABLE_NAMES LIKE CONCAT(SCHEMA_NAME, '.', '%', '${table}', '%')`).join(" OR ");
|
|
915
1410
|
const timeConditions = [];
|
|
916
1411
|
if (from) {
|
|
917
|
-
timeConditions.push(`SUMMARY_BEGIN_TIME >= '${
|
|
1412
|
+
timeConditions.push(`SUMMARY_BEGIN_TIME >= '${formatDateTime2(from)}'`);
|
|
918
1413
|
}
|
|
919
1414
|
if (to) {
|
|
920
|
-
timeConditions.push(`SUMMARY_END_TIME <= '${
|
|
1415
|
+
timeConditions.push(`SUMMARY_END_TIME <= '${formatDateTime2(to)}'`);
|
|
921
1416
|
}
|
|
922
1417
|
let whereClauses;
|
|
923
1418
|
if (tableConditions?.length) {
|
|
@@ -990,12 +1485,167 @@ class ForgeSQLAnalyseOperation {
|
|
|
990
1485
|
return this.analyzeQueriesHistoryRaw(tableNames, fromDate, toDate);
|
|
991
1486
|
}
|
|
992
1487
|
}
|
|
1488
|
+
class ForgeSQLCacheOperations {
|
|
1489
|
+
options;
|
|
1490
|
+
forgeOperations;
|
|
1491
|
+
/**
|
|
1492
|
+
* Creates a new instance of ForgeSQLCacheOperations.
|
|
1493
|
+
*
|
|
1494
|
+
* @param options - Configuration options for the ORM
|
|
1495
|
+
* @param forgeOperations - The ForgeSQL operations instance
|
|
1496
|
+
*/
|
|
1497
|
+
constructor(options, forgeOperations) {
|
|
1498
|
+
this.options = options;
|
|
1499
|
+
this.forgeOperations = forgeOperations;
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* Evicts cache for multiple tables using Drizzle table objects.
|
|
1503
|
+
*
|
|
1504
|
+
* @param tables - Array of Drizzle table objects to clear cache for
|
|
1505
|
+
* @returns Promise that resolves when cache eviction is complete
|
|
1506
|
+
* @throws Error if cacheEntityName is not configured
|
|
1507
|
+
*/
|
|
1508
|
+
async evictCacheEntities(tables) {
|
|
1509
|
+
if (!this.options.cacheEntityName) {
|
|
1510
|
+
throw new Error("cacheEntityName is not configured");
|
|
1511
|
+
}
|
|
1512
|
+
await this.evictCache(tables.map((t) => getTableName(t)));
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* Evicts cache for multiple tables by their names.
|
|
1516
|
+
*
|
|
1517
|
+
* @param tables - Array of table names to clear cache for
|
|
1518
|
+
* @returns Promise that resolves when cache eviction is complete
|
|
1519
|
+
* @throws Error if cacheEntityName is not configured
|
|
1520
|
+
*/
|
|
1521
|
+
async evictCache(tables) {
|
|
1522
|
+
if (!this.options.cacheEntityName) {
|
|
1523
|
+
throw new Error("cacheEntityName is not configured");
|
|
1524
|
+
}
|
|
1525
|
+
await clearTablesCache(tables, this.options);
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Inserts records with optimistic locking/versioning and automatically evicts cache.
|
|
1529
|
+
*
|
|
1530
|
+
* This method uses `modifyWithVersioning().insert()` internally, providing:
|
|
1531
|
+
* - Automatic version field initialization
|
|
1532
|
+
* - Optimistic locking support
|
|
1533
|
+
* - Cache eviction after successful operation
|
|
1534
|
+
*
|
|
1535
|
+
* @param schema - The table schema
|
|
1536
|
+
* @param models - Array of entities to insert
|
|
1537
|
+
* @param updateIfExists - Whether to update existing records
|
|
1538
|
+
* @returns Promise that resolves to the number of inserted rows
|
|
1539
|
+
* @throws Error if cacheEntityName is not configured
|
|
1540
|
+
* @throws Error if optimistic locking check fails
|
|
1541
|
+
*/
|
|
1542
|
+
async insert(schema, models, updateIfExists) {
|
|
1543
|
+
this.validateCacheConfiguration();
|
|
1544
|
+
const number = await this.forgeOperations.modifyWithVersioning().insert(schema, models, updateIfExists);
|
|
1545
|
+
await clearCache(schema, this.options);
|
|
1546
|
+
return number;
|
|
1547
|
+
}
|
|
1548
|
+
/**
|
|
1549
|
+
* Deletes a record by ID with optimistic locking/versioning and automatically evicts cache.
|
|
1550
|
+
*
|
|
1551
|
+
* This method uses `modifyWithVersioning().deleteById()` internally, providing:
|
|
1552
|
+
* - Optimistic locking checks before deletion
|
|
1553
|
+
* - Version field validation
|
|
1554
|
+
* - Cache eviction after successful operation
|
|
1555
|
+
*
|
|
1556
|
+
* @param id - The ID of the record to delete
|
|
1557
|
+
* @param schema - The table schema
|
|
1558
|
+
* @returns Promise that resolves to the number of affected rows
|
|
1559
|
+
* @throws Error if cacheEntityName is not configured
|
|
1560
|
+
* @throws Error if optimistic locking check fails
|
|
1561
|
+
*/
|
|
1562
|
+
async deleteById(id, schema) {
|
|
1563
|
+
this.validateCacheConfiguration();
|
|
1564
|
+
const number = await this.forgeOperations.modifyWithVersioning().deleteById(id, schema);
|
|
1565
|
+
await clearCache(schema, this.options);
|
|
1566
|
+
return number;
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Updates a record by ID with optimistic locking/versioning and automatically evicts cache.
|
|
1570
|
+
*
|
|
1571
|
+
* This method uses `modifyWithVersioning().updateById()` internally, providing:
|
|
1572
|
+
* - Optimistic locking checks before update
|
|
1573
|
+
* - Version field incrementation
|
|
1574
|
+
* - Cache eviction after successful operation
|
|
1575
|
+
*
|
|
1576
|
+
* @param entity - The entity with updated values (must include primary key)
|
|
1577
|
+
* @param schema - The table schema
|
|
1578
|
+
* @returns Promise that resolves to the number of affected rows
|
|
1579
|
+
* @throws Error if cacheEntityName is not configured
|
|
1580
|
+
* @throws Error if optimistic locking check fails
|
|
1581
|
+
*/
|
|
1582
|
+
async updateById(entity, schema) {
|
|
1583
|
+
this.validateCacheConfiguration();
|
|
1584
|
+
const number = await this.forgeOperations.modifyWithVersioning().updateById(entity, schema);
|
|
1585
|
+
await clearCache(schema, this.options);
|
|
1586
|
+
return number;
|
|
1587
|
+
}
|
|
1588
|
+
/**
|
|
1589
|
+
* Updates fields based on conditions with optimistic locking/versioning and automatically evicts cache.
|
|
1590
|
+
*
|
|
1591
|
+
* This method uses `modifyWithVersioning().updateFields()` internally, providing:
|
|
1592
|
+
* - Optimistic locking support (if version field is configured)
|
|
1593
|
+
* - Version field validation and incrementation
|
|
1594
|
+
* - Cache eviction after successful operation
|
|
1595
|
+
*
|
|
1596
|
+
* @param updateData - The data to update
|
|
1597
|
+
* @param schema - The table schema
|
|
1598
|
+
* @param where - Optional WHERE conditions
|
|
1599
|
+
* @returns Promise that resolves to the number of affected rows
|
|
1600
|
+
* @throws Error if cacheEntityName is not configured
|
|
1601
|
+
* @throws Error if optimistic locking check fails
|
|
1602
|
+
*/
|
|
1603
|
+
async updateFields(updateData, schema, where) {
|
|
1604
|
+
this.validateCacheConfiguration();
|
|
1605
|
+
const number = await this.forgeOperations.modifyWithVersioning().updateFields(updateData, schema, where);
|
|
1606
|
+
await clearCache(schema, this.options);
|
|
1607
|
+
return number;
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Executes a query with caching support.
|
|
1611
|
+
* First checks cache, if not found executes query and stores result in cache.
|
|
1612
|
+
*
|
|
1613
|
+
* @param query - The Drizzle query to execute
|
|
1614
|
+
* @param cacheTtl - Optional cache TTL override
|
|
1615
|
+
* @returns Promise that resolves to the query results
|
|
1616
|
+
* @throws Error if cacheEntityName is not configured
|
|
1617
|
+
*/
|
|
1618
|
+
async executeQuery(query, cacheTtl) {
|
|
1619
|
+
this.validateCacheConfiguration();
|
|
1620
|
+
const sqlQuery = query;
|
|
1621
|
+
const cacheResult = await getFromCache(sqlQuery, this.options);
|
|
1622
|
+
if (cacheResult) {
|
|
1623
|
+
return cacheResult;
|
|
1624
|
+
}
|
|
1625
|
+
const results = await query;
|
|
1626
|
+
await setCacheResult(sqlQuery, this.options, results, cacheTtl ?? this.options.cacheTTL ?? 60);
|
|
1627
|
+
return results;
|
|
1628
|
+
}
|
|
1629
|
+
/**
|
|
1630
|
+
* Validates that cache configuration is properly set up.
|
|
1631
|
+
*
|
|
1632
|
+
* @throws Error if cacheEntityName is not configured
|
|
1633
|
+
* @private
|
|
1634
|
+
*/
|
|
1635
|
+
validateCacheConfiguration() {
|
|
1636
|
+
if (!this.options.cacheEntityName) {
|
|
1637
|
+
throw new Error("cacheEntityName is not configured");
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
993
1641
|
class ForgeSQLORMImpl {
|
|
994
1642
|
static instance = null;
|
|
995
1643
|
drizzle;
|
|
996
1644
|
crudOperations;
|
|
997
1645
|
fetchOperations;
|
|
998
1646
|
analyzeOperations;
|
|
1647
|
+
cacheOperations;
|
|
1648
|
+
options;
|
|
999
1649
|
/**
|
|
1000
1650
|
* Private constructor to enforce singleton behavior.
|
|
1001
1651
|
* @param options - Options for configuring ForgeSQL ORM behavior.
|
|
@@ -1004,28 +1654,185 @@ class ForgeSQLORMImpl {
|
|
|
1004
1654
|
try {
|
|
1005
1655
|
const newOptions = options ?? {
|
|
1006
1656
|
logRawSqlQuery: false,
|
|
1007
|
-
disableOptimisticLocking: false
|
|
1657
|
+
disableOptimisticLocking: false,
|
|
1658
|
+
cacheWrapTable: true,
|
|
1659
|
+
cacheTTL: 120,
|
|
1660
|
+
cacheEntityQueryName: "sql",
|
|
1661
|
+
cacheEntityExpirationName: "expiration",
|
|
1662
|
+
cacheEntityDataName: "data"
|
|
1008
1663
|
};
|
|
1664
|
+
this.options = newOptions;
|
|
1009
1665
|
if (newOptions.logRawSqlQuery) {
|
|
1010
1666
|
console.debug("Initializing ForgeSQLORM...");
|
|
1011
1667
|
}
|
|
1012
1668
|
const proxiedDriver = createForgeDriverProxy(newOptions.hints, newOptions.logRawSqlQuery);
|
|
1013
1669
|
this.drizzle = patchDbWithSelectAliased(
|
|
1014
|
-
drizzle(proxiedDriver, { logger: newOptions.logRawSqlQuery })
|
|
1670
|
+
drizzle(proxiedDriver, { logger: newOptions.logRawSqlQuery }),
|
|
1671
|
+
newOptions
|
|
1015
1672
|
);
|
|
1016
1673
|
this.crudOperations = new ForgeSQLCrudOperations(this, newOptions);
|
|
1017
1674
|
this.fetchOperations = new ForgeSQLSelectOperations(newOptions);
|
|
1018
1675
|
this.analyzeOperations = new ForgeSQLAnalyseOperation(this);
|
|
1676
|
+
this.cacheOperations = new ForgeSQLCacheOperations(newOptions, this);
|
|
1019
1677
|
} catch (error) {
|
|
1020
1678
|
console.error("ForgeSQLORM initialization failed:", error);
|
|
1021
1679
|
throw error;
|
|
1022
1680
|
}
|
|
1023
1681
|
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Executes operations within a cache context that collects cache eviction events.
|
|
1684
|
+
* All clearCache calls within the context are collected and executed in batch at the end.
|
|
1685
|
+
* Queries executed within this context will bypass cache for tables that were marked for clearing.
|
|
1686
|
+
*
|
|
1687
|
+
* This is useful for:
|
|
1688
|
+
* - Batch operations that affect multiple tables
|
|
1689
|
+
* - Transaction-like operations where you want to clear cache only at the end
|
|
1690
|
+
* - Performance optimization by reducing cache clear operations
|
|
1691
|
+
*
|
|
1692
|
+
* @param cacheContext - Function containing operations that may trigger cache evictions
|
|
1693
|
+
* @returns Promise that resolves when all operations and cache clearing are complete
|
|
1694
|
+
*
|
|
1695
|
+
* @example
|
|
1696
|
+
* ```typescript
|
|
1697
|
+
* await forgeSQL.executeWithCacheContext(async () => {
|
|
1698
|
+
* await forgeSQL.modifyWithVersioning().insert(users, userData);
|
|
1699
|
+
* await forgeSQL.modifyWithVersioning().insert(orders, orderData);
|
|
1700
|
+
* // Cache for both users and orders tables will be cleared at the end
|
|
1701
|
+
* });
|
|
1702
|
+
* ```
|
|
1703
|
+
*/
|
|
1704
|
+
executeWithCacheContext(cacheContext) {
|
|
1705
|
+
return this.executeWithCacheContextAndReturnValue(cacheContext);
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* Executes operations within a cache context and returns a value.
|
|
1709
|
+
* All clearCache calls within the context are collected and executed in batch at the end.
|
|
1710
|
+
* Queries executed within this context will bypass cache for tables that were marked for clearing.
|
|
1711
|
+
*
|
|
1712
|
+
* @param cacheContext - Function containing operations that may trigger cache evictions
|
|
1713
|
+
* @returns Promise that resolves to the return value of the cacheContext function
|
|
1714
|
+
*
|
|
1715
|
+
* @example
|
|
1716
|
+
* ```typescript
|
|
1717
|
+
* const result = await forgeSQL.executeWithCacheContextAndReturnValue(async () => {
|
|
1718
|
+
* await forgeSQL.modifyWithVersioning().insert(users, userData);
|
|
1719
|
+
* return await forgeSQL.fetch().executeQueryOnlyOne(selectUserQuery);
|
|
1720
|
+
* });
|
|
1721
|
+
* ```
|
|
1722
|
+
*/
|
|
1723
|
+
async executeWithCacheContextAndReturnValue(cacheContext) {
|
|
1724
|
+
return await this.executeWithLocalCacheContextAndReturnValue(
|
|
1725
|
+
async () => await cacheApplicationContext.run({ tables: /* @__PURE__ */ new Set() }, async () => {
|
|
1726
|
+
try {
|
|
1727
|
+
return await cacheContext();
|
|
1728
|
+
} finally {
|
|
1729
|
+
await clearTablesCache(
|
|
1730
|
+
Array.from(cacheApplicationContext.getStore()?.tables ?? []),
|
|
1731
|
+
this.options
|
|
1732
|
+
);
|
|
1733
|
+
}
|
|
1734
|
+
})
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
/**
|
|
1738
|
+
* Executes operations within a local cache context and returns a value.
|
|
1739
|
+
* This provides in-memory caching for select queries within a single request scope.
|
|
1740
|
+
*
|
|
1741
|
+
* @param cacheContext - Function containing operations that will benefit from local caching
|
|
1742
|
+
* @returns Promise that resolves to the return value of the cacheContext function
|
|
1743
|
+
*/
|
|
1744
|
+
async executeWithLocalCacheContextAndReturnValue(cacheContext) {
|
|
1745
|
+
return await localCacheApplicationContext.run({ cache: {} }, async () => {
|
|
1746
|
+
return await cacheContext();
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
/**
|
|
1750
|
+
* Executes operations within a local cache context.
|
|
1751
|
+
* This provides in-memory caching for select queries within a single request scope.
|
|
1752
|
+
*
|
|
1753
|
+
* @param cacheContext - Function containing operations that will benefit from local caching
|
|
1754
|
+
* @returns Promise that resolves when all operations are complete
|
|
1755
|
+
*/
|
|
1756
|
+
executeWithLocalContext(cacheContext) {
|
|
1757
|
+
return this.executeWithLocalCacheContextAndReturnValue(cacheContext);
|
|
1758
|
+
}
|
|
1759
|
+
/**
|
|
1760
|
+
* Creates an insert query builder.
|
|
1761
|
+
*
|
|
1762
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
1763
|
+
* For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
|
|
1764
|
+
*
|
|
1765
|
+
* @param table - The table to insert into
|
|
1766
|
+
* @returns Insert query builder (no versioning, no cache management)
|
|
1767
|
+
*/
|
|
1768
|
+
insert(table) {
|
|
1769
|
+
return this.drizzle.insertWithCacheContext(table);
|
|
1770
|
+
}
|
|
1771
|
+
/**
|
|
1772
|
+
* Creates an insert query builder that automatically evicts cache after execution.
|
|
1773
|
+
*
|
|
1774
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
1775
|
+
* For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
|
|
1776
|
+
*
|
|
1777
|
+
* @param table - The table to insert into
|
|
1778
|
+
* @returns Insert query builder with automatic cache eviction (no versioning)
|
|
1779
|
+
*/
|
|
1780
|
+
insertAndEvictCache(table) {
|
|
1781
|
+
return this.drizzle.insertAndEvictCache(table);
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* Creates an update query builder that automatically evicts cache after execution.
|
|
1785
|
+
*
|
|
1786
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
1787
|
+
* For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
|
|
1788
|
+
*
|
|
1789
|
+
* @param table - The table to update
|
|
1790
|
+
* @returns Update query builder with automatic cache eviction (no versioning)
|
|
1791
|
+
*/
|
|
1792
|
+
updateAndEvictCache(table) {
|
|
1793
|
+
return this.drizzle.updateAndEvictCache(table);
|
|
1794
|
+
}
|
|
1795
|
+
/**
|
|
1796
|
+
* Creates an update query builder.
|
|
1797
|
+
*
|
|
1798
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
1799
|
+
* For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
|
|
1800
|
+
*
|
|
1801
|
+
* @param table - The table to update
|
|
1802
|
+
* @returns Update query builder (no versioning, no cache management)
|
|
1803
|
+
*/
|
|
1804
|
+
update(table) {
|
|
1805
|
+
return this.drizzle.updateWithCacheContext(table);
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Creates a delete query builder.
|
|
1809
|
+
*
|
|
1810
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
1811
|
+
* For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
|
|
1812
|
+
*
|
|
1813
|
+
* @param table - The table to delete from
|
|
1814
|
+
* @returns Delete query builder (no versioning, no cache management)
|
|
1815
|
+
*/
|
|
1816
|
+
delete(table) {
|
|
1817
|
+
return this.drizzle.deleteWithCacheContext(table);
|
|
1818
|
+
}
|
|
1819
|
+
/**
|
|
1820
|
+
* Creates a delete query builder that automatically evicts cache after execution.
|
|
1821
|
+
*
|
|
1822
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
1823
|
+
* For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
|
|
1824
|
+
*
|
|
1825
|
+
* @param table - The table to delete from
|
|
1826
|
+
* @returns Delete query builder with automatic cache eviction (no versioning)
|
|
1827
|
+
*/
|
|
1828
|
+
deleteAndEvictCache(table) {
|
|
1829
|
+
return this.drizzle.deleteAndEvictCache(table);
|
|
1830
|
+
}
|
|
1024
1831
|
/**
|
|
1025
1832
|
* Create the modify operations instance.
|
|
1026
1833
|
* @returns modify operations.
|
|
1027
1834
|
*/
|
|
1028
|
-
|
|
1835
|
+
modifyWithVersioning() {
|
|
1029
1836
|
return this.crudOperations;
|
|
1030
1837
|
}
|
|
1031
1838
|
/**
|
|
@@ -1037,13 +1844,6 @@ class ForgeSQLORMImpl {
|
|
|
1037
1844
|
ForgeSQLORMImpl.instance ??= new ForgeSQLORMImpl(options);
|
|
1038
1845
|
return ForgeSQLORMImpl.instance;
|
|
1039
1846
|
}
|
|
1040
|
-
/**
|
|
1041
|
-
* Retrieves the CRUD operations instance.
|
|
1042
|
-
* @returns CRUD operations.
|
|
1043
|
-
*/
|
|
1044
|
-
crud() {
|
|
1045
|
-
return this.modify();
|
|
1046
|
-
}
|
|
1047
1847
|
/**
|
|
1048
1848
|
* Retrieves the fetch operations instance.
|
|
1049
1849
|
* @returns Fetch operations.
|
|
@@ -1051,9 +1851,26 @@ class ForgeSQLORMImpl {
|
|
|
1051
1851
|
fetch() {
|
|
1052
1852
|
return this.fetchOperations;
|
|
1053
1853
|
}
|
|
1854
|
+
/**
|
|
1855
|
+
* Provides query analysis capabilities including EXPLAIN ANALYZE and slow query analysis.
|
|
1856
|
+
* @returns {SchemaAnalyzeForgeSql} Interface for analyzing query performance
|
|
1857
|
+
*/
|
|
1054
1858
|
analyze() {
|
|
1055
1859
|
return this.analyzeOperations;
|
|
1056
1860
|
}
|
|
1861
|
+
/**
|
|
1862
|
+
* Provides schema-level SQL operations with optimistic locking/versioning and automatic cache eviction.
|
|
1863
|
+
*
|
|
1864
|
+
* This method returns operations that use `modifyWithVersioning()` internally, providing:
|
|
1865
|
+
* - Optimistic locking support
|
|
1866
|
+
* - Automatic version field management
|
|
1867
|
+
* - Cache eviction after successful operations
|
|
1868
|
+
*
|
|
1869
|
+
* @returns {ForgeSQLCacheOperations} Interface for executing versioned SQL operations with cache management
|
|
1870
|
+
*/
|
|
1871
|
+
modifyWithVersioningAndEvictCache() {
|
|
1872
|
+
return this.cacheOperations;
|
|
1873
|
+
}
|
|
1057
1874
|
/**
|
|
1058
1875
|
* Returns a Drizzle query builder instance.
|
|
1059
1876
|
*
|
|
@@ -1110,12 +1927,162 @@ class ForgeSQLORMImpl {
|
|
|
1110
1927
|
}
|
|
1111
1928
|
return this.drizzle.selectAliasedDistinct(fields);
|
|
1112
1929
|
}
|
|
1930
|
+
/**
|
|
1931
|
+
* Creates a cacheable select query with unique field aliases to prevent field name collisions in joins.
|
|
1932
|
+
* This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
|
|
1933
|
+
*
|
|
1934
|
+
* @template TSelection - The type of the selected fields
|
|
1935
|
+
* @param {TSelection} fields - Object containing the fields to select, with table schemas as values
|
|
1936
|
+
* @param {number} cacheTTL - cache ttl optional default is 60 sec.
|
|
1937
|
+
* @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A select query builder with unique field aliases
|
|
1938
|
+
* @throws {Error} If fields parameter is empty
|
|
1939
|
+
* @example
|
|
1940
|
+
* ```typescript
|
|
1941
|
+
* await forgeSQL
|
|
1942
|
+
* .selectCacheable({user: users, order: orders},60)
|
|
1943
|
+
* .from(orders)
|
|
1944
|
+
* .innerJoin(users, eq(orders.userId, users.id));
|
|
1945
|
+
* ```
|
|
1946
|
+
*/
|
|
1947
|
+
selectCacheable(fields, cacheTTL) {
|
|
1948
|
+
if (!fields) {
|
|
1949
|
+
throw new Error("fields is empty");
|
|
1950
|
+
}
|
|
1951
|
+
return this.drizzle.selectAliasedCacheable(fields, cacheTTL);
|
|
1952
|
+
}
|
|
1953
|
+
/**
|
|
1954
|
+
* Creates a cacheable distinct select query with unique field aliases to prevent field name collisions in joins.
|
|
1955
|
+
* This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
|
|
1956
|
+
*
|
|
1957
|
+
* @template TSelection - The type of the selected fields
|
|
1958
|
+
* @param {TSelection} fields - Object containing the fields to select, with table schemas as values
|
|
1959
|
+
* @param {number} cacheTTL - cache ttl optional default is 60 sec.
|
|
1960
|
+
* @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A distinct select query builder with unique field aliases
|
|
1961
|
+
* @throws {Error} If fields parameter is empty
|
|
1962
|
+
* @example
|
|
1963
|
+
* ```typescript
|
|
1964
|
+
* await forgeSQL
|
|
1965
|
+
* .selectDistinctCacheable({user: users, order: orders}, 60)
|
|
1966
|
+
* .from(orders)
|
|
1967
|
+
* .innerJoin(users, eq(orders.userId, users.id));
|
|
1968
|
+
* ```
|
|
1969
|
+
*/
|
|
1970
|
+
selectDistinctCacheable(fields, cacheTTL) {
|
|
1971
|
+
if (!fields) {
|
|
1972
|
+
throw new Error("fields is empty");
|
|
1973
|
+
}
|
|
1974
|
+
return this.drizzle.selectAliasedDistinctCacheable(fields, cacheTTL);
|
|
1975
|
+
}
|
|
1113
1976
|
}
|
|
1114
1977
|
class ForgeSQLORM {
|
|
1115
1978
|
ormInstance;
|
|
1116
1979
|
constructor(options) {
|
|
1117
1980
|
this.ormInstance = ForgeSQLORMImpl.getInstance(options);
|
|
1118
1981
|
}
|
|
1982
|
+
selectCacheable(fields, cacheTTL) {
|
|
1983
|
+
return this.ormInstance.selectCacheable(fields, cacheTTL);
|
|
1984
|
+
}
|
|
1985
|
+
selectDistinctCacheable(fields, cacheTTL) {
|
|
1986
|
+
return this.ormInstance.selectDistinctCacheable(fields, cacheTTL);
|
|
1987
|
+
}
|
|
1988
|
+
executeWithCacheContext(cacheContext) {
|
|
1989
|
+
return this.ormInstance.executeWithCacheContext(cacheContext);
|
|
1990
|
+
}
|
|
1991
|
+
executeWithCacheContextAndReturnValue(cacheContext) {
|
|
1992
|
+
return this.ormInstance.executeWithCacheContextAndReturnValue(cacheContext);
|
|
1993
|
+
}
|
|
1994
|
+
/**
|
|
1995
|
+
* Executes operations within a local cache context.
|
|
1996
|
+
* This provides in-memory caching for select queries within a single request scope.
|
|
1997
|
+
*
|
|
1998
|
+
* @param cacheContext - Function containing operations that will benefit from local caching
|
|
1999
|
+
* @returns Promise that resolves when all operations are complete
|
|
2000
|
+
*/
|
|
2001
|
+
executeWithLocalContext(cacheContext) {
|
|
2002
|
+
return this.ormInstance.executeWithLocalContext(cacheContext);
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* Executes operations within a local cache context and returns a value.
|
|
2006
|
+
* This provides in-memory caching for select queries within a single request scope.
|
|
2007
|
+
*
|
|
2008
|
+
* @param cacheContext - Function containing operations that will benefit from local caching
|
|
2009
|
+
* @returns Promise that resolves to the return value of the cacheContext function
|
|
2010
|
+
*/
|
|
2011
|
+
executeWithLocalCacheContextAndReturnValue(cacheContext) {
|
|
2012
|
+
return this.ormInstance.executeWithLocalCacheContextAndReturnValue(cacheContext);
|
|
2013
|
+
}
|
|
2014
|
+
/**
|
|
2015
|
+
* Creates an insert query builder.
|
|
2016
|
+
*
|
|
2017
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
2018
|
+
* For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
|
|
2019
|
+
*
|
|
2020
|
+
* @param table - The table to insert into
|
|
2021
|
+
* @returns Insert query builder (no versioning, no cache management)
|
|
2022
|
+
*/
|
|
2023
|
+
insert(table) {
|
|
2024
|
+
return this.ormInstance.insert(table);
|
|
2025
|
+
}
|
|
2026
|
+
/**
|
|
2027
|
+
* Creates an insert query builder that automatically evicts cache after execution.
|
|
2028
|
+
*
|
|
2029
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
2030
|
+
* For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
|
|
2031
|
+
*
|
|
2032
|
+
* @param table - The table to insert into
|
|
2033
|
+
* @returns Insert query builder with automatic cache eviction (no versioning)
|
|
2034
|
+
*/
|
|
2035
|
+
insertAndEvictCache(table) {
|
|
2036
|
+
return this.ormInstance.insertAndEvictCache(table);
|
|
2037
|
+
}
|
|
2038
|
+
/**
|
|
2039
|
+
* Creates an update query builder.
|
|
2040
|
+
*
|
|
2041
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
2042
|
+
* For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
|
|
2043
|
+
*
|
|
2044
|
+
* @param table - The table to update
|
|
2045
|
+
* @returns Update query builder (no versioning, no cache management)
|
|
2046
|
+
*/
|
|
2047
|
+
update(table) {
|
|
2048
|
+
return this.ormInstance.update(table);
|
|
2049
|
+
}
|
|
2050
|
+
/**
|
|
2051
|
+
* Creates an update query builder that automatically evicts cache after execution.
|
|
2052
|
+
*
|
|
2053
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
2054
|
+
* For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
|
|
2055
|
+
*
|
|
2056
|
+
* @param table - The table to update
|
|
2057
|
+
* @returns Update query builder with automatic cache eviction (no versioning)
|
|
2058
|
+
*/
|
|
2059
|
+
updateAndEvictCache(table) {
|
|
2060
|
+
return this.ormInstance.updateAndEvictCache(table);
|
|
2061
|
+
}
|
|
2062
|
+
/**
|
|
2063
|
+
* Creates a delete query builder.
|
|
2064
|
+
*
|
|
2065
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
2066
|
+
* For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
|
|
2067
|
+
*
|
|
2068
|
+
* @param table - The table to delete from
|
|
2069
|
+
* @returns Delete query builder (no versioning, no cache management)
|
|
2070
|
+
*/
|
|
2071
|
+
delete(table) {
|
|
2072
|
+
return this.ormInstance.delete(table);
|
|
2073
|
+
}
|
|
2074
|
+
/**
|
|
2075
|
+
* Creates a delete query builder that automatically evicts cache after execution.
|
|
2076
|
+
*
|
|
2077
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
2078
|
+
* For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
|
|
2079
|
+
*
|
|
2080
|
+
* @param table - The table to delete from
|
|
2081
|
+
* @returns Delete query builder with automatic cache eviction (no versioning)
|
|
2082
|
+
*/
|
|
2083
|
+
deleteAndEvictCache(table) {
|
|
2084
|
+
return this.ormInstance.deleteAndEvictCache(table);
|
|
2085
|
+
}
|
|
1119
2086
|
/**
|
|
1120
2087
|
* Creates a select query with unique field aliases to prevent field name collisions in joins.
|
|
1121
2088
|
* This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
|
|
@@ -1154,19 +2121,12 @@ class ForgeSQLORM {
|
|
|
1154
2121
|
selectDistinct(fields) {
|
|
1155
2122
|
return this.ormInstance.selectDistinct(fields);
|
|
1156
2123
|
}
|
|
1157
|
-
/**
|
|
1158
|
-
* Proxies the `crud` method from `ForgeSQLORMImpl`.
|
|
1159
|
-
* @returns CRUD operations.
|
|
1160
|
-
*/
|
|
1161
|
-
crud() {
|
|
1162
|
-
return this.ormInstance.modify();
|
|
1163
|
-
}
|
|
1164
2124
|
/**
|
|
1165
2125
|
* Proxies the `modify` method from `ForgeSQLORMImpl`.
|
|
1166
2126
|
* @returns Modify operations.
|
|
1167
2127
|
*/
|
|
1168
|
-
|
|
1169
|
-
return this.ormInstance.
|
|
2128
|
+
modifyWithVersioning() {
|
|
2129
|
+
return this.ormInstance.modifyWithVersioning();
|
|
1170
2130
|
}
|
|
1171
2131
|
/**
|
|
1172
2132
|
* Proxies the `fetch` method from `ForgeSQLORMImpl`.
|
|
@@ -1182,13 +2142,16 @@ class ForgeSQLORM {
|
|
|
1182
2142
|
analyze() {
|
|
1183
2143
|
return this.ormInstance.analyze();
|
|
1184
2144
|
}
|
|
2145
|
+
/**
|
|
2146
|
+
* Provides schema-level SQL cacheable operations with type safety.
|
|
2147
|
+
* @returns {ForgeSQLCacheOperations} Interface for executing schema-bound SQL queries
|
|
2148
|
+
*/
|
|
2149
|
+
modifyWithVersioningAndEvictCache() {
|
|
2150
|
+
return this.ormInstance.modifyWithVersioningAndEvictCache();
|
|
2151
|
+
}
|
|
1185
2152
|
/**
|
|
1186
2153
|
* Returns a Drizzle query builder instance.
|
|
1187
2154
|
*
|
|
1188
|
-
* ⚠️ IMPORTANT: This method should be used ONLY for query building purposes.
|
|
1189
|
-
* The returned instance should NOT be used for direct database connections or query execution.
|
|
1190
|
-
* All database operations should be performed through Forge SQL's executeRawSQL or executeRawUpdateSQL methods.
|
|
1191
|
-
*
|
|
1192
2155
|
* @returns A Drizzle query builder instance for query construction only.
|
|
1193
2156
|
*/
|
|
1194
2157
|
getDrizzleQueryBuilder() {
|
|
@@ -1200,7 +2163,7 @@ const forgeDateTimeString = customType({
|
|
|
1200
2163
|
return "datetime";
|
|
1201
2164
|
},
|
|
1202
2165
|
toDriver(value) {
|
|
1203
|
-
return
|
|
2166
|
+
return formatDateTime(value, "yyyy-LL-dd' 'HH:mm:ss.SSS");
|
|
1204
2167
|
},
|
|
1205
2168
|
fromDriver(value) {
|
|
1206
2169
|
const format = "yyyy-LL-dd'T'HH:mm:ss.SSS";
|
|
@@ -1212,7 +2175,7 @@ const forgeTimestampString = customType({
|
|
|
1212
2175
|
return "timestamp";
|
|
1213
2176
|
},
|
|
1214
2177
|
toDriver(value) {
|
|
1215
|
-
return
|
|
2178
|
+
return formatDateTime(value, "yyyy-LL-dd' 'HH:mm:ss.SSS");
|
|
1216
2179
|
},
|
|
1217
2180
|
fromDriver(value) {
|
|
1218
2181
|
const format = "yyyy-LL-dd'T'HH:mm:ss.SSS";
|
|
@@ -1224,7 +2187,7 @@ const forgeDateString = customType({
|
|
|
1224
2187
|
return "date";
|
|
1225
2188
|
},
|
|
1226
2189
|
toDriver(value) {
|
|
1227
|
-
return
|
|
2190
|
+
return formatDateTime(value, "yyyy-LL-dd");
|
|
1228
2191
|
},
|
|
1229
2192
|
fromDriver(value) {
|
|
1230
2193
|
const format = "yyyy-LL-dd";
|
|
@@ -1236,7 +2199,7 @@ const forgeTimeString = customType({
|
|
|
1236
2199
|
return "time";
|
|
1237
2200
|
},
|
|
1238
2201
|
toDriver(value) {
|
|
1239
|
-
return
|
|
2202
|
+
return formatDateTime(value, "HH:mm:ss.SSS");
|
|
1240
2203
|
},
|
|
1241
2204
|
fromDriver(value) {
|
|
1242
2205
|
return parseDateTime(value, "HH:mm:ss.SSS");
|
|
@@ -1353,6 +2316,45 @@ async function dropTableSchemaMigrations() {
|
|
|
1353
2316
|
return getHttpResponse(500, errorMessage);
|
|
1354
2317
|
}
|
|
1355
2318
|
}
|
|
2319
|
+
const clearCacheSchedulerTrigger = async (options) => {
|
|
2320
|
+
try {
|
|
2321
|
+
const newOptions = options ?? {
|
|
2322
|
+
logRawSqlQuery: false,
|
|
2323
|
+
disableOptimisticLocking: false,
|
|
2324
|
+
cacheTTL: 120,
|
|
2325
|
+
cacheEntityName: "cache",
|
|
2326
|
+
cacheEntityQueryName: "sql",
|
|
2327
|
+
cacheEntityExpirationName: "expiration",
|
|
2328
|
+
cacheEntityDataName: "data"
|
|
2329
|
+
};
|
|
2330
|
+
if (!newOptions.cacheEntityName) {
|
|
2331
|
+
throw new Error("cacheEntityName is not configured");
|
|
2332
|
+
}
|
|
2333
|
+
await clearExpiredCache(newOptions);
|
|
2334
|
+
return {
|
|
2335
|
+
headers: { "Content-Type": ["application/json"] },
|
|
2336
|
+
statusCode: 200,
|
|
2337
|
+
statusText: "OK",
|
|
2338
|
+
body: JSON.stringify({
|
|
2339
|
+
success: true,
|
|
2340
|
+
message: "Cache cleanup completed successfully",
|
|
2341
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2342
|
+
})
|
|
2343
|
+
};
|
|
2344
|
+
} catch (error) {
|
|
2345
|
+
console.error("Error during cache cleanup: ", JSON.stringify(error));
|
|
2346
|
+
return {
|
|
2347
|
+
headers: { "Content-Type": ["application/json"] },
|
|
2348
|
+
statusCode: 500,
|
|
2349
|
+
statusText: "Internal Server Error",
|
|
2350
|
+
body: JSON.stringify({
|
|
2351
|
+
success: false,
|
|
2352
|
+
error: error instanceof Error ? error.message : "Unknown error during cache cleanup",
|
|
2353
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2354
|
+
})
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
};
|
|
1356
2358
|
const getHttpResponse = (statusCode, body) => {
|
|
1357
2359
|
let statusText = "";
|
|
1358
2360
|
if (statusCode === 200) {
|
|
@@ -1372,6 +2374,7 @@ export {
|
|
|
1372
2374
|
ForgeSQLSelectOperations,
|
|
1373
2375
|
applyFromDriverTransform,
|
|
1374
2376
|
applySchemaMigrations,
|
|
2377
|
+
clearCacheSchedulerTrigger,
|
|
1375
2378
|
ForgeSQLORM as default,
|
|
1376
2379
|
dropSchemaMigrations,
|
|
1377
2380
|
dropTableSchemaMigrations,
|
|
@@ -1382,6 +2385,7 @@ export {
|
|
|
1382
2385
|
forgeSystemTables,
|
|
1383
2386
|
forgeTimeString,
|
|
1384
2387
|
forgeTimestampString,
|
|
2388
|
+
formatDateTime,
|
|
1385
2389
|
formatLimitOffset,
|
|
1386
2390
|
generateDropTableStatements,
|
|
1387
2391
|
getHttpResponse,
|