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
@@ -0,0 +1,46 @@
1
+ import { ForgeSqlOrmOptions } from "../core/ForgeSQLQueryBuilder";
2
+ /**
3
+ * Scheduler trigger for clearing expired cache entries.
4
+ *
5
+ * This trigger should be configured as a Forge scheduler to automatically
6
+ * clean up expired cache entries based on their TTL (Time To Live).
7
+ *
8
+ * @param options - Optional ForgeSQL ORM configuration. If not provided,
9
+ * uses default cache settings with cacheEntityName: "cache"
10
+ * @returns Promise that resolves to HTTP response object
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * // In your index.ts
15
+ * import { clearCacheSchedulerTrigger } from "forge-sql-orm";
16
+ *
17
+ * export const clearCache = () => {
18
+ * return clearCacheSchedulerTrigger({
19
+ * cacheEntityName: "cache",
20
+ * logRawSqlQuery: true
21
+ * });
22
+ * };
23
+ * ```
24
+ *
25
+ * @example
26
+ * ```yaml
27
+ * # In manifest.yml
28
+ * scheduledTrigger:
29
+ * - key: clear-cache-trigger
30
+ * function: clearCache
31
+ * interval: fiveMinute
32
+ *
33
+ * function:
34
+ * - key: clearCache
35
+ * handler: index.clearCache
36
+ * ```
37
+ */
38
+ export declare const clearCacheSchedulerTrigger: (options?: ForgeSqlOrmOptions) => Promise<{
39
+ headers: {
40
+ "Content-Type": string[];
41
+ };
42
+ statusCode: number;
43
+ statusText: string;
44
+ body: string;
45
+ }>;
46
+ //# sourceMappingURL=clearCacheSchedulerTrigger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clearCacheSchedulerTrigger.d.ts","sourceRoot":"","sources":["../../src/webtriggers/clearCacheSchedulerTrigger.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,0BAA0B,GAAU,UAAU,kBAAkB;;;;;;;EAuC5E,CAAC"}
@@ -2,6 +2,7 @@ export * from "./dropMigrationWebTrigger";
2
2
  export * from "./applyMigrationsWebTrigger";
3
3
  export * from "./fetchSchemaWebTrigger";
4
4
  export * from "./dropTablesMigrationWebTrigger";
5
+ export * from "./clearCacheSchedulerTrigger";
5
6
  export interface TriggerResponse<BODY> {
6
7
  body?: BODY;
7
8
  headers?: Record<string, string[]>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/webtriggers/index.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAC;AAC1C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,yBAAyB,CAAC;AACxC,cAAc,iCAAiC,CAAC;AAEhD,MAAM,WAAW,eAAe,CAAC,IAAI;IACnC,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,eAAe,GAAI,IAAI,EAAE,YAAY,MAAM,EAAE,MAAM,IAAI,KAAG,eAAe,CAAC,IAAI,CAc1F,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/webtriggers/index.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAC;AAC1C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,yBAAyB,CAAC;AACxC,cAAc,iCAAiC,CAAC;AAChD,cAAc,8BAA8B,CAAC;AAE7C,MAAM,WAAW,eAAe,CAAC,IAAI;IACnC,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,eAAe,GAAI,IAAI,EAAE,YAAY,MAAM,EAAE,MAAM,IAAI,KAAG,eAAe,CAAC,IAAI,CAc1F,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "forge-sql-orm",
3
- "version": "2.0.30",
4
- "description": "Drizzle ORM integration for Forge-SQL in Atlassian Forge applications.",
3
+ "version": "2.1.0",
4
+ "description": "Drizzle ORM integration for Atlassian @forge/sql. Provides a custom driver, schema migration, two levels of caching (local and global via @forge/kvs), optimistic locking, and query analysis.",
5
5
  "main": "dist/ForgeSQLORM.js",
6
6
  "module": "dist/ForgeSQLORM.mjs",
7
7
  "types": "dist/index.d.ts",
@@ -32,23 +32,23 @@
32
32
  "database"
33
33
  ],
