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