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