forge-sql-orm 2.1.4 → 2.1.5

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 (45) hide show
  1. package/README.md +91 -5
  2. package/dist/ForgeSQLORM.js +122 -17
  3. package/dist/ForgeSQLORM.js.map +1 -1
  4. package/dist/ForgeSQLORM.mjs +122 -17
  5. package/dist/ForgeSQLORM.mjs.map +1 -1
  6. package/dist/core/ForgeSQLCrudOperations.d.ts.map +1 -1
  7. package/dist/core/ForgeSQLORM.d.ts +23 -0
  8. package/dist/core/ForgeSQLORM.d.ts.map +1 -1
  9. package/dist/core/ForgeSQLQueryBuilder.d.ts +36 -5
  10. package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
  11. package/dist/core/ForgeSQLSelectOperations.d.ts.map +1 -1
  12. package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -1
  13. package/dist/utils/cacheContextUtils.d.ts.map +1 -1
  14. package/dist/utils/cacheUtils.d.ts.map +1 -1
  15. package/dist/utils/forgeDriver.d.ts +21 -0
  16. package/dist/utils/forgeDriver.d.ts.map +1 -1
  17. package/dist/utils/forgeDriverProxy.d.ts.map +1 -1
  18. package/dist/utils/metadataContextUtils.d.ts +11 -0
  19. package/dist/utils/metadataContextUtils.d.ts.map +1 -0
  20. package/dist/utils/sqlUtils.d.ts.map +1 -1
  21. package/dist/webtriggers/applyMigrationsWebTrigger.d.ts.map +1 -1
  22. package/dist/webtriggers/clearCacheSchedulerTrigger.d.ts.map +1 -1
  23. package/dist/webtriggers/dropMigrationWebTrigger.d.ts.map +1 -1
  24. package/dist/webtriggers/dropTablesMigrationWebTrigger.d.ts.map +1 -1
  25. package/dist/webtriggers/fetchSchemaWebTrigger.d.ts.map +1 -1
  26. package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts +24 -7
  27. package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts.map +1 -1
  28. package/package.json +1 -1
  29. package/src/core/ForgeSQLCrudOperations.ts +3 -0
  30. package/src/core/ForgeSQLORM.ts +119 -51
  31. package/src/core/ForgeSQLQueryBuilder.ts +51 -17
  32. package/src/core/ForgeSQLSelectOperations.ts +2 -0
  33. package/src/lib/drizzle/extensions/additionalActions.ts +2 -0
  34. package/src/utils/cacheContextUtils.ts +4 -2
  35. package/src/utils/cacheUtils.ts +20 -8
  36. package/src/utils/forgeDriver.ts +22 -1
  37. package/src/utils/forgeDriverProxy.ts +2 -0
  38. package/src/utils/metadataContextUtils.ts +24 -0
  39. package/src/utils/sqlUtils.ts +1 -0
  40. package/src/webtriggers/applyMigrationsWebTrigger.ts +5 -2
  41. package/src/webtriggers/clearCacheSchedulerTrigger.ts +1 -0
  42. package/src/webtriggers/dropMigrationWebTrigger.ts +2 -0
  43. package/src/webtriggers/dropTablesMigrationWebTrigger.ts +2 -0
  44. package/src/webtriggers/fetchSchemaWebTrigger.ts +1 -0
  45. package/src/webtriggers/topSlowestStatementLastHourTrigger.ts +72 -17
@@ -391,7 +391,7 @@ async function clearCursorCache(tables, cursor, options) {
391
391
  entityQueryBuilder = entityQueryBuilder.cursor(cursor);
392
392
  }
393
393
  const listResult = await entityQueryBuilder.limit(100).getMany();
