forge-sql-orm 2.0.30 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +1410 -81
  2. package/dist/ForgeSQLORM.js +1456 -60
  3. package/dist/ForgeSQLORM.js.map +1 -1
  4. package/dist/ForgeSQLORM.mjs +1440 -61
  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 +248 -13
  13. package/dist/core/ForgeSQLORM.d.ts.map +1 -1
  14. package/dist/core/ForgeSQLQueryBuilder.d.ts +394 -19
  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 +90 -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 +743 -34
  35. package/src/core/ForgeSQLQueryBuilder.ts +456 -20
  36. package/src/index.ts +1 -1
  37. package/src/lib/drizzle/extensions/additionalActions.ts +852 -0
  38. package/src/lib/drizzle/extensions/types.d.ts +99 -10
  39. package/src/utils/cacheContextUtils.ts +212 -0
  40. package/src/utils/cacheUtils.ts +403 -0
  41. package/src/utils/sqlUtils.ts +42 -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
@@ -0,0 +1,852 @@
1
+ import {
2
+ MySqlRawQueryResult,
3
+ MySqlRemoteDatabase,
4
+ MySqlRemotePreparedQueryHKT,
5
+ MySqlRemoteQueryResultHKT,
6
+ } from "drizzle-orm/mysql-proxy";
7
+
8
+ import {SelectedFields} from "drizzle-orm/mysql-core/query-builders/select.types";
9
+ import {applyFromDriverTransform, ForgeSqlOrmOptions, mapSelectFieldsWithAlias} from "../../..";
10
+ import {MySqlSelectBase, MySqlSelectBuilder} from "drizzle-orm/mysql-core";
11
+ import type {MySqlTable} from "drizzle-orm/mysql-core/table";
12
+ import {MySqlDeleteBase, MySqlInsertBuilder, MySqlUpdateBuilder,} from "drizzle-orm/mysql-core/query-builders";
13
+ import {clearCache, getFromCache, setCacheResult} from "../../../utils/cacheUtils";
14
+ import {
15
+ cacheApplicationContext,
16
+ evictLocalCacheQuery,
17
+ getQueryLocalCacheQuery,
18
+ saveQueryLocalCacheQuery,
19
+ saveTableIfInsideCacheContext,
20
+ } from "../../../utils/cacheContextUtils";
21
+ import {BuildQueryConfig, isSQLWrapper, SQLWrapper} from "drizzle-orm/sql/sql";
22
+ import type {MySqlQueryResultKind} from "drizzle-orm/mysql-core/session";
23
+ import {getTableColumns, Query} from "drizzle-orm";
24
+ import {MySqlDialect} from "drizzle-orm/mysql-core/dialect";
25
+ import type {GetSelectTableName, GetSelectTableSelection} from "drizzle-orm/query-builders/select.types";
26
+
27
+ // ============================================================================
28
+ // TYPES AND INTERFACES
29
+ // ============================================================================
30
+
31
+ /**
32
+ * Base interface for query builders that can be executed
33
+ */
34
+ interface QueryBuilder {
35
+ execute: (...args: any[]) => Promise<any>;
36
+ then?: (onfulfilled?: any, onrejected?: any) => Promise<any>;
37
+ toSQL?: () => any;
38
+ }
39
+
40
+ /**
41
+ * Error codes that should not trigger cache clearing
42
+ */
43
+ const NON_CACHE_CLEARING_ERROR_CODES = [
44
+ "VALIDATION_ERROR",
45
+ "CONSTRAINT_ERROR"
46
+ ] as const;
47
+
48
+ /**
49
+ * Error codes that should trigger cache clearing
50
+ */
51
+ const CACHE_CLEARING_ERROR_CODES = [
52
+ "DEADLOCK",
53
+ "LOCK_WAIT_TIMEOUT",
54
+ "CONNECTION_ERROR"
55
+ ] as const;
56
+
57
+ /**
58
+ * Error message patterns that should not trigger cache clearing
59
+ */
60
+ const NON_CACHE_CLEARING_PATTERNS = [
61
+ /validation/i,
62
+ /constraint/i
63
+ ] as const;
64
+
65
+ /**
66
+ * Error message patterns that should trigger cache clearing
67
+ */
68
+ const CACHE_CLEARING_PATTERNS = [
69
+ /timeout/i,
70
+ /connection/i
71
+ ] as const;
72
+
73
+ // ============================================================================
74
+ // CACHE MANAGEMENT UTILITIES
75
+ // ============================================================================
76
+
77
+ /**
78
+ * Determines whether cache should be cleared based on the error type.
79
+ * Only clears cache for errors that might indicate data consistency issues.
80
+ *
81
+ * @param error - The error that occurred during query execution
82
+ * @returns true if cache should be cleared, false otherwise
83
+ */
84
+ function shouldClearCacheOnError(error: any): boolean {
85
+ // Don't clear cache for client-side errors (validation, etc.)
86
+ if (error?.code && NON_CACHE_CLEARING_ERROR_CODES.includes(error.code)) {
87
+ return false;
88
+ }
89
+
90
+ if (error?.message && NON_CACHE_CLEARING_PATTERNS.some(pattern => pattern.test(error.message))) {
91
+ return false;
92
+ }
93
+
94
+ // Clear cache for database-level errors that might affect data consistency
95
+ if (error?.code && CACHE_CLEARING_ERROR_CODES.includes(error.code)) {
96
+ return true;
97
+ }
98
+
99
+ if (error?.message && CACHE_CLEARING_PATTERNS.some(pattern => pattern.test(error.message))) {
100
+ return true;
101
+ }
102
+
103
+ // For unknown errors, be conservative and clear cache
104
+ return true;
105
+ }
106
+
107
+ // ============================================================================
108
+ // EXPORTED TYPES
109
+ // ============================================================================
110
+
111
+ /**
112
+ * Type for select queries with field aliasing
113
+ */
114
+ export type SelectAliasedType = <TSelection extends SelectedFields>(
115
+ fields: TSelection,
116
+ ) => MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>;
117
+
118
+ /**
119
+ * Type for select distinct queries with field aliasing
120
+ */
121
+ export type SelectAliasedDistinctType = <TSelection extends SelectedFields>(
122
+ fields: TSelection,
123
+ ) => MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>;
124
+
125
+ /**
126
+ * Type for select queries with field aliasing and caching
127
+ */
128
+ export type SelectAliasedCacheableType = <TSelection extends SelectedFields>(
129
+ fields: TSelection,
130
+ cacheTtl?: number,
131
+ ) => MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>;
132
+
133
+ /**
134
+ * Type for select distinct queries with field aliasing and caching
135
+ */
136
+ export type SelectAliasedDistinctCacheableType = <TSelection extends SelectedFields>(
137
+ fields: TSelection,
138
+ cacheTtl?: number,
139
+ ) => MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>;
140
+
141
+ /**
142
+ * Type for select queries from table with field aliasing
143
+ */
144
+ export type SelectAllFromAliasedType = <T extends MySqlTable>(
145
+ table: T,
146
+ ) => MySqlSelectBase<GetSelectTableName<T>, T["_"]["columns"] extends undefined ? GetSelectTableSelection<T> : T["_"]["columns"], T["_"]["columns"] extends undefined ? "single" : "partial", MySqlRemotePreparedQueryHKT, GetSelectTableName<T> extends string ? Record<string & GetSelectTableName<T>, "not-null"> : {}, false, never, any>;
147
+
148
+ /**
149
+ * Type for select distinct queries from table with field aliasing
150
+ */
151
+ export type SelectAllDistinctFromAliasedType = <T extends MySqlTable>(
152
+ table: T,
153
+ ) => MySqlSelectBase<GetSelectTableName<T>, T["_"]["columns"] extends undefined ? GetSelectTableSelection<T> : T["_"]["columns"], T["_"]["columns"] extends undefined ? "single" : "partial", MySqlRemotePreparedQueryHKT, GetSelectTableName<T> extends string ? Record<string & GetSelectTableName<T>, "not-null"> : {}, false, never, any>;
154
+
155
+ /**
156
+ * Type for select queries from table with field aliasing and caching
157
+ */
158
+ export type SelectAllFromCacheableAliasedType = <T extends MySqlTable>(
159
+ table: T,
160
+ cacheTtl?: number,
161
+ ) => MySqlSelectBase<GetSelectTableName<T>, T["_"]["columns"] extends undefined ? GetSelectTableSelection<T> : T["_"]["columns"], T["_"]["columns"] extends undefined ? "single" : "partial", MySqlRemotePreparedQueryHKT, GetSelectTableName<T> extends string ? Record<string & GetSelectTableName<T>, "not-null"> : {}, false, never, any>;
162
+
163
+ /**
164
+ * Type for select distinct queries from table with field aliasing and caching
165
+ */
166
+ export type SelectAllDistinctFromCacheableAliasedType = <T extends MySqlTable>(
167
+ table: T,
168
+ cacheTtl?: number,
169
+ ) => MySqlSelectBase<GetSelectTableName<T>, T["_"]["columns"] extends undefined ? GetSelectTableSelection<T> : T["_"]["columns"], T["_"]["columns"] extends undefined ? "single" : "partial", MySqlRemotePreparedQueryHKT, GetSelectTableName<T> extends string ? Record<string & GetSelectTableName<T>, "not-null"> : {}, false, never, any>;
170
+
171
+ /**
172
+ * Type for executing raw SQL queries with local cache
173
+ */
174
+ export type ExecuteQuery = (query: SQLWrapper | string) => Promise<MySqlQueryResultKind<MySqlRemoteQueryResultHKT, unknown>>;
175
+
176
+ /**
177
+ * Type for executing raw SQL queries with local and global cache
178
+ */
179
+ export type ExecuteQueryCacheable = (query: SQLWrapper | string, cacheTtl?: number) => Promise<MySqlQueryResultKind<MySqlRemoteQueryResultHKT, unknown>>;
180
+
181
+ /**
182
+ * Type for insert operations with cache eviction
183
+ */
184
+ export type InsertAndEvictCacheType = <TTable extends MySqlTable>(
185
+ table: TTable,
186
+ ) => MySqlInsertBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT>;
187
+
188
+ /**
189
+ * Type for update operations with cache eviction
190
+ */
191
+ export type UpdateAndEvictCacheType = <TTable extends MySqlTable>(
192
+ table: TTable,
193
+ ) => MySqlUpdateBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT>;
194
+
195
+ /**
196
+ * Type for delete operations with cache eviction
197
+ */
198
+ export type DeleteAndEvictCacheType = <TTable extends MySqlTable>(
199
+ table: TTable,
200
+ ) => MySqlDeleteBase<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT>;
201
+
202
+ /**
203
+ * Handles successful query execution with cache management.
204
+ *
205
+ * @param rows - Query result rows
206
+ * @param onfulfilled - Success callback
207
+ * @param table - The table being modified
208
+ * @param options - ForgeSQL ORM options
209
+ * @param isCached - Whether to clear cache immediately
210
+ * @returns Promise with result
211
+ */
212
+ async function handleSuccessfulExecution(
213
+ rows: unknown[],
214
+ onfulfilled: any,
215
+ table: MySqlTable,
216
+ options: ForgeSqlOrmOptions,
217
+ isCached: boolean,
218
+ ): Promise<any> {
219
+ try {
220
+ await evictLocalCacheQuery(table, options);
221
+ await saveTableIfInsideCacheContext(table);
222
+ if (isCached && !cacheApplicationContext.getStore()) {
223
+ await clearCache(table, options);
224
+ }
225
+ const result = onfulfilled?.(rows);
226
+ return result;
227
+ } catch (error) {
228
+ // Only clear cache for certain types of errors
229
+ if (shouldClearCacheOnError(error)) {
230
+ await evictLocalCacheQuery(table, options);
231
+ if (isCached) {
232
+ await clearCache(table, options).catch(() => {
233
+ console.warn("Ignore cache clear errors");
234
+ });
235
+ } else {
236
+ await saveTableIfInsideCacheContext(table);
237
+ }
238
+ }
239
+ throw error;
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Handles function calls on the wrapped builder.
245
+ *
246
+ * @param value - The function to call
247
+ * @param target - The target object
248
+ * @param args - Function arguments
249
+ * @param table - The table being modified
250
+ * @param options - ForgeSQL ORM options
251
+ * @param isCached - Whether to clear cache immediately
252
+ * @returns Function result or wrapped builder
253
+ */
254
+ function handleFunctionCall(
255
+ value: Function,
256
+ target: any,
257
+ args: any[],
258
+ table: MySqlTable,
259
+ options: ForgeSqlOrmOptions,
260
+ isCached: boolean,
261
+ ): any {
262
+ const result = value.apply(target, args);
263
+ if (typeof result === "object" && result !== null && "execute" in result) {
264
+ return wrapCacheEvictBuilder(result as QueryBuilder, table, options, isCached);
265
+ }
266
+ return result;
267
+ }
268
+
269
+ /**
270
+ * Wraps a query builder with cache eviction functionality.
271
+ * Automatically clears cache after successful execution of insert/update/delete operations.
272
+ *
273
+ * @param rawBuilder - The original query builder to wrap
274
+ * @param table - The table being modified (used for cache eviction)
275
+ * @param options - ForgeSQL ORM options including cache configuration
276
+ * @param isCached - Whether to clear cache immediately
277
+ * @returns Wrapped query builder with cache eviction
278
+ */
279
+ const wrapCacheEvictBuilder = <TTable extends MySqlTable>(
280
+ rawBuilder: QueryBuilder,
281
+ table: TTable,
282
+ options: ForgeSqlOrmOptions,
283
+ isCached: boolean,
284
+ ): QueryBuilder => {
285
+ return new Proxy(rawBuilder, {
286
+ get(target, prop, receiver) {
287
+ if (prop === "then") {
288
+ return (onfulfilled?: any, onrejected?: any) =>
289
+ target
290
+ .execute()
291
+ .then(
292
+ (rows: unknown[]) =>
293
+ handleSuccessfulExecution(rows, onfulfilled, table, options, isCached),
294
+ onrejected,
295
+ );
296
+ }
297
+
298
+ const value = Reflect.get(target, prop, receiver);
299
+
300
+ if (typeof value === "function") {
301
+ return (...args: any[]) =>
302
+ handleFunctionCall(value, target, args, table, options, isCached);
303
+ }
304
+
305
+ return value;
306
+ },
307
+ });
308
+ };
309
+
310
+ /**
311
+ * Creates an insert query builder that automatically evicts cache after execution.
312
+ *
313
+ * @param db - The database instance
314
+ * @param table - The table to insert into
315
+ * @param options - ForgeSQL ORM options
316
+ * @returns Insert query builder with cache eviction
317
+ */
318
+ function insertAndEvictCacheBuilder<TTable extends MySqlTable>(
319
+ db: MySqlRemoteDatabase<any>,
320
+ table: TTable,
321
+ options: ForgeSqlOrmOptions,
322
+ isCached: boolean,
323
+ ): MySqlInsertBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
324
+ const builder = db.insert(table);
325
+ return wrapCacheEvictBuilder(
326
+ builder as unknown as QueryBuilder,
327
+ table,
328
+ options,
329
+ isCached,
330
+ ) as unknown as MySqlInsertBuilder<
331
+ TTable,
332
+ MySqlRemoteQueryResultHKT,
333
+ MySqlRemotePreparedQueryHKT
334
+ >;
335
+ }
336
+
337
+ /**
338
+ * Creates an update query builder that automatically evicts cache after execution.
339
+ *
340
+ * @param db - The database instance
341
+ * @param table - The table to update
342
+ * @param options - ForgeSQL ORM options
343
+ * @returns Update query builder with cache eviction
344
+ */
345
+ function updateAndEvictCacheBuilder<TTable extends MySqlTable>(
346
+ db: MySqlRemoteDatabase<any>,
347
+ table: TTable,
348
+ options: ForgeSqlOrmOptions,
349
+ isCached: boolean,
350
+ ): MySqlUpdateBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
351
+ const builder = db.update(table);
352
+ return wrapCacheEvictBuilder(
353
+ builder as unknown as QueryBuilder,
354
+ table,
355
+ options,
356
+ isCached,
357
+ ) as unknown as MySqlUpdateBuilder<
358
+ TTable,
359
+ MySqlRemoteQueryResultHKT,
360
+ MySqlRemotePreparedQueryHKT
361
+ >;
362
+ }
363
+
364
+ /**
365
+ * Creates a delete query builder that automatically evicts cache after execution.
366
+ *
367
+ * @param db - The database instance
368
+ * @param table - The table to delete from
369
+ * @param options - ForgeSQL ORM options
370
+ * @returns Delete query builder with cache eviction
371
+ */
372
+ function deleteAndEvictCacheBuilder<TTable extends MySqlTable>(
373
+ db: MySqlRemoteDatabase<any>,
374
+ table: TTable,
375
+ options: ForgeSqlOrmOptions,
376
+ isCached: boolean,
377
+ ): MySqlDeleteBase<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
378
+ const builder = db.delete(table);
379
+ return wrapCacheEvictBuilder(
380
+ builder as unknown as QueryBuilder,
381
+ table,
382
+ options,
383
+ isCached,
384
+ ) as unknown as MySqlDeleteBase<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT>;
385
+ }
386
+
387
+ /**
388
+ * Handles cached query execution with proper error handling.
389
+ *
390
+ * @param target - The query target
391
+ * @param options - ForgeSQL ORM options
392
+ * @param cacheTtl - Cache TTL
393
+ * @param selections - Field selections
394
+ * @param aliasMap - Field alias mapping
395
+ * @param onfulfilled - Success callback
396
+ * @param onrejected - Error callback
397
+ * @returns Promise with cached result
398
+ */
399
+ async function handleCachedQuery(
400
+ target: any,
401
+ options: ForgeSqlOrmOptions,
402
+ cacheTtl: number,
403
+ selections: any,
404
+ aliasMap: any,
405
+ onfulfilled?: any,
406
+ onrejected?: any,
407
+ ): Promise<any> {
408
+ try {
409
+ const localCached = await getQueryLocalCacheQuery(target);
410
+ if (localCached) {
411
+ return onfulfilled?.(localCached);
412
+ }
413
+ const cacheResult = await getFromCache(target, options);
414
+ if (cacheResult) {
415
+ return onfulfilled?.(cacheResult);
416
+ }
417
+ const rows = await target.execute();
418
+ const transformed = applyFromDriverTransform(rows, selections, aliasMap);
419
+ await saveQueryLocalCacheQuery(target, transformed);
420
+ await setCacheResult(target, options, transformed, cacheTtl).catch((cacheError) => {
421
+ // Log cache error but don't fail the query
422
+ console.warn("Cache set error:", cacheError);
423
+ });
424
+
425
+ return onfulfilled?.(transformed);
426
+ } catch (error) {
427
+ return onrejected?.(error);
428
+ }
429
+ }
430
+
431
+ /**
432
+ * Handles non-cached query execution.
433
+ *
434
+ * @param target - The query target
435
+ * @param selections - Field selections
436
+ * @param aliasMap - Field alias mapping
437
+ * @param onfulfilled - Success callback
438
+ * @param onrejected - Error callback
439
+ * @returns Promise with transformed result
440
+ */
441
+ async function handleNonCachedQuery(
442
+ target: any,
443
+ selections: any,
444
+ aliasMap: any,
445
+ onfulfilled?: any,
446
+ onrejected?: any,
447
+ ): Promise<any> {
448
+ try {
449
+ const localCached = await getQueryLocalCacheQuery(target);
450
+ if (localCached) {
451
+ return onfulfilled?.(localCached);
452
+ }
453
+ const rows = await target.execute();
454
+ const transformed = applyFromDriverTransform(rows, selections, aliasMap);
455
+ await saveQueryLocalCacheQuery(target, transformed);
456
+ return onfulfilled?.(transformed);
457
+ } catch (error) {
458
+ return onrejected?.(error);
459
+ }
460
+ }
461
+
462
+ /**
463
+ * Creates a select query builder with field aliasing and optional caching support.
464
+ *
465
+ * @param db - The database instance
466
+ * @param fields - The fields to select with aliases
467
+ * @param selectFn - Function to create the base select query
468
+ * @param useCache - Whether to enable caching for this query
469
+ * @param options - ForgeSQL ORM options
470
+ * @param cacheTtl - Optional cache TTL override
471
+ * @returns Select query builder with aliasing and optional caching
472
+ */
473
+ function createAliasedSelectBuilder<TSelection extends SelectedFields>(
474
+ db: MySqlRemoteDatabase<any>,
475
+ fields: TSelection,
476
+ selectFn: (selections: any) => MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>,
477
+ useCache: boolean,
478
+ options: ForgeSqlOrmOptions,
479
+ cacheTtl?: number,
480
+ ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
481
+ const { selections, aliasMap } = mapSelectFieldsWithAlias(fields);
482
+ const builder = selectFn(selections);
483
+
484
+ const wrapBuilder = (rawBuilder: any): any => {
485
+ return new Proxy(rawBuilder, {
486
+ get(target, prop, receiver) {
487
+ if (prop === "execute") {
488
+ return async (...args: any[]) => {
489
+ const rows = await target.execute(...args);
490
+ return applyFromDriverTransform(rows, selections, aliasMap);
491
+ };
492
+ }
493
+
494
+ if (prop === "then") {
495
+ return (onfulfilled?: any, onrejected?: any) => {
496
+ if (useCache) {
497
+ const ttl = cacheTtl ?? options.cacheTTL ?? 120;
498
+ return handleCachedQuery(
499
+ target,
500
+ options,
501
+ ttl,
502
+ selections,
503
+ aliasMap,
504
+ onfulfilled,
505
+ onrejected,
506
+ );
507
+ } else {
508
+ return handleNonCachedQuery(target, selections, aliasMap, onfulfilled, onrejected);
509
+ }
510
+ };
511
+ }
512
+
513
+ const value = Reflect.get(target, prop, receiver);
514
+
515
+ if (typeof value === "function") {
516
+ return (...args: any[]) => {
517
+ const result = value.apply(target, args);
518
+
519
+ if (typeof result === "object" && result !== null && "execute" in result) {
520
+ return wrapBuilder(result);
521
+ }
522
+
523
+ return result;
524
+ };
525
+ }
526
+
527
+ return value;
528
+ },
529
+ });
530
+ };
531
+
532
+ return wrapBuilder(builder);
533
+ }
534
+
535
+ // ============================================================================
536
+ // CONFIGURATION AND CONSTANTS
537
+ // ============================================================================
538
+
539
+ /**
540
+ * Default options for ForgeSQL ORM
541
+ */
542
+ const DEFAULT_OPTIONS: ForgeSqlOrmOptions = {
543
+ logRawSqlQuery: false,
544
+ disableOptimisticLocking: false,
545
+ cacheTTL: 120,
546
+ cacheWrapTable: true,
547
+ cacheEntityQueryName: "sql",
548
+ cacheEntityExpirationName: "expiration",
549
+ cacheEntityDataName: "data",
550
+ } as const;
551
+
552
+ // ============================================================================
553
+ // QUERY BUILDER FACTORIES
554
+ // ============================================================================
555
+
556
+
557
+ /**
558
+ * Creates a raw SQL query executor with caching support
559
+ */
560
+ function createRawQueryExecutor(
561
+ db: MySqlRemoteDatabase<any>,
562
+ options: ForgeSqlOrmOptions,
563
+ useGlobalCache: boolean = false
564
+ ) {
565
+ return async function <T extends { [column: string]: any }>(
566
+ query: SQLWrapper | string,
567
+ cacheTtl?: number
568
+ ): Promise<MySqlRawQueryResult> {
569
+ let sql: Query;
570
+
571
+ if (isSQLWrapper(query)) {
572
+ const sqlWrapper = query as SQLWrapper;
573
+ sql = sqlWrapper.getSQL()
574
+ .toQuery(((db as unknown as { dialect: MySqlDialect }).dialect) as unknown as BuildQueryConfig);
575
+ } else {
576
+ sql = {
577
+ sql: query,
578
+ params: []
579
+ };
580
+ }
581
+
582
+ // Check local cache first
583
+ const localCacheResult = await getQueryLocalCacheQuery(sql);
584
+ if (localCacheResult) {
585
+ return localCacheResult as MySqlRawQueryResult;
586
+ }
587
+
588
+ // Check global cache if enabled
589
+ if (useGlobalCache) {
590
+ const cacheResult = await getFromCache({ toSQL: () => sql }, options);
591
+ if (cacheResult) {
592
+ return cacheResult as MySqlRawQueryResult;
593
+ }
594
+ }
595
+
596
+ // Execute query
597
+ const results = await db.execute<T>(query);
598
+
599
+ // Save to local cache
600
+ await saveQueryLocalCacheQuery(sql, results);
601
+
602
+ // Save to global cache if enabled
603
+ if (useGlobalCache) {
604
+ await setCacheResult(
605
+ { toSQL: () => sql },
606
+ options,
607
+ results,
608
+ cacheTtl ?? options.cacheTTL ?? 120
609
+ );
610
+ }
611
+
612
+ return results;
613
+ };
614
+ }
615
+
616
+ // ============================================================================
617
+ // MAIN PATCH FUNCTION
618
+ // ============================================================================
619
+
620
+ /**
621
+ * Patches a Drizzle database instance with additional methods for aliased selects and cache management.
622
+ *
623
+ * This function extends the database instance with:
624
+ * - selectAliased: Select with field aliasing support
625
+ * - selectAliasedDistinct: Select distinct with field aliasing support
626
+ * - selectAliasedCacheable: Select with field aliasing and caching
627
+ * - selectAliasedDistinctCacheable: Select distinct with field aliasing and caching
628
+ * - insertAndEvictCache: Insert operations that automatically evict cache
629
+ * - updateAndEvictCache: Update operations that automatically evict cache
630
+ * - deleteAndEvictCache: Delete operations that automatically evict cache
631
+ *
632
+ * @param db - The Drizzle database instance to patch
633
+ * @param options - Optional ForgeSQL ORM configuration
634
+ * @returns The patched database instance with additional methods
635
+ */
636
+ export function patchDbWithSelectAliased(
637
+ db: MySqlRemoteDatabase<any>,
638
+ options?: ForgeSqlOrmOptions,
639
+ ): MySqlRemoteDatabase<any> & {
640
+ selectAliased: SelectAliasedType;
641
+ selectAliasedDistinct: SelectAliasedDistinctType;
642
+ selectAliasedCacheable: SelectAliasedCacheableType;
643
+ selectAliasedDistinctCacheable: SelectAliasedDistinctCacheableType;
644
+ insertWithCacheContext: InsertAndEvictCacheType;
645
+ insertAndEvictCache: InsertAndEvictCacheType;
646
+ updateAndEvictCache: UpdateAndEvictCacheType;
647
+ updateWithCacheContext: UpdateAndEvictCacheType;
648
+ deleteAndEvictCache: DeleteAndEvictCacheType;
649
+ deleteWithCacheContext: DeleteAndEvictCacheType;
650
+ } {
651
+ const newOptions = { ...DEFAULT_OPTIONS, ...options };
652
+
653
+ // ============================================================================
654
+ // SELECT METHODS WITH FIELD ALIASING
655
+ // ============================================================================
656
+
657
+ // Select aliased without cache
658
+ db.selectAliased = function <TSelection extends SelectedFields>(fields: TSelection) {
659
+ return createAliasedSelectBuilder(
660
+ db,
661
+ fields,
662
+ (selections) => db.select(selections),
663
+ false,
664
+ newOptions,
665
+ );
666
+ };
667
+
668
+ // Select aliased with cache
669
+ db.selectAliasedCacheable = function <TSelection extends SelectedFields>(
670
+ fields: TSelection,
671
+ cacheTtl?: number,
672
+ ) {
673
+ return createAliasedSelectBuilder(
674
+ db,
675
+ fields,
676
+ (selections) => db.select(selections),
677
+ true,
678
+ newOptions,
679
+ cacheTtl,
680
+ );
681
+ };
682
+
683
+ // Select aliased distinct without cache
684
+ db.selectAliasedDistinct = function <TSelection extends SelectedFields>(fields: TSelection) {
685
+ return createAliasedSelectBuilder(
686
+ db,
687
+ fields,
688
+ (selections) => db.selectDistinct(selections),
689
+ false,
690
+ newOptions,
691
+ );
692
+ };
693
+
694
+ // Select aliased distinct with cache
695
+ db.selectAliasedDistinctCacheable = function <TSelection extends SelectedFields>(
696
+ fields: TSelection,
697
+ cacheTtl?: number,
698
+ ) {
699
+ return createAliasedSelectBuilder(
700
+ db,
701
+ fields,
702
+ (selections) => db.selectDistinct(selections),
703
+ true,
704
+ newOptions,
705
+ cacheTtl,
706
+ );
707
+ };
708
+
709
+ // ============================================================================
710
+ // TABLE-BASED SELECT METHODS
711
+ // ============================================================================
712
+
713
+ /**
714
+ * Creates a select query builder for all columns from a table with field aliasing support.
715
+ * This is a convenience method that automatically selects all columns from the specified table.
716
+ *
717
+ * @param table - The table to select from
718
+ * @returns Select query builder with all table columns and field aliasing support
719
+ * @example
720
+ * ```typescript
721
+ * const users = await db.selectFrom(userTable).where(eq(userTable.id, 1));
722
+ * ```
723
+ */
724
+ db.selectFrom = function <T extends MySqlTable>(table: T) {
725
+ return db.selectAliased(getTableColumns(table)).from(table);
726
+ };
727
+
728
+ /**
729
+ * Creates a select query builder for all columns from a table with field aliasing and caching support.
730
+ * This is a convenience method that automatically selects all columns from the specified table with caching enabled.
731
+ *
732
+ * @param table - The table to select from
733
+ * @param cacheTtl - Optional cache TTL override (defaults to global cache TTL)
734
+ * @returns Select query builder with all table columns, field aliasing, and caching support
735
+ * @example
736
+ * ```typescript
737
+ * const users = await db.selectFromCacheable(userTable, 300).where(eq(userTable.id, 1));
738
+ * ```
739
+ */
740
+ db.selectFromCacheable = function <T extends MySqlTable>(table: T, cacheTtl?: number) {
741
+ return db.selectAliasedCacheable(getTableColumns(table), cacheTtl).from(table);
742
+ };
743
+
744
+ /**
745
+ * Creates a select distinct query builder for all columns from a table with field aliasing support.
746
+ * This is a convenience method that automatically selects all distinct columns from the specified table.
747
+ *
748
+ * @param table - The table to select from
749
+ * @returns Select distinct query builder with all table columns and field aliasing support
750
+ * @example
751
+ * ```typescript
752
+ * const uniqueUsers = await db.selectDistinctFrom(userTable).where(eq(userTable.status, 'active'));
753
+ * ```
754
+ */
755
+ db.selectDistinctFrom = function <T extends MySqlTable>(table: T) {
756
+ return db.selectAliasedDistinct(getTableColumns(table)).from(table);
757
+ };
758
+
759
+ /**
760
+ * Creates a select distinct query builder for all columns from a table with field aliasing and caching support.
761
+ * This is a convenience method that automatically selects all distinct columns from the specified table with caching enabled.
762
+ *
763
+ * @param table - The table to select from
764
+ * @param cacheTtl - Optional cache TTL override (defaults to global cache TTL)
765
+ * @returns Select distinct query builder with all table columns, field aliasing, and caching support
766
+ * @example
767
+ * ```typescript
768
+ * const uniqueUsers = await db.selectDistinctFromCacheable(userTable, 300).where(eq(userTable.status, 'active'));
769
+ * ```
770
+ */
771
+ db.selectDistinctFromCacheable = function <T extends MySqlTable>(table: T, cacheTtl?: number) {
772
+ return db.selectAliasedDistinctCacheable(getTableColumns(table), cacheTtl).from(table);
773
+ };
774
+
775
+ // ============================================================================
776
+ // CACHE-AWARE MODIFY OPERATIONS
777
+ // ============================================================================
778
+
779
+ // Insert with cache context support (participates in cache clearing when used within cache context)
780
+ db.insertWithCacheContext = function <TTable extends MySqlTable>(table: TTable) {
781
+ return insertAndEvictCacheBuilder(db, table, newOptions, false);
782
+ };
783
+
784
+ // Insert with cache eviction
785
+ db.insertAndEvictCache = function <TTable extends MySqlTable>(table: TTable) {
786
+ return insertAndEvictCacheBuilder(db, table, newOptions, true);
787
+ };
788
+
789
+ // Update with cache context support (participates in cache clearing when used within cache context)
790
+ db.updateWithCacheContext = function <TTable extends MySqlTable>(table: TTable) {
791
+ return updateAndEvictCacheBuilder(db, table, newOptions, false);
792
+ };
793
+
794
+ // Update with cache eviction
795
+ db.updateAndEvictCache = function <TTable extends MySqlTable>(table: TTable) {
796
+ return updateAndEvictCacheBuilder(db, table, newOptions, true);
797
+ };
798
+
799
+ // Delete with cache context support (participates in cache clearing when used within cache context)
800
+ db.deleteWithCacheContext = function <TTable extends MySqlTable>(table: TTable) {
801
+ return deleteAndEvictCacheBuilder(db, table, newOptions, false);
802
+ };
803
+
804
+ // Delete with cache eviction
805
+ db.deleteAndEvictCache = function <TTable extends MySqlTable>(table: TTable) {
806
+ return deleteAndEvictCacheBuilder(db, table, newOptions, true);
807
+ };
808
+
809
+ // ============================================================================
810
+ // RAW SQL QUERY EXECUTORS
811
+ // ============================================================================
812
+
813
+ /**
814
+ * Executes a raw SQL query with local cache support.
815
+ * This method provides local caching for raw SQL queries within the current invocation context.
816
+ * Results are cached locally and will be returned from cache on subsequent identical queries.
817
+ *
818
+ * @param query - The SQL query to execute (SQLWrapper or string)
819
+ * @returns Promise with query results
820
+ * @example
821
+ * ```typescript
822
+ * // Using SQLWrapper
823
+ * const result = await db.executeQuery(sql`SELECT * FROM users WHERE id = ${userId}`);
824
+ *
825
+ * // Using string
826
+ * const result = await db.executeQuery("SELECT * FROM users WHERE status = 'active'");
827
+ * ```
828
+ */
829
+ db.executeQuery = createRawQueryExecutor(db, newOptions, false);
830
+
831
+ /**
832
+ * Executes a raw SQL query with both local and global cache support.
833
+ * This method provides comprehensive caching for raw SQL queries:
834
+ * - Local cache: Within the current invocation context
835
+ * - Global cache: Cross-invocation caching using @forge/kvs
836
+ *
837
+ * @param query - The SQL query to execute (SQLWrapper or string)
838
+ * @param cacheTtl - Optional cache TTL override (defaults to global cache TTL)
839
+ * @returns Promise with query results
840
+ * @example
841
+ * ```typescript
842
+ * // Using SQLWrapper with custom TTL
843
+ * const result = await db.executeQueryCacheable(sql`SELECT * FROM users WHERE id = ${userId}`, 300);
844
+ *
845
+ * // Using string with default TTL
846
+ * const result = await db.executeQueryCacheable("SELECT * FROM users WHERE status = 'active'");
847
+ * ```
848
+ */
849
+ db.executeQueryCacheable = createRawQueryExecutor(db, newOptions, true);
850
+
851
+ return db;
852
+ }