34
34
  "devDependencies": {
35
- "@eslint/js": "^9.34.0",
35
+ "@eslint/js": "^9.35.0",
36
36
  "@types/luxon": "^3.7.1",
37
- "@types/node": "^24.3.0",
38
- "@typescript-eslint/eslint-plugin": "^8.41.0",
39
- "@typescript-eslint/parser": "^8.41.0",
37
+ "@types/node": "^24.3.3",
38
+ "@typescript-eslint/eslint-plugin": "^8.43.0",
39
+ "@typescript-eslint/parser": "^8.43.0",
40
40
  "@vitest/coverage-v8": "^3.2.4",
41
41
  "@vitest/ui": "^3.2.4",
42
- "eslint": "^9.34.0",
42
+ "eslint": "^9.35.0",
43
43
  "eslint-config-prettier": "^10.1.8",
44
44
  "eslint-plugin-import": "^2.32.0",
45
45
  "eslint-plugin-vitest": "^0.5.4",
46
- "knip": "^5.63.0",
46
+ "knip": "^5.63.1",
47
47
  "prettier": "^3.6.2",
48
48
  "ts-node": "^10.9.2",
49
49
  "typescript": "^5.9.2",
50
- "uuid": "^11.1.0",
51
- "vite": "^7.1.4",
50
+ "uuid": "^13.0.0",
51
+ "vite": "^7.1.5",
52
52
  "vitest": "^3.2.4"
53
53
  },
54
54
  "license": "MIT",
@@ -59,7 +59,7 @@
59
59
  "test:watch": "vitest --watch",
60
60
  "lint": "eslint src --ext .ts,.tsx",
61
61
  "lint:fix": "eslint src --ext .ts,.tsx --fix",
62
- "format": "prettier --write src examples",
62
+ "format": "prettier --write src examples __tests__",
63
63
  "clean": "rm -rf dist",
64
64
  "build": "npm run clean && vite build && npm run build:types",
65
65
  "build:types": "tsc --emitDeclarationOnly",
@@ -78,7 +78,10 @@
78
78
  "@forge/sql": "^3.0.5",
79
79
  "drizzle-orm": "^0.44.5"
80
80
  },
81
+ "optionalDependencies": {
82
+ "@forge/kvs": "^1.0.5"
83
+ },
81
84
  "dependencies": {
82
- "luxon": "^3.7.1"
85
+ "luxon": "^3.7.2"
83
86
  }
84
87
  }
@@ -6,7 +6,7 @@ import {
6
6
  SlowQueryNormalized,
7
7
  } from "./SystemTables";
8
8
  import { SqlParameters } from "@forge/sql/out/sql-statement";
9
- import { AnyMySqlTable } from "drizzle-orm/mysql-core/index";
9
+ import { AnyMySqlTable } from "drizzle-orm/mysql-core";
10
10
  import { getTableName } from "drizzle-orm/table";
11
11
  import { DateTime } from "luxon";
12
12
 
