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