forge-sql-orm 2.0.30 → 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 +1080 -60
- package/dist/ForgeSQLORM.js.map +1 -1
- package/dist/ForgeSQLORM.mjs +1063 -60
- 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 +16 -0
- 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,10 +1,13 @@
|
|
|
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) {
|
|
@@ -27,6 +30,14 @@ const parseDateTime = (value, format) => {
|
|
|
27
30
|
}
|
|
28
31
|
return result;
|
|
29
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
|
+
}
|
|
30
41
|
function getPrimaryKeys(table) {
|
|
31
42
|
const { columns, primaryKeys } = getTableMetadata(table);
|
|
32
43
|
const columnPrimaryKeys = Object.entries(columns).filter(([, column]) => column.primary);
|
|
@@ -289,6 +300,275 @@ function formatLimitOffset(limitOrOffset) {
|
|
|
289
300
|
function nextVal(sequenceName) {
|
|
290
301
|
return sql.raw(`NEXTVAL(${sequenceName})`);
|
|
291
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
|
+
}
|
|
292
572
|
class ForgeSQLCrudOperations {
|
|
293
573
|
forgeOperations;
|
|
294
574
|
options;
|
|
@@ -305,12 +585,17 @@ class ForgeSQLCrudOperations {
|
|
|
305
585
|
* Inserts records into the database with optional versioning support.
|
|
306
586
|
* If a version field exists in the schema, versioning is applied.
|
|
307
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
|
+
*
|
|
308
593
|
* @template T - The type of the table schema
|
|
309
|
-
* @param
|
|
310
|
-
* @param
|
|
311
|
-
* @param
|
|
312
|
-
* @returns
|
|
313
|
-
* @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
|
|
314
599
|
*/
|
|
315
600
|
async insert(schema, models, updateIfExists = false) {
|
|
316
601
|
if (!models?.length) return 0;
|
|
@@ -319,25 +604,32 @@ class ForgeSQLCrudOperations {
|
|
|
319
604
|
const preparedModels = models.map(
|
|
320
605
|
(model) => this.prepareModelWithVersion(model, versionMetadata, columns)
|
|
321
606
|
);
|
|
322
|
-
const queryBuilder = this.forgeOperations.
|
|
607
|
+
const queryBuilder = this.forgeOperations.insert(schema).values(preparedModels);
|
|
323
608
|
const finalQuery = updateIfExists ? queryBuilder.onDuplicateKeyUpdate({
|
|
324
609
|
set: Object.fromEntries(
|
|
325
610
|
Object.keys(preparedModels[0]).map((key) => [key, schema[key]])
|
|
326
611
|
)
|
|
327
612
|
}) : queryBuilder;
|
|
328
613
|
const result = await finalQuery;
|
|
614
|
+
await saveTableIfInsideCacheContext(schema);
|
|
329
615
|
return result[0].insertId;
|
|
330
616
|
}
|
|
331
617
|
/**
|
|
332
618
|
* Deletes a record by its primary key with optional version check.
|
|
333
619
|
* If versioning is enabled, ensures the record hasn't been modified since last read.
|
|
334
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
|
+
*
|
|
335
626
|
* @template T - The type of the table schema
|
|
336
|
-
* @param
|
|
337
|
-
* @param
|
|
338
|
-
* @returns
|
|
339
|
-
* @throws
|
|
340
|
-
* @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
|
|
341
633
|
*/
|
|
342
634
|
async deleteById(id, schema) {
|
|
343
635
|
const { tableName, columns } = getTableMetadata(schema);
|
|
@@ -358,8 +650,12 @@ class ForgeSQLCrudOperations {
|
|
|
358
650
|
conditions.push(eq(versionField, oldModel[versionMetadata.fieldName]));
|
|
359
651
|
}
|
|
360
652
|
}
|
|
361
|
-
const queryBuilder = this.forgeOperations.
|
|
653
|
+
const queryBuilder = this.forgeOperations.delete(schema).where(and(...conditions));
|
|
362
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);
|
|
363
659
|
return result[0].affectedRows;
|
|
364
660
|
}
|
|
365
661
|
/**
|
|
@@ -369,13 +665,19 @@ class ForgeSQLCrudOperations {
|
|
|
369
665
|
* - Checks for concurrent modifications
|
|
370
666
|
* - Increments the version on successful update
|
|
371
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
|
+
*
|
|
372
674
|
* @template T - The type of the table schema
|
|
373
|
-
* @param
|
|
374
|
-
* @param
|
|
375
|
-
* @returns
|
|
376
|
-
* @throws
|
|
377
|
-
* @throws
|
|
378
|
-
* @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
|
|
379
681
|
*/
|
|
380
682
|
async updateById(entity, schema) {
|
|
381
683
|
const { tableName, columns } = getTableMetadata(schema);
|
|
@@ -405,13 +707,14 @@ class ForgeSQLCrudOperations {
|
|
|
405
707
|
conditions.push(eq(versionField, currentVersion));
|
|
406
708
|
}
|
|
407
709
|
}
|
|
408
|
-
const queryBuilder = this.forgeOperations.
|
|
710
|
+
const queryBuilder = this.forgeOperations.update(schema).set(updateData).where(and(...conditions));
|
|
409
711
|
const result = await queryBuilder;
|
|
410
712
|
if (versionMetadata && result[0].affectedRows === 0) {
|
|
411
713
|
throw new Error(
|
|
412
714
|
`Optimistic locking failed: record with primary key ${entity[primaryKeyName]} has been modified`
|
|
413
715
|
);
|
|
414
716
|
}
|
|
717
|
+
await saveTableIfInsideCacheContext(schema);
|
|
415
718
|
return result[0].affectedRows;
|
|
416
719
|
}
|
|
417
720
|
/**
|
|
@@ -430,8 +733,9 @@ class ForgeSQLCrudOperations {
|
|
|
430
733
|
if (!where) {
|
|
431
734
|
throw new Error("WHERE conditions must be provided");
|
|
432
735
|
}
|
|
433
|
-
const queryBuilder = this.forgeOperations.
|
|
736
|
+
const queryBuilder = this.forgeOperations.update(schema).set(updateData).where(where);
|
|
434
737
|
const result = await queryBuilder;
|
|
738
|
+
await saveTableIfInsideCacheContext(schema);
|
|
435
739
|
return result[0].affectedRows;
|
|
436
740
|
}
|
|
437
741
|
// Helper methods
|
|
@@ -575,7 +879,7 @@ class ForgeSQLCrudOperations {
|
|
|
575
879
|
const [versionFieldName, versionFieldColumn] = versionField;
|
|
576
880
|
const primaryKeys = this.getPrimaryKeys(schema);
|
|
577
881
|
const [primaryKeyName, primaryKeyColumn] = primaryKeys[0];
|
|
578
|
-
const resultQuery = this.forgeOperations.
|
|
882
|
+
const resultQuery = this.forgeOperations.select({
|
|
579
883
|
[primaryKeyName]: primaryKeyColumn,
|
|
580
884
|
[versionFieldName]: versionFieldColumn
|
|
581
885
|
}).from(schema).where(eq(primaryKeyColumn, primaryKeyValues[primaryKeyName]));
|
|
@@ -720,7 +1024,125 @@ function createForgeDriverProxy(options, logRawSqlQuery) {
|
|
|
720
1024
|
return forgeDriver(modifiedQuery, params, method);
|
|
721
1025
|
};
|
|
722
1026
|
}
|
|
723
|
-
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) {
|
|
724
1146
|
const { selections, aliasMap } = mapSelectFieldsWithAlias(fields);
|
|
725
1147
|
const builder = selectFn(selections);
|
|
726
1148
|
const wrapBuilder = (rawBuilder) => {
|
|
@@ -733,10 +1155,22 @@ function createAliasedSelectBuilder(db, fields, selectFn) {
|
|
|
733
1155
|
};
|
|
734
1156
|
}
|
|
735
1157
|
if (prop === "then") {
|
|
736
|
-
return (onfulfilled, onrejected) =>
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
+
};
|
|
740
1174
|
}
|
|
741
1175
|
const value = Reflect.get(target, prop, receiver);
|
|
742
1176
|
if (typeof value === "function") {
|
|
@@ -754,12 +1188,72 @@ function createAliasedSelectBuilder(db, fields, selectFn) {
|
|
|
754
1188
|
};
|
|
755
1189
|
return wrapBuilder(builder);
|
|
756
1190
|
}
|
|
757
|
-
|
|
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 };
|
|
758
1202
|
db.selectAliased = function(fields) {
|
|
759
|
-
return createAliasedSelectBuilder(
|
|
1203
|
+
return createAliasedSelectBuilder(
|
|
1204
|
+
db,
|
|
1205
|
+
fields,
|
|
1206
|
+
(selections) => db.select(selections),
|
|
1207
|
+
false,
|
|
1208
|
+
newOptions
|
|
1209
|
+
);
|
|
760
1210
|
};
|
|
761
1211
|
db.selectAliasedDistinct = function(fields) {
|
|
762
|
-
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);
|
|
763
1257
|
};
|
|
764
1258
|
return db;
|
|
765
1259
|
}
|
|
@@ -911,14 +1405,14 @@ class ForgeSQLAnalyseOperation {
|
|
|
911
1405
|
* @returns {string} The SQL query for cluster statement history
|
|
912
1406
|
*/
|
|
913
1407
|
buildClusterStatementQuery(tables, from, to) {
|
|
914
|
-
const
|
|
1408
|
+
const formatDateTime2 = (date) => DateTime.fromJSDate(date).toFormat("yyyy-LL-dd'T'HH:mm:ss.SSS");
|
|
915
1409
|
const tableConditions = tables.map((table) => `TABLE_NAMES LIKE CONCAT(SCHEMA_NAME, '.', '%', '${table}', '%')`).join(" OR ");
|
|
916
1410
|
const timeConditions = [];
|
|
917
1411
|
if (from) {
|
|
918
|
-
timeConditions.push(`SUMMARY_BEGIN_TIME >= '${
|
|
1412
|
+
timeConditions.push(`SUMMARY_BEGIN_TIME >= '${formatDateTime2(from)}'`);
|
|
919
1413
|
}
|
|
920
1414
|
if (to) {
|
|
921
|
-
timeConditions.push(`SUMMARY_END_TIME <= '${
|
|
1415
|
+
timeConditions.push(`SUMMARY_END_TIME <= '${formatDateTime2(to)}'`);
|
|
922
1416
|
}
|
|
923
1417
|
let whereClauses;
|
|
924
1418
|
if (tableConditions?.length) {
|
|
@@ -991,12 +1485,167 @@ class ForgeSQLAnalyseOperation {
|
|
|
991
1485
|
return this.analyzeQueriesHistoryRaw(tableNames, fromDate, toDate);
|
|
992
1486
|
}
|
|
993
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
|
+
}
|
|
994
1641
|
class ForgeSQLORMImpl {
|
|
995
1642
|
static instance = null;
|
|
996
1643
|
drizzle;
|
|
997
1644
|
crudOperations;
|
|
998
1645
|
fetchOperations;
|
|
999
1646
|
analyzeOperations;
|
|
1647
|
+
cacheOperations;
|
|
1648
|
+
options;
|
|
1000
1649
|
/**
|
|
1001
1650
|
* Private constructor to enforce singleton behavior.
|
|
1002
1651
|
* @param options - Options for configuring ForgeSQL ORM behavior.
|
|
@@ -1005,28 +1654,185 @@ class ForgeSQLORMImpl {
|
|
|
1005
1654
|
try {
|
|
1006
1655
|
const newOptions = options ?? {
|
|
1007
1656
|
logRawSqlQuery: false,
|
|
1008
|
-
disableOptimisticLocking: false
|
|
1657
|
+
disableOptimisticLocking: false,
|
|
1658
|
+
cacheWrapTable: true,
|
|
1659
|
+
cacheTTL: 120,
|
|
1660
|
+
cacheEntityQueryName: "sql",
|
|
1661
|
+
cacheEntityExpirationName: "expiration",
|
|
1662
|
+
cacheEntityDataName: "data"
|
|
1009
1663
|
};
|
|
1664
|
+
this.options = newOptions;
|
|
1010
1665
|
if (newOptions.logRawSqlQuery) {
|
|
1011
1666
|
console.debug("Initializing ForgeSQLORM...");
|
|
1012
1667
|
}
|
|
1013
1668
|
const proxiedDriver = createForgeDriverProxy(newOptions.hints, newOptions.logRawSqlQuery);
|
|
1014
1669
|
this.drizzle = patchDbWithSelectAliased(
|
|
1015
|
-
drizzle(proxiedDriver, { logger: newOptions.logRawSqlQuery })
|
|
1670
|
+
drizzle(proxiedDriver, { logger: newOptions.logRawSqlQuery }),
|
|
1671
|
+
newOptions
|
|
1016
1672
|
);
|
|
1017
1673
|
this.crudOperations = new ForgeSQLCrudOperations(this, newOptions);
|
|
1018
1674
|
this.fetchOperations = new ForgeSQLSelectOperations(newOptions);
|
|
1019
1675
|
this.analyzeOperations = new ForgeSQLAnalyseOperation(this);
|
|
1676
|
+
this.cacheOperations = new ForgeSQLCacheOperations(newOptions, this);
|
|
1020
1677
|
} catch (error) {
|
|
1021
1678
|
console.error("ForgeSQLORM initialization failed:", error);
|
|
1022
1679
|
throw error;
|
|
1023
1680
|
}
|
|
1024
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
|
+
}
|
|
1025
1831
|
/**
|
|
1026
1832
|
* Create the modify operations instance.
|
|
1027
1833
|
* @returns modify operations.
|
|
1028
1834
|
*/
|
|
1029
|
-
|
|
1835
|
+
modifyWithVersioning() {
|
|
1030
1836
|
return this.crudOperations;
|
|
1031
1837
|
}
|
|
1032
1838
|
/**
|
|
@@ -1038,13 +1844,6 @@ class ForgeSQLORMImpl {
|
|
|
1038
1844
|
ForgeSQLORMImpl.instance ??= new ForgeSQLORMImpl(options);
|
|
1039
1845
|
return ForgeSQLORMImpl.instance;
|
|
1040
1846
|
}
|
|
1041
|
-
/**
|
|
1042
|
-
* Retrieves the CRUD operations instance.
|
|
1043
|
-
* @returns CRUD operations.
|
|
1044
|
-
*/
|
|
1045
|
-
crud() {
|
|
1046
|
-
return this.modify();
|
|
1047
|
-
}
|
|
1048
1847
|
/**
|
|
1049
1848
|
* Retrieves the fetch operations instance.
|
|
1050
1849
|
* @returns Fetch operations.
|
|
@@ -1052,9 +1851,26 @@ class ForgeSQLORMImpl {
|
|
|
1052
1851
|
fetch() {
|
|
1053
1852
|
return this.fetchOperations;
|
|
1054
1853
|
}
|
|
1854
|
+
/**
|
|
1855
|
+
* Provides query analysis capabilities including EXPLAIN ANALYZE and slow query analysis.
|
|
1856
|
+
* @returns {SchemaAnalyzeForgeSql} Interface for analyzing query performance
|
|
1857
|
+
*/
|
|
1055
1858
|
analyze() {
|
|
1056
1859
|
return this.analyzeOperations;
|
|
1057
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
|
+
}
|
|
1058
1874
|
/**
|
|
1059
1875
|
* Returns a Drizzle query builder instance.
|
|
1060
1876
|
*
|
|
@@ -1111,12 +1927,162 @@ class ForgeSQLORMImpl {
|
|
|
1111
1927
|
}
|
|
1112
1928
|
return this.drizzle.selectAliasedDistinct(fields);
|
|
1113
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
|
+
}
|
|
1114
1976
|
}
|
|
1115
1977
|
class ForgeSQLORM {
|
|
1116
1978
|
ormInstance;
|
|
1117
1979
|
constructor(options) {
|
|
1118
1980
|
this.ormInstance = ForgeSQLORMImpl.getInstance(options);
|
|
1119
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
|
+
}
|
|
1120
2086
|
/**
|
|
1121
2087
|
* Creates a select query with unique field aliases to prevent field name collisions in joins.
|
|
1122
2088
|
* This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
|
|
@@ -1155,19 +2121,12 @@ class ForgeSQLORM {
|
|
|
1155
2121
|
selectDistinct(fields) {
|
|
1156
2122
|
return this.ormInstance.selectDistinct(fields);
|
|
1157
2123
|
}
|
|
1158
|
-
/**
|
|
1159
|
-
* Proxies the `crud` method from `ForgeSQLORMImpl`.
|
|
1160
|
-
* @returns CRUD operations.
|
|
1161
|
-
*/
|
|
1162
|
-
crud() {
|
|
1163
|
-
return this.ormInstance.modify();
|
|
1164
|
-
}
|
|
1165
2124
|
/**
|
|
1166
2125
|
* Proxies the `modify` method from `ForgeSQLORMImpl`.
|
|
1167
2126
|
* @returns Modify operations.
|
|
1168
2127
|
*/
|
|
1169
|
-
|
|
1170
|
-
return this.ormInstance.
|
|
2128
|
+
modifyWithVersioning() {
|
|
2129
|
+
return this.ormInstance.modifyWithVersioning();
|
|
1171
2130
|
}
|
|
1172
2131
|
/**
|
|
1173
2132
|
* Proxies the `fetch` method from `ForgeSQLORMImpl`.
|
|
@@ -1183,13 +2142,16 @@ class ForgeSQLORM {
|
|
|
1183
2142
|
analyze() {
|
|
1184
2143
|
return this.ormInstance.analyze();
|
|
1185
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
|
+
}
|
|
1186
2152
|
/**
|
|
1187
2153
|
* Returns a Drizzle query builder instance.
|
|
1188
2154
|
*
|
|
1189
|
-
* ⚠️ IMPORTANT: This method should be used ONLY for query building purposes.
|
|
1190
|
-
* The returned instance should NOT be used for direct database connections or query execution.
|
|
1191
|
-
* All database operations should be performed through Forge SQL's executeRawSQL or executeRawUpdateSQL methods.
|
|
1192
|
-
*
|
|
1193
2155
|
* @returns A Drizzle query builder instance for query construction only.
|
|
1194
2156
|
*/
|
|
1195
2157
|
getDrizzleQueryBuilder() {
|
|
@@ -1201,7 +2163,7 @@ const forgeDateTimeString = customType({
|
|
|
1201
2163
|
return "datetime";
|
|
1202
2164
|
},
|
|
1203
2165
|
toDriver(value) {
|
|
1204
|
-
return
|
|
2166
|
+
return formatDateTime(value, "yyyy-LL-dd' 'HH:mm:ss.SSS");
|
|
1205
2167
|
},
|
|
1206
2168
|
fromDriver(value) {
|
|
1207
2169
|
const format = "yyyy-LL-dd'T'HH:mm:ss.SSS";
|
|
@@ -1213,7 +2175,7 @@ const forgeTimestampString = customType({
|
|
|
1213
2175
|
return "timestamp";
|
|
1214
2176
|
},
|
|
1215
2177
|
toDriver(value) {
|
|
1216
|
-
return
|
|
2178
|
+
return formatDateTime(value, "yyyy-LL-dd' 'HH:mm:ss.SSS");
|
|
1217
2179
|
},
|
|
1218
2180
|
fromDriver(value) {
|
|
1219
2181
|
const format = "yyyy-LL-dd'T'HH:mm:ss.SSS";
|
|
@@ -1225,7 +2187,7 @@ const forgeDateString = customType({
|
|
|
1225
2187
|
return "date";
|
|
1226
2188
|
},
|
|
1227
2189
|
toDriver(value) {
|
|
1228
|
-
return
|
|
2190
|
+
return formatDateTime(value, "yyyy-LL-dd");
|
|
1229
2191
|
},
|
|
1230
2192
|
fromDriver(value) {
|
|
1231
2193
|
const format = "yyyy-LL-dd";
|
|
@@ -1237,7 +2199,7 @@ const forgeTimeString = customType({
|
|
|
1237
2199
|
return "time";
|
|
1238
2200
|
},
|
|
1239
2201
|
toDriver(value) {
|
|
1240
|
-
return
|
|
2202
|
+
return formatDateTime(value, "HH:mm:ss.SSS");
|
|
1241
2203
|
},
|
|
1242
2204
|
fromDriver(value) {
|
|
1243
2205
|
return parseDateTime(value, "HH:mm:ss.SSS");
|
|
@@ -1354,6 +2316,45 @@ async function dropTableSchemaMigrations() {
|
|
|
1354
2316
|
return getHttpResponse(500, errorMessage);
|
|
1355
2317
|
}
|
|
1356
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
|
+
};
|
|
1357
2358
|
const getHttpResponse = (statusCode, body) => {
|
|
1358
2359
|
let statusText = "";
|
|
1359
2360
|
if (statusCode === 200) {
|
|
@@ -1373,6 +2374,7 @@ export {
|
|
|
1373
2374
|
ForgeSQLSelectOperations,
|
|
1374
2375
|
applyFromDriverTransform,
|
|
1375
2376
|
applySchemaMigrations,
|
|
2377
|
+
clearCacheSchedulerTrigger,
|
|
1376
2378
|
ForgeSQLORM as default,
|
|
1377
2379
|
dropSchemaMigrations,
|
|
1378
2380
|
dropTableSchemaMigrations,
|
|
@@ -1383,6 +2385,7 @@ export {
|
|
|
1383
2385
|
forgeSystemTables,
|
|
1384
2386
|
forgeTimeString,
|
|
1385
2387
|
forgeTimestampString,
|
|
2388
|
+
formatDateTime,
|
|
1386
2389
|
formatLimitOffset,
|
|
1387
2390
|
generateDropTableStatements,
|
|
1388
2391
|
getHttpResponse,
|