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
package/dist/ForgeSQLORM.mjs
CHANGED
|
@@ -1,26 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export * from "@mikro-orm/mysql";
|
|
3
|
-
import { sql } from "@forge/sql";
|
|
1
|
+
import { eq, and, SQL, Column, StringChunk } from "drizzle-orm";
|
|
4
2
|
import moment from "moment";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
case "datetime":
|
|
15
|
-
return wrapIfNeeded(`${moment(value.value).format("YYYY-MM-DDTHH:mm:ss.SSS")}`, wrapValue);
|
|
16
|
-
case "date":
|
|
17
|
-
return wrapIfNeeded(`${moment(value.value).format("YYYY-MM-DD")}`, wrapValue);
|
|
18
|
-
case "time":
|
|
19
|
-
return wrapIfNeeded(`${moment(value.value).format("HH:mm:ss.SSS")}`, wrapValue);
|
|
20
|
-
default:
|
|
21
|
-
return value.value;
|
|
22
|
-
}
|
|
23
|
-
};
|
|
3
|
+
import { PrimaryKeyBuilder } from "drizzle-orm/mysql-core/primary-keys";
|
|
4
|
+
import { IndexBuilder } from "drizzle-orm/mysql-core/indexes";
|
|
5
|
+
import { CheckBuilder } from "drizzle-orm/mysql-core/checks";
|
|
6
|
+
import { ForeignKeyBuilder } from "drizzle-orm/mysql-core/foreign-keys";
|
|
7
|
+
import { UniqueConstraintBuilder } from "drizzle-orm/mysql-core/unique-constraint";
|
|
8
|
+
import { sql } from "@forge/sql";
|
|
9
|
+
import { drizzle } from "drizzle-orm/mysql2";
|
|
10
|
+
import { customType } from "drizzle-orm/mysql-core";
|
|
11
|
+
import moment$1 from "moment/moment.js";
|
|
24
12
|
const parseDateTime = (value, format) => {
|
|
25
13
|
const m = moment(value, format, true);
|
|
26
14
|
if (!m.isValid()) {
|
|
@@ -28,576 +16,536 @@ const parseDateTime = (value, format) => {
|
|
|
28
16
|
}
|
|
29
17
|
return m.toDate();
|
|
30
18
|
};
|
|
19
|
+
function extractAlias(query) {
|
|
20
|
+
const match = query.match(/\bas\s+(['"`]?)([\w*]+)\1$/i);
|
|
21
|
+
return match ? match[2] : query;
|
|
22
|
+
}
|
|
23
|
+
function getPrimaryKeys(table) {
|
|
24
|
+
const { columns, primaryKeys } = getTableMetadata(table);
|
|
25
|
+
const columnPrimaryKeys = Object.entries(columns).filter(([, column]) => column.primary);
|
|
26
|
+
if (columnPrimaryKeys.length > 0) {
|
|
27
|
+
return columnPrimaryKeys;
|
|
28
|
+
}
|
|
29
|
+
if (Array.isArray(primaryKeys) && primaryKeys.length > 0) {
|
|
30
|
+
const primaryKeyColumns = /* @__PURE__ */ new Set();
|
|
31
|
+
primaryKeys.forEach((primaryKeyBuilder) => {
|
|
32
|
+
Object.entries(columns).filter(([, column]) => {
|
|
33
|
+
return primaryKeyBuilder.columns.includes(column);
|
|
34
|
+
}).forEach(([name, column]) => {
|
|
35
|
+
primaryKeyColumns.add([name, column]);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
const result = Array.from(primaryKeyColumns);
|
|
39
|
+
return result.length > 0 ? result : void 0;
|
|
40
|
+
}
|
|
41
|
+
return void 0;
|
|
42
|
+
}
|
|
43
|
+
function getTableMetadata(table) {
|
|
44
|
+
const symbols = Object.getOwnPropertySymbols(table);
|
|
45
|
+
const nameSymbol = symbols.find((s) => s.toString().includes("Name"));
|
|
46
|
+
const columnsSymbol = symbols.find((s) => s.toString().includes("Columns"));
|
|
47
|
+
const extraSymbol = symbols.find((s) => s.toString().includes("ExtraConfigBuilder"));
|
|
48
|
+
const foreignKeysSymbol = symbols.find((s) => s.toString().includes("MySqlInlineForeignKeys)"));
|
|
49
|
+
const builders = {
|
|
50
|
+
indexes: [],
|
|
51
|
+
checks: [],
|
|
52
|
+
foreignKeys: [],
|
|
53
|
+
primaryKeys: [],
|
|
54
|
+
uniqueConstraints: [],
|
|
55
|
+
extras: []
|
|
56
|
+
};
|
|
57
|
+
if (foreignKeysSymbol) {
|
|
58
|
+
const foreignKeys = table[foreignKeysSymbol];
|
|
59
|
+
if (foreignKeys) {
|
|
60
|
+
for (const foreignKey of foreignKeys) {
|
|
61
|
+
builders.foreignKeys.push(foreignKey);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (extraSymbol) {
|
|
66
|
+
const extraConfigBuilder = table[extraSymbol];
|
|
67
|
+
if (extraConfigBuilder && typeof extraConfigBuilder === "function") {
|
|
68
|
+
const configBuilders = extraConfigBuilder(table);
|
|
69
|
+
let configBuildersArray = [];
|
|
70
|
+
if (!Array.isArray(configBuilders)) {
|
|
71
|
+
configBuildersArray = Object.values(configBuilders);
|
|
72
|
+
} else {
|
|
73
|
+
configBuildersArray = configBuilders;
|
|
74
|
+
}
|
|
75
|
+
configBuildersArray.forEach((builder) => {
|
|
76
|
+
if (builder instanceof IndexBuilder) {
|
|
77
|
+
builders.indexes.push(builder);
|
|
78
|
+
} else if (builder instanceof CheckBuilder) {
|
|
79
|
+
builders.checks.push(builder);
|
|
80
|
+
} else if (builder instanceof ForeignKeyBuilder) {
|
|
81
|
+
builders.foreignKeys.push(builder);
|
|
82
|
+
} else if (builder instanceof PrimaryKeyBuilder) {
|
|
83
|
+
builders.primaryKeys.push(builder);
|
|
84
|
+
} else if (builder instanceof UniqueConstraintBuilder) {
|
|
85
|
+
builders.uniqueConstraints.push(builder);
|
|
86
|
+
}
|
|
87
|
+
builders.extras.push(builder);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
tableName: nameSymbol ? table[nameSymbol] : "",
|
|
93
|
+
columns: columnsSymbol ? table[columnsSymbol] : {},
|
|
94
|
+
...builders
|
|
95
|
+
};
|
|
96
|
+
}
|
|
31
97
|
class ForgeSQLCrudOperations {
|
|
32
98
|
forgeOperations;
|
|
33
99
|
options;
|
|
100
|
+
/**
|
|
101
|
+
* Creates a new instance of ForgeSQLCrudOperations.
|
|
102
|
+
* @param forgeSqlOperations - The ForgeSQL operations instance
|
|
103
|
+
* @param options - Configuration options for the ORM
|
|
104
|
+
*/
|
|
34
105
|
constructor(forgeSqlOperations, options) {
|
|
35
106
|
this.forgeOperations = forgeSqlOperations;
|
|
36
107
|
this.options = options;
|
|
37
108
|
}
|
|
38
109
|
/**
|
|
39
|
-
*
|
|
40
|
-
* If a version field exists in the schema, its value is set accordingly.
|
|
41
|
-
*
|
|
42
|
-
* @param schema - The entity schema.
|
|
43
|
-
* @param models - The list of entities to insert.
|
|
44
|
-
* @param updateIfExists - Whether to update the row if it already exists.
|
|
45
|
-
* @returns An object containing the SQL query, column names, and values.
|
|
46
|
-
*/
|
|
47
|
-
async generateInsertScript(schema, models, updateIfExists) {
|
|
48
|
-
const columnNames = /* @__PURE__ */ new Set();
|
|
49
|
-
const modelFieldValues = [];
|
|
50
|
-
models.forEach((model) => {
|
|
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 };
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
modelFieldValues.push(fieldValues);
|
|
61
|
-
});
|
|
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] || {
|
|
87
|
-
type: "string",
|
|
88
|
-
value: null
|
|
89
|
-
}
|
|
90
|
-
)
|
|
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(",")}` : "";
|
|
108
|
-
return {
|
|
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,
|
|
112
|
-
values
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Inserts records into the database.
|
|
110
|
+
* Inserts records into the database with optional versioning support.
|
|
117
111
|
* If a version field exists in the schema, versioning is applied.
|
|
118
112
|
*
|
|
119
|
-
* @
|
|
120
|
-
* @param
|
|
121
|
-
* @param
|
|
122
|
-
* @
|
|
113
|
+
* @template T - The type of the table schema
|
|
114
|
+
* @param {T} schema - The entity schema
|
|
115
|
+
* @param {Partial<InferInsertModel<T>>[]} models - Array of entities to insert
|
|
116
|
+
* @param {boolean} [updateIfExists=false] - Whether to update existing records
|
|
117
|
+
* @returns {Promise<number>} The number of inserted rows
|
|
118
|
+
* @throws {Error} If the insert operation fails
|
|
123
119
|
*/
|
|
124
120
|
async insert(schema, models, updateIfExists = false) {
|
|
125
|
-
if (!models
|
|
126
|
-
const
|
|
121
|
+
if (!models?.length) return 0;
|
|
122
|
+
const { tableName, columns } = getTableMetadata(schema);
|
|
123
|
+
const versionMetadata = this.validateVersionField(tableName, columns);
|
|
124
|
+
const preparedModels = models.map(
|
|
125
|
+
(model) => this.prepareModelWithVersion(model, versionMetadata, columns)
|
|
126
|
+
);
|
|
127
|
+
const queryBuilder = this.forgeOperations.getDrizzleQueryBuilder().insert(schema).values(preparedModels);
|
|
128
|
+
const finalQuery = updateIfExists ? queryBuilder.onDuplicateKeyUpdate({
|
|
129
|
+
set: Object.fromEntries(
|
|
130
|
+
Object.keys(preparedModels[0]).map((key) => [key, schema[key]])
|
|
131
|
+
)
|
|
132
|
+
}) : queryBuilder;
|
|
133
|
+
const query = finalQuery.toSQL();
|
|
127
134
|
if (this.options?.logRawSqlQuery) {
|
|
128
|
-
console.debug("INSERT SQL:
|
|
129
|
-
}
|
|
130
|
-
const sqlStatement = sql.prepare(query.sql);
|
|
131
|
-
const result = await sqlStatement.execute();
|
|
132
|
-
return result.rows.insertId;
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Retrieves the primary key properties from the entity schema.
|
|
136
|
-
*
|
|
137
|
-
* @param schema - The entity schema.
|
|
138
|
-
* @returns An array of primary key properties.
|
|
139
|
-
* @throws If no primary keys are found.
|
|
140
|
-
*/
|
|
141
|
-
getPrimaryKeys(schema) {
|
|
142
|
-
const primaryKeys = schema.meta.props.filter((prop) => prop.primary);
|
|
143
|
-
if (!primaryKeys.length) {
|
|
144
|
-
throw new Error(`No primary keys found for schema: ${schema.meta.className}`);
|
|
135
|
+
console.debug("INSERT SQL:", query.sql);
|
|
145
136
|
}
|
|
146
|
-
|
|
137
|
+
const result = await this.forgeOperations.fetch().executeRawUpdateSQL(query.sql, query.params);
|
|
138
|
+
return result.insertId;
|
|
147
139
|
}
|
|
148
140
|
/**
|
|
149
|
-
* Deletes a record by its primary key.
|
|
141
|
+
* Deletes a record by its primary key with optional version check.
|
|
142
|
+
* If versioning is enabled, ensures the record hasn't been modified since last read.
|
|
150
143
|
*
|
|
151
|
-
* @
|
|
152
|
-
* @param
|
|
153
|
-
* @
|
|
154
|
-
* @
|
|
144
|
+
* @template T - The type of the table schema
|
|
145
|
+
* @param {unknown} id - The ID of the record to delete
|
|
146
|
+
* @param {T} schema - The entity schema
|
|
147
|
+
* @returns {Promise<number>} Number of affected rows
|
|
148
|
+
* @throws {Error} If the delete operation fails
|
|
149
|
+
* @throws {Error} If multiple primary keys are found
|
|
155
150
|
*/
|
|
156
151
|
async deleteById(id, schema) {
|
|
152
|
+
const { tableName, columns } = getTableMetadata(schema);
|
|
157
153
|
const primaryKeys = this.getPrimaryKeys(schema);
|
|
158
|
-
if (primaryKeys.length
|
|
159
|
-
throw new Error("Only
|
|
160
|
-
}
|
|
161
|
-
const primaryKey = primaryKeys[0];
|
|
162
|
-
const queryBuilder = this.forgeOperations.createQueryBuilder(schema.meta.class).delete();
|
|
163
|
-
queryBuilder.andWhere({ [primaryKey.name]: { $eq: id } });
|
|
164
|
-
const query = queryBuilder.getFormattedQuery();
|
|
165
|
-
if (this.options?.logRawSqlQuery) {
|
|
166
|
-
console.debug("DELETE SQL: " + queryBuilder.getQuery());
|
|
154
|
+
if (primaryKeys.length !== 1) {
|
|
155
|
+
throw new Error("Only single primary key is supported");
|
|
167
156
|
}
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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;
|
|
157
|
+
const [primaryKeyName, primaryKeyColumn] = primaryKeys[0];
|
|
158
|
+
const versionMetadata = this.validateVersionField(tableName, columns);
|
|
159
|
+
const conditions = [eq(primaryKeyColumn, id)];
|
|
160
|
+
if (versionMetadata && columns) {
|
|
161
|
+
const versionField = columns[versionMetadata.fieldName];
|
|
162
|
+
if (versionField) {
|
|
163
|
+
const oldModel = await this.getOldModel({ [primaryKeyName]: id }, schema, [
|
|
164
|
+
versionMetadata.fieldName,
|
|
165
|
+
versionField
|
|
166
|
+
]);
|
|
167
|
+
conditions.push(eq(versionField, oldModel[versionMetadata.fieldName]));
|
|
227
168
|
}
|
|
228
|
-
default:
|
|
229
|
-
throw new Error(`Unsupported version field type: ${versionField.type}`);
|
|
230
169
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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}`);
|
|
170
|
+
const queryBuilder = this.forgeOperations.getDrizzleQueryBuilder().delete(schema).where(and(...conditions));
|
|
171
|
+
if (this.options?.logRawSqlQuery) {
|
|
172
|
+
console.debug("DELETE SQL:", queryBuilder.toSQL().sql);
|
|
249
173
|
}
|
|
174
|
+
const result = await this.forgeOperations.fetch().executeRawUpdateSQL(queryBuilder.toSQL().sql, queryBuilder.toSQL().params);
|
|
175
|
+
return result.affectedRows;
|
|
250
176
|
}
|
|
251
177
|
/**
|
|
252
|
-
* Updates a record by its primary key
|
|
178
|
+
* Updates a record by its primary key with optimistic locking support.
|
|
179
|
+
* If versioning is enabled:
|
|
180
|
+
* - Retrieves the current version
|
|
181
|
+
* - Checks for concurrent modifications
|
|
182
|
+
* - Increments the version on successful update
|
|
253
183
|
*
|
|
254
|
-
* @
|
|
255
|
-
* @param
|
|
184
|
+
* @template T - The type of the table schema
|
|
185
|
+
* @param {Partial<InferInsertModel<T>>} entity - The entity with updated values
|
|
186
|
+
* @param {T} schema - The entity schema
|
|
187
|
+
* @returns {Promise<number>} Number of affected rows
|
|
188
|
+
* @throws {Error} If the primary key is not provided
|
|
189
|
+
* @throws {Error} If optimistic locking check fails
|
|
190
|
+
* @throws {Error} If multiple primary keys are found
|
|
256
191
|
*/
|
|
257
192
|
async updateById(entity, schema) {
|
|
258
|
-
const
|
|
259
|
-
|
|
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);
|
|
193
|
+
const { tableName, columns } = getTableMetadata(schema);
|
|
194
|
+
const primaryKeys = this.getPrimaryKeys(schema);
|
|
195
|
+
if (primaryKeys.length !== 1) {
|
|
196
|
+
throw new Error("Only single primary key is supported");
|
|
293
197
|
}
|
|
294
|
-
|
|
295
|
-
|
|
198
|
+
const [primaryKeyName, primaryKeyColumn] = primaryKeys[0];
|
|
199
|
+
const versionMetadata = this.validateVersionField(tableName, columns);
|
|
200
|
+
if (!(primaryKeyName in entity)) {
|
|
201
|
+
throw new Error(`Primary key ${primaryKeyName} must be provided in the entity`);
|
|
296
202
|
}
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
primaryKeys.forEach((pk) => {
|
|
313
|
-
if (!fields.includes(pk.name)) {
|
|
314
|
-
throw new Error("Update fields must include primary key: " + pk.name);
|
|
315
|
-
}
|
|
316
|
-
});
|
|
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));
|
|
203
|
+
const currentVersion = await this.getCurrentVersion(
|
|
204
|
+
entity,
|
|
205
|
+
primaryKeyName,
|
|
206
|
+
versionMetadata,
|
|
207
|
+
columns,
|
|
208
|
+
schema
|
|
209
|
+
);
|
|
210
|
+
const updateData = this.prepareUpdateData(entity, versionMetadata, columns, currentVersion);
|
|
211
|
+
const conditions = [
|
|
212
|
+
eq(primaryKeyColumn, entity[primaryKeyName])
|
|
213
|
+
];
|
|
214
|
+
if (versionMetadata && columns) {
|
|
215
|
+
const versionField = columns[versionMetadata.fieldName];
|
|
216
|
+
if (versionField) {
|
|
217
|
+
conditions.push(eq(versionField, currentVersion));
|
|
336
218
|
}
|
|
337
|
-
} else {
|
|
338
|
-
updateModel = this.modifyModel(updatedEntity, schema);
|
|
339
|
-
queryBuilder.update(updateModel);
|
|
340
219
|
}
|
|
341
|
-
this.
|
|
342
|
-
const sqlQuery = queryBuilder.toQuery();
|
|
220
|
+
const queryBuilder = this.forgeOperations.getDrizzleQueryBuilder().update(schema).set(updateData).where(and(...conditions));
|
|
343
221
|
if (this.options?.logRawSqlQuery) {
|
|
344
|
-
console.debug("UPDATE SQL:
|
|
222
|
+
console.debug("UPDATE SQL:", queryBuilder.toSQL().sql);
|
|
345
223
|
}
|
|
346
|
-
const
|
|
347
|
-
if (
|
|
224
|
+
const result = await this.forgeOperations.fetch().executeRawUpdateSQL(queryBuilder.toSQL().sql, queryBuilder.toSQL().params);
|
|
225
|
+
if (versionMetadata && result.affectedRows === 0) {
|
|
348
226
|
throw new Error(
|
|
349
|
-
|
|
227
|
+
`Optimistic locking failed: record with primary key ${entity[primaryKeyName]} has been modified`
|
|
350
228
|
);
|
|
351
229
|
}
|
|
230
|
+
return result.affectedRows;
|
|
352
231
|
}
|
|
353
232
|
/**
|
|
354
|
-
*
|
|
233
|
+
* Updates specified fields of records based on provided conditions.
|
|
234
|
+
* This method does not support versioning and should be used with caution.
|
|
355
235
|
*
|
|
356
|
-
* @
|
|
357
|
-
* @param
|
|
358
|
-
* @
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
236
|
+
* @template T - The type of the table schema
|
|
237
|
+
* @param {Partial<InferInsertModel<T>>} updateData - The data to update
|
|
238
|
+
* @param {T} schema - The entity schema
|
|
239
|
+
* @param {SQL<unknown>} where - The WHERE conditions
|
|
240
|
+
* @returns {Promise<number>} Number of affected rows
|
|
241
|
+
* @throws {Error} If WHERE conditions are not provided
|
|
242
|
+
* @throws {Error} If the update operation fails
|
|
243
|
+
*/
|
|
244
|
+
async updateFields(updateData, schema, where) {
|
|
245
|
+
if (!where) {
|
|
246
|
+
throw new Error("WHERE conditions must be provided");
|
|
247
|
+
}
|
|
248
|
+
const queryBuilder = this.forgeOperations.getDrizzleQueryBuilder().update(schema).set(updateData).where(where);
|
|
249
|
+
if (this.options?.logRawSqlQuery) {
|
|
250
|
+
console.debug("UPDATE SQL:", queryBuilder.toSQL().sql);
|
|
251
|
+
}
|
|
252
|
+
const result = await this.forgeOperations.fetch().executeRawUpdateSQL(queryBuilder.toSQL().sql, queryBuilder.toSQL().params);
|
|
253
|
+
return result.affectedRows;
|
|
366
254
|
}
|
|
255
|
+
// Helper methods
|
|
367
256
|
/**
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
* @param
|
|
371
|
-
* @
|
|
372
|
-
* @
|
|
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.
|
|
257
|
+
* Gets primary keys from the schema.
|
|
258
|
+
* @template T - The type of the table schema
|
|
259
|
+
* @param {T} schema - The table schema
|
|
260
|
+
* @returns {[string, AnyColumn][]} Array of primary key name and column pairs
|
|
261
|
+
* @throws {Error} If no primary keys are found
|
|
376
262
|
*/
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
);
|
|
263
|
+
getPrimaryKeys(schema) {
|
|
264
|
+
const primaryKeys = getPrimaryKeys(schema);
|
|
265
|
+
if (!primaryKeys) {
|
|
266
|
+
throw new Error(`No primary keys found for schema: ${schema}`);
|
|
391
267
|
}
|
|
392
|
-
return
|
|
268
|
+
return primaryKeys;
|
|
393
269
|
}
|
|
394
270
|
/**
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
* @param
|
|
398
|
-
* @
|
|
399
|
-
* @param entity - The entity containing primary key values.
|
|
400
|
-
* @throws If any primary key value is missing.
|
|
271
|
+
* Validates and retrieves version field metadata.
|
|
272
|
+
* @param {string} tableName - The name of the table
|
|
273
|
+
* @param {Record<string, AnyColumn>} columns - The table columns
|
|
274
|
+
* @returns {Object | undefined} Version field metadata if valid, undefined otherwise
|
|
401
275
|
*/
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
276
|
+
validateVersionField(tableName, columns) {
|
|
277
|
+
if (this.options.disableOptimisticLocking) {
|
|
278
|
+
return void 0;
|
|
279
|
+
}
|
|
280
|
+
const versionMetadata = this.options.additionalMetadata?.[tableName]?.versionField;
|
|
281
|
+
if (!versionMetadata) return void 0;
|
|
282
|
+
const versionField = columns[versionMetadata.fieldName];
|
|
283
|
+
if (!versionField) {
|
|
284
|
+
console.warn(
|
|
285
|
+
`Version field "${versionMetadata.fieldName}" not found in table ${tableName}. Versioning will be skipped.`
|
|
286
|
+
);
|
|
287
|
+
return void 0;
|
|
288
|
+
}
|
|
289
|
+
if (!versionField.notNull) {
|
|
290
|
+
console.warn(
|
|
291
|
+
`Version field "${versionMetadata.fieldName}" in table ${tableName} is nullable. Versioning may not work correctly.`
|
|
292
|
+
);
|
|
293
|
+
return void 0;
|
|
294
|
+
}
|
|
295
|
+
const fieldType = versionField.getSQLType();
|
|
296
|
+
const isSupportedType = fieldType === "datetime" || fieldType === "timestamp" || fieldType === "int" || fieldType === "number" || fieldType === "decimal";
|
|
297
|
+
if (!isSupportedType) {
|
|
298
|
+
console.warn(
|
|
299
|
+
`Version field "${versionMetadata.fieldName}" in table ${tableName} has unsupported type "${fieldType}". Only datetime, timestamp, int, and decimal types are supported for versioning. Versioning will be skipped.`
|
|
300
|
+
);
|
|
301
|
+
return void 0;
|
|
302
|
+
}
|
|
303
|
+
return { fieldName: versionMetadata.fieldName, type: fieldType };
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Gets the current version of an entity.
|
|
307
|
+
* @template T - The type of the table schema
|
|
308
|
+
* @param {Partial<InferInsertModel<T>>} entity - The entity
|
|
309
|
+
* @param {string} primaryKeyName - The name of the primary key
|
|
310
|
+
* @param {Object | undefined} versionMetadata - Version field metadata
|
|
311
|
+
* @param {Record<string, AnyColumn>} columns - The table columns
|
|
312
|
+
* @param {T} schema - The table schema
|
|
313
|
+
* @returns {Promise<unknown>} The current version value
|
|
314
|
+
*/
|
|
315
|
+
async getCurrentVersion(entity, primaryKeyName, versionMetadata, columns, schema) {
|
|
316
|
+
if (!versionMetadata || !columns) return void 0;
|
|
317
|
+
const versionField = columns[versionMetadata.fieldName];
|
|
318
|
+
if (!versionField) return void 0;
|
|
319
|
+
if (versionMetadata.fieldName in entity) {
|
|
320
|
+
return entity[versionMetadata.fieldName];
|
|
321
|
+
}
|
|
322
|
+
const oldModel = await this.getOldModel(
|
|
323
|
+
{ [primaryKeyName]: entity[primaryKeyName] },
|
|
324
|
+
schema,
|
|
325
|
+
[versionMetadata.fieldName, versionField]
|
|
326
|
+
);
|
|
327
|
+
return oldModel[versionMetadata.fieldName];
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Prepares a model for insertion with version field.
|
|
331
|
+
* @template T - The type of the table schema
|
|
332
|
+
* @param {Partial<InferInsertModel<T>>} model - The model to prepare
|
|
333
|
+
* @param {Object | undefined} versionMetadata - Version field metadata
|
|
334
|
+
* @param {Record<string, AnyColumn>} columns - The table columns
|
|
335
|
+
* @returns {InferInsertModel<T>} The prepared model
|
|
336
|
+
*/
|
|
337
|
+
prepareModelWithVersion(model, versionMetadata, columns) {
|
|
338
|
+
if (!versionMetadata || !columns) return model;
|
|
339
|
+
const versionField = columns[versionMetadata.fieldName];
|
|
340
|
+
if (!versionField) return model;
|
|
341
|
+
const modelWithVersion = { ...model };
|
|
342
|
+
const fieldType = versionField.getSQLType();
|
|
343
|
+
const versionValue = fieldType === "datetime" || fieldType === "timestamp" ? /* @__PURE__ */ new Date() : 1;
|
|
344
|
+
modelWithVersion[versionMetadata.fieldName] = versionValue;
|
|
345
|
+
return modelWithVersion;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Prepares update data with version field.
|
|
349
|
+
* @template T - The type of the table schema
|
|
350
|
+
* @param {Partial<InferInsertModel<T>>} entity - The entity to update
|
|
351
|
+
* @param {Object | undefined} versionMetadata - Version field metadata
|
|
352
|
+
* @param {Record<string, AnyColumn>} columns - The table columns
|
|
353
|
+
* @param {unknown} currentVersion - The current version value
|
|
354
|
+
* @returns {Partial<InferInsertModel<T>>} The prepared update data
|
|
355
|
+
*/
|
|
356
|
+
prepareUpdateData(entity, versionMetadata, columns, currentVersion) {
|
|
357
|
+
const updateData = { ...entity };
|
|
358
|
+
if (versionMetadata && columns) {
|
|
359
|
+
const versionField = columns[versionMetadata.fieldName];
|
|
360
|
+
if (versionField) {
|
|
361
|
+
const fieldType = versionField.getSQLType();
|
|
362
|
+
updateData[versionMetadata.fieldName] = fieldType === "datetime" || fieldType === "timestamp" ? /* @__PURE__ */ new Date() : currentVersion + 1;
|
|
408
363
|
}
|
|
409
|
-
|
|
410
|
-
|
|
364
|
+
}
|
|
365
|
+
return updateData;
|
|
411
366
|
}
|
|
412
367
|
/**
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
* @param
|
|
416
|
-
* @param
|
|
417
|
-
* @
|
|
368
|
+
* Retrieves an existing model by primary key.
|
|
369
|
+
* @template T - The type of the table schema
|
|
370
|
+
* @param {Record<string, unknown>} primaryKeyValues - The primary key values
|
|
371
|
+
* @param {T} schema - The table schema
|
|
372
|
+
* @param {[string, AnyColumn]} versionField - The version field name and column
|
|
373
|
+
* @returns {Promise<Awaited<T> extends Array<any> ? Awaited<T>[number] | undefined : Awaited<T> | undefined>} The existing model
|
|
374
|
+
* @throws {Error} If the record is not found
|
|
418
375
|
*/
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
376
|
+
async getOldModel(primaryKeyValues, schema, versionField) {
|
|
377
|
+
const [versionFieldName, versionFieldColumn] = versionField;
|
|
378
|
+
const primaryKeys = this.getPrimaryKeys(schema);
|
|
379
|
+
const [primaryKeyName, primaryKeyColumn] = primaryKeys[0];
|
|
380
|
+
const resultQuery = this.forgeOperations.getDrizzleQueryBuilder().select({
|
|
381
|
+
[primaryKeyName]: primaryKeyColumn,
|
|
382
|
+
[versionFieldName]: versionFieldColumn
|
|
383
|
+
}).from(schema).where(eq(primaryKeyColumn, primaryKeyValues[primaryKeyName]));
|
|
384
|
+
const model = await this.forgeOperations.fetch().executeQueryOnlyOne(resultQuery);
|
|
385
|
+
if (!model) {
|
|
386
|
+
throw new Error(`Record not found in table ${schema}`);
|
|
422
387
|
}
|
|
423
|
-
return
|
|
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;
|
|
388
|
+
return model;
|
|
442
389
|
}
|
|
390
|
+
}
|
|
391
|
+
class ForgeSQLSelectOperations {
|
|
392
|
+
options;
|
|
443
393
|
/**
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
* @param p - The entity property.
|
|
447
|
-
* @returns The real field name.
|
|
394
|
+
* Creates a new instance of ForgeSQLSelectOperations.
|
|
395
|
+
* @param {ForgeSqlOrmOptions} options - Configuration options for the ORM
|
|
448
396
|
*/
|
|
449
|
-
|
|
450
|
-
|
|
397
|
+
constructor(options) {
|
|
398
|
+
this.options = options;
|
|
451
399
|
}
|
|
452
400
|
/**
|
|
453
|
-
*
|
|
401
|
+
* Executes a Drizzle query and returns the results.
|
|
402
|
+
* Maps the raw database results to the appropriate entity types.
|
|
454
403
|
*
|
|
455
|
-
* @
|
|
456
|
-
* @
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
404
|
+
* @template T - The type of the query builder
|
|
405
|
+
* @param {T} query - The Drizzle query to execute
|
|
406
|
+
* @returns {Promise<Awaited<T>>} The query results mapped to entity types
|
|
407
|
+
*/
|
|
408
|
+
async executeQuery(query) {
|
|
409
|
+
const queryType = query;
|
|
410
|
+
const querySql = queryType.toSQL();
|
|
411
|
+
const datas = await this.executeRawSQL(querySql.sql, querySql.params);
|
|
412
|
+
if (!datas.length) return [];
|
|
413
|
+
return datas.map((r) => {
|
|
414
|
+
const rawModel = r;
|
|
415
|
+
const newModel = {};
|
|
416
|
+
const columns = queryType.config.fields;
|
|
417
|
+
Object.entries(columns).forEach(([name, column]) => {
|
|
418
|
+
const { realColumn, aliasName } = this.extractColumnInfo(column);
|
|
419
|
+
const value = rawModel[aliasName];
|
|
420
|
+
if (value === null || value === void 0) {
|
|
421
|
+
newModel[name] = value;
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
newModel[name] = this.parseColumnValue(realColumn, value);
|
|
425
|
+
});
|
|
426
|
+
return newModel;
|
|
427
|
+
});
|
|
463
428
|
}
|
|
464
|
-
}
|
|
465
|
-
class DynamicEntity {
|
|
466
429
|
/**
|
|
467
|
-
*
|
|
468
|
-
* @param
|
|
469
|
-
* @returns
|
|
430
|
+
* Extracts column information and alias name from a column definition.
|
|
431
|
+
* @param {any} column - The column definition from Drizzle
|
|
432
|
+
* @returns {Object} Object containing the real column and its alias name
|
|
470
433
|
*/
|
|
471
|
-
|
|
472
|
-
|
|
434
|
+
extractColumnInfo(column) {
|
|
435
|
+
if (column instanceof SQL) {
|
|
436
|
+
const realColumnSql = column;
|
|
437
|
+
const realColumn = realColumnSql.queryChunks.find(
|
|
438
|
+
(q) => q instanceof Column
|
|
439
|
+
);
|
|
440
|
+
let stringChunk = this.findAliasChunk(realColumnSql);
|
|
441
|
+
let withoutAlias = false;
|
|
442
|
+
if (!realColumn && (!stringChunk || !stringChunk.value || !stringChunk.value?.length)) {
|
|
443
|
+
stringChunk = realColumnSql.queryChunks.filter((q) => q instanceof StringChunk).find((q) => q.value[0]);
|
|
444
|
+
withoutAlias = true;
|
|
445
|
+
}
|
|
446
|
+
const aliasName = this.resolveAliasName(stringChunk, realColumn, withoutAlias);
|
|
447
|
+
return { realColumn, aliasName };
|
|
448
|
+
}
|
|
449
|
+
return { realColumn: column, aliasName: column.name };
|
|
473
450
|
}
|
|
474
451
|
/**
|
|
475
|
-
*
|
|
476
|
-
* @param
|
|
477
|
-
* @returns The
|
|
452
|
+
* Finds the alias chunk in SQL query chunks.
|
|
453
|
+
* @param {SQL} realColumnSql - The SQL query chunks
|
|
454
|
+
* @returns {StringChunk | undefined} The string chunk containing the alias or undefined
|
|
478
455
|
*/
|
|
479
|
-
|
|
480
|
-
return
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
class EntitySchemaBuilder {
|
|
484
|
-
constructor(entityClass) {
|
|
485
|
-
this.entityClass = entityClass;
|
|
456
|
+
findAliasChunk(realColumnSql) {
|
|
457
|
+
return realColumnSql.queryChunks.filter((q) => q instanceof StringChunk).find(
|
|
458
|
+
(q) => q.value.find((f) => f.toLowerCase().includes("as"))
|
|
459
|
+
);
|
|
486
460
|
}
|
|
487
|
-
properties = {};
|
|
488
461
|
/**
|
|
489
|
-
*
|
|
490
|
-
* @param
|
|
491
|
-
* @param
|
|
492
|
-
* @
|
|
462
|
+
* Resolves the alias name from the string chunk or column.
|
|
463
|
+
* @param {StringChunk | undefined} stringChunk - The string chunk containing the alias
|
|
464
|
+
* @param {Column | undefined} realColumn - The real column definition
|
|
465
|
+
* @param {boolean} withoutAlias - Whether the column has no alias
|
|
466
|
+
* @returns {string} The resolved alias name
|
|
493
467
|
*/
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
468
|
+
resolveAliasName(stringChunk, realColumn, withoutAlias) {
|
|
469
|
+
if (stringChunk) {
|
|
470
|
+
if (withoutAlias) {
|
|
471
|
+
return stringChunk.value[0];
|
|
472
|
+
}
|
|
473
|
+
const asClause = stringChunk.value.find((f) => f.toLowerCase().includes("as"));
|
|
474
|
+
return asClause ? extractAlias(asClause.trim()) : realColumn?.name || "";
|
|
475
|
+
}
|
|
476
|
+
return realColumn?.name || "";
|
|
499
477
|
}
|
|
500
478
|
/**
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
options;
|
|
521
|
-
constructor(options) {
|
|
522
|
-
this.options = options;
|
|
479
|
+
* Parses a column value based on its SQL type.
|
|
480
|
+
* Handles datetime, date, and time types with appropriate formatting.
|
|
481
|
+
*
|
|
482
|
+
* @param {Column} column - The column definition
|
|
483
|
+
* @param {unknown} value - The raw value to parse
|
|
484
|
+
* @returns {unknown} The parsed value
|
|
485
|
+
*/
|
|
486
|
+
parseColumnValue(column, value) {
|
|
487
|
+
if (!column) return value;
|
|
488
|
+
switch (column.getSQLType()) {
|
|
489
|
+
case "datetime":
|
|
490
|
+
return parseDateTime(value, "YYYY-MM-DDTHH:mm:ss.SSS");
|
|
491
|
+
case "date":
|
|
492
|
+
return parseDateTime(value, "YYYY-MM-DD");
|
|
493
|
+
case "time":
|
|
494
|
+
return parseDateTime(value, "HH:mm:ss.SSS");
|
|
495
|
+
default:
|
|
496
|
+
return value;
|
|
497
|
+
}
|
|
523
498
|
}
|
|
524
499
|
/**
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
* @
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
async
|
|
534
|
-
const results = await this.
|
|
535
|
-
|
|
500
|
+
* Executes a Drizzle query and returns a single result.
|
|
501
|
+
* Throws an error if more than one record is returned.
|
|
502
|
+
*
|
|
503
|
+
* @template T - The type of the query builder
|
|
504
|
+
* @param {T} query - The Drizzle query to execute
|
|
505
|
+
* @returns {Promise<Awaited<T> extends Array<any> ? Awaited<T>[number] | undefined : Awaited<T> | undefined>} A single result object or undefined
|
|
506
|
+
* @throws {Error} If more than one record is returned
|
|
507
|
+
*/
|
|
508
|
+
async executeQueryOnlyOne(query) {
|
|
509
|
+
const results = await this.executeQuery(query);
|
|
510
|
+
const datas = results;
|
|
511
|
+
if (!datas.length) {
|
|
536
512
|
return void 0;
|
|
537
513
|
}
|
|
538
|
-
if (
|
|
539
|
-
throw new Error(
|
|
514
|
+
if (datas.length > 1) {
|
|
515
|
+
throw new Error(`Expected 1 record but returned ${datas.length}`);
|
|
540
516
|
}
|
|
541
|
-
return
|
|
542
|
-
}
|
|
543
|
-
/**
|
|
544
|
-
* Executes a schema-based SQL query and maps the result to the entity schema.
|
|
545
|
-
* @param query - The SQL query to execute.
|
|
546
|
-
* @param schema - The entity schema defining the structure.
|
|
547
|
-
* @returns A list of mapped entity objects.
|
|
548
|
-
*/
|
|
549
|
-
async executeSchemaSQL(query, schema) {
|
|
550
|
-
const datas = await this.executeRawSQL(query);
|
|
551
|
-
if (!datas.length) return [];
|
|
552
|
-
return datas.map((r) => {
|
|
553
|
-
const rawModel = r;
|
|
554
|
-
const newModel = Object.create(schema.meta.prototype);
|
|
555
|
-
schema.meta.props.filter((p) => p.kind === "scalar").forEach((p) => {
|
|
556
|
-
const fieldName = p.name;
|
|
557
|
-
const fieldNames = p.fieldNames;
|
|
558
|
-
const rawFieldName = fieldNames && Array.isArray(fieldNames) ? fieldNames[0] : p.name;
|
|
559
|
-
switch (p.type) {
|
|
560
|
-
case "datetime":
|
|
561
|
-
newModel[fieldName] = parseDateTime(
|
|
562
|
-
rawModel[rawFieldName],
|
|
563
|
-
"YYYY-MM-DDTHH:mm:ss.SSS"
|
|
564
|
-
);
|
|
565
|
-
break;
|
|
566
|
-
case "date":
|
|
567
|
-
newModel[fieldName] = parseDateTime(rawModel[rawFieldName], "YYYY-MM-DD");
|
|
568
|
-
break;
|
|
569
|
-
case "time":
|
|
570
|
-
newModel[fieldName] = parseDateTime(rawModel[rawFieldName], "HH:mm:ss.SSS");
|
|
571
|
-
break;
|
|
572
|
-
default:
|
|
573
|
-
newModel[fieldName] = rawModel[rawFieldName];
|
|
574
|
-
}
|
|
575
|
-
});
|
|
576
|
-
return newModel;
|
|
577
|
-
});
|
|
517
|
+
return datas[0];
|
|
578
518
|
}
|
|
579
519
|
/**
|
|
580
520
|
* Executes a raw SQL query and returns the results.
|
|
581
|
-
*
|
|
582
|
-
*
|
|
521
|
+
* Logs the query if logging is enabled.
|
|
522
|
+
*
|
|
523
|
+
* @template T - The type of the result objects
|
|
524
|
+
* @param {string} query - The raw SQL query to execute
|
|
525
|
+
* @param {SqlParameters[]} [params] - Optional SQL parameters
|
|
526
|
+
* @returns {Promise<T[]>} A list of results as objects
|
|
583
527
|
*/
|
|
584
|
-
async executeRawSQL(query) {
|
|
528
|
+
async executeRawSQL(query, params) {
|
|
585
529
|
if (this.options.logRawSqlQuery) {
|
|
586
530
|
console.debug("Executing raw SQL: " + query);
|
|
587
531
|
}
|
|
588
|
-
const sqlStatement =
|
|
589
|
-
|
|
532
|
+
const sqlStatement = sql.prepare(query);
|
|
533
|
+
if (params) {
|
|
534
|
+
sqlStatement.bindParams(...params);
|
|
535
|
+
}
|
|
536
|
+
const result = await sqlStatement.execute();
|
|
537
|
+
return result.rows;
|
|
590
538
|
}
|
|
591
539
|
/**
|
|
592
540
|
* Executes a raw SQL update query.
|
|
593
|
-
* @param query - The raw SQL update query
|
|
594
|
-
* @param params -
|
|
595
|
-
* @returns The update response containing affected rows
|
|
541
|
+
* @param {string} query - The raw SQL update query
|
|
542
|
+
* @param {SqlParameters[]} [params] - Optional SQL parameters
|
|
543
|
+
* @returns {Promise<UpdateQueryResponse>} The update response containing affected rows
|
|
596
544
|
*/
|
|
597
545
|
async executeRawUpdateSQL(query, params) {
|
|
598
546
|
const sqlStatement = sql.prepare(query);
|
|
599
547
|
if (params) {
|
|
600
|
-
sqlStatement.bindParams(params);
|
|
548
|
+
sqlStatement.bindParams(...params);
|
|
601
549
|
}
|
|
602
550
|
const updateQueryResponseResults = await sqlStatement.execute();
|
|
603
551
|
return updateQueryResponseResults.rows;
|
|
@@ -605,39 +553,23 @@ class ForgeSQLSelectOperations {
|
|
|
605
553
|
}
|
|
606
554
|
class ForgeSQLORMImpl {
|
|
607
555
|
static instance = null;
|
|
608
|
-
|
|
556
|
+
drizzle;
|
|
609
557
|
crudOperations;
|
|
610
558
|
fetchOperations;
|
|
611
559
|
/**
|
|
612
560
|
* Private constructor to enforce singleton behavior.
|
|
613
|
-
* @param entities - The list of entities for ORM initialization.
|
|
614
561
|
* @param options - Options for configuring ForgeSQL ORM behavior.
|
|
615
562
|
*/
|
|
616
|
-
constructor(
|
|
563
|
+
constructor(options) {
|
|
617
564
|
try {
|
|
618
|
-
const newOptions = options ?? {
|
|
565
|
+
const newOptions = options ?? {
|
|
566
|
+
logRawSqlQuery: false,
|
|
567
|
+
disableOptimisticLocking: false
|
|
568
|
+
};
|
|
619
569
|
if (newOptions.logRawSqlQuery) {
|
|
620
570
|
console.debug("Initializing ForgeSQLORM...");
|
|
621
571
|
}
|
|
622
|
-
this.
|
|
623
|
-
dbName: "inmemory",
|
|
624
|
-
schemaGenerator: {
|
|
625
|
-
disableForeignKeys: false
|
|
626
|
-
},
|
|
627
|
-
discovery: {
|
|
628
|
-
warnWhenNoEntities: true
|
|
629
|
-
},
|
|
630
|
-
resultCache: {
|
|
631
|
-
adapter: NullCacheAdapter
|
|
632
|
-
},
|
|
633
|
-
metadataCache: {
|
|
634
|
-
enabled: false,
|
|
635
|
-
adapter: MemoryCacheAdapter
|
|
636
|
-
},
|
|
637
|
-
entities,
|
|
638
|
-
preferTs: false,
|
|
639
|
-
debug: false
|
|
640
|
-
});
|
|
572
|
+
this.drizzle = drizzle("");
|
|
641
573
|
this.crudOperations = new ForgeSQLCrudOperations(this, newOptions);
|
|
642
574
|
this.fetchOperations = new ForgeSQLSelectOperations(newOptions);
|
|
643
575
|
} catch (error) {
|
|
@@ -647,13 +579,12 @@ class ForgeSQLORMImpl {
|
|
|
647
579
|
}
|
|
648
580
|
/**
|
|
649
581
|
* Returns the singleton instance of ForgeSQLORMImpl.
|
|
650
|
-
* @param entities - List of entities (required only on first initialization).
|
|
651
582
|
* @param options - Options for configuring ForgeSQL ORM behavior.
|
|
652
583
|
* @returns The singleton instance of ForgeSQLORMImpl.
|
|
653
584
|
*/
|
|
654
|
-
static getInstance(
|
|
585
|
+
static getInstance(options) {
|
|
655
586
|
if (!ForgeSQLORMImpl.instance) {
|
|
656
|
-
ForgeSQLORMImpl.instance = new ForgeSQLORMImpl(
|
|
587
|
+
ForgeSQLORMImpl.instance = new ForgeSQLORMImpl(options);
|
|
657
588
|
}
|
|
658
589
|
return ForgeSQLORMImpl.instance;
|
|
659
590
|
}
|
|
@@ -672,28 +603,22 @@ class ForgeSQLORMImpl {
|
|
|
672
603
|
return this.fetchOperations;
|
|
673
604
|
}
|
|
674
605
|
/**
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
return this.mikroORM.em.createQueryBuilder(entityName, alias, void 0, loggerContext);
|
|
683
|
-
}
|
|
684
|
-
/**
|
|
685
|
-
* Provides access to the underlying Knex instance for building complex query parts.
|
|
686
|
-
* enabling advanced query customization and performance tuning.
|
|
687
|
-
* @returns The Knex instance, which can be used for query building.
|
|
606
|
+
* Returns a Drizzle query builder instance.
|
|
607
|
+
*
|
|
608
|
+
* ⚠️ IMPORTANT: This method should be used ONLY for query building purposes.
|
|
609
|
+
* The returned instance should NOT be used for direct database connections or query execution.
|
|
610
|
+
* All database operations should be performed through Forge SQL's executeRawSQL or executeRawUpdateSQL methods.
|
|
611
|
+
*
|
|
612
|
+
* @returns A Drizzle query builder instance for query construction only.
|
|
688
613
|
*/
|
|
689
|
-
|
|
690
|
-
return this.
|
|
614
|
+
getDrizzleQueryBuilder() {
|
|
615
|
+
return this.drizzle;
|
|
691
616
|
}
|
|
692
617
|
}
|
|
693
618
|
class ForgeSQLORM {
|
|
694
619
|
ormInstance;
|
|
695
|
-
constructor(
|
|
696
|
-
this.ormInstance = ForgeSQLORMImpl.getInstance(
|
|
620
|
+
constructor(options) {
|
|
621
|
+
this.ormInstance = ForgeSQLORMImpl.getInstance(options);
|
|
697
622
|
}
|
|
698
623
|
/**
|
|
699
624
|
* Proxies the `crud` method from `ForgeSQLORMImpl`.
|
|
@@ -709,20 +634,77 @@ class ForgeSQLORM {
|
|
|
709
634
|
fetch() {
|
|
710
635
|
return this.ormInstance.fetch();
|
|
711
636
|
}
|
|
712
|
-
getKnex() {
|
|
713
|
-
return this.ormInstance.getKnex();
|
|
714
|
-
}
|
|
715
637
|
/**
|
|
716
|
-
*
|
|
717
|
-
*
|
|
638
|
+
* Returns a Drizzle query builder instance.
|
|
639
|
+
*
|
|
640
|
+
* ⚠️ IMPORTANT: This method should be used ONLY for query building purposes.
|
|
641
|
+
* The returned instance should NOT be used for direct database connections or query execution.
|
|
642
|
+
* All database operations should be performed through Forge SQL's executeRawSQL or executeRawUpdateSQL methods.
|
|
643
|
+
*
|
|
644
|
+
* @returns A Drizzle query builder instance for query construction only.
|
|
718
645
|
*/
|
|
719
|
-
|
|
720
|
-
return this.ormInstance.
|
|
646
|
+
getDrizzleQueryBuilder() {
|
|
647
|
+
return this.ormInstance.getDrizzleQueryBuilder();
|
|
721
648
|
}
|
|
722
649
|
}
|
|
650
|
+
const mySqlDateTimeString = customType({
|
|
651
|
+
dataType() {
|
|
652
|
+
return "datetime";
|
|
653
|
+
},
|
|
654
|
+
toDriver(value) {
|
|
655
|
+
return moment$1(value).format("YYYY-MM-DDTHH:mm:ss.SSS");
|
|
656
|
+
},
|
|
657
|
+
fromDriver(value) {
|
|
658
|
+
const format = "YYYY-MM-DDTHH:mm:ss.SSS";
|
|
659
|
+
return parseDateTime(value, format);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
const mySqlTimestampString = customType({
|
|
663
|
+
dataType() {
|
|
664
|
+
return "timestamp";
|
|
665
|
+
},
|
|
666
|
+
toDriver(value) {
|
|
667
|
+
return moment$1(value).format("YYYY-MM-DDTHH:mm:ss.SSS");
|
|
668
|
+
},
|
|
669
|
+
fromDriver(value) {
|
|
670
|
+
const format = "YYYY-MM-DDTHH:mm:ss.SSS";
|
|
671
|
+
return parseDateTime(value, format);
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
const mySqlDateString = customType({
|
|
675
|
+
dataType() {
|
|
676
|
+
return "date";
|
|
677
|
+
},
|
|
678
|
+
toDriver(value) {
|
|
679
|
+
return moment$1(value).format("YYYY-MM-DD");
|
|
680
|
+
},
|
|
681
|
+
fromDriver(value) {
|
|
682
|
+
const format = "YYYY-MM-DD";
|
|
683
|
+
return parseDateTime(value, format);
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
const mySqlTimeString = customType({
|
|
687
|
+
dataType() {
|
|
688
|
+
return "time";
|
|
689
|
+
},
|
|
690
|
+
toDriver(value) {
|
|
691
|
+
return moment$1(value).format("HH:mm:ss.SSS");
|
|
692
|
+
},
|
|
693
|
+
fromDriver(value) {
|
|
694
|
+
return parseDateTime(value, "HH:mm:ss.SSS");
|
|
695
|
+
}
|
|
696
|
+
});
|
|
723
697
|
export {
|
|
724
698
|
ForgeSQLCrudOperations,
|
|
725
699
|
ForgeSQLSelectOperations,
|
|
726
|
-
ForgeSQLORM as default
|
|
700
|
+
ForgeSQLORM as default,
|
|
701
|
+
extractAlias,
|
|
702
|
+
getPrimaryKeys,
|
|
703
|
+
getTableMetadata,
|
|
704
|
+
mySqlDateString,
|
|
705
|
+
mySqlDateTimeString,
|
|
706
|
+
mySqlTimeString,
|
|
707
|
+
mySqlTimestampString,
|
|
708
|
+
parseDateTime
|
|
727
709
|
};
|
|
728
710
|
//# sourceMappingURL=ForgeSQLORM.mjs.map
|