@@ -0,0 +1,195 @@
1
+ import { MySqlSelectDynamic } from "drizzle-orm/mysql-core/query-builders/select.types";
2
+ import { AnyMySqlSelectQueryBuilder, AnyMySqlTable } from "drizzle-orm/mysql-core";
3
+ import { CacheForgeSQL, ForgeSqlOperation, ForgeSqlOrmOptions } from "./ForgeSQLQueryBuilder";
4
+ import { InferInsertModel, Query, SQL } from "drizzle-orm";
5
+ import { clearCache, getFromCache, setCacheResult, clearTablesCache } from "../utils/cacheUtils";
6
+ import { getTableName } from "drizzle-orm/table";
7
+
8
+ /**
9
+ * Implementation of cache operations for ForgeSQL ORM.
10
+ * Provides methods for cacheable database operations with automatic cache management.
11
+ *
12
+ * ⚠️ **IMPORTANT**: All modification methods in this class use optimistic locking/versioning
13
+ * through `modifyWithVersioning()` internally. This ensures data consistency and prevents
14
+ * concurrent modification conflicts.
15
+ */
16
+ export class ForgeSQLCacheOperations implements CacheForgeSQL {
17
+ private readonly options: ForgeSqlOrmOptions;
18
+ private readonly forgeOperations: ForgeSqlOperation;
19
+
20
+ /**
21
+ * Creates a new instance of ForgeSQLCacheOperations.
22
+ *
23
+ * @param options - Configuration options for the ORM
24
+ * @param forgeOperations - The ForgeSQL operations instance
25
+ */
26
+ constructor(options: ForgeSqlOrmOptions, forgeOperations: ForgeSqlOperation) {
27
+ this.options = options;
28
+ this.forgeOperations = forgeOperations;
29
+ }
30
+
31
+ /**
32
+ * Evicts cache for multiple tables using Drizzle table objects.
33
+ *
34
+ * @param tables - Array of Drizzle table objects to clear cache for
35
+ * @returns Promise that resolves when cache eviction is complete
36
+ * @throws Error if cacheEntityName is not configured
37
+ */
38
+ async evictCacheEntities(tables: AnyMySqlTable[]): Promise<void> {
39
+ if (!this.options.cacheEntityName) {
40
+ throw new Error("cacheEntityName is not configured");
41
+ }
42
+ await this.evictCache(tables.map((t) => getTableName(t)));
43
+ }
44
+
45
+ /**
46
+ * Evicts cache for multiple tables by their names.
47
+ *
48
+ * @param tables - Array of table names to clear cache for
49
+ * @returns Promise that resolves when cache eviction is complete
50
+ * @throws Error if cacheEntityName is not configured
51
+ */
52
+ async evictCache(tables: string[]): Promise<void> {
53
+ if (!this.options.cacheEntityName) {
54
+ throw new Error("cacheEntityName is not configured");
55
+ }
56
+ await clearTablesCache(tables, this.options);
57
+ }
58
+
59
+ /**
60
+ * Inserts records with optimistic locking/versioning and automatically evicts cache.
61
+ *
62
+ * This method uses `modifyWithVersioning().insert()` internally, providing:
63
+ * - Automatic version field initialization
64
+ * - Optimistic locking support
65
+ * - Cache eviction after successful operation
66
+ *
67
+ * @param schema - The table schema
68
+ * @param models - Array of entities to insert
69
+ * @param updateIfExists - Whether to update existing records
70
+ * @returns Promise that resolves to the number of inserted rows
71
+ * @throws Error if cacheEntityName is not configured
72
+ * @throws Error if optimistic locking check fails
73
+ */
74
+ async insert<T extends AnyMySqlTable>(
75
+ schema: T,
76
+ models: InferInsertModel<T>[],
77
+ updateIfExists?: boolean,
78
+ ): Promise<number> {
79
+ this.validateCacheConfiguration();
80
+ const number = await this.forgeOperations
81
+ .modifyWithVersioning()
82
+ .insert(schema, models, updateIfExists);
83
+ await clearCache(schema, this.options);
84
+ return number;
85
+ }
86
+
87
+ /**
88
+ * Deletes a record by ID with optimistic locking/versioning and automatically evicts cache.
89
+ *
90
+ * This method uses `modifyWithVersioning().deleteById()` internally, providing:
91
+ * - Optimistic locking checks before deletion
92
+ * - Version field validation
93
+ * - Cache eviction after successful operation
94
+ *
95
+ * @param id - The ID of the record to delete
96
+ * @param schema - The table schema
97
+ * @returns Promise that resolves to the number of affected rows
98
+ * @throws Error if cacheEntityName is not configured
99
+ * @throws Error if optimistic locking check fails
100
+ */
101
+ async deleteById<T extends AnyMySqlTable>(id: unknown, schema: T): Promise<number> {
102
+ this.validateCacheConfiguration();
103
+ const number = await this.forgeOperations.modifyWithVersioning().deleteById(id, schema);
104
+ await clearCache(schema, this.options);
105
+ return number;
106
+ }
107
+
108
+ /**
109
+ * Updates a record by ID with optimistic locking/versioning and automatically evicts cache.
110
+ *
111
+ * This method uses `modifyWithVersioning().updateById()` internally, providing:
112
+ * - Optimistic locking checks before update
113
+ * - Version field incrementation
114
+ * - Cache eviction after successful operation
115
+ *
116
+ * @param entity - The entity with updated values (must include primary key)
117
+ * @param schema - The table schema
118
+ * @returns Promise that resolves to the number of affected rows
119
+ * @throws Error if cacheEntityName is not configured
120
+ * @throws Error if optimistic locking check fails
121
+ */
122
+ async updateById<T extends AnyMySqlTable>(
123
+ entity: Partial<InferInsertModel<T>>,
124
+ schema: T,
125
+ ): Promise<number> {
126
+ this.validateCacheConfiguration();
127
+ const number = await this.forgeOperations.modifyWithVersioning().updateById(entity, schema);
128
+ await clearCache(schema, this.options);
129
+ return number;
130
+ }
131
+
132
+ /**
133
+ * Updates fields based on conditions with optimistic locking/versioning and automatically evicts cache.
134
+ *
135
+ * This method uses `modifyWithVersioning().updateFields()` internally, providing:
136
+ * - Optimistic locking support (if version field is configured)
137
+ * - Version field validation and incrementation
138
+ * - Cache eviction after successful operation
139
+ *
140
+ * @param updateData - The data to update
141
+ * @param schema - The table schema
142
+ * @param where - Optional WHERE conditions
143
+ * @returns Promise that resolves to the number of affected rows
144
+ * @throws Error if cacheEntityName is not configured
145
+ * @throws Error if optimistic locking check fails
146
+ */
147
+ async updateFields<T extends AnyMySqlTable>(
148
+ updateData: Partial<InferInsertModel<T>>,
149
+ schema: T,
150
+ where?: SQL<unknown>,
151
+ ): Promise<number> {
152
+ this.validateCacheConfiguration();
153
+ const number = await this.forgeOperations
154
+ .modifyWithVersioning()
155
+ .updateFields(updateData, schema, where);
156
+ await clearCache(schema, this.options);
157
+ return number;
158
+ }
159
+
160
+ /**
161
+ * Executes a query with caching support.
162
+ * First checks cache, if not found executes query and stores result in cache.
163
+ *
164
+ * @param query - The Drizzle query to execute
165
+ * @param cacheTtl - Optional cache TTL override
166
+ * @returns Promise that resolves to the query results
167
+ * @throws Error if cacheEntityName is not configured
168
+ */
169
+ async executeQuery<T extends MySqlSelectDynamic<AnyMySqlSelectQueryBuilder>>(
170
+ query: T,
171
+ cacheTtl?: number,
172
+ ): Promise<Awaited<T>> {
173
+ this.validateCacheConfiguration();
174
+ const sqlQuery = query as { toSQL: () => Query };
175
+ const cacheResult = await getFromCache<Awaited<T>>(sqlQuery, this.options);
176
+ if (cacheResult) {
177
+ return cacheResult;
178
+ }
179
+ const results = await query;
180
+ await setCacheResult(sqlQuery, this.options, results, cacheTtl ?? this.options.cacheTTL ?? 60);
181
+ return results;
182
+ }
183
+
184
+ /**
185
+ * Validates that cache configuration is properly set up.
186
+ *
187
+ * @throws Error if cacheEntityName is not configured
188
+ * @private
189
+ */
190
+ private validateCacheConfiguration(): void {
191
+ if (!this.options.cacheEntityName) {
192
+ throw new Error("cacheEntityName is not configured");
193
+ }
194
+ }
195
+ }
@@ -1,16 +1,15 @@
1
1
  import { ForgeSqlOrmOptions } from "..";
