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.
- package/README.md +91 -5
- package/dist/ForgeSQLORM.js +122 -17
- package/dist/ForgeSQLORM.js.map +1 -1
- package/dist/ForgeSQLORM.mjs +122 -17
- package/dist/ForgeSQLORM.mjs.map +1 -1
- package/dist/core/ForgeSQLCrudOperations.d.ts.map +1 -1
- package/dist/core/ForgeSQLORM.d.ts +23 -0
- package/dist/core/ForgeSQLORM.d.ts.map +1 -1
- package/dist/core/ForgeSQLQueryBuilder.d.ts +36 -5
- package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
- package/dist/core/ForgeSQLSelectOperations.d.ts.map +1 -1
- package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -1
- package/dist/utils/cacheContextUtils.d.ts.map +1 -1
- package/dist/utils/cacheUtils.d.ts.map +1 -1
- package/dist/utils/forgeDriver.d.ts +21 -0
- package/dist/utils/forgeDriver.d.ts.map +1 -1
- package/dist/utils/forgeDriverProxy.d.ts.map +1 -1
- package/dist/utils/metadataContextUtils.d.ts +11 -0
- package/dist/utils/metadataContextUtils.d.ts.map +1 -0
- package/dist/utils/sqlUtils.d.ts.map +1 -1
- package/dist/webtriggers/applyMigrationsWebTrigger.d.ts.map +1 -1
- package/dist/webtriggers/clearCacheSchedulerTrigger.d.ts.map +1 -1
- package/dist/webtriggers/dropMigrationWebTrigger.d.ts.map +1 -1
- package/dist/webtriggers/dropTablesMigrationWebTrigger.d.ts.map +1 -1
- package/dist/webtriggers/fetchSchemaWebTrigger.d.ts.map +1 -1
- package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts +24 -7
- package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/ForgeSQLCrudOperations.ts +3 -0
- package/src/core/ForgeSQLORM.ts +119 -51
- package/src/core/ForgeSQLQueryBuilder.ts +51 -17
- package/src/core/ForgeSQLSelectOperations.ts +2 -0
- package/src/lib/drizzle/extensions/additionalActions.ts +2 -0
- package/src/utils/cacheContextUtils.ts +4 -2
- package/src/utils/cacheUtils.ts +20 -8
- package/src/utils/forgeDriver.ts +22 -1
- package/src/utils/forgeDriverProxy.ts +2 -0
- package/src/utils/metadataContextUtils.ts +24 -0
- package/src/utils/sqlUtils.ts +1 -0
- package/src/webtriggers/applyMigrationsWebTrigger.ts +5 -2
- package/src/webtriggers/clearCacheSchedulerTrigger.ts +1 -0
- package/src/webtriggers/dropMigrationWebTrigger.ts +2 -0
- package/src/webtriggers/dropTablesMigrationWebTrigger.ts +2 -0
- package/src/webtriggers/fetchSchemaWebTrigger.ts +1 -0
- package/src/webtriggers/topSlowestStatementLastHourTrigger.ts +72 -17
package/dist/ForgeSQLORM.mjs
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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?.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
})
|