forge-sql-orm 2.0.30 → 2.1.1
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 +1410 -81
- package/dist/ForgeSQLORM.js +1456 -60
- package/dist/ForgeSQLORM.js.map +1 -1
- package/dist/ForgeSQLORM.mjs +1440 -61
- 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 +248 -13
- package/dist/core/ForgeSQLORM.d.ts.map +1 -1
- package/dist/core/ForgeSQLQueryBuilder.d.ts +394 -19
- 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 +90 -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 +743 -34
- package/src/core/ForgeSQLQueryBuilder.ts +456 -20
- package/src/index.ts +1 -1
- package/src/lib/drizzle/extensions/additionalActions.ts +852 -0
- package/src/lib/drizzle/extensions/types.d.ts +99 -10
- package/src/utils/cacheContextUtils.ts +212 -0
- package/src/utils/cacheUtils.ts +403 -0
- package/src/utils/sqlUtils.ts +42 -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
|
-
import { isTable, sql, eq, and } from "drizzle-orm";
|
|
1
|
+
import { isTable, sql, eq, and, getTableColumns } 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,36 @@ const parseDateTime = (value, format) => {
|
|
|
27
30
|
}
|
|
28
31
|
return result;
|
|
29
32
|
};
|
|
33
|
+
function formatDateTime(value, format) {
|
|
34
|
+
let dt = null;
|
|
35
|
+
if (value instanceof Date) {
|
|
36
|
+
dt = DateTime.fromJSDate(value);
|
|
37
|
+
} else if (typeof value === "string") {
|
|
38
|
+
for (const parser of [
|
|
39
|
+
DateTime.fromISO,
|
|
40
|
+
DateTime.fromRFC2822,
|
|
41
|
+
DateTime.fromSQL,
|
|
42
|
+
DateTime.fromHTTP
|
|
43
|
+
]) {
|
|
44
|
+
dt = parser(value);
|
|
45
|
+
if (dt.isValid) break;
|
|
46
|
+
}
|
|
47
|
+
if (!dt?.isValid) {
|
|
48
|
+
const parsed = Number(value);
|
|
49
|
+
if (!isNaN(parsed)) {
|
|
50
|
+
dt = DateTime.fromMillis(parsed);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} else if (typeof value === "number") {
|
|
54
|
+
dt = DateTime.fromMillis(value);
|
|
55
|
+
} else {
|
|
56
|
+
throw new Error("Unsupported type");
|
|
57
|
+
}
|
|
58
|
+
if (!dt?.isValid) {
|
|
59
|
+
throw new Error("Invalid Date");
|
|
60
|
+
}
|
|
61
|
+
return dt.toFormat(format);
|
|
62
|
+
}
|
|
30
63
|
function getPrimaryKeys(table) {
|
|
31
64
|
const { columns, primaryKeys } = getTableMetadata(table);
|
|
32
65
|
const columnPrimaryKeys = Object.entries(columns).filter(([, column]) => column.primary);
|
|
@@ -289,6 +322,275 @@ function formatLimitOffset(limitOrOffset) {
|
|
|
289
322
|
function nextVal(sequenceName) {
|
|
290
323
|
return sql.raw(`NEXTVAL(${sequenceName})`);
|
|
291
324
|
}
|
|
325
|
+
const CACHE_CONSTANTS = {
|
|
326
|
+
BATCH_SIZE: 25,
|
|
327
|
+
MAX_RETRY_ATTEMPTS: 3,
|
|
328
|
+
INITIAL_RETRY_DELAY: 1e3,
|
|
329
|
+
RETRY_DELAY_MULTIPLIER: 2,
|
|
330
|
+
DEFAULT_ENTITY_QUERY_NAME: "sql",
|
|
331
|
+
DEFAULT_EXPIRATION_NAME: "expiration",
|
|
332
|
+
DEFAULT_DATA_NAME: "data",
|
|
333
|
+
HASH_LENGTH: 32
|
|
334
|
+
};
|
|
335
|
+
function getCurrentTime() {
|
|
336
|
+
const dt = DateTime.now();
|
|
337
|
+
return Math.floor(dt.toSeconds());
|
|
338
|
+
}
|
|
339
|
+
function nowPlusSeconds(secondsToAdd) {
|
|
340
|
+
const dt = DateTime.now().plus({ seconds: secondsToAdd });
|
|
341
|
+
return Math.floor(dt.toSeconds());
|
|
342
|
+
}
|
|
343
|
+
function hashKey(query) {
|
|
344
|
+
const h = crypto.createHash("sha256");
|
|
345
|
+
h.update(query.sql.toLowerCase());
|
|
346
|
+
h.update(JSON.stringify(query.params));
|
|
347
|
+
return "CachedQuery_" + h.digest("hex").slice(0, CACHE_CONSTANTS.HASH_LENGTH);
|
|
348
|
+
}
|
|
349
|
+
async function deleteCacheEntriesInBatches(results, cacheEntityName) {
|
|
350
|
+
for (let i = 0; i < results.length; i += CACHE_CONSTANTS.BATCH_SIZE) {
|
|
351
|
+
const batch = results.slice(i, i + CACHE_CONSTANTS.BATCH_SIZE);
|
|
352
|
+
let transactionBuilder = kvs.transact();
|
|
353
|
+
batch.forEach((result) => {
|
|
354
|
+
transactionBuilder = transactionBuilder.delete(result.key, { entityName: cacheEntityName });
|
|
355
|
+
});
|
|
356
|
+
await transactionBuilder.execute();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async function clearCursorCache(tables, cursor, options) {
|
|
360
|
+
const cacheEntityName = options.cacheEntityName;
|
|
361
|
+
if (!cacheEntityName) {
|
|
362
|
+
throw new Error("cacheEntityName is not configured");
|
|
363
|
+
}
|
|
364
|
+
const entityQueryName = options.cacheEntityQueryName ?? CACHE_CONSTANTS.DEFAULT_ENTITY_QUERY_NAME;
|
|
365
|
+
let filters = new Filter();
|
|
366
|
+
for (const table of tables) {
|
|
367
|
+
const wrapIfNeeded = options.cacheWrapTable ? `\`${table}\`` : table;
|
|
368
|
+
filters.or(entityQueryName, FilterConditions.contains(wrapIfNeeded?.toLowerCase()));
|
|
369
|
+
}
|
|
370
|
+
let entityQueryBuilder = kvs.entity(cacheEntityName).query().index(entityQueryName).filters(filters);
|
|
371
|
+
if (cursor) {
|
|
372
|
+
entityQueryBuilder = entityQueryBuilder.cursor(cursor);
|
|
373
|
+
}
|
|
374
|
+
const listResult = await entityQueryBuilder.limit(100).getMany();
|
|
375
|
+
if (options.logRawSqlQuery) {
|
|
376
|
+
console.warn(`clear cache Records: ${JSON.stringify(listResult.results.map((r) => r.key))}`);
|
|
377
|
+
}
|
|
378
|
+
await deleteCacheEntriesInBatches(listResult.results, cacheEntityName);
|
|
379
|
+
if (listResult.nextCursor) {
|
|
380
|
+
return listResult.results.length + await clearCursorCache(tables, listResult.nextCursor, options);
|
|
381
|
+
} else {
|
|
382
|
+
return listResult.results.length;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async function clearExpirationCursorCache(cursor, options) {
|
|
386
|
+
const cacheEntityName = options.cacheEntityName;
|
|
387
|
+
if (!cacheEntityName) {
|
|
388
|
+
throw new Error("cacheEntityName is not configured");
|
|
389
|
+
}
|
|
390
|
+
const entityExpirationName = options.cacheEntityExpirationName ?? CACHE_CONSTANTS.DEFAULT_EXPIRATION_NAME;
|
|
391
|
+
let entityQueryBuilder = kvs.entity(cacheEntityName).query().index(entityExpirationName).where(WhereConditions.lessThan(Math.floor(DateTime.now().toSeconds())));
|
|
392
|
+
if (cursor) {
|
|
393
|
+
entityQueryBuilder = entityQueryBuilder.cursor(cursor);
|
|
394
|
+
}
|
|
395
|
+
const listResult = await entityQueryBuilder.limit(100).getMany();
|
|
396
|
+
if (options.logRawSqlQuery) {
|
|
397
|
+
console.warn(`clear expired Records: ${JSON.stringify(listResult.results.map((r) => r.key))}`);
|
|
398
|
+
}
|
|
399
|
+
await deleteCacheEntriesInBatches(listResult.results, cacheEntityName);
|
|
400
|
+
if (listResult.nextCursor) {
|
|
401
|
+
return listResult.results.length + await clearExpirationCursorCache(listResult.nextCursor, options);
|
|
402
|
+
} else {
|
|
403
|
+
return listResult.results.length;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async function executeWithRetry(operation, operationName) {
|
|
407
|
+
let attempt = 0;
|
|
408
|
+
let delay = CACHE_CONSTANTS.INITIAL_RETRY_DELAY;
|
|
409
|
+
while (attempt < CACHE_CONSTANTS.MAX_RETRY_ATTEMPTS) {
|
|
410
|
+
try {
|
|
411
|
+
return await operation();
|
|
412
|
+
} catch (err) {
|
|
413
|
+
console.warn(`Error during ${operationName}: ${err.message}, retry ${attempt}`, err);
|
|
414
|
+
attempt++;
|
|
415
|
+
if (attempt >= CACHE_CONSTANTS.MAX_RETRY_ATTEMPTS) {
|
|
416
|
+
console.error(`Error during ${operationName}: ${err.message}`, err);
|
|
417
|
+
throw err;
|
|
418
|
+
}
|
|
419
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
420
|
+
delay *= CACHE_CONSTANTS.RETRY_DELAY_MULTIPLIER;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
throw new Error(`Maximum retry attempts exceeded for ${operationName}`);
|
|
424
|
+
}
|
|
425
|
+
async function clearCache(schema, options) {
|
|
426
|
+
const tableName = getTableName(schema);
|
|
427
|
+
if (cacheApplicationContext.getStore()) {
|
|
428
|
+
cacheApplicationContext.getStore()?.tables.add(tableName);
|
|
429
|
+
} else {
|
|
430
|
+
await clearTablesCache([tableName], options);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
async function clearTablesCache(tables, options) {
|
|
434
|
+
if (!options.cacheEntityName) {
|
|
435
|
+
throw new Error("cacheEntityName is not configured");
|
|
436
|
+
}
|
|
437
|
+
const startTime = DateTime.now();
|
|
438
|
+
let totalRecords = 0;
|
|
439
|
+
try {
|
|
440
|
+
totalRecords = await executeWithRetry(
|
|
441
|
+
() => clearCursorCache(tables, "", options),
|
|
442
|
+
"clearing cache"
|
|
443
|
+
);
|
|
444
|
+
} finally {
|
|
445
|
+
if (options.logRawSqlQuery) {
|
|
446
|
+
const duration = DateTime.now().toSeconds() - startTime.toSeconds();
|
|
447
|
+
console.info(`Cleared ${totalRecords} cache records in ${duration} seconds`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async function clearExpiredCache(options) {
|
|
452
|
+
if (!options.cacheEntityName) {
|
|
453
|
+
throw new Error("cacheEntityName is not configured");
|
|
454
|
+
}
|
|
455
|
+
const startTime = DateTime.now();
|
|
456
|
+
let totalRecords = 0;
|
|
457
|
+
try {
|
|
458
|
+
totalRecords = await executeWithRetry(
|
|
459
|
+
() => clearExpirationCursorCache("", options),
|
|
460
|
+
"clearing expired cache"
|
|
461
|
+
);
|
|
462
|
+
} finally {
|
|
463
|
+
const duration = DateTime.now().toSeconds() - startTime.toSeconds();
|
|
464
|
+
console.info(`Cleared ${totalRecords} expired cache records in ${duration} seconds`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
async function getFromCache(query, options) {
|
|
468
|
+
if (!options.cacheEntityName) {
|
|
469
|
+
throw new Error("cacheEntityName is not configured");
|
|
470
|
+
}
|
|
471
|
+
const entityQueryName = options.cacheEntityQueryName ?? CACHE_CONSTANTS.DEFAULT_ENTITY_QUERY_NAME;
|
|
472
|
+
const expirationName = options.cacheEntityExpirationName ?? CACHE_CONSTANTS.DEFAULT_EXPIRATION_NAME;
|
|
473
|
+
const dataName = options.cacheEntityDataName ?? CACHE_CONSTANTS.DEFAULT_DATA_NAME;
|
|
474
|
+
const sqlQuery = query.toSQL();
|
|
475
|
+
const key = hashKey(sqlQuery);
|
|
476
|
+
if (await isTableContainsTableInCacheContext(sqlQuery.sql, options)) {
|
|
477
|
+
if (options.logRawSqlQuery) {
|
|
478
|
+
console.warn(`Context contains value to clear. Skip getting from cache`);
|
|
479
|
+
}
|
|
480
|
+
return void 0;
|
|
481
|
+
}
|
|
482
|
+
try {
|
|
483
|
+
const cacheResult = await kvs.entity(options.cacheEntityName).get(key);
|
|
484
|
+
if (cacheResult && cacheResult[expirationName] >= getCurrentTime() && sqlQuery.sql.toLowerCase() === cacheResult[entityQueryName]) {
|
|
485
|
+
if (options.logRawSqlQuery) {
|
|
486
|
+
console.warn(`Get value from cache, cacheKey: ${key}`);
|
|
487
|
+
}
|
|
488
|
+
const results = cacheResult[dataName];
|
|
489
|
+
return JSON.parse(results);
|
|
490
|
+
}
|
|
491
|
+
} catch (error) {
|
|
492
|
+
console.error(`Error getting from cache: ${error.message}`, error);
|
|
493
|
+
}
|
|
494
|
+
return void 0;
|
|
495
|
+
}
|
|
496
|
+
async function setCacheResult(query, options, results, cacheTtl) {
|
|
497
|
+
if (!options.cacheEntityName) {
|
|
498
|
+
throw new Error("cacheEntityName is not configured");
|
|
499
|
+
}
|
|
500
|
+
try {
|
|
501
|
+
const entityQueryName = options.cacheEntityQueryName ?? CACHE_CONSTANTS.DEFAULT_ENTITY_QUERY_NAME;
|
|
502
|
+
const expirationName = options.cacheEntityExpirationName ?? CACHE_CONSTANTS.DEFAULT_EXPIRATION_NAME;
|
|
503
|
+
const dataName = options.cacheEntityDataName ?? CACHE_CONSTANTS.DEFAULT_DATA_NAME;
|
|
504
|
+
const sqlQuery = query.toSQL();
|
|
505
|
+
if (await isTableContainsTableInCacheContext(sqlQuery.sql, options)) {
|
|
506
|
+
if (options.logRawSqlQuery) {
|
|
507
|
+
console.warn(`Context contains value to clear. Skip setting from cache`);
|
|
508
|
+
}
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const key = hashKey(sqlQuery);
|
|
512
|
+
await kvs.transact().set(
|
|
513
|
+
key,
|
|
514
|
+
{
|
|
515
|
+
[entityQueryName]: sqlQuery.sql.toLowerCase(),
|
|
516
|
+
[expirationName]: nowPlusSeconds(cacheTtl),
|
|
517
|
+
[dataName]: JSON.stringify(results)
|
|
518
|
+
},
|
|
519
|
+
{ entityName: options.cacheEntityName }
|
|
520
|
+
).execute();
|
|
521
|
+
if (options.logRawSqlQuery) {
|
|
522
|
+
console.warn(`Store value to cache, cacheKey: ${key}`);
|
|
523
|
+
}
|
|
524
|
+
} catch (error) {
|
|
525
|
+
console.error(`Error setting cache: ${error.message}`, error);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
const cacheApplicationContext = new AsyncLocalStorage();
|
|
529
|
+
const localCacheApplicationContext = new AsyncLocalStorage();
|
|
530
|
+
async function saveTableIfInsideCacheContext(table) {
|
|
531
|
+
const context = cacheApplicationContext.getStore();
|
|
532
|
+
if (context) {
|
|
533
|
+
const tableName = getTableName(table).toLowerCase();
|
|
534
|
+
context.tables.add(tableName);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
async function saveQueryLocalCacheQuery(query, rows) {
|
|
538
|
+
const context = localCacheApplicationContext.getStore();
|
|
539
|
+
if (context) {
|
|
540
|
+
if (!context.cache) {
|
|
541
|
+
context.cache = {};
|
|
542
|
+
}
|
|
543
|
+
const sql2 = query;
|
|
544
|
+
const key = hashKey(sql2.toSQL());
|
|
545
|
+
context.cache[key] = {
|
|
546
|
+
sql: sql2.toSQL().sql.toLowerCase(),
|
|
547
|
+
data: rows
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
async function getQueryLocalCacheQuery(query) {
|
|
552
|
+
const context = localCacheApplicationContext.getStore();
|
|
553
|
+
if (context) {
|
|
554
|
+
if (!context.cache) {
|
|
555
|
+
context.cache = {};
|
|
556
|
+
}
|
|
557
|
+
const sql2 = query;
|
|
558
|
+
const key = hashKey(sql2.toSQL());
|
|
559
|
+
if (context.cache[key] && context.cache[key].sql === sql2.toSQL().sql.toLowerCase()) {
|
|
560
|
+
return context.cache[key].data;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return void 0;
|
|
564
|
+
}
|
|
565
|
+
async function evictLocalCacheQuery(table, options) {
|
|
566
|
+
const context = localCacheApplicationContext.getStore();
|
|
567
|
+
if (context) {
|
|
568
|
+
if (!context.cache) {
|
|
569
|
+
context.cache = {};
|
|
570
|
+
}
|
|
571
|
+
const tableName = getTableName(table);
|
|
572
|
+
const searchString = options.cacheWrapTable ? `\`${tableName}\`` : tableName;
|
|
573
|
+
const keyToEvicts = [];
|
|
574
|
+
Object.keys(context.cache).forEach((key) => {
|
|
575
|
+
if (context.cache[key].sql.includes(searchString)) {
|
|
576
|
+
keyToEvicts.push(key);
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
keyToEvicts.forEach((key) => delete context.cache[key]);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
async function isTableContainsTableInCacheContext(sql2, options) {
|
|
583
|
+
const context = cacheApplicationContext.getStore();
|
|
584
|
+
if (!context) {
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
const tables = Array.from(context.tables);
|
|
588
|
+
const lowerSql = sql2.toLowerCase();
|
|
589
|
+
return tables.some((table) => {
|
|
590
|
+
const tablePattern = options.cacheWrapTable ? `\`${table}\`` : table;
|
|
591
|
+
return lowerSql.includes(tablePattern);
|
|
592
|
+
});
|
|
593
|
+
}
|
|
292
594
|
class ForgeSQLCrudOperations {
|
|
293
595
|
forgeOperations;
|
|
294
596
|
options;
|
|
@@ -305,12 +607,17 @@ class ForgeSQLCrudOperations {
|
|
|
305
607
|
* Inserts records into the database with optional versioning support.
|
|
306
608
|
* If a version field exists in the schema, versioning is applied.
|
|
307
609
|
*
|
|
610
|
+
* This method automatically handles:
|
|
611
|
+
* - Version field initialization for optimistic locking
|
|
612
|
+
* - Batch insertion for multiple records
|
|
613
|
+
* - Duplicate key handling with optional updates
|
|
614
|
+
*
|
|
308
615
|
* @template T - The type of the table schema
|
|
309
|
-
* @param
|
|
310
|
-
* @param
|
|
311
|
-
* @param
|
|
312
|
-
* @returns
|
|
313
|
-
* @throws
|
|
616
|
+
* @param schema - The entity schema
|
|
617
|
+
* @param models - Array of entities to insert
|
|
618
|
+
* @param updateIfExists - Whether to update existing records (default: false)
|
|
619
|
+
* @returns Promise that resolves to the number of inserted rows
|
|
620
|
+
* @throws Error if the insert operation fails
|
|
314
621
|
*/
|
|
315
622
|
async insert(schema, models, updateIfExists = false) {
|
|
316
623
|
if (!models?.length) return 0;
|
|
@@ -319,25 +626,32 @@ class ForgeSQLCrudOperations {
|
|
|
319
626
|
const preparedModels = models.map(
|
|
320
627
|
(model) => this.prepareModelWithVersion(model, versionMetadata, columns)
|
|
321
628
|
);
|
|
322
|
-
const queryBuilder = this.forgeOperations.
|
|
629
|
+
const queryBuilder = this.forgeOperations.insert(schema).values(preparedModels);
|
|
323
630
|
const finalQuery = updateIfExists ? queryBuilder.onDuplicateKeyUpdate({
|
|
324
631
|
set: Object.fromEntries(
|
|
325
632
|
Object.keys(preparedModels[0]).map((key) => [key, schema[key]])
|
|
326
633
|
)
|
|
327
634
|
}) : queryBuilder;
|
|
328
635
|
const result = await finalQuery;
|
|
636
|
+
await saveTableIfInsideCacheContext(schema);
|
|
329
637
|
return result[0].insertId;
|
|
330
638
|
}
|
|
331
639
|
/**
|
|
332
640
|
* Deletes a record by its primary key with optional version check.
|
|
333
641
|
* If versioning is enabled, ensures the record hasn't been modified since last read.
|
|
334
642
|
*
|
|
643
|
+
* This method automatically handles:
|
|
644
|
+
* - Single primary key validation
|
|
645
|
+
* - Optimistic locking checks if versioning is enabled
|
|
646
|
+
* - Version field validation before deletion
|
|
647
|
+
*
|
|
335
648
|
* @template T - The type of the table schema
|
|
336
|
-
* @param
|
|
337
|
-
* @param
|
|
338
|
-
* @returns
|
|
339
|
-
* @throws
|
|
340
|
-
* @throws
|
|
649
|
+
* @param id - The ID of the record to delete
|
|
650
|
+
* @param schema - The entity schema
|
|
651
|
+
* @returns Promise that resolves to the number of affected rows
|
|
652
|
+
* @throws Error if the delete operation fails
|
|
653
|
+
* @throws Error if multiple primary keys are found
|
|
654
|
+
* @throws Error if optimistic locking check fails
|
|
341
655
|
*/
|
|
342
656
|
async deleteById(id, schema) {
|
|
343
657
|
const { tableName, columns } = getTableMetadata(schema);
|
|
@@ -358,8 +672,12 @@ class ForgeSQLCrudOperations {
|
|
|
358
672
|
conditions.push(eq(versionField, oldModel[versionMetadata.fieldName]));
|
|
359
673
|
}
|
|
360
674
|
}
|
|
361
|
-
const queryBuilder = this.forgeOperations.
|
|
675
|
+
const queryBuilder = this.forgeOperations.delete(schema).where(and(...conditions));
|
|
362
676
|
const result = await queryBuilder;
|
|
677
|
+
if (versionMetadata && result[0].affectedRows === 0) {
|
|
678
|
+
throw new Error(`Optimistic locking failed: record with primary key ${id} has been modified`);
|
|
679
|
+
}
|
|
680
|
+
await saveTableIfInsideCacheContext(schema);
|
|
363
681
|
return result[0].affectedRows;
|
|
364
682
|
}
|
|
365
683
|
/**
|
|
@@ -369,13 +687,19 @@ class ForgeSQLCrudOperations {
|
|
|
369
687
|
* - Checks for concurrent modifications
|
|
370
688
|
* - Increments the version on successful update
|
|
371
689
|
*
|
|
690
|
+
* This method automatically handles:
|
|
691
|
+
* - Primary key validation
|
|
692
|
+
* - Version field retrieval and validation
|
|
693
|
+
* - Optimistic locking conflict detection
|
|
694
|
+
* - Version field incrementation
|
|
695
|
+
*
|
|
372
696
|
* @template T - The type of the table schema
|
|
373
|
-
* @param
|
|
374
|
-
* @param
|
|
375
|
-
* @returns
|
|
376
|
-
* @throws
|
|
377
|
-
* @throws
|
|
378
|
-
* @throws
|
|
697
|
+
* @param entity - The entity with updated values (must include primary key)
|
|
698
|
+
* @param schema - The entity schema
|
|
699
|
+
* @returns Promise that resolves to the number of affected rows
|
|
700
|
+
* @throws Error if the primary key is not provided
|
|
701
|
+
* @throws Error if optimistic locking check fails
|
|
702
|
+
* @throws Error if multiple primary keys are found
|
|
379
703
|
*/
|
|
380
704
|
async updateById(entity, schema) {
|
|
381
705
|
const { tableName, columns } = getTableMetadata(schema);
|
|
@@ -405,13 +729,14 @@ class ForgeSQLCrudOperations {
|
|
|
405
729
|
conditions.push(eq(versionField, currentVersion));
|
|
406
730
|
}
|
|
407
731
|
}
|
|
408
|
-
const queryBuilder = this.forgeOperations.
|
|
732
|
+
const queryBuilder = this.forgeOperations.update(schema).set(updateData).where(and(...conditions));
|
|
409
733
|
const result = await queryBuilder;
|
|
410
734
|
if (versionMetadata && result[0].affectedRows === 0) {
|
|
411
735
|
throw new Error(
|
|
412
736
|
`Optimistic locking failed: record with primary key ${entity[primaryKeyName]} has been modified`
|
|
413
737
|
);
|
|
414
738
|
}
|
|
739
|
+
await saveTableIfInsideCacheContext(schema);
|
|
415
740
|
return result[0].affectedRows;
|
|
416
741
|
}
|
|
417
742
|
/**
|
|
@@ -430,8 +755,9 @@ class ForgeSQLCrudOperations {
|
|
|
430
755
|
if (!where) {
|
|
431
756
|
throw new Error("WHERE conditions must be provided");
|
|
432
757
|
}
|
|
433
|
-
const queryBuilder = this.forgeOperations.
|
|
758
|
+
const queryBuilder = this.forgeOperations.update(schema).set(updateData).where(where);
|
|
434
759
|
const result = await queryBuilder;
|
|
760
|
+
await saveTableIfInsideCacheContext(schema);
|
|
435
761
|
return result[0].affectedRows;
|
|
436
762
|
}
|
|
437
763
|
// Helper methods
|
|
@@ -575,7 +901,7 @@ class ForgeSQLCrudOperations {
|
|
|
575
901
|
const [versionFieldName, versionFieldColumn] = versionField;
|
|
576
902
|
const primaryKeys = this.getPrimaryKeys(schema);
|
|
577
903
|
const [primaryKeyName, primaryKeyColumn] = primaryKeys[0];
|
|
578
|
-
const resultQuery = this.forgeOperations.
|
|
904
|
+
const resultQuery = this.forgeOperations.select({
|
|
579
905
|
[primaryKeyName]: primaryKeyColumn,
|
|
580
906
|
[versionFieldName]: versionFieldColumn
|
|
581
907
|
}).from(schema).where(eq(primaryKeyColumn, primaryKeyValues[primaryKeyName]));
|
|
@@ -720,7 +1046,148 @@ function createForgeDriverProxy(options, logRawSqlQuery) {
|
|
|
720
1046
|
return forgeDriver(modifiedQuery, params, method);
|
|
721
1047
|
};
|
|
722
1048
|
}
|
|
723
|
-
|
|
1049
|
+
const NON_CACHE_CLEARING_ERROR_CODES = [
|
|
1050
|
+
"VALIDATION_ERROR",
|
|
1051
|
+
"CONSTRAINT_ERROR"
|
|
1052
|
+
];
|
|
1053
|
+
const CACHE_CLEARING_ERROR_CODES = [
|
|
1054
|
+
"DEADLOCK",
|
|
1055
|
+
"LOCK_WAIT_TIMEOUT",
|
|
1056
|
+
"CONNECTION_ERROR"
|
|
1057
|
+
];
|
|
1058
|
+
const NON_CACHE_CLEARING_PATTERNS = [
|
|
1059
|
+
/validation/i,
|
|
1060
|
+
/constraint/i
|
|
1061
|
+
];
|
|
1062
|
+
const CACHE_CLEARING_PATTERNS = [
|
|
1063
|
+
/timeout/i,
|
|
1064
|
+
/connection/i
|
|
1065
|
+
];
|
|
1066
|
+
function shouldClearCacheOnError(error) {
|
|
1067
|
+
if (error?.code && NON_CACHE_CLEARING_ERROR_CODES.includes(error.code)) {
|
|
1068
|
+
return false;
|
|
1069
|
+
}
|
|
1070
|
+
if (error?.message && NON_CACHE_CLEARING_PATTERNS.some((pattern) => pattern.test(error.message))) {
|
|
1071
|
+
return false;
|
|
1072
|
+
}
|
|
1073
|
+
if (error?.code && CACHE_CLEARING_ERROR_CODES.includes(error.code)) {
|
|
1074
|
+
return true;
|
|
1075
|
+
}
|
|
1076
|
+
if (error?.message && CACHE_CLEARING_PATTERNS.some((pattern) => pattern.test(error.message))) {
|
|
1077
|
+
return true;
|
|
1078
|
+
}
|
|
1079
|
+
return true;
|
|
1080
|
+
}
|
|
1081
|
+
async function handleSuccessfulExecution(rows, onfulfilled, table, options, isCached) {
|
|
1082
|
+
try {
|
|
1083
|
+
await evictLocalCacheQuery(table, options);
|
|
1084
|
+
await saveTableIfInsideCacheContext(table);
|
|
1085
|
+
if (isCached && !cacheApplicationContext.getStore()) {
|
|
1086
|
+
await clearCache(table, options);
|
|
1087
|
+
}
|
|
1088
|
+
const result = onfulfilled?.(rows);
|
|
1089
|
+
return result;
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
if (shouldClearCacheOnError(error)) {
|
|
1092
|
+
await evictLocalCacheQuery(table, options);
|
|
1093
|
+
if (isCached) {
|
|
1094
|
+
await clearCache(table, options).catch(() => {
|
|
1095
|
+
console.warn("Ignore cache clear errors");
|
|
1096
|
+
});
|
|
1097
|
+
} else {
|
|
1098
|
+
await saveTableIfInsideCacheContext(table);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
throw error;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
function handleFunctionCall(value, target, args, table, options, isCached) {
|
|
1105
|
+
const result = value.apply(target, args);
|
|
1106
|
+
if (typeof result === "object" && result !== null && "execute" in result) {
|
|
1107
|
+
return wrapCacheEvictBuilder(result, table, options, isCached);
|
|
1108
|
+
}
|
|
1109
|
+
return result;
|
|
1110
|
+
}
|
|
1111
|
+
const wrapCacheEvictBuilder = (rawBuilder, table, options, isCached) => {
|
|
1112
|
+
return new Proxy(rawBuilder, {
|
|
1113
|
+
get(target, prop, receiver) {
|
|
1114
|
+
if (prop === "then") {
|
|
1115
|
+
return (onfulfilled, onrejected) => target.execute().then(
|
|
1116
|
+
(rows) => handleSuccessfulExecution(rows, onfulfilled, table, options, isCached),
|
|
1117
|
+
onrejected
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
const value = Reflect.get(target, prop, receiver);
|
|
1121
|
+
if (typeof value === "function") {
|
|
1122
|
+
return (...args) => handleFunctionCall(value, target, args, table, options, isCached);
|
|
1123
|
+
}
|
|
1124
|
+
return value;
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1127
|
+
};
|
|
1128
|
+
function insertAndEvictCacheBuilder(db, table, options, isCached) {
|
|
1129
|
+
const builder = db.insert(table);
|
|
1130
|
+
return wrapCacheEvictBuilder(
|
|
1131
|
+
builder,
|
|
1132
|
+
table,
|
|
1133
|
+
options,
|
|
1134
|
+
isCached
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
function updateAndEvictCacheBuilder(db, table, options, isCached) {
|
|
1138
|
+
const builder = db.update(table);
|
|
1139
|
+
return wrapCacheEvictBuilder(
|
|
1140
|
+
builder,
|
|
1141
|
+
table,
|
|
1142
|
+
options,
|
|
1143
|
+
isCached
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
function deleteAndEvictCacheBuilder(db, table, options, isCached) {
|
|
1147
|
+
const builder = db.delete(table);
|
|
1148
|
+
return wrapCacheEvictBuilder(
|
|
1149
|
+
builder,
|
|
1150
|
+
table,
|
|
1151
|
+
options,
|
|
1152
|
+
isCached
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
async function handleCachedQuery(target, options, cacheTtl, selections, aliasMap, onfulfilled, onrejected) {
|
|
1156
|
+
try {
|
|
1157
|
+
const localCached = await getQueryLocalCacheQuery(target);
|
|
1158
|
+
if (localCached) {
|
|
1159
|
+
return onfulfilled?.(localCached);
|
|
1160
|
+
}
|
|
1161
|
+
const cacheResult = await getFromCache(target, options);
|
|
1162
|
+
if (cacheResult) {
|
|
1163
|
+
return onfulfilled?.(cacheResult);
|
|
1164
|
+
}
|
|
1165
|
+
const rows = await target.execute();
|
|
1166
|
+
const transformed = applyFromDriverTransform(rows, selections, aliasMap);
|
|
1167
|
+
await saveQueryLocalCacheQuery(target, transformed);
|
|
1168
|
+
await setCacheResult(target, options, transformed, cacheTtl).catch((cacheError) => {
|
|
1169
|
+
console.warn("Cache set error:", cacheError);
|
|
1170
|
+
});
|
|
1171
|
+
return onfulfilled?.(transformed);
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
return onrejected?.(error);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
async function handleNonCachedQuery(target, selections, aliasMap, onfulfilled, onrejected) {
|
|
1177
|
+
try {
|
|
1178
|
+
const localCached = await getQueryLocalCacheQuery(target);
|
|
1179
|
+
if (localCached) {
|
|
1180
|
+
return onfulfilled?.(localCached);
|
|
1181
|
+
}
|
|
1182
|
+
const rows = await target.execute();
|
|
1183
|
+
const transformed = applyFromDriverTransform(rows, selections, aliasMap);
|
|
1184
|
+
await saveQueryLocalCacheQuery(target, transformed);
|
|
1185
|
+
return onfulfilled?.(transformed);
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
return onrejected?.(error);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
function createAliasedSelectBuilder(db, fields, selectFn, useCache, options, cacheTtl) {
|
|
724
1191
|
const { selections, aliasMap } = mapSelectFieldsWithAlias(fields);
|
|
725
1192
|
const builder = selectFn(selections);
|
|
726
1193
|
const wrapBuilder = (rawBuilder) => {
|
|
@@ -733,10 +1200,22 @@ function createAliasedSelectBuilder(db, fields, selectFn) {
|
|
|
733
1200
|
};
|
|
734
1201
|
}
|
|
735
1202
|
if (prop === "then") {
|
|
736
|
-
return (onfulfilled, onrejected) =>
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
1203
|
+
return (onfulfilled, onrejected) => {
|
|
1204
|
+
if (useCache) {
|
|
1205
|
+
const ttl = cacheTtl ?? options.cacheTTL ?? 120;
|
|
1206
|
+
return handleCachedQuery(
|
|
1207
|
+
target,
|
|
1208
|
+
options,
|
|
1209
|
+
ttl,
|
|
1210
|
+
selections,
|
|
1211
|
+
aliasMap,
|
|
1212
|
+
onfulfilled,
|
|
1213
|
+
onrejected
|
|
1214
|
+
);
|
|
1215
|
+
} else {
|
|
1216
|
+
return handleNonCachedQuery(target, selections, aliasMap, onfulfilled, onrejected);
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
740
1219
|
}
|
|
741
1220
|
const value = Reflect.get(target, prop, receiver);
|
|
742
1221
|
if (typeof value === "function") {
|
|
@@ -754,13 +1233,122 @@ function createAliasedSelectBuilder(db, fields, selectFn) {
|
|
|
754
1233
|
};
|
|
755
1234
|
return wrapBuilder(builder);
|
|
756
1235
|
}
|
|
757
|
-
|
|
1236
|
+
const DEFAULT_OPTIONS = {
|
|
1237
|
+
logRawSqlQuery: false,
|
|
1238
|
+
disableOptimisticLocking: false,
|
|
1239
|
+
cacheTTL: 120,
|
|
1240
|
+
cacheWrapTable: true,
|
|
1241
|
+
cacheEntityQueryName: "sql",
|
|
1242
|
+
cacheEntityExpirationName: "expiration",
|
|
1243
|
+
cacheEntityDataName: "data"
|
|
1244
|
+
};
|
|
1245
|
+
function createRawQueryExecutor(db, options, useGlobalCache = false) {
|
|
1246
|
+
return async function(query, cacheTtl) {
|
|
1247
|
+
let sql2;
|
|
1248
|
+
if (isSQLWrapper(query)) {
|
|
1249
|
+
const sqlWrapper = query;
|
|
1250
|
+
sql2 = sqlWrapper.getSQL().toQuery(db.dialect);
|
|
1251
|
+
} else {
|
|
1252
|
+
sql2 = {
|
|
1253
|
+
sql: query,
|
|
1254
|
+
params: []
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
const localCacheResult = await getQueryLocalCacheQuery(sql2);
|
|
1258
|
+
if (localCacheResult) {
|
|
1259
|
+
return localCacheResult;
|
|
1260
|
+
}
|
|
1261
|
+
if (useGlobalCache) {
|
|
1262
|
+
const cacheResult = await getFromCache({ toSQL: () => sql2 }, options);
|
|
1263
|
+
if (cacheResult) {
|
|
1264
|
+
return cacheResult;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
const results = await db.execute(query);
|
|
1268
|
+
await saveQueryLocalCacheQuery(sql2, results);
|
|
1269
|
+
if (useGlobalCache) {
|
|
1270
|
+
await setCacheResult(
|
|
1271
|
+
{ toSQL: () => sql2 },
|
|
1272
|
+
options,
|
|
1273
|
+
results,
|
|
1274
|
+
cacheTtl ?? options.cacheTTL ?? 120
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
return results;
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
function patchDbWithSelectAliased(db, options) {
|
|
1281
|
+
const newOptions = { ...DEFAULT_OPTIONS, ...options };
|
|
758
1282
|
db.selectAliased = function(fields) {
|
|
759
|
-
return createAliasedSelectBuilder(
|
|
1283
|
+
return createAliasedSelectBuilder(
|
|
1284
|
+
db,
|
|
1285
|
+
fields,
|
|
1286
|
+
(selections) => db.select(selections),
|
|
1287
|
+
false,
|
|
1288
|
+
newOptions
|
|
1289
|
+
);
|
|
1290
|
+
};
|
|
1291
|
+
db.selectAliasedCacheable = function(fields, cacheTtl) {
|
|
1292
|
+
return createAliasedSelectBuilder(
|
|
1293
|
+
db,
|
|
1294
|
+
fields,
|
|
1295
|
+
(selections) => db.select(selections),
|
|
1296
|
+
true,
|
|
1297
|
+
newOptions,
|
|
1298
|
+
cacheTtl
|
|
1299
|
+
);
|
|
760
1300
|
};
|
|
761
1301
|
db.selectAliasedDistinct = function(fields) {
|
|
762
|
-
return createAliasedSelectBuilder(
|
|
1302
|
+
return createAliasedSelectBuilder(
|
|
1303
|
+
db,
|
|
1304
|
+
fields,
|
|
1305
|
+
(selections) => db.selectDistinct(selections),
|
|
1306
|
+
false,
|
|
1307
|
+
newOptions
|
|
1308
|
+
);
|
|
1309
|
+
};
|
|
1310
|
+
db.selectAliasedDistinctCacheable = function(fields, cacheTtl) {
|
|
1311
|
+
return createAliasedSelectBuilder(
|
|
1312
|
+
db,
|
|
1313
|
+
fields,
|
|
1314
|
+
(selections) => db.selectDistinct(selections),
|
|
1315
|
+
true,
|
|
1316
|
+
newOptions,
|
|
1317
|
+
cacheTtl
|
|
1318
|
+
);
|
|
1319
|
+
};
|
|
1320
|
+
db.selectFrom = function(table) {
|
|
1321
|
+
return db.selectAliased(getTableColumns(table)).from(table);
|
|
1322
|
+
};
|
|
1323
|
+
db.selectFromCacheable = function(table, cacheTtl) {
|
|
1324
|
+
return db.selectAliasedCacheable(getTableColumns(table), cacheTtl).from(table);
|
|
1325
|
+
};
|
|
1326
|
+
db.selectDistinctFrom = function(table) {
|
|
1327
|
+
return db.selectAliasedDistinct(getTableColumns(table)).from(table);
|
|
1328
|
+
};
|
|
1329
|
+
db.selectDistinctFromCacheable = function(table, cacheTtl) {
|
|
1330
|
+
return db.selectAliasedDistinctCacheable(getTableColumns(table), cacheTtl).from(table);
|
|
1331
|
+
};
|
|
1332
|
+
db.insertWithCacheContext = function(table) {
|
|
1333
|
+
return insertAndEvictCacheBuilder(db, table, newOptions, false);
|
|
1334
|
+
};
|
|
1335
|
+
db.insertAndEvictCache = function(table) {
|
|
1336
|
+
return insertAndEvictCacheBuilder(db, table, newOptions, true);
|
|
1337
|
+
};
|
|
1338
|
+
db.updateWithCacheContext = function(table) {
|
|
1339
|
+
return updateAndEvictCacheBuilder(db, table, newOptions, false);
|
|
1340
|
+
};
|
|
1341
|
+
db.updateAndEvictCache = function(table) {
|
|
1342
|
+
return updateAndEvictCacheBuilder(db, table, newOptions, true);
|
|
763
1343
|
};
|
|
1344
|
+
db.deleteWithCacheContext = function(table) {
|
|
1345
|
+
return deleteAndEvictCacheBuilder(db, table, newOptions, false);
|
|
1346
|
+
};
|
|
1347
|
+
db.deleteAndEvictCache = function(table) {
|
|
1348
|
+
return deleteAndEvictCacheBuilder(db, table, newOptions, true);
|
|
1349
|
+
};
|
|
1350
|
+
db.executeQuery = createRawQueryExecutor(db, newOptions, false);
|
|
1351
|
+
db.executeQueryCacheable = createRawQueryExecutor(db, newOptions, true);
|
|
764
1352
|
return db;
|
|
765
1353
|
}
|
|
766
1354
|
class ForgeSQLAnalyseOperation {
|
|
@@ -911,14 +1499,14 @@ class ForgeSQLAnalyseOperation {
|
|
|
911
1499
|
* @returns {string} The SQL query for cluster statement history
|
|
912
1500
|
*/
|
|
913
1501
|
buildClusterStatementQuery(tables, from, to) {
|
|
914
|
-
const
|
|
1502
|
+
const formatDateTime2 = (date) => DateTime.fromJSDate(date).toFormat("yyyy-LL-dd'T'HH:mm:ss.SSS");
|
|
915
1503
|
const tableConditions = tables.map((table) => `TABLE_NAMES LIKE CONCAT(SCHEMA_NAME, '.', '%', '${table}', '%')`).join(" OR ");
|
|
916
1504
|
const timeConditions = [];
|
|
917
1505
|
if (from) {
|
|
918
|
-
timeConditions.push(`SUMMARY_BEGIN_TIME >= '${
|
|
1506
|
+
timeConditions.push(`SUMMARY_BEGIN_TIME >= '${formatDateTime2(from)}'`);
|
|
919
1507
|
}
|
|
920
1508
|
if (to) {
|
|
921
|
-
timeConditions.push(`SUMMARY_END_TIME <= '${
|
|
1509
|
+
timeConditions.push(`SUMMARY_END_TIME <= '${formatDateTime2(to)}'`);
|
|
922
1510
|
}
|
|
923
1511
|
let whereClauses;
|
|
924
1512
|
if (tableConditions?.length) {
|
|
@@ -991,12 +1579,167 @@ class ForgeSQLAnalyseOperation {
|
|
|
991
1579
|
return this.analyzeQueriesHistoryRaw(tableNames, fromDate, toDate);
|
|
992
1580
|
}
|
|
993
1581
|
}
|
|
1582
|
+
class ForgeSQLCacheOperations {
|
|
1583
|
+
options;
|
|
1584
|
+
forgeOperations;
|
|
1585
|
+
/**
|
|
1586
|
+
* Creates a new instance of ForgeSQLCacheOperations.
|
|
1587
|
+
*
|
|
1588
|
+
* @param options - Configuration options for the ORM
|
|
1589
|
+
* @param forgeOperations - The ForgeSQL operations instance
|
|
1590
|
+
*/
|
|
1591
|
+
constructor(options, forgeOperations) {
|
|
1592
|
+
this.options = options;
|
|
1593
|
+
this.forgeOperations = forgeOperations;
|
|
1594
|
+
}
|
|
1595
|
+
/**
|
|
1596
|
+
* Evicts cache for multiple tables using Drizzle table objects.
|
|
1597
|
+
*
|
|
1598
|
+
* @param tables - Array of Drizzle table objects to clear cache for
|
|
1599
|
+
* @returns Promise that resolves when cache eviction is complete
|
|
1600
|
+
* @throws Error if cacheEntityName is not configured
|
|
1601
|
+
*/
|
|
1602
|
+
async evictCacheEntities(tables) {
|
|
1603
|
+
if (!this.options.cacheEntityName) {
|
|
1604
|
+
throw new Error("cacheEntityName is not configured");
|
|
1605
|
+
}
|
|
1606
|
+
await this.evictCache(tables.map((t) => getTableName(t)));
|
|
1607
|
+
}
|
|
1608
|
+
/**
|
|
1609
|
+
* Evicts cache for multiple tables by their names.
|
|
1610
|
+
*
|
|
1611
|
+
* @param tables - Array of table names to clear cache for
|
|
1612
|
+
* @returns Promise that resolves when cache eviction is complete
|
|
1613
|
+
* @throws Error if cacheEntityName is not configured
|
|
1614
|
+
*/
|
|
1615
|
+
async evictCache(tables) {
|
|
1616
|
+
if (!this.options.cacheEntityName) {
|
|
1617
|
+
throw new Error("cacheEntityName is not configured");
|
|
1618
|
+
}
|
|
1619
|
+
await clearTablesCache(tables, this.options);
|
|
1620
|
+
}
|
|
1621
|
+
/**
|
|
1622
|
+
* Inserts records with optimistic locking/versioning and automatically evicts cache.
|
|
1623
|
+
*
|
|
1624
|
+
* This method uses `modifyWithVersioning().insert()` internally, providing:
|
|
1625
|
+
* - Automatic version field initialization
|
|
1626
|
+
* - Optimistic locking support
|
|
1627
|
+
* - Cache eviction after successful operation
|
|
1628
|
+
*
|
|
1629
|
+
* @param schema - The table schema
|
|
1630
|
+
* @param models - Array of entities to insert
|
|
1631
|
+
* @param updateIfExists - Whether to update existing records
|
|
1632
|
+
* @returns Promise that resolves to the number of inserted rows
|
|
1633
|
+
* @throws Error if cacheEntityName is not configured
|
|
1634
|
+
* @throws Error if optimistic locking check fails
|
|
1635
|
+
*/
|
|
1636
|
+
async insert(schema, models, updateIfExists) {
|
|
1637
|
+
this.validateCacheConfiguration();
|
|
1638
|
+
const number = await this.forgeOperations.modifyWithVersioning().insert(schema, models, updateIfExists);
|
|
1639
|
+
await clearCache(schema, this.options);
|
|
1640
|
+
return number;
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Deletes a record by ID with optimistic locking/versioning and automatically evicts cache.
|
|
1644
|
+
*
|
|
1645
|
+
* This method uses `modifyWithVersioning().deleteById()` internally, providing:
|
|
1646
|
+
* - Optimistic locking checks before deletion
|
|
1647
|
+
* - Version field validation
|
|
1648
|
+
* - Cache eviction after successful operation
|
|
1649
|
+
*
|
|
1650
|
+
* @param id - The ID of the record to delete
|
|
1651
|
+
* @param schema - The table schema
|
|
1652
|
+
* @returns Promise that resolves to the number of affected rows
|
|
1653
|
+
* @throws Error if cacheEntityName is not configured
|
|
1654
|
+
* @throws Error if optimistic locking check fails
|
|
1655
|
+
*/
|
|
1656
|
+
async deleteById(id, schema) {
|
|
1657
|
+
this.validateCacheConfiguration();
|
|
1658
|
+
const number = await this.forgeOperations.modifyWithVersioning().deleteById(id, schema);
|
|
1659
|
+
await clearCache(schema, this.options);
|
|
1660
|
+
return number;
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Updates a record by ID with optimistic locking/versioning and automatically evicts cache.
|
|
1664
|
+
*
|
|
1665
|
+
* This method uses `modifyWithVersioning().updateById()` internally, providing:
|
|
1666
|
+
* - Optimistic locking checks before update
|
|
1667
|
+
* - Version field incrementation
|
|
1668
|
+
* - Cache eviction after successful operation
|
|
1669
|
+
*
|
|
1670
|
+
* @param entity - The entity with updated values (must include primary key)
|
|
1671
|
+
* @param schema - The table schema
|
|
1672
|
+
* @returns Promise that resolves to the number of affected rows
|
|
1673
|
+
* @throws Error if cacheEntityName is not configured
|
|
1674
|
+
* @throws Error if optimistic locking check fails
|
|
1675
|
+
*/
|
|
1676
|
+
async updateById(entity, schema) {
|
|
1677
|
+
this.validateCacheConfiguration();
|
|
1678
|
+
const number = await this.forgeOperations.modifyWithVersioning().updateById(entity, schema);
|
|
1679
|
+
await clearCache(schema, this.options);
|
|
1680
|
+
return number;
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Updates fields based on conditions with optimistic locking/versioning and automatically evicts cache.
|
|
1684
|
+
*
|
|
1685
|
+
* This method uses `modifyWithVersioning().updateFields()` internally, providing:
|
|
1686
|
+
* - Optimistic locking support (if version field is configured)
|
|
1687
|
+
* - Version field validation and incrementation
|
|
1688
|
+
* - Cache eviction after successful operation
|
|
1689
|
+
*
|
|
1690
|
+
* @param updateData - The data to update
|
|
1691
|
+
* @param schema - The table schema
|
|
1692
|
+
* @param where - Optional WHERE conditions
|
|
1693
|
+
* @returns Promise that resolves to the number of affected rows
|
|
1694
|
+
* @throws Error if cacheEntityName is not configured
|
|
1695
|
+
* @throws Error if optimistic locking check fails
|
|
1696
|
+
*/
|
|
1697
|
+
async updateFields(updateData, schema, where) {
|
|
1698
|
+
this.validateCacheConfiguration();
|
|
1699
|
+
const number = await this.forgeOperations.modifyWithVersioning().updateFields(updateData, schema, where);
|
|
1700
|
+
await clearCache(schema, this.options);
|
|
1701
|
+
return number;
|
|
1702
|
+
}
|
|
1703
|
+
/**
|
|
1704
|
+
* Executes a query with caching support.
|
|
1705
|
+
* First checks cache, if not found executes query and stores result in cache.
|
|
1706
|
+
*
|
|
1707
|
+
* @param query - The Drizzle query to execute
|
|
1708
|
+
* @param cacheTtl - Optional cache TTL override
|
|
1709
|
+
* @returns Promise that resolves to the query results
|
|
1710
|
+
* @throws Error if cacheEntityName is not configured
|
|
1711
|
+
*/
|
|
1712
|
+
async executeQuery(query, cacheTtl) {
|
|
1713
|
+
this.validateCacheConfiguration();
|
|
1714
|
+
const sqlQuery = query;
|
|
1715
|
+
const cacheResult = await getFromCache(sqlQuery, this.options);
|
|
1716
|
+
if (cacheResult) {
|
|
1717
|
+
return cacheResult;
|
|
1718
|
+
}
|
|
1719
|
+
const results = await query;
|
|
1720
|
+
await setCacheResult(sqlQuery, this.options, results, cacheTtl ?? this.options.cacheTTL ?? 60);
|
|
1721
|
+
return results;
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Validates that cache configuration is properly set up.
|
|
1725
|
+
*
|
|
1726
|
+
* @throws Error if cacheEntityName is not configured
|
|
1727
|
+
* @private
|
|
1728
|
+
*/
|
|
1729
|
+
validateCacheConfiguration() {
|
|
1730
|
+
if (!this.options.cacheEntityName) {
|
|
1731
|
+
throw new Error("cacheEntityName is not configured");
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
994
1735
|
class ForgeSQLORMImpl {
|
|
995
1736
|
static instance = null;
|
|
996
1737
|
drizzle;
|
|
997
1738
|
crudOperations;
|
|
998
1739
|
fetchOperations;
|
|
999
1740
|
analyzeOperations;
|
|
1741
|
+
cacheOperations;
|
|
1742
|
+
options;
|
|
1000
1743
|
/**
|
|
1001
1744
|
* Private constructor to enforce singleton behavior.
|
|
1002
1745
|
* @param options - Options for configuring ForgeSQL ORM behavior.
|
|
@@ -1005,28 +1748,185 @@ class ForgeSQLORMImpl {
|
|
|
1005
1748
|
try {
|
|
1006
1749
|
const newOptions = options ?? {
|
|
1007
1750
|
logRawSqlQuery: false,
|
|
1008
|
-
disableOptimisticLocking: false
|
|
1751
|
+
disableOptimisticLocking: false,
|
|
1752
|
+
cacheWrapTable: true,
|
|
1753
|
+
cacheTTL: 120,
|
|
1754
|
+
cacheEntityQueryName: "sql",
|
|
1755
|
+
cacheEntityExpirationName: "expiration",
|
|
1756
|
+
cacheEntityDataName: "data"
|
|
1009
1757
|
};
|
|
1758
|
+
this.options = newOptions;
|
|
1010
1759
|
if (newOptions.logRawSqlQuery) {
|
|
1011
1760
|
console.debug("Initializing ForgeSQLORM...");
|
|
1012
1761
|
}
|
|
1013
1762
|
const proxiedDriver = createForgeDriverProxy(newOptions.hints, newOptions.logRawSqlQuery);
|
|
1014
1763
|
this.drizzle = patchDbWithSelectAliased(
|
|
1015
|
-
drizzle(proxiedDriver, { logger: newOptions.logRawSqlQuery })
|
|
1764
|
+
drizzle(proxiedDriver, { logger: newOptions.logRawSqlQuery }),
|
|
1765
|
+
newOptions
|
|
1016
1766
|
);
|
|
1017
1767
|
this.crudOperations = new ForgeSQLCrudOperations(this, newOptions);
|
|
1018
1768
|
this.fetchOperations = new ForgeSQLSelectOperations(newOptions);
|
|
1019
1769
|
this.analyzeOperations = new ForgeSQLAnalyseOperation(this);
|
|
1770
|
+
this.cacheOperations = new ForgeSQLCacheOperations(newOptions, this);
|
|
1020
1771
|
} catch (error) {
|
|
1021
1772
|
console.error("ForgeSQLORM initialization failed:", error);
|
|
1022
1773
|
throw error;
|
|
1023
1774
|
}
|
|
1024
1775
|
}
|
|
1776
|
+
/**
|
|
1777
|
+
* Executes operations within a cache context that collects cache eviction events.
|
|
1778
|
+
* All clearCache calls within the context are collected and executed in batch at the end.
|
|
1779
|
+
* Queries executed within this context will bypass cache for tables that were marked for clearing.
|
|
1780
|
+
*
|
|
1781
|
+
* This is useful for:
|
|
1782
|
+
* - Batch operations that affect multiple tables
|
|
1783
|
+
* - Transaction-like operations where you want to clear cache only at the end
|
|
1784
|
+
* - Performance optimization by reducing cache clear operations
|
|
1785
|
+
*
|
|
1786
|
+
* @param cacheContext - Function containing operations that may trigger cache evictions
|
|
1787
|
+
* @returns Promise that resolves when all operations and cache clearing are complete
|
|
1788
|
+
*
|
|
1789
|
+
* @example
|
|
1790
|
+
* ```typescript
|
|
1791
|
+
* await forgeSQL.executeWithCacheContext(async () => {
|
|
1792
|
+
* await forgeSQL.modifyWithVersioning().insert(users, userData);
|
|
1793
|
+
* await forgeSQL.modifyWithVersioning().insert(orders, orderData);
|
|
1794
|
+
* // Cache for both users and orders tables will be cleared at the end
|
|
1795
|
+
* });
|
|
1796
|
+
* ```
|
|
1797
|
+
*/
|
|
1798
|
+
executeWithCacheContext(cacheContext) {
|
|
1799
|
+
return this.executeWithCacheContextAndReturnValue(cacheContext);
|
|
1800
|
+
}
|
|
1801
|
+
/**
|
|
1802
|
+
* Executes operations within a cache context and returns a value.
|
|
1803
|
+
* All clearCache calls within the context are collected and executed in batch at the end.
|
|
1804
|
+
* Queries executed within this context will bypass cache for tables that were marked for clearing.
|
|
1805
|
+
*
|
|
1806
|
+
* @param cacheContext - Function containing operations that may trigger cache evictions
|
|
1807
|
+
* @returns Promise that resolves to the return value of the cacheContext function
|
|
1808
|
+
*
|
|
1809
|
+
* @example
|
|
1810
|
+
* ```typescript
|
|
1811
|
+
* const result = await forgeSQL.executeWithCacheContextAndReturnValue(async () => {
|
|
1812
|
+
* await forgeSQL.modifyWithVersioning().insert(users, userData);
|
|
1813
|
+
* return await forgeSQL.fetch().executeQueryOnlyOne(selectUserQuery);
|
|
1814
|
+
* });
|
|
1815
|
+
* ```
|
|
1816
|
+
*/
|
|
1817
|
+
async executeWithCacheContextAndReturnValue(cacheContext) {
|
|
1818
|
+
return await this.executeWithLocalCacheContextAndReturnValue(
|
|
1819
|
+
async () => await cacheApplicationContext.run(cacheApplicationContext.getStore() ?? { tables: /* @__PURE__ */ new Set() }, async () => {
|
|
1820
|
+
try {
|
|
1821
|
+
return await cacheContext();
|
|
1822
|
+
} finally {
|
|
1823
|
+
await clearTablesCache(
|
|
1824
|
+
Array.from(cacheApplicationContext.getStore()?.tables ?? []),
|
|
1825
|
+
this.options
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1828
|
+
})
|
|
1829
|
+
);
|
|
1830
|
+
}
|
|
1831
|
+
/**
|
|
1832
|
+
* Executes operations within a local cache context and returns a value.
|
|
1833
|
+
* This provides in-memory caching for select queries within a single request scope.
|
|
1834
|
+
*
|
|
1835
|
+
* @param cacheContext - Function containing operations that will benefit from local caching
|
|
1836
|
+
* @returns Promise that resolves to the return value of the cacheContext function
|
|
1837
|
+
*/
|
|
1838
|
+
async executeWithLocalCacheContextAndReturnValue(cacheContext) {
|
|
1839
|
+
return await localCacheApplicationContext.run(localCacheApplicationContext.getStore() ?? { cache: {} }, async () => {
|
|
1840
|
+
return await cacheContext();
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
/**
|
|
1844
|
+
* Executes operations within a local cache context.
|
|
1845
|
+
* This provides in-memory caching for select queries within a single request scope.
|
|
1846
|
+
*
|
|
1847
|
+
* @param cacheContext - Function containing operations that will benefit from local caching
|
|
1848
|
+
* @returns Promise that resolves when all operations are complete
|
|
1849
|
+
*/
|
|
1850
|
+
executeWithLocalContext(cacheContext) {
|
|
1851
|
+
return this.executeWithLocalCacheContextAndReturnValue(cacheContext);
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Creates an insert query builder.
|
|
1855
|
+
*
|
|
1856
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
1857
|
+
* For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
|
|
1858
|
+
*
|
|
1859
|
+
* @param table - The table to insert into
|
|
1860
|
+
* @returns Insert query builder (no versioning, no cache management)
|
|
1861
|
+
*/
|
|
1862
|
+
insert(table) {
|
|
1863
|
+
return this.drizzle.insertWithCacheContext(table);
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* Creates an insert query builder that automatically evicts cache after execution.
|
|
1867
|
+
*
|
|
1868
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
1869
|
+
* For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
|
|
1870
|
+
*
|
|
1871
|
+
* @param table - The table to insert into
|
|
1872
|
+
* @returns Insert query builder with automatic cache eviction (no versioning)
|
|
1873
|
+
*/
|
|
1874
|
+
insertAndEvictCache(table) {
|
|
1875
|
+
return this.drizzle.insertAndEvictCache(table);
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* Creates an update query builder that automatically evicts cache after execution.
|
|
1879
|
+
*
|
|
1880
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
1881
|
+
* For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
|
|
1882
|
+
*
|
|
1883
|
+
* @param table - The table to update
|
|
1884
|
+
* @returns Update query builder with automatic cache eviction (no versioning)
|
|
1885
|
+
*/
|
|
1886
|
+
updateAndEvictCache(table) {
|
|
1887
|
+
return this.drizzle.updateAndEvictCache(table);
|
|
1888
|
+
}
|
|
1889
|
+
/**
|
|
1890
|
+
* Creates an update query builder.
|
|
1891
|
+
*
|
|
1892
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
1893
|
+
* For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
|
|
1894
|
+
*
|
|
1895
|
+
* @param table - The table to update
|
|
1896
|
+
* @returns Update query builder (no versioning, no cache management)
|
|
1897
|
+
*/
|
|
1898
|
+
update(table) {
|
|
1899
|
+
return this.drizzle.updateWithCacheContext(table);
|
|
1900
|
+
}
|
|
1901
|
+
/**
|
|
1902
|
+
* Creates a delete query builder.
|
|
1903
|
+
*
|
|
1904
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
1905
|
+
* For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
|
|
1906
|
+
*
|
|
1907
|
+
* @param table - The table to delete from
|
|
1908
|
+
* @returns Delete query builder (no versioning, no cache management)
|
|
1909
|
+
*/
|
|
1910
|
+
delete(table) {
|
|
1911
|
+
return this.drizzle.deleteWithCacheContext(table);
|
|
1912
|
+
}
|
|
1913
|
+
/**
|
|
1914
|
+
* Creates a delete query builder that automatically evicts cache after execution.
|
|
1915
|
+
*
|
|
1916
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
1917
|
+
* For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
|
|
1918
|
+
*
|
|
1919
|
+
* @param table - The table to delete from
|
|
1920
|
+
* @returns Delete query builder with automatic cache eviction (no versioning)
|
|
1921
|
+
*/
|
|
1922
|
+
deleteAndEvictCache(table) {
|
|
1923
|
+
return this.drizzle.deleteAndEvictCache(table);
|
|
1924
|
+
}
|
|
1025
1925
|
/**
|
|
1026
1926
|
* Create the modify operations instance.
|
|
1027
1927
|
* @returns modify operations.
|
|
1028
1928
|
*/
|
|
1029
|
-
|
|
1929
|
+
modifyWithVersioning() {
|
|
1030
1930
|
return this.crudOperations;
|
|
1031
1931
|
}
|
|
1032
1932
|
/**
|
|
@@ -1038,13 +1938,6 @@ class ForgeSQLORMImpl {
|
|
|
1038
1938
|
ForgeSQLORMImpl.instance ??= new ForgeSQLORMImpl(options);
|
|
1039
1939
|
return ForgeSQLORMImpl.instance;
|
|
1040
1940
|
}
|
|
1041
|
-
/**
|
|
1042
|
-
* Retrieves the CRUD operations instance.
|
|
1043
|
-
* @returns CRUD operations.
|
|
1044
|
-
*/
|
|
1045
|
-
crud() {
|
|
1046
|
-
return this.modify();
|
|
1047
|
-
}
|
|
1048
1941
|
/**
|
|
1049
1942
|
* Retrieves the fetch operations instance.
|
|
1050
1943
|
* @returns Fetch operations.
|
|
@@ -1052,9 +1945,26 @@ class ForgeSQLORMImpl {
|
|
|
1052
1945
|
fetch() {
|
|
1053
1946
|
return this.fetchOperations;
|
|
1054
1947
|
}
|
|
1948
|
+
/**
|
|
1949
|
+
* Provides query analysis capabilities including EXPLAIN ANALYZE and slow query analysis.
|
|
1950
|
+
* @returns {SchemaAnalyzeForgeSql} Interface for analyzing query performance
|
|
1951
|
+
*/
|
|
1055
1952
|
analyze() {
|
|
1056
1953
|
return this.analyzeOperations;
|
|
1057
1954
|
}
|
|
1955
|
+
/**
|
|
1956
|
+
* Provides schema-level SQL operations with optimistic locking/versioning and automatic cache eviction.
|
|
1957
|
+
*
|
|
1958
|
+
* This method returns operations that use `modifyWithVersioning()` internally, providing:
|
|
1959
|
+
* - Optimistic locking support
|
|
1960
|
+
* - Automatic version field management
|
|
1961
|
+
* - Cache eviction after successful operations
|
|
1962
|
+
*
|
|
1963
|
+
* @returns {ForgeSQLCacheOperations} Interface for executing versioned SQL operations with cache management
|
|
1964
|
+
*/
|
|
1965
|
+
modifyWithVersioningAndEvictCache() {
|
|
1966
|
+
return this.cacheOperations;
|
|
1967
|
+
}
|
|
1058
1968
|
/**
|
|
1059
1969
|
* Returns a Drizzle query builder instance.
|
|
1060
1970
|
*
|
|
@@ -1111,12 +2021,365 @@ class ForgeSQLORMImpl {
|
|
|
1111
2021
|
}
|
|
1112
2022
|
return this.drizzle.selectAliasedDistinct(fields);
|
|
1113
2023
|
}
|
|
2024
|
+
/**
|
|
2025
|
+
* Creates a cacheable select query with unique field aliases to prevent field name collisions in joins.
|
|
2026
|
+
* This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
|
|
2027
|
+
*
|
|
2028
|
+
* @template TSelection - The type of the selected fields
|
|
2029
|
+
* @param {TSelection} fields - Object containing the fields to select, with table schemas as values
|
|
2030
|
+
* @param {number} cacheTTL - cache ttl optional default is 60 sec.
|
|
2031
|
+
* @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A select query builder with unique field aliases
|
|
2032
|
+
* @throws {Error} If fields parameter is empty
|
|
2033
|
+
* @example
|
|
2034
|
+
* ```typescript
|
|
2035
|
+
* await forgeSQL
|
|
2036
|
+
* .selectCacheable({user: users, order: orders},60)
|
|
2037
|
+
* .from(orders)
|
|
2038
|
+
* .innerJoin(users, eq(orders.userId, users.id));
|
|
2039
|
+
* ```
|
|
2040
|
+
*/
|
|
2041
|
+
selectCacheable(fields, cacheTTL) {
|
|
2042
|
+
if (!fields) {
|
|
2043
|
+
throw new Error("fields is empty");
|
|
2044
|
+
}
|
|
2045
|
+
return this.drizzle.selectAliasedCacheable(fields, cacheTTL);
|
|
2046
|
+
}
|
|
2047
|
+
/**
|
|
2048
|
+
* Creates a cacheable distinct select query with unique field aliases to prevent field name collisions in joins.
|
|
2049
|
+
* This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
|
|
2050
|
+
*
|
|
2051
|
+
* @template TSelection - The type of the selected fields
|
|
2052
|
+
* @param {TSelection} fields - Object containing the fields to select, with table schemas as values
|
|
2053
|
+
* @param {number} cacheTTL - cache ttl optional default is 60 sec.
|
|
2054
|
+
* @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A distinct select query builder with unique field aliases
|
|
2055
|
+
* @throws {Error} If fields parameter is empty
|
|
2056
|
+
* @example
|
|
2057
|
+
* ```typescript
|
|
2058
|
+
* await forgeSQL
|
|
2059
|
+
* .selectDistinctCacheable({user: users, order: orders}, 60)
|
|
2060
|
+
* .from(orders)
|
|
2061
|
+
* .innerJoin(users, eq(orders.userId, users.id));
|
|
2062
|
+
* ```
|
|
2063
|
+
*/
|
|
2064
|
+
selectDistinctCacheable(fields, cacheTTL) {
|
|
2065
|
+
if (!fields) {
|
|
2066
|
+
throw new Error("fields is empty");
|
|
2067
|
+
}
|
|
2068
|
+
return this.drizzle.selectAliasedDistinctCacheable(fields, cacheTTL);
|
|
2069
|
+
}
|
|
2070
|
+
/**
|
|
2071
|
+
* Creates a select query builder for all columns from a table with field aliasing support.
|
|
2072
|
+
* This is a convenience method that automatically selects all columns from the specified table.
|
|
2073
|
+
*
|
|
2074
|
+
* @template T - The type of the table
|
|
2075
|
+
* @param table - The table to select from
|
|
2076
|
+
* @returns Select query builder with all table columns and field aliasing support
|
|
2077
|
+
* @example
|
|
2078
|
+
* ```typescript
|
|
2079
|
+
* const users = await forgeSQL.selectFrom(userTable).where(eq(userTable.id, 1));
|
|
2080
|
+
* ```
|
|
2081
|
+
*/
|
|
2082
|
+
selectFrom(table) {
|
|
2083
|
+
return this.drizzle.selectFrom(table);
|
|
2084
|
+
}
|
|
2085
|
+
/**
|
|
2086
|
+
* Creates a select distinct query builder for all columns from a table with field aliasing support.
|
|
2087
|
+
* This is a convenience method that automatically selects all distinct columns from the specified table.
|
|
2088
|
+
*
|
|
2089
|
+
* @template T - The type of the table
|
|
2090
|
+
* @param table - The table to select from
|
|
2091
|
+
* @returns Select distinct query builder with all table columns and field aliasing support
|
|
2092
|
+
* @example
|
|
2093
|
+
* ```typescript
|
|
2094
|
+
* const uniqueUsers = await forgeSQL.selectDistinctFrom(userTable).where(eq(userTable.status, 'active'));
|
|
2095
|
+
* ```
|
|
2096
|
+
*/
|
|
2097
|
+
selectDistinctFrom(table) {
|
|
2098
|
+
return this.drizzle.selectDistinctFrom(table);
|
|
2099
|
+
}
|
|
2100
|
+
/**
|
|
2101
|
+
* Creates a cacheable select query builder for all columns from a table with field aliasing and caching support.
|
|
2102
|
+
* This is a convenience method that automatically selects all columns from the specified table with caching enabled.
|
|
2103
|
+
*
|
|
2104
|
+
* @template T - The type of the table
|
|
2105
|
+
* @param table - The table to select from
|
|
2106
|
+
* @param cacheTTL - Optional cache TTL override (defaults to global cache TTL)
|
|
2107
|
+
* @returns Select query builder with all table columns, field aliasing, and caching support
|
|
2108
|
+
* @example
|
|
2109
|
+
* ```typescript
|
|
2110
|
+
* const users = await forgeSQL.selectCacheableFrom(userTable, 300).where(eq(userTable.id, 1));
|
|
2111
|
+
* ```
|
|
2112
|
+
*/
|
|
2113
|
+
selectCacheableFrom(table, cacheTTL) {
|
|
2114
|
+
return this.drizzle.selectFromCacheable(table, cacheTTL);
|
|
2115
|
+
}
|
|
2116
|
+
/**
|
|
2117
|
+
* Creates a cacheable select distinct query builder for all columns from a table with field aliasing and caching support.
|
|
2118
|
+
* This is a convenience method that automatically selects all distinct columns from the specified table with caching enabled.
|
|
2119
|
+
*
|
|
2120
|
+
* @template T - The type of the table
|
|
2121
|
+
* @param table - The table to select from
|
|
2122
|
+
* @param cacheTTL - Optional cache TTL override (defaults to global cache TTL)
|
|
2123
|
+
* @returns Select distinct query builder with all table columns, field aliasing, and caching support
|
|
2124
|
+
* @example
|
|
2125
|
+
* ```typescript
|
|
2126
|
+
* const uniqueUsers = await forgeSQL.selectDistinctCacheableFrom(userTable, 300).where(eq(userTable.status, 'active'));
|
|
2127
|
+
* ```
|
|
2128
|
+
*/
|
|
2129
|
+
selectDistinctCacheableFrom(table, cacheTTL) {
|
|
2130
|
+
return this.drizzle.selectDistinctFromCacheable(table, cacheTTL);
|
|
2131
|
+
}
|
|
2132
|
+
/**
|
|
2133
|
+
* Executes a raw SQL query with local cache support.
|
|
2134
|
+
* This method provides local caching for raw SQL queries within the current invocation context.
|
|
2135
|
+
* Results are cached locally and will be returned from cache on subsequent identical queries.
|
|
2136
|
+
*
|
|
2137
|
+
* @param query - The SQL query to execute (SQLWrapper or string)
|
|
2138
|
+
* @returns Promise with query results
|
|
2139
|
+
* @example
|
|
2140
|
+
* ```typescript
|
|
2141
|
+
* // Using SQLWrapper
|
|
2142
|
+
* const result = await forgeSQL.execute(sql`SELECT * FROM users WHERE id = ${userId}`);
|
|
2143
|
+
*
|
|
2144
|
+
* // Using string
|
|
2145
|
+
* const result = await forgeSQL.execute("SELECT * FROM users WHERE status = 'active'");
|
|
2146
|
+
* ```
|
|
2147
|
+
*/
|
|
2148
|
+
execute(query) {
|
|
2149
|
+
return this.drizzle.executeQuery(query);
|
|
2150
|
+
}
|
|
2151
|
+
/**
|
|
2152
|
+
* Executes a raw SQL query with both local and global cache support.
|
|
2153
|
+
* This method provides comprehensive caching for raw SQL queries:
|
|
2154
|
+
* - Local cache: Within the current invocation context
|
|
2155
|
+
* - Global cache: Cross-invocation caching using @forge/kvs
|
|
2156
|
+
*
|
|
2157
|
+
* @param query - The SQL query to execute (SQLWrapper or string)
|
|
2158
|
+
* @param cacheTtl - Optional cache TTL override (defaults to global cache TTL)
|
|
2159
|
+
* @returns Promise with query results
|
|
2160
|
+
* @example
|
|
2161
|
+
* ```typescript
|
|
2162
|
+
* // Using SQLWrapper with custom TTL
|
|
2163
|
+
* const result = await forgeSQL.executeCacheable(sql`SELECT * FROM users WHERE id = ${userId}`, 300);
|
|
2164
|
+
*
|
|
2165
|
+
* // Using string with default TTL
|
|
2166
|
+
* const result = await forgeSQL.executeCacheable("SELECT * FROM users WHERE status = 'active'");
|
|
2167
|
+
* ```
|
|
2168
|
+
*/
|
|
2169
|
+
executeCacheable(query, cacheTtl) {
|
|
2170
|
+
return this.drizzle.executeQueryCacheable(query, cacheTtl);
|
|
2171
|
+
}
|
|
2172
|
+
/**
|
|
2173
|
+
* Creates a Common Table Expression (CTE) builder for complex queries.
|
|
2174
|
+
* CTEs allow you to define temporary named result sets that exist within the scope of a single query.
|
|
2175
|
+
*
|
|
2176
|
+
* @returns WithBuilder for creating CTEs
|
|
2177
|
+
* @example
|
|
2178
|
+
* ```typescript
|
|
2179
|
+
* const withQuery = forgeSQL.$with('userStats').as(
|
|
2180
|
+
* forgeSQL.select({ userId: users.id, count: sql<number>`count(*)` })
|
|
2181
|
+
* .from(users)
|
|
2182
|
+
* .groupBy(users.id)
|
|
2183
|
+
* );
|
|
2184
|
+
* ```
|
|
2185
|
+
*/
|
|
2186
|
+
get $with() {
|
|
2187
|
+
return this.drizzle.$with;
|
|
2188
|
+
}
|
|
2189
|
+
/**
|
|
2190
|
+
* Creates a query builder that uses Common Table Expressions (CTEs).
|
|
2191
|
+
* CTEs allow you to define temporary named result sets that exist within the scope of a single query.
|
|
2192
|
+
*
|
|
2193
|
+
* @param queries - Array of CTE queries created with $with()
|
|
2194
|
+
* @returns Query builder with CTE support
|
|
2195
|
+
* @example
|
|
2196
|
+
* ```typescript
|
|
2197
|
+
* const withQuery = forgeSQL.$with('userStats').as(
|
|
2198
|
+
* forgeSQL.select({ userId: users.id, count: sql<number>`count(*)` })
|
|
2199
|
+
* .from(users)
|
|
2200
|
+
* .groupBy(users.id)
|
|
2201
|
+
* );
|
|
2202
|
+
*
|
|
2203
|
+
* const result = await forgeSQL.with(withQuery)
|
|
2204
|
+
* .select({ userId: withQuery.userId, count: withQuery.count })
|
|
2205
|
+
* .from(withQuery);
|
|
2206
|
+
* ```
|
|
2207
|
+
*/
|
|
2208
|
+
with(...queries) {
|
|
2209
|
+
return this.drizzle.with(...queries);
|
|
2210
|
+
}
|
|
1114
2211
|
}
|
|
1115
2212
|
class ForgeSQLORM {
|
|
1116
2213
|
ormInstance;
|
|
1117
2214
|
constructor(options) {
|
|
1118
2215
|
this.ormInstance = ForgeSQLORMImpl.getInstance(options);
|
|
1119
2216
|
}
|
|
2217
|
+
selectCacheable(fields, cacheTTL) {
|
|
2218
|
+
return this.ormInstance.selectCacheable(fields, cacheTTL);
|
|
2219
|
+
}
|
|
2220
|
+
selectDistinctCacheable(fields, cacheTTL) {
|
|
2221
|
+
return this.ormInstance.selectDistinctCacheable(fields, cacheTTL);
|
|
2222
|
+
}
|
|
2223
|
+
/**
|
|
2224
|
+
* Creates a select query builder for all columns from a table with field aliasing support.
|
|
2225
|
+
* This is a convenience method that automatically selects all columns from the specified table.
|
|
2226
|
+
*
|
|
2227
|
+
* @template T - The type of the table
|
|
2228
|
+
* @param table - The table to select from
|
|
2229
|
+
* @returns Select query builder with all table columns and field aliasing support
|
|
2230
|
+
* @example
|
|
2231
|
+
* ```typescript
|
|
2232
|
+
* const users = await forgeSQL.selectFrom(userTable).where(eq(userTable.id, 1));
|
|
2233
|
+
* ```
|
|
2234
|
+
*/
|
|
2235
|
+
selectFrom(table) {
|
|
2236
|
+
return this.ormInstance.getDrizzleQueryBuilder().selectFrom(table);
|
|
2237
|
+
}
|
|
2238
|
+
/**
|
|
2239
|
+
* Creates a select distinct query builder for all columns from a table with field aliasing support.
|
|
2240
|
+
* This is a convenience method that automatically selects all distinct columns from the specified table.
|
|
2241
|
+
*
|
|
2242
|
+
* @template T - The type of the table
|
|
2243
|
+
* @param table - The table to select from
|
|
2244
|
+
* @returns Select distinct query builder with all table columns and field aliasing support
|
|
2245
|
+
* @example
|
|
2246
|
+
* ```typescript
|
|
2247
|
+
* const uniqueUsers = await forgeSQL.selectDistinctFrom(userTable).where(eq(userTable.status, 'active'));
|
|
2248
|
+
* ```
|
|
2249
|
+
*/
|
|
2250
|
+
selectDistinctFrom(table) {
|
|
2251
|
+
return this.ormInstance.getDrizzleQueryBuilder().selectDistinctFrom(table);
|
|
2252
|
+
}
|
|
2253
|
+
/**
|
|
2254
|
+
* Creates a cacheable select query builder for all columns from a table with field aliasing and caching support.
|
|
2255
|
+
* This is a convenience method that automatically selects all columns from the specified table with caching enabled.
|
|
2256
|
+
*
|
|
2257
|
+
* @template T - The type of the table
|
|
2258
|
+
* @param table - The table to select from
|
|
2259
|
+
* @param cacheTTL - Optional cache TTL override (defaults to global cache TTL)
|
|
2260
|
+
* @returns Select query builder with all table columns, field aliasing, and caching support
|
|
2261
|
+
* @example
|
|
2262
|
+
* ```typescript
|
|
2263
|
+
* const users = await forgeSQL.selectCacheableFrom(userTable, 300).where(eq(userTable.id, 1));
|
|
2264
|
+
* ```
|
|
2265
|
+
*/
|
|
2266
|
+
selectCacheableFrom(table, cacheTTL) {
|
|
2267
|
+
return this.ormInstance.getDrizzleQueryBuilder().selectFromCacheable(table, cacheTTL);
|
|
2268
|
+
}
|
|
2269
|
+
/**
|
|
2270
|
+
* Creates a cacheable select distinct query builder for all columns from a table with field aliasing and caching support.
|
|
2271
|
+
* This is a convenience method that automatically selects all distinct columns from the specified table with caching enabled.
|
|
2272
|
+
*
|
|
2273
|
+
* @template T - The type of the table
|
|
2274
|
+
* @param table - The table to select from
|
|
2275
|
+
* @param cacheTTL - Optional cache TTL override (defaults to global cache TTL)
|
|
2276
|
+
* @returns Select distinct query builder with all table columns, field aliasing, and caching support
|
|
2277
|
+
* @example
|
|
2278
|
+
* ```typescript
|
|
2279
|
+
* const uniqueUsers = await forgeSQL.selectDistinctCacheableFrom(userTable, 300).where(eq(userTable.status, 'active'));
|
|
2280
|
+
* ```
|
|
2281
|
+
*/
|
|
2282
|
+
selectDistinctCacheableFrom(table, cacheTTL) {
|
|
2283
|
+
return this.ormInstance.getDrizzleQueryBuilder().selectDistinctFromCacheable(table, cacheTTL);
|
|
2284
|
+
}
|
|
2285
|
+
executeWithCacheContext(cacheContext) {
|
|
2286
|
+
return this.ormInstance.executeWithCacheContext(cacheContext);
|
|
2287
|
+
}
|
|
2288
|
+
executeWithCacheContextAndReturnValue(cacheContext) {
|
|
2289
|
+
return this.ormInstance.executeWithCacheContextAndReturnValue(cacheContext);
|
|
2290
|
+
}
|
|
2291
|
+
/**
|
|
2292
|
+
* Executes operations within a local cache context.
|
|
2293
|
+
* This provides in-memory caching for select queries within a single request scope.
|
|
2294
|
+
*
|
|
2295
|
+
* @param cacheContext - Function containing operations that will benefit from local caching
|
|
2296
|
+
* @returns Promise that resolves when all operations are complete
|
|
2297
|
+
*/
|
|
2298
|
+
executeWithLocalContext(cacheContext) {
|
|
2299
|
+
return this.ormInstance.executeWithLocalContext(cacheContext);
|
|
2300
|
+
}
|
|
2301
|
+
/**
|
|
2302
|
+
* Executes operations within a local cache context and returns a value.
|
|
2303
|
+
* This provides in-memory caching for select queries within a single request scope.
|
|
2304
|
+
*
|
|
2305
|
+
* @param cacheContext - Function containing operations that will benefit from local caching
|
|
2306
|
+
* @returns Promise that resolves to the return value of the cacheContext function
|
|
2307
|
+
*/
|
|
2308
|
+
executeWithLocalCacheContextAndReturnValue(cacheContext) {
|
|
2309
|
+
return this.ormInstance.executeWithLocalCacheContextAndReturnValue(cacheContext);
|
|
2310
|
+
}
|
|
2311
|
+
/**
|
|
2312
|
+
* Creates an insert query builder.
|
|
2313
|
+
*
|
|
2314
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
2315
|
+
* For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
|
|
2316
|
+
*
|
|
2317
|
+
* @param table - The table to insert into
|
|
2318
|
+
* @returns Insert query builder (no versioning, no cache management)
|
|
2319
|
+
*/
|
|
2320
|
+
insert(table) {
|
|
2321
|
+
return this.ormInstance.insert(table);
|
|
2322
|
+
}
|
|
2323
|
+
/**
|
|
2324
|
+
* Creates an insert query builder that automatically evicts cache after execution.
|
|
2325
|
+
*
|
|
2326
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
2327
|
+
* For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
|
|
2328
|
+
*
|
|
2329
|
+
* @param table - The table to insert into
|
|
2330
|
+
* @returns Insert query builder with automatic cache eviction (no versioning)
|
|
2331
|
+
*/
|
|
2332
|
+
insertAndEvictCache(table) {
|
|
2333
|
+
return this.ormInstance.insertAndEvictCache(table);
|
|
2334
|
+
}
|
|
2335
|
+
/**
|
|
2336
|
+
* Creates an update query builder.
|
|
2337
|
+
*
|
|
2338
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
2339
|
+
* For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
|
|
2340
|
+
*
|
|
2341
|
+
* @param table - The table to update
|
|
2342
|
+
* @returns Update query builder (no versioning, no cache management)
|
|
2343
|
+
*/
|
|
2344
|
+
update(table) {
|
|
2345
|
+
return this.ormInstance.update(table);
|
|
2346
|
+
}
|
|
2347
|
+
/**
|
|
2348
|
+
* Creates an update query builder that automatically evicts cache after execution.
|
|
2349
|
+
*
|
|
2350
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
2351
|
+
* For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
|
|
2352
|
+
*
|
|
2353
|
+
* @param table - The table to update
|
|
2354
|
+
* @returns Update query builder with automatic cache eviction (no versioning)
|
|
2355
|
+
*/
|
|
2356
|
+
updateAndEvictCache(table) {
|
|
2357
|
+
return this.ormInstance.updateAndEvictCache(table);
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
* Creates a delete query builder.
|
|
2361
|
+
*
|
|
2362
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
2363
|
+
* For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
|
|
2364
|
+
*
|
|
2365
|
+
* @param table - The table to delete from
|
|
2366
|
+
* @returns Delete query builder (no versioning, no cache management)
|
|
2367
|
+
*/
|
|
2368
|
+
delete(table) {
|
|
2369
|
+
return this.ormInstance.delete(table);
|
|
2370
|
+
}
|
|
2371
|
+
/**
|
|
2372
|
+
* Creates a delete query builder that automatically evicts cache after execution.
|
|
2373
|
+
*
|
|
2374
|
+
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
|
|
2375
|
+
* For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
|
|
2376
|
+
*
|
|
2377
|
+
* @param table - The table to delete from
|
|
2378
|
+
* @returns Delete query builder with automatic cache eviction (no versioning)
|
|
2379
|
+
*/
|
|
2380
|
+
deleteAndEvictCache(table) {
|
|
2381
|
+
return this.ormInstance.deleteAndEvictCache(table);
|
|
2382
|
+
}
|
|
1120
2383
|
/**
|
|
1121
2384
|
* Creates a select query with unique field aliases to prevent field name collisions in joins.
|
|
1122
2385
|
* This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
|
|
@@ -1155,19 +2418,12 @@ class ForgeSQLORM {
|
|
|
1155
2418
|
selectDistinct(fields) {
|
|
1156
2419
|
return this.ormInstance.selectDistinct(fields);
|
|
1157
2420
|
}
|
|
1158
|
-
/**
|
|
1159
|
-
* Proxies the `crud` method from `ForgeSQLORMImpl`.
|
|
1160
|
-
* @returns CRUD operations.
|
|
1161
|
-
*/
|
|
1162
|
-
crud() {
|
|
1163
|
-
return this.ormInstance.modify();
|
|
1164
|
-
}
|
|
1165
2421
|
/**
|
|
1166
2422
|
* Proxies the `modify` method from `ForgeSQLORMImpl`.
|
|
1167
2423
|
* @returns Modify operations.
|
|
1168
2424
|
*/
|
|
1169
|
-
|
|
1170
|
-
return this.ormInstance.
|
|
2425
|
+
modifyWithVersioning() {
|
|
2426
|
+
return this.ormInstance.modifyWithVersioning();
|
|
1171
2427
|
}
|
|
1172
2428
|
/**
|
|
1173
2429
|
* Proxies the `fetch` method from `ForgeSQLORMImpl`.
|
|
@@ -1183,25 +2439,107 @@ class ForgeSQLORM {
|
|
|
1183
2439
|
analyze() {
|
|
1184
2440
|
return this.ormInstance.analyze();
|
|
1185
2441
|
}
|
|
2442
|
+
/**
|
|
2443
|
+
* Provides schema-level SQL cacheable operations with type safety.
|
|
2444
|
+
* @returns {ForgeSQLCacheOperations} Interface for executing schema-bound SQL queries
|
|
2445
|
+
*/
|
|
2446
|
+
modifyWithVersioningAndEvictCache() {
|
|
2447
|
+
return this.ormInstance.modifyWithVersioningAndEvictCache();
|
|
2448
|
+
}
|
|
1186
2449
|
/**
|
|
1187
2450
|
* Returns a Drizzle query builder instance.
|
|
1188
2451
|
*
|
|
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
2452
|
* @returns A Drizzle query builder instance for query construction only.
|
|
1194
2453
|
*/
|
|
1195
2454
|
getDrizzleQueryBuilder() {
|
|
1196
2455
|
return this.ormInstance.getDrizzleQueryBuilder();
|
|
1197
2456
|
}
|
|
2457
|
+
/**
|
|
2458
|
+
* Executes a raw SQL query with local cache support.
|
|
2459
|
+
* This method provides local caching for raw SQL queries within the current invocation context.
|
|
2460
|
+
* Results are cached locally and will be returned from cache on subsequent identical queries.
|
|
2461
|
+
*
|
|
2462
|
+
* @param query - The SQL query to execute (SQLWrapper or string)
|
|
2463
|
+
* @returns Promise with query results
|
|
2464
|
+
* @example
|
|
2465
|
+
* ```typescript
|
|
2466
|
+
* // Using SQLWrapper
|
|
2467
|
+
* const result = await forgeSQL.execute(sql`SELECT * FROM users WHERE id = ${userId}`);
|
|
2468
|
+
*
|
|
2469
|
+
* // Using string
|
|
2470
|
+
* const result = await forgeSQL.execute("SELECT * FROM users WHERE status = 'active'");
|
|
2471
|
+
* ```
|
|
2472
|
+
*/
|
|
2473
|
+
execute(query) {
|
|
2474
|
+
return this.ormInstance.getDrizzleQueryBuilder().executeQuery(query);
|
|
2475
|
+
}
|
|
2476
|
+
/**
|
|
2477
|
+
* Executes a raw SQL query with both local and global cache support.
|
|
2478
|
+
* This method provides comprehensive caching for raw SQL queries:
|
|
2479
|
+
* - Local cache: Within the current invocation context
|
|
2480
|
+
* - Global cache: Cross-invocation caching using @forge/kvs
|
|
2481
|
+
*
|
|
2482
|
+
* @param query - The SQL query to execute (SQLWrapper or string)
|
|
2483
|
+
* @param cacheTtl - Optional cache TTL override (defaults to global cache TTL)
|
|
2484
|
+
* @returns Promise with query results
|
|
2485
|
+
* @example
|
|
2486
|
+
* ```typescript
|
|
2487
|
+
* // Using SQLWrapper with custom TTL
|
|
2488
|
+
* const result = await forgeSQL.executeCacheable(sql`SELECT * FROM users WHERE id = ${userId}`, 300);
|
|
2489
|
+
*
|
|
2490
|
+
* // Using string with default TTL
|
|
2491
|
+
* const result = await forgeSQL.executeCacheable("SELECT * FROM users WHERE status = 'active'");
|
|
2492
|
+
* ```
|
|
2493
|
+
*/
|
|
2494
|
+
executeCacheable(query, cacheTtl) {
|
|
2495
|
+
return this.ormInstance.getDrizzleQueryBuilder().executeQueryCacheable(query, cacheTtl);
|
|
2496
|
+
}
|
|
2497
|
+
/**
|
|
2498
|
+
* Creates a Common Table Expression (CTE) builder for complex queries.
|
|
2499
|
+
* CTEs allow you to define temporary named result sets that exist within the scope of a single query.
|
|
2500
|
+
*
|
|
2501
|
+
* @returns WithBuilder for creating CTEs
|
|
2502
|
+
* @example
|
|
2503
|
+
* ```typescript
|
|
2504
|
+
* const withQuery = forgeSQL.$with('userStats').as(
|
|
2505
|
+
* forgeSQL.select({ userId: users.id, count: sql<number>`count(*)` })
|
|
2506
|
+
* .from(users)
|
|
2507
|
+
* .groupBy(users.id)
|
|
2508
|
+
* );
|
|
2509
|
+
* ```
|
|
2510
|
+
*/
|
|
2511
|
+
get $with() {
|
|
2512
|
+
return this.ormInstance.getDrizzleQueryBuilder().$with;
|
|
2513
|
+
}
|
|
2514
|
+
/**
|
|
2515
|
+
* Creates a query builder that uses Common Table Expressions (CTEs).
|
|
2516
|
+
* CTEs allow you to define temporary named result sets that exist within the scope of a single query.
|
|
2517
|
+
*
|
|
2518
|
+
* @param queries - Array of CTE queries created with $with()
|
|
2519
|
+
* @returns Query builder with CTE support
|
|
2520
|
+
* @example
|
|
2521
|
+
* ```typescript
|
|
2522
|
+
* const withQuery = forgeSQL.$with('userStats').as(
|
|
2523
|
+
* forgeSQL.select({ userId: users.id, count: sql<number>`count(*)` })
|
|
2524
|
+
* .from(users)
|
|
2525
|
+
* .groupBy(users.id)
|
|
2526
|
+
* );
|
|
2527
|
+
*
|
|
2528
|
+
* const result = await forgeSQL.with(withQuery)
|
|
2529
|
+
* .select({ userId: withQuery.userId, count: withQuery.count })
|
|
2530
|
+
* .from(withQuery);
|
|
2531
|
+
* ```
|
|
2532
|
+
*/
|
|
2533
|
+
with(...queries) {
|
|
2534
|
+
return this.ormInstance.getDrizzleQueryBuilder().with(...queries);
|
|
2535
|
+
}
|
|
1198
2536
|
}
|
|
1199
2537
|
const forgeDateTimeString = customType({
|
|
1200
2538
|
dataType() {
|
|
1201
2539
|
return "datetime";
|
|
1202
2540
|
},
|
|
1203
2541
|
toDriver(value) {
|
|
1204
|
-
return
|
|
2542
|
+
return formatDateTime(value, "yyyy-LL-dd' 'HH:mm:ss.SSS");
|
|
1205
2543
|
},
|
|
1206
2544
|
fromDriver(value) {
|
|
1207
2545
|
const format = "yyyy-LL-dd'T'HH:mm:ss.SSS";
|
|
@@ -1213,7 +2551,7 @@ const forgeTimestampString = customType({
|
|
|
1213
2551
|
return "timestamp";
|
|
1214
2552
|
},
|
|
1215
2553
|
toDriver(value) {
|
|
1216
|
-
return
|
|
2554
|
+
return formatDateTime(value, "yyyy-LL-dd' 'HH:mm:ss.SSS");
|
|
1217
2555
|
},
|
|
1218
2556
|
fromDriver(value) {
|
|
1219
2557
|
const format = "yyyy-LL-dd'T'HH:mm:ss.SSS";
|
|
@@ -1225,7 +2563,7 @@ const forgeDateString = customType({
|
|
|
1225
2563
|
return "date";
|
|
1226
2564
|
},
|
|
1227
2565
|
toDriver(value) {
|
|
1228
|
-
return
|
|
2566
|
+
return formatDateTime(value, "yyyy-LL-dd");
|
|
1229
2567
|
},
|
|
1230
2568
|
fromDriver(value) {
|
|
1231
2569
|
const format = "yyyy-LL-dd";
|
|
@@ -1237,7 +2575,7 @@ const forgeTimeString = customType({
|
|
|
1237
2575
|
return "time";
|
|
1238
2576
|
},
|
|
1239
2577
|
toDriver(value) {
|
|
1240
|
-
return
|
|
2578
|
+
return formatDateTime(value, "HH:mm:ss.SSS");
|
|
1241
2579
|
},
|
|
1242
2580
|
fromDriver(value) {
|
|
1243
2581
|
return parseDateTime(value, "HH:mm:ss.SSS");
|
|
@@ -1354,6 +2692,45 @@ async function dropTableSchemaMigrations() {
|
|
|
1354
2692
|
return getHttpResponse(500, errorMessage);
|
|
1355
2693
|
}
|
|
1356
2694
|
}
|
|
2695
|
+
const clearCacheSchedulerTrigger = async (options) => {
|
|
2696
|
+
try {
|
|
2697
|
+
const newOptions = options ?? {
|
|
2698
|
+
logRawSqlQuery: false,
|
|
2699
|
+
disableOptimisticLocking: false,
|
|
2700
|
+
cacheTTL: 120,
|
|
2701
|
+
cacheEntityName: "cache",
|
|
2702
|
+
cacheEntityQueryName: "sql",
|
|
2703
|
+
cacheEntityExpirationName: "expiration",
|
|
2704
|
+
cacheEntityDataName: "data"
|
|
2705
|
+
};
|
|
2706
|
+
if (!newOptions.cacheEntityName) {
|
|
2707
|
+
throw new Error("cacheEntityName is not configured");
|
|
2708
|
+
}
|
|
2709
|
+
await clearExpiredCache(newOptions);
|
|
2710
|
+
return {
|
|
2711
|
+
headers: { "Content-Type": ["application/json"] },
|
|
2712
|
+
statusCode: 200,
|
|
2713
|
+
statusText: "OK",
|
|
2714
|
+
body: JSON.stringify({
|
|
2715
|
+
success: true,
|
|
2716
|
+
message: "Cache cleanup completed successfully",
|
|
2717
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2718
|
+
})
|
|
2719
|
+
};
|
|
2720
|
+
} catch (error) {
|
|
2721
|
+
console.error("Error during cache cleanup: ", JSON.stringify(error));
|
|
2722
|
+
return {
|
|
2723
|
+
headers: { "Content-Type": ["application/json"] },
|
|
2724
|
+
statusCode: 500,
|
|
2725
|
+
statusText: "Internal Server Error",
|
|
2726
|
+
body: JSON.stringify({
|
|
2727
|
+
success: false,
|
|
2728
|
+
error: error instanceof Error ? error.message : "Unknown error during cache cleanup",
|
|
2729
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2730
|
+
})
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
};
|
|
1357
2734
|
const getHttpResponse = (statusCode, body) => {
|
|
1358
2735
|
let statusText = "";
|
|
1359
2736
|
if (statusCode === 200) {
|
|
@@ -1373,6 +2750,7 @@ export {
|
|
|
1373
2750
|
ForgeSQLSelectOperations,
|
|
1374
2751
|
applyFromDriverTransform,
|
|
1375
2752
|
applySchemaMigrations,
|
|
2753
|
+
clearCacheSchedulerTrigger,
|
|
1376
2754
|
ForgeSQLORM as default,
|
|
1377
2755
|
dropSchemaMigrations,
|
|
1378
2756
|
dropTableSchemaMigrations,
|
|
@@ -1383,6 +2761,7 @@ export {
|
|
|
1383
2761
|
forgeSystemTables,
|
|
1384
2762
|
forgeTimeString,
|
|
1385
2763
|
forgeTimestampString,
|
|
2764
|
+
formatDateTime,
|
|
1386
2765
|
formatLimitOffset,
|
|
1387
2766
|
generateDropTableStatements,
|
|
1388
2767
|
getHttpResponse,
|