2
- import { CRUDForgeSQL, ForgeSqlOperation } from "./ForgeSQLQueryBuilder";
3
- import { AnyMySqlTable } from "drizzle-orm/mysql-core/index";
4
- import { AnyColumn, InferInsertModel } from "drizzle-orm";
5
- import { eq, and } from "drizzle-orm";
6
- import { SQL } from "drizzle-orm";
2
+ import { VerioningModificationForgeSQL, ForgeSqlOperation } from "./ForgeSQLQueryBuilder";
3
+ import { AnyMySqlTable } from "drizzle-orm/mysql-core";
4
+ import { and, AnyColumn, eq, InferInsertModel, SQL } from "drizzle-orm";
7
5
  import { getPrimaryKeys, getTableMetadata } from "../utils/sqlUtils";
6
+ import { saveTableIfInsideCacheContext } from "../utils/cacheContextUtils";
8
7
 
9
8
  /**
10
- * Class implementing CRUD operations for ForgeSQL ORM.
9
+ * Class implementing Modification operations for ForgeSQL ORM.
11
10
  * Provides methods for inserting, updating, and deleting records with support for optimistic locking.
12
11
  */
13
- export class ForgeSQLCrudOperations implements CRUDForgeSQL {
12
+ export class ForgeSQLCrudOperations implements VerioningModificationForgeSQL {
14
13
  private readonly forgeOperations: ForgeSqlOperation;
15
14
  private readonly options: ForgeSqlOrmOptions;
16
15
 
@@ -28,12 +27,17 @@ export class ForgeSQLCrudOperations implements CRUDForgeSQL {
28
27
  * Inserts records into the database with optional versioning support.
29
28
  * If a version field exists in the schema, versioning is applied.
30
29
  *
30
+ * This method automatically handles:
31
+ * - Version field initialization for optimistic locking
32
+ * - Batch insertion for multiple records
33
+ * - Duplicate key handling with optional updates
34
+ *
31
35
  * @template T - The type of the table schema
32
- * @param {T} schema - The entity schema
33
- * @param {Partial<InferInsertModel<T>>[]} models - Array of entities to insert
34
- * @param {boolean} [updateIfExists=false] - Whether to update existing records
35
- * @returns {Promise<number>} The number of inserted rows
36
- * @throws {Error} If the insert operation fails
36
+ * @param schema - The entity schema
37
+ * @param models - Array of entities to insert
38
+ * @param updateIfExists - Whether to update existing records (default: false)
39
+ * @returns Promise that resolves to the number of inserted rows
40
+ * @throws Error if the insert operation fails
37
41
  */
38
42
  async insert<T extends AnyMySqlTable>(
39
43
  schema: T,
@@ -51,10 +55,7 @@ export class ForgeSQLCrudOperations implements CRUDForgeSQL {
51
55
  );
52
56
 
53
57
  // Build insert query
54
- const queryBuilder = this.forgeOperations
55
- .getDrizzleQueryBuilder()
56
- .insert(schema)
57
- .values(preparedModels);
58
+ const queryBuilder = this.forgeOperations.insert(schema).values(preparedModels);
58
59
 
59
60
  // Add onDuplicateKeyUpdate if needed
60
61
  const finalQuery = updateIfExists
@@ -67,6 +68,7 @@ export class ForgeSQLCrudOperations implements CRUDForgeSQL {
67
68
 
68
69
  // Execute query
69
70
  const result = await finalQuery;
71
+ await saveTableIfInsideCacheContext(schema);
70
72
  return result[0].insertId;
71
73
  }
72
74
 
@@ -74,12 +76,18 @@ export class ForgeSQLCrudOperations implements CRUDForgeSQL {
74
76
  * Deletes a record by its primary key with optional version check.
75
77
  * If versioning is enabled, ensures the record hasn't been modified since last read.
76
78
  *
79
+ * This method automatically handles:
80
+ * - Single primary key validation
81
+ * - Optimistic locking checks if versioning is enabled
82
+ * - Version field validation before deletion
83
+ *
77
84
  * @template T - The type of the table schema
78
- * @param {unknown} id - The ID of the record to delete
79
- * @param {T} schema - The entity schema
80
- * @returns {Promise<number>} Number of affected rows
81
- * @throws {Error} If the delete operation fails
82
- * @throws {Error} If multiple primary keys are found
85
+ * @param id - The ID of the record to delete
86
+ * @param schema - The entity schema
87
+ * @returns Promise that resolves to the number of affected rows
88
+ * @throws Error if the delete operation fails
89
+ * @throws Error if multiple primary keys are found
90
+ * @throws Error if optimistic locking check fails
83
91
  */
84
92
  async deleteById<T extends AnyMySqlTable>(id: unknown, schema: T): Promise<number> {
85
93
  const { tableName, columns } = getTableMetadata(schema);
@@ -108,13 +116,13 @@ export class ForgeSQLCrudOperations implements CRUDForgeSQL {
108
116
  }
109
117
 
110
118
  // Execute delete query
111
- const queryBuilder = this.forgeOperations
112
- .getDrizzleQueryBuilder()
113
- .delete(schema)
114
- .where(and(...conditions));
119
+ const queryBuilder = this.forgeOperations.delete(schema).where(and(...conditions));
115
120
 
116
121
  const result = await queryBuilder;
117
-
122
+ if (versionMetadata && result[0].affectedRows === 0) {
123
+ throw new Error(`Optimistic locking failed: record with primary key ${id} has been modified`);
124
+ }
125
+ await saveTableIfInsideCacheContext(schema);
118
126
  return result[0].affectedRows;
119
127
  }
120
128
 
@@ -125,13 +133,19 @@ export class ForgeSQLCrudOperations implements CRUDForgeSQL {
125
133
  * - Checks for concurrent modifications
126
134
  * - Increments the version on successful update
127
135
  *
136
+ * This method automatically handles:
137
+ * - Primary key validation
138
+ * - Version field retrieval and validation
139
+ * - Optimistic locking conflict detection
140
+ * - Version field incrementation
141
+ *
128
142
  * @template T - The type of the table schema
129
- * @param {Partial<InferInsertModel<T>>} entity - The entity with updated values
130
- * @param {T} schema - The entity schema
131
- * @returns {Promise<number>} Number of affected rows
132
- * @throws {Error} If the primary key is not provided
133
- * @throws {Error} If optimistic locking check fails
134
- * @throws {Error} If multiple primary keys are found
143
+ * @param entity - The entity with updated values (must include primary key)
144
+ * @param schema - The entity schema
145
+ * @returns Promise that resolves to the number of affected rows
146
+ * @throws Error if the primary key is not provided
147
+ * @throws Error if optimistic locking check fails
148
+ * @throws Error if multiple primary keys are found
135
149
  */
136
150
  async updateById<T extends AnyMySqlTable>(
137
151
  entity: Partial<InferInsertModel<T>>,
@@ -177,7 +191,6 @@ export class ForgeSQLCrudOperations implements CRUDForgeSQL {
177
191
 
178
192
  // Execute update query
179
193
  const queryBuilder = this.forgeOperations
180
- .getDrizzleQueryBuilder()
181
194
  .update(schema)
182
195
  .set(updateData)
183
196
  .where(and(...conditions));
@@ -189,7 +202,7 @@ export class ForgeSQLCrudOperations implements CRUDForgeSQL {
189
202
  `Optimistic locking failed: record with primary key ${entity[primaryKeyName as keyof typeof entity]} has been modified`,
190
203
  );
191
204
  }
192
-
205
+ await saveTableIfInsideCacheContext(schema);
193
206
  return result[0].affectedRows;
194
207
  }
195
208
 
@@ -214,13 +227,10 @@ export class ForgeSQLCrudOperations implements CRUDForgeSQL {
214
227
  throw new Error("WHERE conditions must be provided");
215
228
  }
216
229
 
217
- const queryBuilder = this.forgeOperations
218
- .getDrizzleQueryBuilder()
219
- .update(schema)
220
- .set(updateData)
221
- .where(where);
230
+ const queryBuilder = this.forgeOperations.update(schema).set(updateData).where(where);
222
231
 
223
232
  const result = await queryBuilder;
233
+ await saveTableIfInsideCacheContext(schema);
224
234
  return result[0].affectedRows;
225
235
  }
226
236
 
@@ -421,7 +431,6 @@ export class ForgeSQLCrudOperations implements CRUDForgeSQL {
421
431
  const [primaryKeyName, primaryKeyColumn] = primaryKeys[0];
422
432
 
423
433
  const resultQuery = this.forgeOperations
424
- .getDrizzleQueryBuilder()
425
434
  .select({
426
435
  [primaryKeyName]: primaryKeyColumn as any,
427
436
  [versionFieldName]: versionFieldColumn as any,