forge-sql-orm 1.0.23 → 1.0.25
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 +289 -82
- package/dist/ForgeSQLORM.js +386 -66
- package/dist/ForgeSQLORM.js.map +1 -1
- package/dist/ForgeSQLORM.mjs +386 -66
- package/dist/ForgeSQLORM.mjs.map +1 -1
- package/dist/core/ForgeSQLCrudOperations.d.ts +119 -8
- package/dist/core/ForgeSQLCrudOperations.d.ts.map +1 -1
- package/dist/core/ForgeSQLORM.d.ts +2 -2
- package/dist/core/ForgeSQLORM.d.ts.map +1 -1
- package/dist/core/ForgeSQLQueryBuilder.d.ts +49 -4
- package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
- package/dist/core/ForgeSQLSelectOperations.d.ts +4 -1
- package/dist/core/ForgeSQLSelectOperations.d.ts.map +1 -1
- package/dist/utils/sqlUtils.d.ts +3 -3
- package/dist/utils/sqlUtils.d.ts.map +1 -1
- package/dist-cli/cli.js +84 -13
- package/dist-cli/cli.js.map +1 -1
- package/dist-cli/cli.mjs +84 -13
- package/dist-cli/cli.mjs.map +1 -1
- package/package.json +11 -8
- package/src/core/ForgeSQLCrudOperations.ts +462 -68
- package/src/core/ForgeSQLORM.ts +10 -5
- package/src/core/ForgeSQLQueryBuilder.ts +62 -4
- package/src/core/ForgeSQLSelectOperations.ts +21 -8
- package/src/utils/sqlUtils.ts +17 -8
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sql, UpdateQueryResponse } from "@forge/sql";
|
|
2
2
|
import { EntityProperty, EntitySchema, ForgeSqlOrmOptions } from "..";
|
|
3
3
|
import type { types } from "@mikro-orm/core/types";
|
|
4
4
|
import { transformValue } from "../utils/sqlUtils";
|
|
5
5
|
import { CRUDForgeSQL, ForgeSqlOperation } from "./ForgeSQLQueryBuilder";
|
|
6
|
+
import { EntityKey, QBFilterQuery } from "@mikro-orm/core";
|
|
7
|
+
import Knex from "../knex";
|
|
6
8
|
|
|
7
9
|
export class ForgeSQLCrudOperations implements CRUDForgeSQL {
|
|
8
10
|
private readonly forgeOperations: ForgeSqlOperation;
|
|
@@ -14,104 +16,149 @@ export class ForgeSQLCrudOperations implements CRUDForgeSQL {
|
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
|
-
* Generates an SQL
|
|
19
|
+
* Generates an SQL INSERT statement for the provided models.
|
|
20
|
+
* If a version field exists in the schema, its value is set accordingly.
|
|
21
|
+
*
|
|
18
22
|
* @param schema - The entity schema.
|
|
19
23
|
* @param models - The list of entities to insert.
|
|
20
24
|
* @param updateIfExists - Whether to update the row if it already exists.
|
|
21
|
-
* @returns An object containing the SQL query,
|
|
25
|
+
* @returns An object containing the SQL query, column names, and values.
|
|
22
26
|
*/
|
|
23
27
|
private async generateInsertScript<T extends object>(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
schema: EntitySchema<T>,
|
|
29
|
+
models: T[],
|
|
30
|
+
updateIfExists: boolean,
|
|
27
31
|
): Promise<{
|
|
28
32
|
sql: string;
|
|
33
|
+
query: string;
|
|
29
34
|
fields: string[];
|
|
30
35
|
values: { type: keyof typeof types; value: unknown }[];
|
|
31
36
|
}> {
|
|
32
|
-
const
|
|
33
|
-
const
|
|
37
|
+
const columnNames = new Set<string>();
|
|
38
|
+
const modelFieldValues: Record<string, { type: keyof typeof types; value: unknown }>[] = [];
|
|
34
39
|
|
|
40
|
+
// Build field values for each model.
|
|
35
41
|
models.forEach((model) => {
|
|
36
|
-
const
|
|
37
|
-
schema.meta.props.forEach((
|
|
38
|
-
const
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
value: modelValue,
|
|
44
|
-
};
|
|
42
|
+
const fieldValues: Record<string, { type: keyof typeof types; value: unknown }> = {};
|
|
43
|
+
schema.meta.props.forEach((prop) => {
|
|
44
|
+
const value = model[prop.name];
|
|
45
|
+
if (prop.kind === "scalar" && value !== undefined) {
|
|
46
|
+
const columnName = this.getRealFieldNameFromSchema(prop);
|
|
47
|
+
columnNames.add(columnName);
|
|
48
|
+
fieldValues[columnName] = { type: prop.type as keyof typeof types, value };
|
|
45
49
|
}
|
|
46
50
|
});
|
|
47
|
-
|
|
51
|
+
modelFieldValues.push(fieldValues);
|
|
48
52
|
});
|
|
49
53
|
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
// If a version field exists, set or update its value.
|
|
55
|
+
const versionField = this.getVersionField(schema);
|
|
56
|
+
if (versionField) {
|
|
57
|
+
modelFieldValues.forEach((mv) => {
|
|
58
|
+
const versionRealName = this.getRealFieldNameFromSchema(versionField);
|
|
59
|
+
if (mv[versionRealName]) {
|
|
60
|
+
mv[versionRealName].value = transformValue(
|
|
61
|
+
{ value: this.createVersionField(versionField), type: versionField.name },
|
|
62
|
+
true,
|
|
63
|
+
);
|
|
64
|
+
} else {
|
|
65
|
+
mv[versionRealName] = {
|
|
66
|
+
type: versionField.type as keyof typeof types,
|
|
67
|
+
value: transformValue(
|
|
68
|
+
{ value: this.createVersionField(versionField), type: versionField.name },
|
|
69
|
+
true,
|
|
70
|
+
),
|
|
71
|
+
};
|
|
72
|
+
columnNames.add(versionField.name);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const columns = Array.from(columnNames);
|
|
78
|
+
|
|
79
|
+
// Flatten values for each row in the order of columns.
|
|
80
|
+
const values = modelFieldValues.flatMap((fieldValueMap) =>
|
|
81
|
+
columns.map(
|
|
82
|
+
(column) =>
|
|
83
|
+
fieldValueMap[column] || {
|
|
84
|
+
type: "string",
|
|
85
|
+
value: null,
|
|
86
|
+
},
|
|
87
|
+
),
|
|
59
88
|
);
|
|
60
89
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
.map(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
90
|
+
// Build the VALUES clause.
|
|
91
|
+
const insertValues = modelFieldValues
|
|
92
|
+
.map((fieldValueMap) => {
|
|
93
|
+
const rowValues = columns
|
|
94
|
+
.map((column) =>
|
|
95
|
+
transformValue(
|
|
96
|
+
fieldValueMap[column] || { type: "string", value: null },
|
|
97
|
+
true,
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
.join(",");
|
|
101
|
+
return `(${rowValues})`;
|
|
102
|
+
})
|
|
103
|
+
.join(", ");
|
|
104
|
+
// Build the VALUES ? clause.
|
|
105
|
+
const insertEmptyValues = modelFieldValues
|
|
106
|
+
.map(() => {
|
|
107
|
+
const rowValues = columns
|
|
108
|
+
.map(() =>
|
|
109
|
+
'?',
|
|
73
110
|
)
|
|
74
|
-
.join(",")
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
111
|
+
.join(",");
|
|
112
|
+
return `(${rowValues})`;
|
|
113
|
+
})
|
|
114
|
+
.join(", ");
|
|
115
|
+
|
|
116
|
+
const updateClause = updateIfExists
|
|
117
|
+
? ` ON DUPLICATE KEY UPDATE ${columns.map((col) => `${col} = VALUES(${col})`).join(",")}`
|
|
118
|
+
: "";
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
sql: `INSERT INTO ${schema.meta.collection} (${columns.join(",")}) VALUES ${insertValues}${updateClause}`,
|
|
122
|
+
query: `INSERT INTO ${schema.meta.collection} (${columns.join(",")}) VALUES ${insertEmptyValues}${updateClause}`,
|
|
123
|
+
fields: columns,
|
|
80
124
|
values,
|
|
81
125
|
};
|
|
82
126
|
}
|
|
83
127
|
|
|
84
128
|
/**
|
|
85
129
|
* Inserts records into the database.
|
|
130
|
+
* If a version field exists in the schema, versioning is applied.
|
|
131
|
+
*
|
|
86
132
|
* @param schema - The entity schema.
|
|
87
133
|
* @param models - The list of entities to insert.
|
|
88
134
|
* @param updateIfExists - Whether to update the row if it already exists.
|
|
89
135
|
* @returns The ID of the inserted row.
|
|
90
136
|
*/
|
|
91
137
|
async insert<T extends object>(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
138
|
+
schema: EntitySchema<T>,
|
|
139
|
+
models: T[],
|
|
140
|
+
updateIfExists: boolean = false,
|
|
95
141
|
): Promise<number> {
|
|
96
142
|
if (!models || models.length === 0) return 0;
|
|
97
143
|
|
|
98
144
|
const query = await this.generateInsertScript(schema, models, updateIfExists);
|
|
99
145
|
if (this.options?.logRawSqlQuery) {
|
|
100
|
-
console.debug("INSERT SQL: " + query.
|
|
146
|
+
console.debug("INSERT SQL: " + query.query);
|
|
101
147
|
}
|
|
102
148
|
const sqlStatement = sql.prepare<UpdateQueryResponse>(query.sql);
|
|
103
|
-
const
|
|
104
|
-
return
|
|
149
|
+
const result = await sqlStatement.execute();
|
|
150
|
+
return result.rows.insertId;
|
|
105
151
|
}
|
|
106
152
|
|
|
107
153
|
/**
|
|
108
|
-
* Retrieves the primary
|
|
154
|
+
* Retrieves the primary key properties from the entity schema.
|
|
155
|
+
*
|
|
109
156
|
* @param schema - The entity schema.
|
|
110
157
|
* @returns An array of primary key properties.
|
|
111
158
|
* @throws If no primary keys are found.
|
|
112
159
|
*/
|
|
113
160
|
private getPrimaryKeys<T extends object>(schema: EntitySchema<T>): EntityProperty<T, unknown>[] {
|
|
114
|
-
const primaryKeys = schema.meta.props.filter((
|
|
161
|
+
const primaryKeys = schema.meta.props.filter((prop) => prop.primary);
|
|
115
162
|
if (!primaryKeys.length) {
|
|
116
163
|
throw new Error(`No primary keys found for schema: ${schema.meta.className}`);
|
|
117
164
|
}
|
|
@@ -119,7 +166,8 @@ export class ForgeSQLCrudOperations implements CRUDForgeSQL {
|
|
|
119
166
|
}
|
|
120
167
|
|
|
121
168
|
/**
|
|
122
|
-
* Deletes a record by its
|
|
169
|
+
* Deletes a record by its primary key.
|
|
170
|
+
*
|
|
123
171
|
* @param id - The ID of the record to delete.
|
|
124
172
|
* @param schema - The entity schema.
|
|
125
173
|
* @returns The number of rows affected.
|
|
@@ -137,34 +185,380 @@ export class ForgeSQLCrudOperations implements CRUDForgeSQL {
|
|
|
137
185
|
|
|
138
186
|
const query = queryBuilder.getFormattedQuery();
|
|
139
187
|
if (this.options?.logRawSqlQuery) {
|
|
140
|
-
console.debug("DELETE SQL: " +
|
|
188
|
+
console.debug("DELETE SQL: " + queryBuilder.getQuery());
|
|
141
189
|
}
|
|
142
190
|
const sqlStatement = sql.prepare<UpdateQueryResponse>(query);
|
|
143
|
-
const
|
|
144
|
-
return
|
|
191
|
+
const result = await sqlStatement.execute();
|
|
192
|
+
return result.rows.affectedRows;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Retrieves the version field from the entity schema.
|
|
197
|
+
* The version field must be of type datetime, integer, or decimal, not a primary key, and not nullable.
|
|
198
|
+
*
|
|
199
|
+
* @param schema - The entity schema.
|
|
200
|
+
* @returns The version field property if it exists.
|
|
201
|
+
*/
|
|
202
|
+
getVersionField<T>(schema: EntitySchema<T>) {
|
|
203
|
+
if (this.options.disableOptimisticLocking){
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
return schema.meta.props
|
|
207
|
+
.filter((prop) => prop.version)
|
|
208
|
+
.filter((prop) => {
|
|
209
|
+
const validType =
|
|
210
|
+
prop.type === "datetime" || prop.type === "integer" || prop.type === "decimal";
|
|
211
|
+
if (!validType) {
|
|
212
|
+
console.warn(
|
|
213
|
+
`Version field "${prop.name}" in table ${schema.meta.tableName} must be datetime, integer, or decimal, but is "${prop.type}"`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
return validType;
|
|
217
|
+
})
|
|
218
|
+
.filter((prop) => {
|
|
219
|
+
if (prop.primary) {
|
|
220
|
+
console.warn(
|
|
221
|
+
`Version field "${prop.name}" in table ${schema.meta.tableName} cannot be a primary key`,
|
|
222
|
+
);
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
})
|
|
227
|
+
.find((prop) => {
|
|
228
|
+
if (prop.nullable) {
|
|
229
|
+
console.warn(
|
|
230
|
+
`Version field "${prop.name}" in table ${schema.meta.tableName} should not be nullable`,
|
|
231
|
+
);
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
return true;
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Increments the version field of an entity.
|
|
240
|
+
* For datetime types, sets the current date; for numeric types, increments by 1.
|
|
241
|
+
*
|
|
242
|
+
* @param versionField - The version field property.
|
|
243
|
+
* @param updateModel - The entity to update.
|
|
244
|
+
*/
|
|
245
|
+
incrementVersionField<T>(versionField: EntityProperty<T, any>, updateModel: T): void {
|
|
246
|
+
const key = versionField.name as keyof T;
|
|
247
|
+
switch (versionField.type) {
|
|
248
|
+
case "datetime": {
|
|
249
|
+
updateModel[key] = new Date() as unknown as T[keyof T];
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
case "decimal":
|
|
253
|
+
case "integer": {
|
|
254
|
+
updateModel[key] = ((updateModel[key] as number) + 1) as unknown as T[keyof T];
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
default:
|
|
258
|
+
throw new Error(`Unsupported version field type: ${versionField.type}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Creates the initial version field value for an entity.
|
|
264
|
+
* For datetime types, returns the current date; for numeric types, returns 0.
|
|
265
|
+
*
|
|
266
|
+
* @param versionField - The version field property.
|
|
267
|
+
*/
|
|
268
|
+
createVersionField<T>(versionField: EntityProperty<T>): unknown {
|
|
269
|
+
switch (versionField.type) {
|
|
270
|
+
case "datetime": {
|
|
271
|
+
return new Date() as unknown as T[keyof T];
|
|
272
|
+
}
|
|
273
|
+
case "decimal":
|
|
274
|
+
case "integer": {
|
|
275
|
+
return 0;
|
|
276
|
+
}
|
|
277
|
+
default:
|
|
278
|
+
throw new Error(`Unsupported version field type: ${versionField.type}`);
|
|
279
|
+
}
|
|
145
280
|
}
|
|
146
281
|
|
|
147
282
|
/**
|
|
148
|
-
* Updates a record by its
|
|
283
|
+
* Updates a record by its primary key using the provided entity data.
|
|
284
|
+
*
|
|
149
285
|
* @param entity - The entity with updated values.
|
|
150
286
|
* @param schema - The entity schema.
|
|
151
|
-
* @throws If the primary key value is missing in the entity.
|
|
152
287
|
*/
|
|
153
|
-
async updateById<T extends object>(entity: T
|
|
288
|
+
async updateById<T extends object>(entity: Partial<T>, schema: EntitySchema<T>): Promise<void> {
|
|
289
|
+
const fields = schema.meta.props
|
|
290
|
+
.filter((prop) => prop.kind === "scalar")
|
|
291
|
+
.map((prop) => prop.name);
|
|
292
|
+
await this.updateFieldById(entity as T, fields, schema);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Updates specified fields of records based on provided conditions.
|
|
297
|
+
* If the "where" parameter is not provided, the WHERE clause is built from the entity fields
|
|
298
|
+
* that are not included in the list of fields to update.
|
|
299
|
+
*
|
|
300
|
+
* @param entity - The object containing values to update and potential criteria for filtering.
|
|
301
|
+
* @param fields - Array of field names to update.
|
|
302
|
+
* @param schema - The entity schema.
|
|
303
|
+
* @param where - Optional filtering conditions for the WHERE clause.
|
|
304
|
+
* @returns The number of affected rows.
|
|
305
|
+
* @throws If no filtering criteria are provided (either via "where" or from the remaining entity fields).
|
|
306
|
+
*/
|
|
307
|
+
async updateFields<T extends object>(
|
|
308
|
+
entity: Partial<T>,
|
|
309
|
+
fields: EntityKey<T>[],
|
|
310
|
+
schema: EntitySchema<T>,
|
|
311
|
+
where?: QBFilterQuery<T>,
|
|
312
|
+
): Promise<number> {
|
|
313
|
+
// Extract update data from the entity based on the provided fields.
|
|
314
|
+
const updateData = this.filterEntityFields(entity, fields);
|
|
315
|
+
const updateModel = this.modifyModel(updateData as T, schema);
|
|
316
|
+
|
|
317
|
+
// Create the query builder for the entity.
|
|
318
|
+
let queryBuilder = this.forgeOperations
|
|
319
|
+
.createQueryBuilder(schema.meta.class)
|
|
320
|
+
.getKnexQuery();
|
|
321
|
+
|
|
322
|
+
// Set the update data.
|
|
323
|
+
queryBuilder.update(updateModel as T);
|
|
324
|
+
|
|
325
|
+
// Use the provided "where" conditions if available; otherwise, build conditions from the remaining entity fields.
|
|
326
|
+
if (where) {
|
|
327
|
+
queryBuilder.where(where);
|
|
328
|
+
} else {
|
|
329
|
+
const filterCriteria = (Object.keys(entity) as Array<keyof T>)
|
|
330
|
+
.filter((key: keyof T) => !fields.includes(key as EntityKey<T>))
|
|
331
|
+
.reduce((criteria, key) => {
|
|
332
|
+
if (entity[key] !== undefined) {
|
|
333
|
+
// Cast key to string to use it as an object key.
|
|
334
|
+
criteria[key as string] = entity[key];
|
|
335
|
+
}
|
|
336
|
+
return criteria;
|
|
337
|
+
}, {} as Record<string, unknown>);
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
if (Object.keys(filterCriteria).length === 0) {
|
|
341
|
+
throw new Error(
|
|
342
|
+
"Filtering criteria (WHERE clause) must be provided either via the 'where' parameter or through non-updated entity fields"
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
queryBuilder.where(filterCriteria);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (this.options?.logRawSqlQuery) {
|
|
349
|
+
console.debug("UPDATE SQL (updateFields): " + queryBuilder.toSQL().sql);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Execute the update query.
|
|
353
|
+
const sqlQuery = queryBuilder.toQuery();
|
|
354
|
+
const updateQueryResponse = await this.forgeOperations.fetch().executeRawUpdateSQL(sqlQuery);
|
|
355
|
+
return updateQueryResponse.affectedRows;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Updates specific fields of a record identified by its primary key.
|
|
361
|
+
* If a version field exists in the schema, versioning is applied.
|
|
362
|
+
*
|
|
363
|
+
* @param entity - The entity with updated values.
|
|
364
|
+
* @param fields - The list of field names to update.
|
|
365
|
+
* @param schema - The entity schema.
|
|
366
|
+
* @throws If the primary key is not included in the update fields.
|
|
367
|
+
*/
|
|
368
|
+
async updateFieldById<T extends object>(
|
|
369
|
+
entity: T,
|
|
370
|
+
fields: EntityKey<T>[],
|
|
371
|
+
schema: EntitySchema<T>,
|
|
372
|
+
): Promise<void> {
|
|
154
373
|
const primaryKeys = this.getPrimaryKeys(schema);
|
|
155
|
-
|
|
374
|
+
primaryKeys.forEach((pk) => {
|
|
375
|
+
if (!fields.includes(pk.name)) {
|
|
376
|
+
throw new Error("Update fields must include primary key: " + pk.name);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// Prepare updated entity and query builder.
|
|
381
|
+
const updatedEntity = this.filterEntityFields(entity, fields);
|
|
382
|
+
let queryBuilder = this.forgeOperations.createQueryBuilder(schema.meta.class).getKnexQuery();
|
|
383
|
+
const versionField = this.getVersionField(schema);
|
|
384
|
+
const useVersion = Boolean(versionField);
|
|
385
|
+
let updateModel = { ...updatedEntity };
|
|
386
|
+
|
|
387
|
+
if (useVersion && versionField) {
|
|
388
|
+
// If the version field is missing from the entity, load the old record.
|
|
389
|
+
let oldModel = entity;
|
|
390
|
+
if (entity[versionField.name] === undefined || entity[versionField.name] === null) {
|
|
391
|
+
oldModel = await this.getOldModel(primaryKeys, entity, schema, versionField);
|
|
392
|
+
}
|
|
393
|
+
const primaryFieldNames = primaryKeys.map((pk) => pk.name);
|
|
394
|
+
const fieldsToRetain = primaryFieldNames.concat(versionField.name);
|
|
395
|
+
const fromEntries = Object.fromEntries(fieldsToRetain.map((key) => [key, oldModel[key]]));
|
|
396
|
+
updateModel = { ...updatedEntity, ...fromEntries };
|
|
397
|
+
|
|
398
|
+
// Increment the version field.
|
|
399
|
+
this.incrementVersionField(versionField, updateModel as T);
|
|
156
400
|
|
|
401
|
+
updateModel = this.modifyModel(updateModel as T, schema);
|
|
402
|
+
queryBuilder.update(updateModel as T);
|
|
403
|
+
if (oldModel[versionField.name]!==undefined || oldModel[versionField.name]!==null && this.isValid(oldModel[versionField.name])) {
|
|
404
|
+
queryBuilder.andWhere(this.optWhere(oldModel, versionField));
|
|
405
|
+
}
|
|
406
|
+
} else {
|
|
407
|
+
updateModel = this.modifyModel(updatedEntity as T, schema);
|
|
408
|
+
queryBuilder.update(updateModel as T);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
this.addPrimaryWhere(queryBuilder as unknown as Knex.QueryBuilder<any, any>, primaryKeys, updateModel as T);
|
|
412
|
+
const sqlQuery = queryBuilder.toQuery();
|
|
413
|
+
|
|
414
|
+
if (this.options?.logRawSqlQuery) {
|
|
415
|
+
console.debug("UPDATE SQL: " + queryBuilder.toSQL().sql);
|
|
416
|
+
}
|
|
417
|
+
const updateQueryResponse = await this.forgeOperations.fetch().executeRawUpdateSQL(sqlQuery);
|
|
418
|
+
if (versionField && !updateQueryResponse.affectedRows) {
|
|
419
|
+
throw new Error(
|
|
420
|
+
"Optimistic locking failed: the record with primary key(s) " +
|
|
421
|
+
primaryKeys.map((p) => updateModel[p.name]).join(", ") +
|
|
422
|
+
" has been modified by another process.",
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Constructs an optional WHERE clause for the version field.
|
|
429
|
+
*
|
|
430
|
+
* @param updateModel - The model containing the current version field value.
|
|
431
|
+
* @param versionField - The version field property.
|
|
432
|
+
* @returns A filter query for the version field.
|
|
433
|
+
*/
|
|
434
|
+
private optWhere<T>(
|
|
435
|
+
updateModel: T,
|
|
436
|
+
versionField: EntityProperty<T>,
|
|
437
|
+
): QBFilterQuery<unknown> {
|
|
438
|
+
const currentVersionValue = transformValue(
|
|
439
|
+
{ value: updateModel[versionField.name], type: versionField.type },
|
|
440
|
+
false,
|
|
441
|
+
);
|
|
442
|
+
return { [versionField.name]: currentVersionValue };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Retrieves the current state of a record from the database.
|
|
447
|
+
*
|
|
448
|
+
* @param primaryKeys - The primary key properties.
|
|
449
|
+
* @param entity - The entity with updated values.
|
|
450
|
+
* @param schema - The entity schema.
|
|
451
|
+
* @param versionField - The version field property.
|
|
452
|
+
* @returns The existing record from the database.
|
|
453
|
+
* @throws If the record does not exist or if multiple records are found.
|
|
454
|
+
*/
|
|
455
|
+
private async getOldModel<T>(
|
|
456
|
+
primaryKeys: EntityProperty<T, unknown>[],
|
|
457
|
+
entity: T,
|
|
458
|
+
schema: EntitySchema<T>,
|
|
459
|
+
versionField: EntityProperty<T>,
|
|
460
|
+
): Promise<T> {
|
|
461
|
+
const primaryFieldNames = primaryKeys.map((pk) => pk.name);
|
|
462
|
+
const fieldsToSelect = primaryFieldNames.concat(versionField.name);
|
|
463
|
+
const queryBuilder = this.forgeOperations
|
|
464
|
+
.createQueryBuilder(schema as EntitySchema)
|
|
465
|
+
.select(fieldsToSelect);
|
|
466
|
+
this.addPrimaryWhere(queryBuilder, primaryKeys, entity);
|
|
467
|
+
const formattedQuery = queryBuilder.getFormattedQuery();
|
|
468
|
+
const models: T[] = await this.forgeOperations.fetch().executeSchemaSQL(formattedQuery, schema as EntitySchema);
|
|
469
|
+
|
|
470
|
+
if (!models || models.length === 0) {
|
|
471
|
+
throw new Error(`Cannot modify record because it does not exist in table ${schema.meta.tableName}`);
|
|
472
|
+
}
|
|
473
|
+
if (models.length > 1) {
|
|
474
|
+
throw new Error(
|
|
475
|
+
`Cannot modify record because multiple rows with the same ID were found in table ${schema.meta.tableName}. Please verify the table metadata.`,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
return models[0];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Adds primary key conditions to the query builder.
|
|
483
|
+
*
|
|
484
|
+
* @param queryBuilder - The Knex query builder instance.
|
|
485
|
+
* @param primaryKeys - The primary key properties.
|
|
486
|
+
* @param entity - The entity containing primary key values.
|
|
487
|
+
* @throws If any primary key value is missing.
|
|
488
|
+
*/
|
|
489
|
+
private addPrimaryWhere<T>(
|
|
490
|
+
queryBuilder: Knex.QueryBuilder<any, any>,
|
|
491
|
+
primaryKeys: EntityProperty<T, unknown>[],
|
|
492
|
+
entity: T,
|
|
493
|
+
) {
|
|
157
494
|
primaryKeys.forEach((pk) => {
|
|
158
|
-
const
|
|
495
|
+
const fieldName = this.getRealFieldNameFromSchema(pk);
|
|
496
|
+
const value = entity[fieldName];
|
|
159
497
|
if (value === null || value === undefined) {
|
|
160
|
-
throw new Error(`Primary
|
|
498
|
+
throw new Error(`Primary key ${fieldName} must exist in the model`);
|
|
161
499
|
}
|
|
162
|
-
queryBuilder.andWhere({ [
|
|
500
|
+
queryBuilder.andWhere({ [fieldName]: value });
|
|
163
501
|
});
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Filters the entity to include only the specified fields.
|
|
506
|
+
*
|
|
507
|
+
* @param entity - The original entity.
|
|
508
|
+
* @param fields - The list of fields to retain.
|
|
509
|
+
* @returns A partial entity object containing only the specified fields.
|
|
510
|
+
*/
|
|
511
|
+
filterEntityFields = <T extends object>(entity: T, fields: (keyof T)[]): Partial<T> =>
|
|
512
|
+
fields.reduce((result, field) => {
|
|
513
|
+
if (field in entity) {
|
|
514
|
+
result[field] = entity[field];
|
|
515
|
+
}
|
|
516
|
+
return result;
|
|
517
|
+
}, {} as Partial<T>);
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Transforms and modifies the updated entity model based on the schema.
|
|
521
|
+
*
|
|
522
|
+
* @param updatedEntity - The updated entity.
|
|
523
|
+
* @param schema - The entity schema.
|
|
524
|
+
* @returns The modified entity.
|
|
525
|
+
*/
|
|
526
|
+
private modifyModel<T>(updatedEntity: T, schema: EntitySchema<T>): T {
|
|
527
|
+
const modifiedModel: Record<string, any> = {};
|
|
528
|
+
schema.meta.props
|
|
529
|
+
.filter((p) => p.kind === "scalar")
|
|
530
|
+
.forEach((p) => {
|
|
531
|
+
const value = updatedEntity[p.name];
|
|
532
|
+
if (value !== undefined && value !== null) {
|
|
533
|
+
const fieldName = this.getRealFieldNameFromSchema(p);
|
|
534
|
+
modifiedModel[fieldName] = transformValue({ value, type: p.type }, false);
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
return modifiedModel as T;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Returns the real field name from the entity property based on the schema.
|
|
542
|
+
*
|
|
543
|
+
* @param p - The entity property.
|
|
544
|
+
* @returns The real field name.
|
|
545
|
+
*/
|
|
546
|
+
private getRealFieldNameFromSchema<T>(p: EntityProperty<T>): EntityKey<T> {
|
|
547
|
+
return p.fieldNames && p.fieldNames.length
|
|
548
|
+
? (p.fieldNames[0] as EntityKey<T>)
|
|
549
|
+
: p.name;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Validates the provided value.
|
|
554
|
+
*
|
|
555
|
+
* @param value - The value to validate.
|
|
556
|
+
* @returns True if the value is valid, false otherwise.
|
|
557
|
+
*/
|
|
558
|
+
isValid(value: any): boolean {
|
|
559
|
+
if (value instanceof Date) {
|
|
560
|
+
return !isNaN(value.getTime());
|
|
167
561
|
}
|
|
168
|
-
|
|
562
|
+
return true;
|
|
169
563
|
}
|
|
170
564
|
}
|
package/src/core/ForgeSQLORM.ts
CHANGED
|
@@ -53,7 +53,7 @@ class ForgeSQLORMImpl implements ForgeSqlOperation {
|
|
|
53
53
|
preferTs: false,
|
|
54
54
|
debug: false,
|
|
55
55
|
});
|
|
56
|
-
const newOptions: ForgeSqlOrmOptions = options ?? { logRawSqlQuery: false };
|
|
56
|
+
const newOptions: ForgeSqlOrmOptions = options ?? { logRawSqlQuery: false, disableOptimisticLocking: false };
|
|
57
57
|
this.crudOperations = new ForgeSQLCrudOperations(this, newOptions);
|
|
58
58
|
this.fetchOperations = new ForgeSQLSelectOperations(newOptions);
|
|
59
59
|
} catch (error) {
|
|
@@ -65,13 +65,15 @@ class ForgeSQLORMImpl implements ForgeSqlOperation {
|
|
|
65
65
|
/**
|
|
66
66
|
* Returns the singleton instance of ForgeSQLORMImpl.
|
|
67
67
|
* @param entities - List of entities (required only on first initialization).
|
|
68
|
+
* @param options - Options for configuring ForgeSQL ORM behavior.
|
|
68
69
|
* @returns The singleton instance of ForgeSQLORMImpl.
|
|
69
70
|
*/
|
|
70
71
|
static getInstance(
|
|
71
72
|
entities: (EntityClass<AnyEntity> | EntityClassGroup<AnyEntity> | EntitySchema)[],
|
|
73
|
+
options?: ForgeSqlOrmOptions,
|
|
72
74
|
): ForgeSqlOperation {
|
|
73
75
|
if (!ForgeSQLORMImpl.instance) {
|
|
74
|
-
ForgeSQLORMImpl.instance = new ForgeSQLORMImpl(entities);
|
|
76
|
+
ForgeSQLORMImpl.instance = new ForgeSQLORMImpl(entities, options);
|
|
75
77
|
}
|
|
76
78
|
return ForgeSQLORMImpl.instance;
|
|
77
79
|
}
|
|
@@ -108,7 +110,7 @@ class ForgeSQLORMImpl implements ForgeSqlOperation {
|
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
/**
|
|
111
|
-
* Provides access to the underlying Knex instance for
|
|
113
|
+
* Provides access to the underlying Knex instance for building complex query parts.
|
|
112
114
|
* enabling advanced query customization and performance tuning.
|
|
113
115
|
* @returns The Knex instance, which can be used for query building.
|
|
114
116
|
*/
|
|
@@ -123,8 +125,11 @@ class ForgeSQLORMImpl implements ForgeSqlOperation {
|
|
|
123
125
|
class ForgeSQLORM {
|
|
124
126
|
private readonly ormInstance: ForgeSqlOperation;
|
|
125
127
|
|
|
126
|
-
constructor(
|
|
127
|
-
|
|
128
|
+
constructor(
|
|
129
|
+
entities: (EntityClass<AnyEntity> | EntityClassGroup<AnyEntity> | EntitySchema)[],
|
|
130
|
+
options?: ForgeSqlOrmOptions,
|
|
131
|
+
) {
|
|
132
|
+
this.ormInstance = ForgeSQLORMImpl.getInstance(entities, options);
|
|
128
133
|
}
|
|
129
134
|
|
|
130
135
|
/**
|