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