forge-sql-orm 1.0.31 → 1.1.31
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 +216 -695
- package/dist/ForgeSQLORM.js +538 -567
- package/dist/ForgeSQLORM.js.map +1 -1
- package/dist/ForgeSQLORM.mjs +536 -554
- package/dist/ForgeSQLORM.mjs.map +1 -1
- package/dist/core/ForgeSQLCrudOperations.d.ts +101 -130
- package/dist/core/ForgeSQLCrudOperations.d.ts.map +1 -1
- package/dist/core/ForgeSQLORM.d.ts +11 -10
- package/dist/core/ForgeSQLORM.d.ts.map +1 -1
- package/dist/core/ForgeSQLQueryBuilder.d.ts +271 -113
- package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
- package/dist/core/ForgeSQLSelectOperations.d.ts +65 -22
- package/dist/core/ForgeSQLSelectOperations.d.ts.map +1 -1
- package/dist/core/SystemTables.d.ts +59 -0
- package/dist/core/SystemTables.d.ts.map +1 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/utils/sqlUtils.d.ts +53 -6
- package/dist/utils/sqlUtils.d.ts.map +1 -1
- package/dist-cli/cli.js +471 -397
- package/dist-cli/cli.js.map +1 -1
- package/dist-cli/cli.mjs +471 -397
- package/dist-cli/cli.mjs.map +1 -1
- package/package.json +21 -22
- package/src/core/ForgeSQLCrudOperations.ts +360 -473
- package/src/core/ForgeSQLORM.ts +38 -79
- package/src/core/ForgeSQLQueryBuilder.ts +250 -133
- package/src/core/ForgeSQLSelectOperations.ts +182 -72
- package/src/core/SystemTables.ts +7 -0
- package/src/index.ts +1 -2
- package/src/utils/sqlUtils.ts +155 -23
- package/dist/core/ComplexQuerySchemaBuilder.d.ts +0 -38
- package/dist/core/ComplexQuerySchemaBuilder.d.ts.map +0 -1
- package/dist/knex/index.d.ts +0 -4
- package/dist/knex/index.d.ts.map +0 -1
- package/src/core/ComplexQuerySchemaBuilder.ts +0 -63
- package/src/knex/index.ts +0 -4
|
@@ -1,564 +1,451 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { EntityProperty, EntitySchema, ForgeSqlOrmOptions } from "..";
|
|
3
|
-
import type { types } from "@mikro-orm/core/types";
|
|
4
|
-
import { transformValue } from "../utils/sqlUtils";
|
|
1
|
+
import { ForgeSqlOrmOptions } from "..";
|
|
5
2
|
import { CRUDForgeSQL, ForgeSqlOperation } from "./ForgeSQLQueryBuilder";
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
|
|
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";
|
|
7
|
+
import { getPrimaryKeys, getTableMetadata } from "../utils/sqlUtils";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Class implementing CRUD operations for ForgeSQL ORM.
|
|
11
|
+
* Provides methods for inserting, updating, and deleting records with support for optimistic locking.
|
|
12
|
+
*/
|
|
9
13
|
export class ForgeSQLCrudOperations implements CRUDForgeSQL {
|
|
10
14
|
private readonly forgeOperations: ForgeSqlOperation;
|
|
11
15
|
private readonly options: ForgeSqlOrmOptions;
|
|
12
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Creates a new instance of ForgeSQLCrudOperations.
|
|
19
|
+
* @param forgeSqlOperations - The ForgeSQL operations instance
|
|
20
|
+
* @param options - Configuration options for the ORM
|
|
21
|
+
*/
|
|
13
22
|
constructor(forgeSqlOperations: ForgeSqlOperation, options: ForgeSqlOrmOptions) {
|
|
14
23
|
this.forgeOperations = forgeSqlOperations;
|
|
15
24
|
this.options = options;
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
/**
|
|
19
|
-
*
|
|
20
|
-
* If a version field exists in the schema,
|
|
28
|
+
* Inserts records into the database with optional versioning support.
|
|
29
|
+
* If a version field exists in the schema, versioning is applied.
|
|
21
30
|
*
|
|
22
|
-
* @
|
|
23
|
-
* @param
|
|
24
|
-
* @param
|
|
25
|
-
* @
|
|
31
|
+
* @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
|
|
26
37
|
*/
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
): Promise<{
|
|
32
|
-
|
|
33
|
-
query: string;
|
|
34
|
-
fields: string[];
|
|
35
|
-
values: { type: keyof typeof types; value: unknown }[];
|
|
36
|
-
}> {
|
|
37
|
-
const columnNames = new Set<string>();
|
|
38
|
-
const modelFieldValues: Record<string, { type: keyof typeof types; value: unknown }>[] = [];
|
|
39
|
-
|
|
40
|
-
// Build field values for each model.
|
|
41
|
-
models.forEach((model) => {
|
|
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 };
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
modelFieldValues.push(fieldValues);
|
|
52
|
-
});
|
|
53
|
-
|
|
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
|
-
}
|
|
38
|
+
async insert<T extends AnyMySqlTable>(
|
|
39
|
+
schema: T,
|
|
40
|
+
models: Partial<InferInsertModel<T>>[],
|
|
41
|
+
updateIfExists: boolean = false,
|
|
42
|
+
): Promise<number> {
|
|
43
|
+
if (!models?.length) return 0;
|
|
76
44
|
|
|
77
|
-
const columns =
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
fieldValueMap[column] || {
|
|
84
|
-
type: "string",
|
|
85
|
-
value: null,
|
|
86
|
-
},
|
|
87
|
-
),
|
|
45
|
+
const { tableName, columns } = getTableMetadata(schema);
|
|
46
|
+
const versionMetadata = this.validateVersionField(tableName, columns);
|
|
47
|
+
|
|
48
|
+
// Prepare models with version field if needed
|
|
49
|
+
const preparedModels = models.map((model) =>
|
|
50
|
+
this.prepareModelWithVersion(model, versionMetadata, columns),
|
|
88
51
|
);
|
|
89
52
|
|
|
90
|
-
// Build
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
})
|
|
103
|
-
.join(", ");
|
|
104
|
-
// Build the VALUES ? clause.
|
|
105
|
-
const insertEmptyValues = modelFieldValues
|
|
106
|
-
.map(() => {
|
|
107
|
-
const rowValues = columns
|
|
108
|
-
.map(() =>
|
|
109
|
-
'?',
|
|
110
|
-
)
|
|
111
|
-
.join(",");
|
|
112
|
-
return `(${rowValues})`;
|
|
53
|
+
// Build insert query
|
|
54
|
+
const queryBuilder = this.forgeOperations
|
|
55
|
+
.getDrizzleQueryBuilder()
|
|
56
|
+
.insert(schema)
|
|
57
|
+
.values(preparedModels);
|
|
58
|
+
|
|
59
|
+
// Add onDuplicateKeyUpdate if needed
|
|
60
|
+
const finalQuery = updateIfExists
|
|
61
|
+
? queryBuilder.onDuplicateKeyUpdate({
|
|
62
|
+
set: Object.fromEntries(
|
|
63
|
+
Object.keys(preparedModels[0]).map((key) => [key, (schema as any)[key]]),
|
|
64
|
+
) as any,
|
|
113
65
|
})
|
|
114
|
-
|
|
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,
|
|
124
|
-
values,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Inserts records into the database.
|
|
130
|
-
* If a version field exists in the schema, versioning is applied.
|
|
131
|
-
*
|
|
132
|
-
* @param schema - The entity schema.
|
|
133
|
-
* @param models - The list of entities to insert.
|
|
134
|
-
* @param updateIfExists - Whether to update the row if it already exists.
|
|
135
|
-
* @returns The ID of the inserted row.
|
|
136
|
-
*/
|
|
137
|
-
async insert<T extends object>(
|
|
138
|
-
schema: EntitySchema<T>,
|
|
139
|
-
models: T[],
|
|
140
|
-
updateIfExists: boolean = false,
|
|
141
|
-
): Promise<number> {
|
|
142
|
-
if (!models || models.length === 0) return 0;
|
|
66
|
+
: queryBuilder;
|
|
143
67
|
|
|
144
|
-
|
|
68
|
+
// Execute query
|
|
69
|
+
const query = finalQuery.toSQL();
|
|
145
70
|
if (this.options?.logRawSqlQuery) {
|
|
146
|
-
console.debug("INSERT SQL:
|
|
71
|
+
console.debug("INSERT SQL:", query.sql);
|
|
147
72
|
}
|
|
148
|
-
const sqlStatement = sql.prepare<UpdateQueryResponse>(query.sql);
|
|
149
|
-
const result = await sqlStatement.execute();
|
|
150
|
-
return result.rows.insertId;
|
|
151
|
-
}
|
|
152
73
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
* @param schema - The entity schema.
|
|
157
|
-
* @returns An array of primary key properties.
|
|
158
|
-
* @throws If no primary keys are found.
|
|
159
|
-
*/
|
|
160
|
-
private getPrimaryKeys<T extends object>(schema: EntitySchema<T>): EntityProperty<T, unknown>[] {
|
|
161
|
-
const primaryKeys = schema.meta.props.filter((prop) => prop.primary);
|
|
162
|
-
if (!primaryKeys.length) {
|
|
163
|
-
throw new Error(`No primary keys found for schema: ${schema.meta.className}`);
|
|
164
|
-
}
|
|
165
|
-
return primaryKeys;
|
|
74
|
+
const result = await this.forgeOperations.fetch().executeRawUpdateSQL(query.sql, query.params);
|
|
75
|
+
|
|
76
|
+
return result.insertId;
|
|
166
77
|
}
|
|
167
78
|
|
|
168
79
|
/**
|
|
169
|
-
* Deletes a record by its primary key.
|
|
80
|
+
* Deletes a record by its primary key with optional version check.
|
|
81
|
+
* If versioning is enabled, ensures the record hasn't been modified since last read.
|
|
170
82
|
*
|
|
171
|
-
* @
|
|
172
|
-
* @param
|
|
173
|
-
* @
|
|
174
|
-
* @
|
|
83
|
+
* @template T - The type of the table schema
|
|
84
|
+
* @param {unknown} id - The ID of the record to delete
|
|
85
|
+
* @param {T} schema - The entity schema
|
|
86
|
+
* @returns {Promise<number>} Number of affected rows
|
|
87
|
+
* @throws {Error} If the delete operation fails
|
|
88
|
+
* @throws {Error} If multiple primary keys are found
|
|
175
89
|
*/
|
|
176
|
-
async deleteById<T extends
|
|
90
|
+
async deleteById<T extends AnyMySqlTable>(id: unknown, schema: T): Promise<number> {
|
|
91
|
+
const { tableName, columns } = getTableMetadata(schema);
|
|
177
92
|
const primaryKeys = this.getPrimaryKeys(schema);
|
|
178
|
-
|
|
179
|
-
|
|
93
|
+
|
|
94
|
+
if (primaryKeys.length !== 1) {
|
|
95
|
+
throw new Error("Only single primary key is supported");
|
|
180
96
|
}
|
|
181
97
|
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
|
|
98
|
+
const [primaryKeyName, primaryKeyColumn] = primaryKeys[0];
|
|
99
|
+
const versionMetadata = this.validateVersionField(tableName, columns);
|
|
100
|
+
|
|
101
|
+
// Build delete conditions
|
|
102
|
+
const conditions: SQL<unknown>[] = [eq(primaryKeyColumn, id)];
|
|
103
|
+
|
|
104
|
+
// Add version check if needed
|
|
105
|
+
if (versionMetadata && columns) {
|
|
106
|
+
const versionField = columns[versionMetadata.fieldName];
|
|
107
|
+
if (versionField) {
|
|
108
|
+
const oldModel = await this.getOldModel({ [primaryKeyName]: id }, schema, [
|
|
109
|
+
versionMetadata.fieldName,
|
|
110
|
+
versionField,
|
|
111
|
+
]);
|
|
112
|
+
conditions.push(eq(versionField, (oldModel as any)[versionMetadata.fieldName]));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Execute delete query
|
|
117
|
+
const queryBuilder = this.forgeOperations
|
|
118
|
+
.getDrizzleQueryBuilder()
|
|
119
|
+
.delete(schema)
|
|
120
|
+
.where(and(...conditions));
|
|
185
121
|
|
|
186
|
-
const query = queryBuilder.getFormattedQuery();
|
|
187
122
|
if (this.options?.logRawSqlQuery) {
|
|
188
|
-
console.debug("DELETE SQL:
|
|
123
|
+
console.debug("DELETE SQL:", queryBuilder.toSQL().sql);
|
|
189
124
|
}
|
|
190
|
-
|
|
191
|
-
const result = await
|
|
192
|
-
|
|
125
|
+
|
|
126
|
+
const result = await this.forgeOperations
|
|
127
|
+
.fetch()
|
|
128
|
+
.executeRawUpdateSQL(queryBuilder.toSQL().sql, queryBuilder.toSQL().params);
|
|
129
|
+
|
|
130
|
+
return result.affectedRows;
|
|
193
131
|
}
|
|
194
132
|
|
|
195
133
|
/**
|
|
196
|
-
*
|
|
197
|
-
*
|
|
134
|
+
* Updates a record by its primary key with optimistic locking support.
|
|
135
|
+
* If versioning is enabled:
|
|
136
|
+
* - Retrieves the current version
|
|
137
|
+
* - Checks for concurrent modifications
|
|
138
|
+
* - Increments the version on successful update
|
|
198
139
|
*
|
|
199
|
-
* @
|
|
200
|
-
* @
|
|
140
|
+
* @template T - The type of the table schema
|
|
141
|
+
* @param {Partial<InferInsertModel<T>>} entity - The entity with updated values
|
|
142
|
+
* @param {T} schema - The entity schema
|
|
143
|
+
* @returns {Promise<number>} Number of affected rows
|
|
144
|
+
* @throws {Error} If the primary key is not provided
|
|
145
|
+
* @throws {Error} If optimistic locking check fails
|
|
146
|
+
* @throws {Error} If multiple primary keys are found
|
|
201
147
|
*/
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
148
|
+
async updateById<T extends AnyMySqlTable>(
|
|
149
|
+
entity: Partial<InferInsertModel<T>>,
|
|
150
|
+
schema: T,
|
|
151
|
+
): Promise<number> {
|
|
152
|
+
const { tableName, columns } = getTableMetadata(schema);
|
|
153
|
+
const primaryKeys = this.getPrimaryKeys(schema);
|
|
154
|
+
|
|
155
|
+
if (primaryKeys.length !== 1) {
|
|
156
|
+
throw new Error("Only single primary key is supported");
|
|
205
157
|
}
|
|
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
158
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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}`);
|
|
159
|
+
const [primaryKeyName, primaryKeyColumn] = primaryKeys[0];
|
|
160
|
+
const versionMetadata = this.validateVersionField(tableName, columns);
|
|
161
|
+
|
|
162
|
+
// Validate primary key
|
|
163
|
+
if (!(primaryKeyName in entity)) {
|
|
164
|
+
throw new Error(`Primary key ${primaryKeyName} must be provided in the entity`);
|
|
259
165
|
}
|
|
260
|
-
}
|
|
261
166
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
167
|
+
// Get current version if needed
|
|
168
|
+
const currentVersion = await this.getCurrentVersion(
|
|
169
|
+
entity,
|
|
170
|
+
primaryKeyName,
|
|
171
|
+
versionMetadata,
|
|
172
|
+
columns,
|
|
173
|
+
schema,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// Prepare update data with version
|
|
177
|
+
const updateData = this.prepareUpdateData(entity, versionMetadata, columns, currentVersion);
|
|
178
|
+
|
|
179
|
+
// Build update conditions
|
|
180
|
+
const conditions: SQL<unknown>[] = [
|
|
181
|
+
eq(primaryKeyColumn, entity[primaryKeyName as keyof typeof entity]),
|
|
182
|
+
];
|
|
183
|
+
if (versionMetadata && columns) {
|
|
184
|
+
const versionField = columns[versionMetadata.fieldName];
|
|
185
|
+
if (versionField) {
|
|
186
|
+
conditions.push(eq(versionField, currentVersion));
|
|
276
187
|
}
|
|
277
|
-
default:
|
|
278
|
-
throw new Error(`Unsupported version field type: ${versionField.type}`);
|
|
279
188
|
}
|
|
280
|
-
}
|
|
281
189
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
190
|
+
// Execute update query
|
|
191
|
+
const queryBuilder = this.forgeOperations
|
|
192
|
+
.getDrizzleQueryBuilder()
|
|
193
|
+
.update(schema)
|
|
194
|
+
.set(updateData)
|
|
195
|
+
.where(and(...conditions));
|
|
196
|
+
|
|
197
|
+
if (this.options?.logRawSqlQuery) {
|
|
198
|
+
console.debug("UPDATE SQL:", queryBuilder.toSQL().sql);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const result = await this.forgeOperations
|
|
202
|
+
.fetch()
|
|
203
|
+
.executeRawUpdateSQL(queryBuilder.toSQL().sql, queryBuilder.toSQL().params);
|
|
204
|
+
|
|
205
|
+
// Check optimistic locking
|
|
206
|
+
if (versionMetadata && result.affectedRows === 0) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`Optimistic locking failed: record with primary key ${entity[primaryKeyName as keyof typeof entity]} has been modified`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return result.affectedRows;
|
|
293
213
|
}
|
|
294
214
|
|
|
295
215
|
/**
|
|
296
216
|
* Updates specified fields of records based on provided conditions.
|
|
297
|
-
*
|
|
298
|
-
* that are not included in the list of fields to update.
|
|
217
|
+
* This method does not support versioning and should be used with caution.
|
|
299
218
|
*
|
|
300
|
-
* @
|
|
301
|
-
* @param
|
|
302
|
-
* @param schema - The entity schema
|
|
303
|
-
* @param where -
|
|
304
|
-
* @returns
|
|
305
|
-
* @throws If
|
|
219
|
+
* @template T - The type of the table schema
|
|
220
|
+
* @param {Partial<InferInsertModel<T>>} updateData - The data to update
|
|
221
|
+
* @param {T} schema - The entity schema
|
|
222
|
+
* @param {SQL<unknown>} where - The WHERE conditions
|
|
223
|
+
* @returns {Promise<number>} Number of affected rows
|
|
224
|
+
* @throws {Error} If WHERE conditions are not provided
|
|
225
|
+
* @throws {Error} If the update operation fails
|
|
306
226
|
*/
|
|
307
|
-
async updateFields<T extends
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
where?: QBFilterQuery<T>,
|
|
227
|
+
async updateFields<T extends AnyMySqlTable>(
|
|
228
|
+
updateData: Partial<InferInsertModel<T>>,
|
|
229
|
+
schema: T,
|
|
230
|
+
where?: SQL<unknown>,
|
|
312
231
|
): Promise<number> {
|
|
313
|
-
|
|
314
|
-
|
|
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);
|
|
232
|
+
if (!where) {
|
|
233
|
+
throw new Error("WHERE conditions must be provided");
|
|
346
234
|
}
|
|
347
235
|
|
|
236
|
+
const queryBuilder = this.forgeOperations
|
|
237
|
+
.getDrizzleQueryBuilder()
|
|
238
|
+
.update(schema)
|
|
239
|
+
.set(updateData)
|
|
240
|
+
.where(where);
|
|
241
|
+
|
|
348
242
|
if (this.options?.logRawSqlQuery) {
|
|
349
|
-
console.debug("UPDATE SQL
|
|
243
|
+
console.debug("UPDATE SQL:", queryBuilder.toSQL().sql);
|
|
350
244
|
}
|
|
351
245
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
246
|
+
const result = await this.forgeOperations
|
|
247
|
+
.fetch()
|
|
248
|
+
.executeRawUpdateSQL(queryBuilder.toSQL().sql, queryBuilder.toSQL().params);
|
|
249
|
+
|
|
250
|
+
return result.affectedRows;
|
|
356
251
|
}
|
|
357
252
|
|
|
253
|
+
// Helper methods
|
|
358
254
|
|
|
359
255
|
/**
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
* @
|
|
364
|
-
* @
|
|
365
|
-
* @param schema - The entity schema.
|
|
366
|
-
* @throws If the primary key is not included in the update fields.
|
|
256
|
+
* Gets primary keys from the schema.
|
|
257
|
+
* @template T - The type of the table schema
|
|
258
|
+
* @param {T} schema - The table schema
|
|
259
|
+
* @returns {[string, AnyColumn][]} Array of primary key name and column pairs
|
|
260
|
+
* @throws {Error} If no primary keys are found
|
|
367
261
|
*/
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
schema:
|
|
372
|
-
): Promise<void> {
|
|
373
|
-
const primaryKeys = this.getPrimaryKeys(schema);
|
|
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);
|
|
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);
|
|
262
|
+
private getPrimaryKeys<T extends AnyMySqlTable>(schema: T): [string, AnyColumn][] {
|
|
263
|
+
const primaryKeys = getPrimaryKeys(schema);
|
|
264
|
+
if (!primaryKeys) {
|
|
265
|
+
throw new Error(`No primary keys found for schema: ${schema}`);
|
|
409
266
|
}
|
|
410
267
|
|
|
411
|
-
|
|
412
|
-
|
|
268
|
+
return primaryKeys;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Validates and retrieves version field metadata.
|
|
273
|
+
* @param {string} tableName - The name of the table
|
|
274
|
+
* @param {Record<string, AnyColumn>} columns - The table columns
|
|
275
|
+
* @returns {Object | undefined} Version field metadata if valid, undefined otherwise
|
|
276
|
+
*/
|
|
277
|
+
private validateVersionField(
|
|
278
|
+
tableName: string,
|
|
279
|
+
columns: Record<string, AnyColumn>,
|
|
280
|
+
): { fieldName: string; type: string } | undefined {
|
|
281
|
+
if (this.options.disableOptimisticLocking) {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
const versionMetadata = this.options.additionalMetadata?.[tableName]?.versionField;
|
|
285
|
+
if (!versionMetadata) return undefined;
|
|
413
286
|
|
|
414
|
-
|
|
415
|
-
|
|
287
|
+
const versionField = columns[versionMetadata.fieldName];
|
|
288
|
+
if (!versionField) {
|
|
289
|
+
console.warn(
|
|
290
|
+
`Version field "${versionMetadata.fieldName}" not found in table ${tableName}. Versioning will be skipped.`,
|
|
291
|
+
);
|
|
292
|
+
return undefined;
|
|
416
293
|
}
|
|
417
|
-
|
|
418
|
-
if (versionField
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
primaryKeys.map((p) => updateModel[p.name]).join(", ") +
|
|
422
|
-
" has been modified by another process.",
|
|
294
|
+
|
|
295
|
+
if (!versionField.notNull) {
|
|
296
|
+
console.warn(
|
|
297
|
+
`Version field "${versionMetadata.fieldName}" in table ${tableName} is nullable. Versioning may not work correctly.`,
|
|
423
298
|
);
|
|
299
|
+
return undefined;
|
|
424
300
|
}
|
|
301
|
+
|
|
302
|
+
const fieldType = versionField.getSQLType();
|
|
303
|
+
const isSupportedType =
|
|
304
|
+
fieldType === "datetime" ||
|
|
305
|
+
fieldType === "timestamp" ||
|
|
306
|
+
fieldType === "int" ||
|
|
307
|
+
fieldType === "number" ||
|
|
308
|
+
fieldType === "decimal";
|
|
309
|
+
|
|
310
|
+
if (!isSupportedType) {
|
|
311
|
+
console.warn(
|
|
312
|
+
`Version field "${versionMetadata.fieldName}" in table ${tableName} has unsupported type "${fieldType}". ` +
|
|
313
|
+
`Only datetime, timestamp, int, and decimal types are supported for versioning. Versioning will be skipped.`,
|
|
314
|
+
);
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return { fieldName: versionMetadata.fieldName, type: fieldType };
|
|
425
319
|
}
|
|
426
320
|
|
|
427
321
|
/**
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
* @param
|
|
431
|
-
* @param
|
|
432
|
-
* @
|
|
322
|
+
* Gets the current version of an entity.
|
|
323
|
+
* @template T - The type of the table schema
|
|
324
|
+
* @param {Partial<InferInsertModel<T>>} entity - The entity
|
|
325
|
+
* @param {string} primaryKeyName - The name of the primary key
|
|
326
|
+
* @param {Object | undefined} versionMetadata - Version field metadata
|
|
327
|
+
* @param {Record<string, AnyColumn>} columns - The table columns
|
|
328
|
+
* @param {T} schema - The table schema
|
|
329
|
+
* @returns {Promise<unknown>} The current version value
|
|
433
330
|
*/
|
|
434
|
-
private
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
331
|
+
private async getCurrentVersion<T extends AnyMySqlTable>(
|
|
332
|
+
entity: Partial<InferInsertModel<T>>,
|
|
333
|
+
primaryKeyName: string,
|
|
334
|
+
versionMetadata: { fieldName: string; type: string } | undefined,
|
|
335
|
+
columns: Record<string, AnyColumn>,
|
|
336
|
+
schema: T,
|
|
337
|
+
): Promise<unknown> {
|
|
338
|
+
if (!versionMetadata || !columns) return undefined;
|
|
339
|
+
|
|
340
|
+
const versionField = columns[versionMetadata.fieldName];
|
|
341
|
+
if (!versionField) return undefined;
|
|
342
|
+
|
|
343
|
+
if (versionMetadata.fieldName in entity) {
|
|
344
|
+
return entity[versionMetadata.fieldName as keyof typeof entity];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const oldModel = await this.getOldModel(
|
|
348
|
+
{ [primaryKeyName]: entity[primaryKeyName as keyof typeof entity] },
|
|
349
|
+
schema,
|
|
350
|
+
[versionMetadata.fieldName, versionField],
|
|
441
351
|
);
|
|
442
|
-
|
|
352
|
+
|
|
353
|
+
return (oldModel as any)[versionMetadata.fieldName];
|
|
443
354
|
}
|
|
444
355
|
|
|
445
356
|
/**
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
* @param
|
|
449
|
-
* @param
|
|
450
|
-
* @param
|
|
451
|
-
* @
|
|
452
|
-
* @returns The existing record from the database.
|
|
453
|
-
* @throws If the record does not exist or if multiple records are found.
|
|
357
|
+
* Prepares a model for insertion with version field.
|
|
358
|
+
* @template T - The type of the table schema
|
|
359
|
+
* @param {Partial<InferInsertModel<T>>} model - The model to prepare
|
|
360
|
+
* @param {Object | undefined} versionMetadata - Version field metadata
|
|
361
|
+
* @param {Record<string, AnyColumn>} columns - The table columns
|
|
362
|
+
* @returns {InferInsertModel<T>} The prepared model
|
|
454
363
|
*/
|
|
455
|
-
private
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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];
|
|
364
|
+
private prepareModelWithVersion<T extends AnyMySqlTable>(
|
|
365
|
+
model: Partial<InferInsertModel<T>>,
|
|
366
|
+
versionMetadata: { fieldName: string; type: string } | undefined,
|
|
367
|
+
columns: Record<string, AnyColumn>,
|
|
368
|
+
): InferInsertModel<T> {
|
|
369
|
+
if (!versionMetadata || !columns) return model as InferInsertModel<T>;
|
|
370
|
+
|
|
371
|
+
const versionField = columns[versionMetadata.fieldName];
|
|
372
|
+
if (!versionField) return model as InferInsertModel<T>;
|
|
373
|
+
|
|
374
|
+
const modelWithVersion = { ...model };
|
|
375
|
+
const fieldType = versionField.getSQLType();
|
|
376
|
+
const versionValue = fieldType === "datetime" || fieldType === "timestamp" ? new Date() : 1;
|
|
377
|
+
modelWithVersion[versionMetadata.fieldName as keyof typeof modelWithVersion] =
|
|
378
|
+
versionValue as any;
|
|
379
|
+
|
|
380
|
+
return modelWithVersion as InferInsertModel<T>;
|
|
479
381
|
}
|
|
480
382
|
|
|
481
383
|
/**
|
|
482
|
-
*
|
|
483
|
-
*
|
|
484
|
-
* @param
|
|
485
|
-
* @param
|
|
486
|
-
* @param
|
|
487
|
-
* @
|
|
384
|
+
* Prepares update data with version field.
|
|
385
|
+
* @template T - The type of the table schema
|
|
386
|
+
* @param {Partial<InferInsertModel<T>>} entity - The entity to update
|
|
387
|
+
* @param {Object | undefined} versionMetadata - Version field metadata
|
|
388
|
+
* @param {Record<string, AnyColumn>} columns - The table columns
|
|
389
|
+
* @param {unknown} currentVersion - The current version value
|
|
390
|
+
* @returns {Partial<InferInsertModel<T>>} The prepared update data
|
|
488
391
|
*/
|
|
489
|
-
private
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
392
|
+
private prepareUpdateData<T extends AnyMySqlTable>(
|
|
393
|
+
entity: Partial<InferInsertModel<T>>,
|
|
394
|
+
versionMetadata: { fieldName: string; type: string } | undefined,
|
|
395
|
+
columns: Record<string, AnyColumn>,
|
|
396
|
+
currentVersion: unknown,
|
|
397
|
+
): Partial<InferInsertModel<T>> {
|
|
398
|
+
const updateData = { ...entity };
|
|
399
|
+
|
|
400
|
+
if (versionMetadata && columns) {
|
|
401
|
+
const versionField = columns[versionMetadata.fieldName];
|
|
402
|
+
if (versionField) {
|
|
403
|
+
const fieldType = versionField.getSQLType();
|
|
404
|
+
updateData[versionMetadata.fieldName as keyof typeof updateData] =
|
|
405
|
+
fieldType === "datetime" || fieldType === "timestamp"
|
|
406
|
+
? new Date()
|
|
407
|
+
: (((currentVersion as number) + 1) as any);
|
|
499
408
|
}
|
|
500
|
-
|
|
501
|
-
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return updateData;
|
|
502
412
|
}
|
|
503
413
|
|
|
504
414
|
/**
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
* @param
|
|
508
|
-
* @param
|
|
509
|
-
* @
|
|
415
|
+
* Retrieves an existing model by primary key.
|
|
416
|
+
* @template T - The type of the table schema
|
|
417
|
+
* @param {Record<string, unknown>} primaryKeyValues - The primary key values
|
|
418
|
+
* @param {T} schema - The table schema
|
|
419
|
+
* @param {[string, AnyColumn]} versionField - The version field name and column
|
|
420
|
+
* @returns {Promise<Awaited<T> extends Array<any> ? Awaited<T>[number] | undefined : Awaited<T> | undefined>} The existing model
|
|
421
|
+
* @throws {Error} If the record is not found
|
|
510
422
|
*/
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
423
|
+
private async getOldModel<T extends AnyMySqlTable>(
|
|
424
|
+
primaryKeyValues: Record<string, unknown>,
|
|
425
|
+
schema: T,
|
|
426
|
+
versionField: [string, AnyColumn],
|
|
427
|
+
): Promise<
|
|
428
|
+
Awaited<T> extends Array<any> ? Awaited<T>[number] | undefined : Awaited<T> | undefined
|
|
429
|
+
> {
|
|
430
|
+
const [versionFieldName, versionFieldColumn] = versionField;
|
|
431
|
+
const primaryKeys = this.getPrimaryKeys(schema);
|
|
432
|
+
const [primaryKeyName, primaryKeyColumn] = primaryKeys[0];
|
|
518
433
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
-
}
|
|
434
|
+
const resultQuery = this.forgeOperations
|
|
435
|
+
.getDrizzleQueryBuilder()
|
|
436
|
+
.select({
|
|
437
|
+
[primaryKeyName]: primaryKeyColumn as any,
|
|
438
|
+
[versionFieldName]: versionFieldColumn as any,
|
|
439
|
+
})
|
|
440
|
+
.from(schema)
|
|
441
|
+
.where(eq(primaryKeyColumn, primaryKeyValues[primaryKeyName]));
|
|
539
442
|
|
|
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
|
-
}
|
|
443
|
+
const model = await this.forgeOperations.fetch().executeQueryOnlyOne(resultQuery);
|
|
551
444
|
|
|
552
|
-
|
|
553
|
-
|
|
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());
|
|
445
|
+
if (!model) {
|
|
446
|
+
throw new Error(`Record not found in table ${schema}`);
|
|
561
447
|
}
|
|
562
|
-
|
|
448
|
+
|
|
449
|
+
return model as Awaited<T> extends Array<any> ? Awaited<T>[number] : Awaited<T>;
|
|
563
450
|
}
|
|
564
451
|
}
|