394
- if (options.logRawSqlQuery) {
394
+ if (options.logCache) {
395
395
  console.warn(`clear cache Records: ${JSON.stringify(listResult.results.map((r) => r.key))}`);
396
396
  }
397
397
  await deleteCacheEntriesInBatches(listResult.results, cacheEntityName);
@@ -412,7 +412,7 @@ async function clearExpirationCursorCache(cursor, options) {
412
412
  entityQueryBuilder = entityQueryBuilder.cursor(cursor);
413
413
  }
414
414
  const listResult = await entityQueryBuilder.limit(100).getMany();
415
- if (options.logRawSqlQuery) {
415
+ if (options.logCache) {
416
416
  console.warn(`clear expired Records: ${JSON.stringify(listResult.results.map((r) => r.key))}`);
417
417
  }
418
418
  await deleteCacheEntriesInBatches(listResult.results, cacheEntityName);
@@ -461,7 +461,7 @@ async function clearTablesCache(tables, options) {
461
461
  "clearing cache"
462
462
  );
463
463
  } finally {
464
- if (options.logRawSqlQuery) {
464
+ if (options.logCache) {
465
465
  const duration = DateTime.now().toSeconds() - startTime.toSeconds();
466
466
  console.info(`Cleared ${totalRecords} cache records in ${duration} seconds`);
467
467
  }
@@ -480,7 +480,7 @@ async function clearExpiredCache(options) {
480
480
  );
481
481
  } finally {
482
482
  const duration = DateTime.now().toSeconds() - startTime.toSeconds();
483
- if (options?.logRawSqlQuery) {
483
+ if (options?.logCache) {
484
484
  console.debug(`Cleared ${totalRecords} expired cache records in ${duration} seconds`);
485
485
  }
486
486
  }
@@ -495,7 +495,7 @@ async function getFromCache(query, options) {
495
495
  const sqlQuery = query.toSQL();
496
496
  const key = hashKey(sqlQuery);
497
497
  if (await isTableContainsTableInCacheContext(sqlQuery.sql, options)) {
498
- if (options.logRawSqlQuery) {
498
+ if (options.logCache) {
499
499
  console.warn(`Context contains value to clear. Skip getting from cache`);
500
500
  }
501
501
  return void 0;
@@ -503,7 +503,7 @@ async function getFromCache(query, options) {
503
503
  try {
504
504
  const cacheResult = await kvs.entity(options.cacheEntityName).get(key);
505
505
  if (cacheResult && cacheResult[expirationName] >= getCurrentTime() && sqlQuery.sql.toLowerCase() === cacheResult[entityQueryName]) {
506
- if (options.logRawSqlQuery) {
506
+ if (options.logCache) {
507
507
  console.warn(`Get value from cache, cacheKey: ${key}`);
508
508
  }
509
509
  const results = cacheResult[dataName];
@@ -524,7 +524,7 @@ async function setCacheResult(query, options, results, cacheTtl) {
524
524
  const dataName = options.cacheEntityDataName ?? CACHE_CONSTANTS.DEFAULT_DATA_NAME;
525
525
  const sqlQuery = query.toSQL();
526
526
  if (await isTableContainsTableInCacheContext(sqlQuery.sql, options)) {
527
- if (options.logRawSqlQuery) {
527
+ if (options.logCache) {
528
528
  console.warn(`Context contains value to clear. Skip setting from cache`);
529
529
  }
530
530
  return;
@@ -539,7 +539,7 @@ async function setCacheResult(query, options, results, cacheTtl) {
539
539
  },
540
540
  { entityName: options.cacheEntityName }
541
541
  ).execute();
542
- if (options.logRawSqlQuery) {
542
+ if (options.logCache) {
543
543
  console.warn(`Store value to cache, cacheKey: ${key}`);
544
544
  }
545
545
  } catch (error) {
@@ -567,7 +567,7 @@ async function saveQueryLocalCacheQuery(query, rows, options) {
567
567
  sql: sql2.toSQL().sql.toLowerCase(),
568
568
  data: rows
569
569
  };
570
- if (options.logRawSqlQuery) {
570
+ if (options.logCache) {
571
571
  const q = sql2.toSQL();
572
572
  console.debug(
573
573
  `[forge-sql-orm][local-cache][SAVE] Stored result in cache. sql="${q.sql}", params=${JSON.stringify(q.params)}`
@@ -584,7 +584,7 @@ async function getQueryLocalCacheQuery(query, options) {
584
584
  const sql2 = query;
585
585
  const key = hashKey(sql2.toSQL());
586
586
  if (context.cache[key] && context.cache[key].sql === sql2.toSQL().sql.toLowerCase()) {
587
- if (options.logRawSqlQuery) {
587
+ if (options.logCache) {
588
588
  const q = sql2.toSQL();
589
589
  console.debug(
590
590
  `[forge-sql-orm][local-cache][HIT] Returned cached result. sql="${q.sql}", params=${JSON.stringify(q.params)}`
@@ -1015,6 +1015,18 @@ class ForgeSQLSelectOperations {
1015
1015
  return updateQueryResponseResults.rows;
1016
1016
  }
1017
1017
  }
1018
+ const metadataQueryContext = new AsyncLocalStorage();
1019
+ async function saveMetaDataInContextContext(metadata) {
1020
+ const context = metadataQueryContext.getStore();
1021
+ if (context && metadata) {
1022
+ context.totalResponseSize += metadata.responseSize;
1023
+ context.totalDbExecutionTime += metadata.dbExecutionTime;
1024
+ context.lastMetadata = metadata;
1025
+ }
1026
+ }
1027
+ async function getLastestMetadata() {
1028
+ return metadataQueryContext.getStore();
1029
+ }
1018
1030
  const forgeDriver = async (query, params, method) => {
1019
1031
  if (method == "execute") {
1020
1032
  const sqlStatement = sql$1.prepare(query);
@@ -1030,6 +1042,7 @@ const forgeDriver = async (query, params, method) => {
1030
1042
  await sqlStatement.bindParams(...params);
1031
1043
  }
1032
1044
  const result = await sqlStatement.execute();
1045
+ await saveMetaDataInContextContext(result.metadata);
1033
1046
  let rows;
1034
1047
  rows = result.rows.map((r) => Object.values(r));
1035
1048
  return { rows };
@@ -1779,6 +1792,7 @@ class ForgeSQLORMImpl {
1779
1792
  try {
1780
1793
  const newOptions = options ?? {
1781
1794
  logRawSqlQuery: false,
1795
+ logCache: false,
1782
1796
  disableOptimisticLocking: false,
1783
1797
  cacheWrapTable: true,
1784
1798
  cacheTTL: 120,
@@ -1804,6 +1818,42 @@ class ForgeSQLORMImpl {
1804
1818
  throw error;
1805
1819
  }
1806
1820
  }
1821
+ /**
1822
+ * Executes a query and provides access to execution metadata.
1823
+ * This method allows you to capture detailed information about query execution
1824
+ * including database execution time, response size, and Forge SQL metadata.
1825
+ *
1826
+ * @template T - The return type of the query
1827
+ * @param query - A function that returns a Promise with the query result
1828
+ * @param onMetadata - Callback function that receives execution metadata
1829
+ * @returns Promise with the query result
1830
+ * @example
1831
+ * ```typescript
1832
+ * const result = await forgeSQL.executeWithMetadata(
1833
+ * async () => await forgeSQL.select().from(users).where(eq(users.id, 1)),
1834
+ * (dbTime, responseSize, metadata) => {
1835
+ * console.log(`DB execution time: ${dbTime}ms`);
1836
+ * console.log(`Response size: ${responseSize} bytes`);
1837
+ * console.log('Forge metadata:', metadata);
1838
+ * }
1839
+ * );
1840
+ * ```
1841
+ */
1842
+ async executeWithMetadata(query, onMetadata) {
1843
+ return metadataQueryContext.run({
1844
+ totalDbExecutionTime: 0,
1845
+ totalResponseSize: 0
1846
+ }, async () => {
1847
+ try {
1848
+ return await query();
1849
+ } finally {
1850
+ const metadata = await getLastestMetadata();
1851
+ if (metadata && metadata.lastMetadata) {
1852
+ await onMetadata(metadata.totalDbExecutionTime, metadata.totalResponseSize, metadata.lastMetadata);
1853
+ }
1854
+ }
1855
+ });
1856
+ }
1807
1857
  /**
1808
1858
  * Executes operations within a cache context that collects cache eviction events.
1809
1859
  * All clearCache calls within the context are collected and executed in batch at the end.
@@ -2251,6 +2301,30 @@ class ForgeSQLORM {
2251
2301
  constructor(options) {
2252
2302
  this.ormInstance = ForgeSQLORMImpl.getInstance(options);
2253
2303
  }
2304
+ /**
2305
+ * Executes a query and provides access to execution metadata.
2306
+ * This method allows you to capture detailed information about query execution
2307
+ * including database execution time, response size, and Forge SQL metadata.
2308
+ *
2309
+ * @template T - The return type of the query
2310
+ * @param query - A function that returns a Promise with the query result
2311
+ * @param onMetadata - Callback function that receives execution metadata
2312
+ * @returns Promise with the query result
2313
+ * @example
2314
+ * ```typescript
2315
+ * const result = await forgeSQL.executeWithMetadata(
2316
+ * async () => await forgeSQL.select().from(users).where(eq(users.id, 1)),
2317
+ * (dbTime, responseSize, metadata) => {
2318
+ * console.log(`DB execution time: ${dbTime}ms`);
2319
+ * console.log(`Response size: ${responseSize} bytes`);
2320
+ * console.log('Forge metadata:', metadata);
2321
+ * }
2322
+ * );
2323
+ * ```
2324
+ */
2325
+ async executeWithMetadata(query, onMetadata) {
2326
+ return this.ormInstance.executeWithMetadata(query, onMetadata);
2327
+ }
2254
2328
  selectCacheable(fields, cacheTTL) {
2255
2329
  return this.ormInstance.selectCacheable(fields, cacheTTL);
2256
2330
  }
@@ -3199,7 +3273,25 @@ const clearCacheSchedulerTrigger = async (options) => {
3199
3273
  };
3200
3274
  }
3201
3275
  };
3202
- const topSlowestStatementLastHourTrigger = async (orm, warnThresholdMs = 300, memoryThresholdBytes = 8 * 1024 * 1024) => {
3276
+ const DEFAULT_MEMORY_THRESHOLD = 8 * 1024 * 1024;
3277
+ const DEFAULT_TIMEOUT = 300;
3278
+ const topSlowestStatementLastHourTrigger = async (orm, options) => {
3279
+ if (!orm) {
3280
+ return {
3281
+ statusCode: 500,
3282
+ headers: { "Content-Type": ["application/json"] },
3283
+ body: JSON.stringify({
3284
+ success: false,
3285
+ message: "ORM instance is required",
3286
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3287
+ })
3288
+ };
3289
+ }
3290
+ let newOptions = options ?? {
3291
+ warnThresholdMs: DEFAULT_TIMEOUT,
3292
+ memoryThresholdBytes: DEFAULT_MEMORY_THRESHOLD,
3293
+ showPlan: false
3294
+ };
3203
3295
  const nsToMs = (v) => {
3204
3296
  const n = Number(v);
3205
3297
  return Number.isFinite(n) ? n / 1e6 : NaN;
@@ -3209,6 +3301,17 @@ const topSlowestStatementLastHourTrigger = async (orm, warnThresholdMs = 300, me
3209
3301
  return Number.isFinite(n) ? n / (1024 * 1024) : NaN;
3210
3302
  };
3211
3303
  const jsonSafeStringify = (value) => JSON.stringify(value, (_k, v) => typeof v === "bigint" ? v.toString() : v);
3304
+ function sanitizeSQL(sql2, maxLen = 1e3) {
3305
+ let s = sql2;
3306
+ s = s.replace(/--[^\n\r]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
3307
+ s = s.replace(/'(?:\\'|[^'])*'/g, "?");
3308
+ s = s.replace(/\b-?\d+(?:\.\d+)?\b/g, "?");
3309
+ s = s.replace(/\s+/g, " ").trim();
3310
+ if (s.length > maxLen) {
3311
+ s = s.slice(0, maxLen) + " …[truncated]";
3312
+ }
3313
+ return s;
3314
+ }
3212
3315
  const TOP_N = 1;
3213
3316
  try {
3214
3317
  const summaryHistory = clusterStatementsSummaryHistory;
@@ -3245,7 +3348,8 @@ const topSlowestStatementLastHourTrigger = async (orm, warnThresholdMs = 300, me
3245
3348
  const qHistory = orm.getDrizzleQueryBuilder().select(selectShape(summaryHistory)).from(summaryHistory).where(lastHourFilterHistory);
3246
3349
  const qSummary = orm.getDrizzleQueryBuilder().select(selectShape(summary)).from(summary).where(lastHourFilterSummary);
3247
3350
  const combined = unionAll(qHistory, qSummary).as("combined");
3248
- const thresholdNs = Math.floor(warnThresholdMs * 1e6);
3351
+ const thresholdNs = Math.floor((newOptions.warnThresholdMs ?? DEFAULT_TIMEOUT) * 1e6);
3352
+ const memoryThresholdBytes = newOptions.memoryThresholdBytes ?? DEFAULT_MEMORY_THRESHOLD;
3249
3353
  const grouped = orm.getDrizzleQueryBuilder().select({
3250
3354
  digest: combined.digest,
3251
3355
  stmtType: combined.stmtType,
@@ -3316,8 +3420,8 @@ const topSlowestStatementLastHourTrigger = async (orm, warnThresholdMs = 300, me
3316
3420
  lastSeen: r.lastSeen,
3317
3421
  planInCache: r.planInCache,
3318
3422
  planCacheHits: r.planCacheHits,
3319
- digestText: r.digestText,
3320
- plan: r.plan
3423
+ digestText: sanitizeSQL(r.digestText),
3424
+ plan: newOptions.showPlan ? r.plan : void 0
3321
3425
  }));
3322
3426
  for (const f of formatted) {
3323
3427
  console.warn(
@@ -3325,7 +3429,7 @@ const topSlowestStatementLastHourTrigger = async (orm, warnThresholdMs = 300, me
3325
3429
  digest=${f.digest}
3326
3430
  sql=${(f.digestText || "").slice(0, 300)}${f.digestText && f.digestText.length > 300 ? "…" : ""}`
3327
3431
  );
3328
- if (f.plan) {
3432
+ if (newOptions.showPlan && f.plan) {
3329
3433
  console.warn(` full plan:
3330
3434
  ${f.plan}`);
3331
3435
  }
@@ -3338,8 +3442,9 @@ ${f.plan}`);
3338
3442
  success: true,
3339
3443
  window: "last_1h",
3340
3444
  top: TOP_N,
3341
- warnThresholdMs,
3342
- memoryThresholdBytes,
3445
+ warnThresholdMs: newOptions.warnThresholdMs,
3446
+ memoryThresholdBytes: newOptions.memoryThresholdBytes,
3447
+ showPlan: newOptions.showPlan,
3343
3448
  rows: formatted,
3344
3449
  generatedAt: (/* @__PURE__ */ new Date()).toISOString()
3345
3450
  })