ts-prorm-orm 1.2.3 → 2.1.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/CHANGELOG.md +331 -1
- package/README.md +32 -24
- package/dist/compliance/data-classifier.d.ts +6 -0
- package/dist/compliance/data-classifier.js +6 -0
- package/dist/compliance/data-lineage.d.ts +4 -0
- package/dist/compliance/data-lineage.js +4 -0
- package/dist/compliance/data-masker.d.ts +4 -0
- package/dist/compliance/data-masker.js +4 -0
- package/dist/compliance/index.d.ts +4 -0
- package/dist/compliance/index.js +15 -1
- package/dist/compliance/pseudonymization.d.ts +4 -0
- package/dist/compliance/pseudonymization.js +4 -0
- package/dist/compliance/query-firewall.d.ts +9 -9
- package/dist/compliance/query-firewall.js +9 -9
- package/dist/compliance/rate-limiter.d.ts +4 -0
- package/dist/compliance/rate-limiter.js +4 -0
- package/dist/compliance/row-level-security.d.ts +2 -0
- package/dist/compliance/row-level-security.js +2 -0
- package/dist/compliance/security-decorator.d.ts +8 -0
- package/dist/compliance/security-decorator.js +8 -0
- package/dist/compliance/session-isolation.d.ts +4 -0
- package/dist/compliance/session-isolation.js +4 -0
- package/dist/compliance/worm-storage.d.ts +4 -0
- package/dist/compliance/worm-storage.js +4 -0
- package/dist/connection-manager.js +13 -1
- package/dist/core/expressions.d.ts +58 -0
- package/dist/core/expressions.js +83 -0
- package/dist/core/hooks-manager.d.ts +179 -0
- package/dist/core/hooks-manager.js +291 -0
- package/dist/core/types.d.ts +235 -0
- package/dist/core/types.js +9 -0
- package/dist/decorators/audit.d.ts +3 -1
- package/dist/decorators/audit.js +3 -1
- package/dist/decorators/check.d.ts +15 -15
- package/dist/decorators/check.js +15 -15
- package/dist/decorators/collate.d.ts +16 -0
- package/dist/decorators/collate.js +16 -0
- package/dist/decorators/comment.d.ts +3 -1
- package/dist/decorators/comment.js +3 -1
- package/dist/decorators/database-settings.d.ts +6 -0
- package/dist/decorators/database-settings.js +6 -0
- package/dist/decorators/default.d.ts +2 -0
- package/dist/decorators/default.js +2 -0
- package/dist/decorators/encryption.d.ts +7 -1
- package/dist/decorators/encryption.js +7 -1
- package/dist/decorators/engine.d.ts +4 -0
- package/dist/decorators/engine.js +4 -0
- package/dist/decorators/fk-constraints.d.ts +32 -32
- package/dist/decorators/fk-constraints.js +32 -32
- package/dist/decorators/foreign-table.d.ts +1 -1
- package/dist/decorators/generated.d.ts +8 -0
- package/dist/decorators/generated.js +8 -0
- package/dist/decorators/hstore.d.ts +6 -0
- package/dist/decorators/hstore.js +6 -0
- package/dist/decorators/index.d.ts +2 -0
- package/dist/decorators/index.js +11 -1
- package/dist/decorators/json-column.d.ts +2 -0
- package/dist/decorators/json-column.js +2 -0
- package/dist/decorators/jsonb.d.ts +6 -0
- package/dist/decorators/jsonb.js +6 -0
- package/dist/decorators/permissions.d.ts +12 -0
- package/dist/decorators/permissions.js +12 -0
- package/dist/decorators/procedure.d.ts +4 -2
- package/dist/decorators/procedure.js +4 -2
- package/dist/decorators/query-options.d.ts +24 -0
- package/dist/decorators/query-options.js +24 -0
- package/dist/decorators/range.d.ts +8 -0
- package/dist/decorators/range.js +8 -0
- package/dist/decorators/set-column.d.ts +2 -0
- package/dist/decorators/set-column.js +2 -0
- package/dist/decorators/spatial.d.ts +2 -0
- package/dist/decorators/spatial.js +2 -0
- package/dist/decorators/storage.d.ts +24 -0
- package/dist/decorators/storage.js +24 -0
- package/dist/decorators/timezone.d.ts +7 -7
- package/dist/decorators/timezone.js +7 -7
- package/dist/decorators/trigger.d.ts +3 -1
- package/dist/decorators/trigger.js +3 -1
- package/dist/decorators/uuid.d.ts +6 -0
- package/dist/decorators/uuid.js +6 -0
- package/dist/decorators/view.d.ts +7 -1
- package/dist/decorators/view.js +7 -1
- package/dist/diagrams/chen-diagram.d.ts +15 -12
- package/dist/diagrams/chen-diagram.js +76 -85
- package/dist/diagrams/class-diagram.d.ts +9 -8
- package/dist/diagrams/class-diagram.js +60 -82
- package/dist/diagrams/core/index.d.ts +13 -0
- package/dist/diagrams/core/index.js +33 -0
- package/dist/diagrams/core/layout.d.ts +104 -0
- package/dist/diagrams/core/layout.js +223 -0
- package/dist/diagrams/{svg-dom.d.ts → core/svg.d.ts} +8 -20
- package/dist/diagrams/{svg-dom.js → core/svg.js} +21 -29
- package/dist/diagrams/core/theme.d.ts +84 -0
- package/dist/diagrams/core/theme.js +62 -0
- package/dist/diagrams/dependency-diagram.d.ts +15 -14
- package/dist/diagrams/dependency-diagram.js +71 -105
- package/dist/diagrams/er-diagram.d.ts +13 -26
- package/dist/diagrams/er-diagram.js +180 -259
- package/dist/diagrams/flow-diagram.d.ts +16 -11
- package/dist/diagrams/flow-diagram.js +96 -91
- package/dist/diagrams/gantt-diagram.d.ts +10 -5
- package/dist/diagrams/gantt-diagram.js +77 -103
- package/dist/diagrams/index-diagram.d.ts +10 -10
- package/dist/diagrams/index-diagram.js +106 -116
- package/dist/diagrams/index.d.ts +6 -9
- package/dist/diagrams/index.js +9 -24
- package/dist/diagrams/migration-diagram.d.ts +8 -4
- package/dist/diagrams/migration-diagram.js +189 -161
- package/dist/diagrams/model-diagram.d.ts +34 -34
- package/dist/diagrams/model-diagram.js +106 -485
- package/dist/diagrams/package-diagram.d.ts +9 -6
- package/dist/diagrams/package-diagram.js +64 -101
- package/dist/diagrams/relational-diagram.d.ts +12 -10
- package/dist/diagrams/relational-diagram.js +87 -114
- package/dist/diagrams/schemadoc-diagram.d.ts +8 -8
- package/dist/diagrams/schemadoc-diagram.js +71 -93
- package/dist/diagrams/sequence-diagram.d.ts +10 -10
- package/dist/diagrams/sequence-diagram.js +64 -89
- package/dist/diagrams/state-diagram.d.ts +10 -9
- package/dist/diagrams/state-diagram.js +69 -92
- package/dist/diagrams/tree-diagram.d.ts +11 -5
- package/dist/diagrams/tree-diagram.js +41 -65
- package/dist/dialects/clickhouse/index.js +15 -0
- package/dist/dialects/cockroachdb/index.d.ts +1 -1
- package/dist/dialects/cockroachdb/index.js +32 -63
- package/dist/dialects/db2/index.js +23 -8
- package/dist/dialects/dialect.d.ts +141 -5
- package/dist/dialects/duckdb/index.js +9 -2
- package/dist/dialects/hana/index.js +22 -5
- package/dist/dialects/lock-clause-helper.d.ts +167 -0
- package/dist/dialects/lock-clause-helper.js +316 -0
- package/dist/dialects/mariadb/index.d.ts +7 -4
- package/dist/dialects/mariadb/index.js +36 -60
- package/dist/dialects/mssql/index.js +34 -3
- package/dist/dialects/mysql/index.d.ts +1 -1
- package/dist/dialects/mysql/index.js +17 -45
- package/dist/dialects/oracle/index.d.ts +14 -3
- package/dist/dialects/oracle/index.js +66 -36
- package/dist/dialects/order-expression-helper.d.ts +72 -0
- package/dist/dialects/order-expression-helper.js +154 -0
- package/dist/dialects/partial-index.d.ts +52 -0
- package/dist/dialects/partial-index.js +251 -0
- package/dist/dialects/postgres/index.d.ts +1 -1
- package/dist/dialects/postgres/index.js +43 -68
- package/dist/dialects/redshift/index.js +9 -2
- package/dist/dialects/snowflake/index.js +17 -0
- package/dist/dialects/spanner/index.js +7 -2
- package/dist/dialects/sqlite/driver.d.ts +55 -0
- package/dist/dialects/sqlite/driver.js +102 -0
- package/dist/dialects/sqlite/index.d.ts +1 -1
- package/dist/dialects/sqlite/index.js +49 -27
- package/dist/external-fields/decorator.d.ts +2 -0
- package/dist/external-fields/decorator.js +2 -0
- package/dist/graph/base-graph-dialect.d.ts +24 -3
- package/dist/graph/base-graph-dialect.js +43 -2
- package/dist/graph/dgraph/index.js +6 -0
- package/dist/graph/gremlin/index.d.ts +6 -0
- package/dist/graph/gremlin/index.js +12 -1
- package/dist/graph/index.d.ts +1 -1
- package/dist/graph/neo4j/index.d.ts +2 -0
- package/dist/graph/neo4j/index.js +6 -1
- package/dist/graph/types.d.ts +12 -1
- package/dist/index.d.ts +12 -3
- package/dist/index.js +53 -7
- package/dist/logging/index.d.ts +10 -0
- package/dist/logging/index.js +23 -0
- package/dist/logging/query-logging.d.ts +82 -0
- package/dist/logging/query-logging.js +102 -0
- package/dist/models/associations.d.ts +38 -0
- package/dist/models/associations.js +16 -0
- package/dist/models/decorators.d.ts +55 -19
- package/dist/models/decorators.js +95 -19
- package/dist/models/eager-load.d.ts +99 -0
- package/dist/models/eager-load.js +348 -5
- package/dist/models/indexes.d.ts +3 -2
- package/dist/models/indexes.js +7 -1
- package/dist/models/model.js +69 -43
- package/dist/prorm.d.ts +175 -409
- package/dist/prorm.js +1132 -553
- package/dist/query-builders/order-limit-builder.js +12 -10
- package/dist/query-builders/sql-compiler.d.ts +10 -3
- package/dist/query-builders/sql-compiler.js +14 -43
- package/dist/query-interface.d.ts +2 -1
- package/dist/schema-objects.d.ts +42 -0
- package/dist/schema-objects.js +28 -0
- package/dist/sql-constants.d.ts +44 -0
- package/dist/sql-constants.js +138 -1
- package/dist/types/index.d.ts +387 -17
- package/dist/types/index.js +10 -15
- package/dist/types/query-types.d.ts +26 -0
- package/dist/types/query-types.js +30 -0
- package/package.json +39 -8
- package/dist/decorators/belongs-to-many.js +0 -115
- package/dist/decorators/belongs-to.js +0 -115
- package/dist/decorators/has-many.js +0 -127
- package/dist/decorators/has-one.js +0 -116
- package/dist/diagrams/palette.d.ts +0 -138
- package/dist/diagrams/palette.js +0 -194
- package/dist/hooks/hooks-manager.d.ts +0 -189
- package/dist/hooks/hooks-manager.js +0 -350
- package/dist/hooks/index.d.ts +0 -8
- package/dist/hooks/index.js +0 -37
- /package/dist/{logging.d.ts → logging/logger.d.ts} +0 -0
- /package/dist/{logging.js → logging/logger.js} +0 -0
package/dist/prorm.js
CHANGED
|
@@ -78,372 +78,21 @@ const query_interface_1 = require("./query-interface");
|
|
|
78
78
|
// Import UUIDV4 from data-types
|
|
79
79
|
const data_types_1 = require("./models/data-types");
|
|
80
80
|
const sql_constants_1 = require("./sql-constants");
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
this.afterSyncHooks = [];
|
|
90
|
-
this.beforeConnectHooks = [];
|
|
91
|
-
this.afterConnectHooks = [];
|
|
92
|
-
this.beforeDisconnectHooks = [];
|
|
93
|
-
this.afterDisconnectHooks = [];
|
|
94
|
-
this.beforeDestroyHooks = [];
|
|
95
|
-
this.afterDestroyHooks = [];
|
|
96
|
-
this.beforeUpsertHooks = [];
|
|
97
|
-
this.afterUpsertHooks = [];
|
|
98
|
-
this.beforeReloadHooks = [];
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Register a beforeDefine hook
|
|
102
|
-
*/
|
|
103
|
-
beforeDefine(callback) {
|
|
104
|
-
this.beforeDefineHooks.push(callback);
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Register an afterDefine hook
|
|
108
|
-
*/
|
|
109
|
-
afterDefine(callback) {
|
|
110
|
-
this.afterDefineHooks.push(callback);
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Register a beforeSync hook
|
|
114
|
-
*/
|
|
115
|
-
beforeSync(callback) {
|
|
116
|
-
this.beforeSyncHooks.push(callback);
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Register an afterSync hook
|
|
120
|
-
*/
|
|
121
|
-
afterSync(callback) {
|
|
122
|
-
this.afterSyncHooks.push(callback);
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Register a beforeConnect hook
|
|
126
|
-
*/
|
|
127
|
-
beforeConnect(callback) {
|
|
128
|
-
this.beforeConnectHooks.push(callback);
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Register an afterConnect hook
|
|
132
|
-
*/
|
|
133
|
-
afterConnect(callback) {
|
|
134
|
-
this.afterConnectHooks.push(callback);
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Register a beforeDisconnect hook
|
|
138
|
-
*/
|
|
139
|
-
beforeDisconnect(callback) {
|
|
140
|
-
this.beforeDisconnectHooks.push(callback);
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Register an afterDisconnect hook
|
|
144
|
-
*/
|
|
145
|
-
afterDisconnect(callback) {
|
|
146
|
-
this.afterDisconnectHooks.push(callback);
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Register a beforeDestroy hook
|
|
150
|
-
*/
|
|
151
|
-
beforeDestroy(callback) {
|
|
152
|
-
this.beforeDestroyHooks.push(callback);
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Register an afterDestroy hook
|
|
156
|
-
*/
|
|
157
|
-
afterDestroy(callback) {
|
|
158
|
-
this.afterDestroyHooks.push(callback);
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* Register a beforeUpsert hook
|
|
162
|
-
*/
|
|
163
|
-
beforeUpsert(callback) {
|
|
164
|
-
this.beforeUpsertHooks.push(callback);
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Register an afterUpsert hook
|
|
168
|
-
*/
|
|
169
|
-
afterUpsert(callback) {
|
|
170
|
-
this.afterUpsertHooks.push(callback);
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Register a beforeReload hook
|
|
174
|
-
*/
|
|
175
|
-
beforeReload(callback) {
|
|
176
|
-
this.beforeReloadHooks.push(callback);
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Execute all beforeDefine hooks
|
|
180
|
-
*/
|
|
181
|
-
async runBeforeDefine(modelName, attributes, options) {
|
|
182
|
-
for (const hook of this.beforeDefineHooks) {
|
|
183
|
-
await hook({ modelName, attributes, options });
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* Execute all afterDefine hooks
|
|
188
|
-
*/
|
|
189
|
-
async runAfterDefine(model) {
|
|
190
|
-
for (const hook of this.afterDefineHooks) {
|
|
191
|
-
await hook({ model });
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* Execute all beforeSync hooks
|
|
196
|
-
*/
|
|
197
|
-
async runBeforeSync(options) {
|
|
198
|
-
for (const hook of this.beforeSyncHooks) {
|
|
199
|
-
await hook({ options });
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Execute all afterSync hooks
|
|
204
|
-
*/
|
|
205
|
-
async runAfterSync(options) {
|
|
206
|
-
for (const hook of this.afterSyncHooks) {
|
|
207
|
-
await hook({ options });
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Execute all beforeConnect hooks
|
|
212
|
-
*/
|
|
213
|
-
async runBeforeConnect() {
|
|
214
|
-
for (const hook of this.beforeConnectHooks) {
|
|
215
|
-
await hook();
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* Execute all afterConnect hooks
|
|
220
|
-
* @param connection - The connection object to pass to hooks
|
|
221
|
-
*/
|
|
222
|
-
async runAfterConnect(connection) {
|
|
223
|
-
for (const hook of this.afterConnectHooks) {
|
|
224
|
-
await hook(connection);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Execute all beforeDisconnect hooks
|
|
229
|
-
* @param connection - The connection object to pass to hooks
|
|
230
|
-
*/
|
|
231
|
-
async runBeforeDisconnect(connection) {
|
|
232
|
-
for (const hook of this.beforeDisconnectHooks) {
|
|
233
|
-
await hook(connection);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
/**
|
|
237
|
-
* Execute all afterDisconnect hooks
|
|
238
|
-
* @param connection - The connection object to pass to hooks
|
|
239
|
-
*/
|
|
240
|
-
async runAfterDisconnect(connection) {
|
|
241
|
-
for (const hook of this.afterDisconnectHooks) {
|
|
242
|
-
await hook(connection);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Execute all beforeDestroy hooks
|
|
247
|
-
* @param options - The destroy options
|
|
248
|
-
*/
|
|
249
|
-
async runBeforeDestroy(options) {
|
|
250
|
-
for (const hook of this.beforeDestroyHooks) {
|
|
251
|
-
await hook({ options });
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* Execute all afterDestroy hooks
|
|
256
|
-
* @param options - The destroy options
|
|
257
|
-
*/
|
|
258
|
-
async runAfterDestroy(options) {
|
|
259
|
-
for (const hook of this.afterDestroyHooks) {
|
|
260
|
-
await hook({ options });
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* Execute all beforeUpsert hooks
|
|
265
|
-
* @param options - The upsert options
|
|
266
|
-
*/
|
|
267
|
-
async runBeforeUpsert(options) {
|
|
268
|
-
for (const hook of this.beforeUpsertHooks) {
|
|
269
|
-
await hook({ options });
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* Execute all afterUpsert hooks
|
|
274
|
-
* @param options - The upsert options
|
|
275
|
-
*/
|
|
276
|
-
async runAfterUpsert(options) {
|
|
277
|
-
for (const hook of this.afterUpsertHooks) {
|
|
278
|
-
await hook({ options });
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
/**
|
|
282
|
-
* Execute all beforeReload hooks
|
|
283
|
-
* @param options - The reload options
|
|
284
|
-
*/
|
|
285
|
-
async runBeforeReload(options) {
|
|
286
|
-
for (const hook of this.beforeReloadHooks) {
|
|
287
|
-
await hook({ options });
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* Add a hook by name
|
|
292
|
-
* @param hookName - The name of the hook (e.g., 'beforeDefine', 'afterSync')
|
|
293
|
-
* @param callback - The hook callback function
|
|
294
|
-
*/
|
|
295
|
-
addHook(hookName, callback) {
|
|
296
|
-
const methodName = hookName;
|
|
297
|
-
if (typeof this[methodName] === 'function') {
|
|
298
|
-
this[methodName](callback);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
/**
|
|
302
|
-
* Clear all hooks
|
|
303
|
-
*/
|
|
304
|
-
clearHooks() {
|
|
305
|
-
this.beforeDefineHooks = [];
|
|
306
|
-
this.afterDefineHooks = [];
|
|
307
|
-
this.beforeSyncHooks = [];
|
|
308
|
-
this.afterSyncHooks = [];
|
|
309
|
-
this.beforeConnectHooks = [];
|
|
310
|
-
this.afterConnectHooks = [];
|
|
311
|
-
this.beforeDisconnectHooks = [];
|
|
312
|
-
this.afterDisconnectHooks = [];
|
|
313
|
-
this.beforeDestroyHooks = [];
|
|
314
|
-
this.afterDestroyHooks = [];
|
|
315
|
-
this.beforeUpsertHooks = [];
|
|
316
|
-
this.afterUpsertHooks = [];
|
|
317
|
-
this.beforeReloadHooks = [];
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
exports.HooksManager = HooksManager;
|
|
321
|
-
/**
|
|
322
|
-
* Model-level hooks manager
|
|
323
|
-
*/
|
|
324
|
-
class ModelHooks {
|
|
325
|
-
constructor() {
|
|
326
|
-
this.beforeSyncHooks = [];
|
|
327
|
-
this.afterSyncHooks = [];
|
|
328
|
-
}
|
|
329
|
-
/**
|
|
330
|
-
* Register a beforeSync hook on the model
|
|
331
|
-
*/
|
|
332
|
-
beforeSync(callback) {
|
|
333
|
-
this.beforeSyncHooks.push(callback);
|
|
334
|
-
}
|
|
335
|
-
/**
|
|
336
|
-
* Register an afterSync hook on the model
|
|
337
|
-
*/
|
|
338
|
-
afterSync(callback) {
|
|
339
|
-
this.afterSyncHooks.push(callback);
|
|
340
|
-
}
|
|
341
|
-
/**
|
|
342
|
-
* Run beforeSync hooks
|
|
343
|
-
*/
|
|
344
|
-
async runBeforeSync(options) {
|
|
345
|
-
for (const hook of this.beforeSyncHooks) {
|
|
346
|
-
await hook({ options });
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
/**
|
|
350
|
-
* Run afterSync hooks
|
|
351
|
-
*/
|
|
352
|
-
async runAfterSync(options) {
|
|
353
|
-
for (const hook of this.afterSyncHooks) {
|
|
354
|
-
await hook({ options });
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
/**
|
|
359
|
-
* Represents a raw SQL literal value
|
|
360
|
-
* Used for inserting raw SQL expressions into queries
|
|
361
|
-
*
|
|
362
|
-
* @example
|
|
363
|
-
* // Use in default values
|
|
364
|
-
* createdAt: { type: DATE, defaultValue: prorm.literal('NOW()') }
|
|
365
|
-
*
|
|
366
|
-
* @example
|
|
367
|
-
* // Use in updates
|
|
368
|
-
* User.update({ count: prorm.literal('count + 1') }, {...})
|
|
369
|
-
*
|
|
370
|
-
* @example
|
|
371
|
-
* // Use in select with calculations
|
|
372
|
-
* User.findAll({ attributes: [[prorm.literal('1 + 1'), 'two']] })
|
|
373
|
-
*/
|
|
374
|
-
class Literal {
|
|
375
|
-
constructor(sql) {
|
|
376
|
-
this.val = sql;
|
|
377
|
-
}
|
|
378
|
-
/**
|
|
379
|
-
* Get the raw SQL string
|
|
380
|
-
*/
|
|
381
|
-
toString() {
|
|
382
|
-
return this.val;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
exports.Literal = Literal;
|
|
386
|
-
/**
|
|
387
|
-
* Class representing a column reference for use with Prorm functions
|
|
388
|
-
* Used to create properly quoted column identifiers
|
|
389
|
-
*
|
|
390
|
-
* @example
|
|
391
|
-
* // Single column: col('name') -> "name"
|
|
392
|
-
* // Table and column: col('User', 'name') -> "User"."name"
|
|
393
|
-
* // Dot notation: col('table.column') -> "table"."column"
|
|
394
|
-
*/
|
|
395
|
-
class Col {
|
|
396
|
-
/**
|
|
397
|
-
* Create a column reference
|
|
398
|
-
* @param tableOrColumn - Table name (if second param provided) or column name
|
|
399
|
-
* @param column - Column name (if first param is table name)
|
|
400
|
-
*/
|
|
401
|
-
constructor(tableOrColumn, column) {
|
|
402
|
-
if (column !== undefined) {
|
|
403
|
-
// col('User', 'name') -> "User"."name"
|
|
404
|
-
this.col = `"${tableOrColumn}"."${column}"`;
|
|
405
|
-
}
|
|
406
|
-
else if (tableOrColumn.includes('.')) {
|
|
407
|
-
// col('table.column') -> "table"."column"
|
|
408
|
-
const [table, col] = tableOrColumn.split('.');
|
|
409
|
-
this.col = `"${table}"."${col}"`;
|
|
410
|
-
}
|
|
411
|
-
else {
|
|
412
|
-
// col('name') -> "name"
|
|
413
|
-
this.col = `"${tableOrColumn}"`;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
/**
|
|
417
|
-
* Get the SQL representation of the column
|
|
418
|
-
*/
|
|
419
|
-
toSql() {
|
|
420
|
-
return this.col;
|
|
421
|
-
}
|
|
422
|
-
/**
|
|
423
|
-
* Get the column as a string
|
|
424
|
-
*/
|
|
425
|
-
toString() {
|
|
426
|
-
return this.col;
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
exports.Col = Col;
|
|
81
|
+
// Instance-API types, hook registries and SQL expression wrappers now live in
|
|
82
|
+
// ./core. They are imported for local use and re-exported, so the public API
|
|
83
|
+
// exposed from this module is unchanged.
|
|
84
|
+
const hooks_manager_1 = require("./core/hooks-manager");
|
|
85
|
+
Object.defineProperty(exports, "HooksManager", { enumerable: true, get: function () { return hooks_manager_1.HooksManager; } });
|
|
86
|
+
const expressions_1 = require("./core/expressions");
|
|
87
|
+
Object.defineProperty(exports, "Literal", { enumerable: true, get: function () { return expressions_1.Literal; } });
|
|
88
|
+
Object.defineProperty(exports, "Col", { enumerable: true, get: function () { return expressions_1.Col; } });
|
|
430
89
|
/**
|
|
431
90
|
* Query types for raw SQL queries
|
|
432
91
|
*/
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
QueryTypes["UPDATE"] = "UPDATE";
|
|
438
|
-
QueryTypes["DELETE"] = "DELETE";
|
|
439
|
-
QueryTypes["BULKDELETE"] = "BULKDELETE";
|
|
440
|
-
QueryTypes["BULKINSERT"] = "BULKINSERT";
|
|
441
|
-
QueryTypes["UPSERT"] = "UPSERT";
|
|
442
|
-
QueryTypes["VERSION"] = "VERSION";
|
|
443
|
-
QueryTypes["SHOWTABLES"] = "SHOWTABLES";
|
|
444
|
-
QueryTypes["DESCRIBE"] = "DESCRIBE";
|
|
445
|
-
QueryTypes["RAW"] = "RAW";
|
|
446
|
-
})(QueryTypes || (exports.QueryTypes = QueryTypes = {}));
|
|
92
|
+
// QueryTypes is defined once in ./types/query-types. Imported for local use
|
|
93
|
+
// and re-exported, since the package root has always exported it from here.
|
|
94
|
+
const query_types_1 = require("./types/query-types");
|
|
95
|
+
Object.defineProperty(exports, "QueryTypes", { enumerable: true, get: function () { return query_types_1.QueryTypes; } });
|
|
447
96
|
/**
|
|
448
97
|
* Resolve the primary-key attribute name for a model, based on its `rawAttributes`
|
|
449
98
|
* (looking for the field marked `primaryKey: true`). Falls back to `'id'` when the
|
|
@@ -493,6 +142,48 @@ function extractAssociationConditions(where) {
|
|
|
493
142
|
}
|
|
494
143
|
return { rest, conditions };
|
|
495
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Model options inherited from an `@AbstractModel` base.
|
|
147
|
+
*
|
|
148
|
+
* A concrete model with its own `@Table` uses that; one without falls back to
|
|
149
|
+
* the nearest annotated ancestor, so timestamps/paranoid/scopes declared once
|
|
150
|
+
* on a base apply to every subclass.
|
|
151
|
+
*/
|
|
152
|
+
function inheritedModelMetadata(modelClass) {
|
|
153
|
+
let current = Object.getPrototypeOf(modelClass);
|
|
154
|
+
while (current && current.name) {
|
|
155
|
+
const meta = (0, decorators_1.getModelMetadata)(current.name);
|
|
156
|
+
if (meta)
|
|
157
|
+
return meta;
|
|
158
|
+
current = Object.getPrototypeOf(current);
|
|
159
|
+
}
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The column names a model's table actually has.
|
|
164
|
+
*
|
|
165
|
+
* `rawAttributes` holds only what the caller declared. `createTableFromModel`
|
|
166
|
+
* additionally synthesizes an `id` primary key when none was declared, plus the
|
|
167
|
+
* timestamp columns - so a model defined as `{ name }` yields a table with
|
|
168
|
+
* `id, name, createdAt, updatedAt`. Validating an attribute name against
|
|
169
|
+
* `rawAttributes` alone therefore rejects columns that genuinely exist.
|
|
170
|
+
*/
|
|
171
|
+
function modelColumnNames(model) {
|
|
172
|
+
const names = new Set(Object.keys(model?.rawAttributes || {}));
|
|
173
|
+
const options = (model?.options || {});
|
|
174
|
+
// Implicit primary key: added only when the model declared none.
|
|
175
|
+
const hasDeclaredPk = Object.values(model?.rawAttributes || {}).some((a) => a?.primaryKey);
|
|
176
|
+
if (!hasDeclaredPk && !names.has('id'))
|
|
177
|
+
names.add('id');
|
|
178
|
+
if (options?.timestamps !== false) {
|
|
179
|
+
names.add(options?.createdAt || 'createdAt');
|
|
180
|
+
names.add(options?.updatedAt || 'updatedAt');
|
|
181
|
+
}
|
|
182
|
+
if (options?.paranoid) {
|
|
183
|
+
names.add(options?.deletedAt || 'deletedAt');
|
|
184
|
+
}
|
|
185
|
+
return names;
|
|
186
|
+
}
|
|
496
187
|
/**
|
|
497
188
|
* Derive a model's table name from its name and options.
|
|
498
189
|
*
|
|
@@ -554,6 +245,127 @@ function resolveForeignKeyColumnType(refModel, refField) {
|
|
|
554
245
|
// Otherwise mirror the referenced column's own type (UUID, STRING(n), BIGINT, ...).
|
|
555
246
|
return (type ?? 'INTEGER');
|
|
556
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Resolve the physical column name a model attribute maps to.
|
|
250
|
+
*
|
|
251
|
+
* `field` renames a column explicitly and `underscored` snake_cases the rest,
|
|
252
|
+
* but a constraint has to name the column the CREATE TABLE statement actually
|
|
253
|
+
* emitted. This DDL path keys its column map by attribute name, so when the
|
|
254
|
+
* renamed name is not among the emitted columns the attribute name is used
|
|
255
|
+
* instead: naming a column that does not exist would turn a silently-missing
|
|
256
|
+
* constraint into a hard CREATE TABLE failure, which is not an improvement.
|
|
257
|
+
*/
|
|
258
|
+
function resolvePhysicalColumnName(attrName, attributes, options, emittedColumns) {
|
|
259
|
+
const attr = attributes[attrName];
|
|
260
|
+
const renamed = attr?.field ?? (options?.underscored ? (0, utils_2.snakeCase)(attrName) : undefined);
|
|
261
|
+
if (!renamed || renamed === attrName)
|
|
262
|
+
return attrName;
|
|
263
|
+
if (emittedColumns[renamed])
|
|
264
|
+
return renamed;
|
|
265
|
+
return emittedColumns[attrName] ? attrName : renamed;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Collect every composite-unique declaration on a model into one canonical list.
|
|
269
|
+
*
|
|
270
|
+
* Three documented, typed ways to declare a composite unique constraint all
|
|
271
|
+
* used to be read by nobody, so they emitted no SQL at all: the developer read
|
|
272
|
+
* back a `CREATE TABLE` with no UNIQUE in it only if they went looking, the
|
|
273
|
+
* database never enforced anything, and the duplicate rows turned up later.
|
|
274
|
+
*
|
|
275
|
+
* - attribute-level `uniqueKey: 'name'` - every attribute sharing that value
|
|
276
|
+
* joins one constraint of that name, in declaration order (as documented on
|
|
277
|
+
* `AttributeOptions.uniqueKey`);
|
|
278
|
+
* - model-level `uniqueKeys: UniqueKeyOptions[]`, whose optional `unique: true`
|
|
279
|
+
* selects a unique index over a table constraint;
|
|
280
|
+
* - the legacy `uniqueKeysLegacy: Record<name, fields[]>` map, still declared
|
|
281
|
+
* on `ModelOptions` and still supported here so that models written against
|
|
282
|
+
* it keep working.
|
|
283
|
+
*
|
|
284
|
+
* Declaring the same constraint through more than one form is deliberately not
|
|
285
|
+
* an error - it is the natural thing to do when migrating from one form to
|
|
286
|
+
* another - so entries are collapsed both by name and by column set, and the
|
|
287
|
+
* first declaration of each wins.
|
|
288
|
+
*
|
|
289
|
+
* @throws {Error} when a declaration names a column the table does not have -
|
|
290
|
+
* the whole point of this function is that a unique key which cannot be
|
|
291
|
+
* enforced must not pass silently.
|
|
292
|
+
*/
|
|
293
|
+
function collectUniqueKeys(tableName, attributes, options, emittedColumns) {
|
|
294
|
+
const opts = options;
|
|
295
|
+
const collected = [];
|
|
296
|
+
const seenNames = new Set();
|
|
297
|
+
const seenColumnSets = new Set();
|
|
298
|
+
const columnOf = (attrName) => resolvePhysicalColumnName(attrName, attributes, options, emittedColumns);
|
|
299
|
+
const add = (name, columns, asIndex, source) => {
|
|
300
|
+
if (columns.length === 0)
|
|
301
|
+
return;
|
|
302
|
+
for (const column of columns) {
|
|
303
|
+
if (!emittedColumns[column]) {
|
|
304
|
+
throw new Error(`Unique key "${name}" on table "${tableName}" (${source}) references ` +
|
|
305
|
+
`column "${column}", which the table does not have. ` +
|
|
306
|
+
`Known columns: ${Object.keys(emittedColumns).join(', ')}.`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Column order is part of a composite key's identity for index purposes but
|
|
310
|
+
// not for uniqueness, so the de-duplication signature is order-independent:
|
|
311
|
+
// (a, b) and (b, a) enforce exactly the same rule and one of them is enough.
|
|
312
|
+
const signature = [...columns].sort().join(' ');
|
|
313
|
+
if (seenNames.has(name) || seenColumnSets.has(signature))
|
|
314
|
+
return;
|
|
315
|
+
seenNames.add(name);
|
|
316
|
+
seenColumnSets.add(signature);
|
|
317
|
+
collected.push({ name, fields: columns, asIndex });
|
|
318
|
+
};
|
|
319
|
+
// 1. Attribute-level grouping. Insertion order of `attributes` is the
|
|
320
|
+
// declaration order the user wrote, which is the order the columns take.
|
|
321
|
+
const groups = new Map();
|
|
322
|
+
for (const [attrName, attr] of Object.entries(attributes)) {
|
|
323
|
+
const key = attr?.uniqueKey;
|
|
324
|
+
if (typeof key !== 'string' || key.length === 0)
|
|
325
|
+
continue;
|
|
326
|
+
const group = groups.get(key);
|
|
327
|
+
if (group)
|
|
328
|
+
group.push(columnOf(attrName));
|
|
329
|
+
else
|
|
330
|
+
groups.set(key, [columnOf(attrName)]);
|
|
331
|
+
}
|
|
332
|
+
for (const [name, columns] of groups) {
|
|
333
|
+
add(name, columns, false, 'attribute-level uniqueKey');
|
|
334
|
+
}
|
|
335
|
+
// 2. Model-level `uniqueKeys: UniqueKeyOptions[]`. Tolerate the legacy record
|
|
336
|
+
// shape here too - `ModelOptions.uniqueKeys` is typed as an array, but the
|
|
337
|
+
// dialects have always accepted `Record<name, fields[]>` under the same
|
|
338
|
+
// option name, and an untyped caller passing one should not be dropped.
|
|
339
|
+
const uniqueKeys = opts?.uniqueKeys;
|
|
340
|
+
if (Array.isArray(uniqueKeys)) {
|
|
341
|
+
for (const uk of uniqueKeys) {
|
|
342
|
+
if (!uk || !Array.isArray(uk.fields))
|
|
343
|
+
continue;
|
|
344
|
+
const columns = uk.fields.map(columnOf);
|
|
345
|
+
const name = uk.name || `${tableName}_${columns.join('_')}_key`;
|
|
346
|
+
add(name, columns, uk.unique === true, 'model-level uniqueKeys');
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
else if (uniqueKeys && typeof uniqueKeys === 'object') {
|
|
350
|
+
for (const [name, fields] of Object.entries(uniqueKeys)) {
|
|
351
|
+
if (!Array.isArray(fields))
|
|
352
|
+
continue;
|
|
353
|
+
const columns = fields.map(columnOf);
|
|
354
|
+
add(name || `${tableName}_${columns.join('_')}_key`, columns, false, 'model-level uniqueKeys');
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// 3. Legacy `uniqueKeysLegacy` map.
|
|
358
|
+
const legacy = opts?.uniqueKeysLegacy;
|
|
359
|
+
if (legacy && typeof legacy === 'object') {
|
|
360
|
+
for (const [name, fields] of Object.entries(legacy)) {
|
|
361
|
+
if (!Array.isArray(fields))
|
|
362
|
+
continue;
|
|
363
|
+
const columns = fields.map(columnOf);
|
|
364
|
+
add(name || `${tableName}_${columns.join('_')}_key`, columns, false, 'uniqueKeysLegacy');
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return collected;
|
|
368
|
+
}
|
|
557
369
|
// Tracks (via Node's AsyncLocalStorage, which correctly follows a logical
|
|
558
370
|
// call chain across `await`s regardless of how concurrent operations
|
|
559
371
|
// interleave) whether the currently-executing code is running inside an
|
|
@@ -648,6 +460,17 @@ class Prorm extends events_1.EventEmitter {
|
|
|
648
460
|
this._fdwManager = null;
|
|
649
461
|
this._userManager = null;
|
|
650
462
|
this._models = new Map();
|
|
463
|
+
/**
|
|
464
|
+
* The registry this instance was created by, if any.
|
|
465
|
+
*
|
|
466
|
+
* `ConnectionManager.addConnection()` sets this on the instance it builds,
|
|
467
|
+
* which is what makes `FindOptions.using` work: a query can name a sibling
|
|
468
|
+
* connection and be routed to that connection's copy of the model. Nothing
|
|
469
|
+
* else assigned it before, so `using` threw "Available: none" on every call.
|
|
470
|
+
* Typed structurally (rather than as `ConnectionManager`) to keep prorm.ts
|
|
471
|
+
* free of an import cycle with connection-manager.ts.
|
|
472
|
+
*/
|
|
473
|
+
this.connectionManager = null;
|
|
651
474
|
this.currentTransaction = null;
|
|
652
475
|
// Serializes root-level (non-nested) transactions for dialects backed by a
|
|
653
476
|
// single shared connection (currently just SQLite/better-sqlite3) - see
|
|
@@ -680,7 +503,7 @@ class Prorm extends events_1.EventEmitter {
|
|
|
680
503
|
// i.e. missing the FOREIGN KEY/REFERENCES clause associations imply.
|
|
681
504
|
this.pendingTableCreates = [];
|
|
682
505
|
this._isConnected = false;
|
|
683
|
-
this.hooksManager = new HooksManager();
|
|
506
|
+
this.hooksManager = new hooks_manager_1.HooksManager();
|
|
684
507
|
this.modelHooks = new Map();
|
|
685
508
|
/**
|
|
686
509
|
* The connection pool instance
|
|
@@ -721,9 +544,11 @@ class Prorm extends events_1.EventEmitter {
|
|
|
721
544
|
logSQL: options.logSQL || false,
|
|
722
545
|
slowQueryThreshold: options.slowQueryThreshold || 1000,
|
|
723
546
|
logSlowQueries: options.logSlowQueries !== false,
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
547
|
+
// Deliberately NOT wired to `options.logging`. User-facing query logging
|
|
548
|
+
// is owned by `_logQuery`, which honours per-query overrides; handing the
|
|
549
|
+
// same callback to this logger too made it fire a second time and ignore
|
|
550
|
+
// `{ logging: false }` on an individual call. This logger's remaining job
|
|
551
|
+
// is slow-query detection and the debug/info/warn/error levels.
|
|
727
552
|
colors: options.colors !== false,
|
|
728
553
|
});
|
|
729
554
|
// Set up query logging event handlers
|
|
@@ -780,7 +605,7 @@ class Prorm extends events_1.EventEmitter {
|
|
|
780
605
|
* });
|
|
781
606
|
*/
|
|
782
607
|
literal(sql) {
|
|
783
|
-
return new Literal((0, operators_1.literal)(sql).$literal);
|
|
608
|
+
return new expressions_1.Literal((0, operators_1.literal)(sql).$literal);
|
|
784
609
|
}
|
|
785
610
|
/**
|
|
786
611
|
* Set up the connection pool with event forwarding
|
|
@@ -1194,7 +1019,7 @@ class Prorm extends events_1.EventEmitter {
|
|
|
1194
1019
|
sql: 'SELECT 1',
|
|
1195
1020
|
duration,
|
|
1196
1021
|
timestamp: new Date(),
|
|
1197
|
-
type: QueryTypes.SELECT,
|
|
1022
|
+
type: query_types_1.QueryTypes.SELECT,
|
|
1198
1023
|
});
|
|
1199
1024
|
}
|
|
1200
1025
|
catch (error) {
|
|
@@ -1293,40 +1118,31 @@ class Prorm extends events_1.EventEmitter {
|
|
|
1293
1118
|
}
|
|
1294
1119
|
/**
|
|
1295
1120
|
* Disable foreign key checks for the current session
|
|
1121
|
+
*
|
|
1122
|
+
* On PostgreSQL this issues `SET session_replication_role = 'replica'`, which
|
|
1123
|
+
* needs superuser or a role granted `SET ON PARAMETER
|
|
1124
|
+
* session_replication_role` (PostgreSQL 15+).
|
|
1125
|
+
*
|
|
1126
|
+
* @throws {UnsupportedForeignKeyChecksError} on dialects with no
|
|
1127
|
+
* session-level switch (SQL Server, Oracle and Db2 are per-table; Redshift,
|
|
1128
|
+
* Snowflake and ClickHouse never enforce foreign keys).
|
|
1296
1129
|
* @returns Promise that resolves when foreign key checks are disabled
|
|
1297
1130
|
*/
|
|
1298
1131
|
async disableForeignKeyChecks() {
|
|
1299
|
-
|
|
1300
|
-
let sql;
|
|
1301
|
-
if (dialect === 'sqlite' || dialect === 'turso') {
|
|
1302
|
-
sql = sql_constants_1.SQLITE.PRAGMA.foreign_keys.off;
|
|
1303
|
-
}
|
|
1304
|
-
else if (dialect === 'postgres') {
|
|
1305
|
-
// PostgreSQL uses deferrable constraints, not FK checks
|
|
1306
|
-
sql = 'SET FOREIGN_KEY_CHECKS = 0'; // Fallback
|
|
1307
|
-
}
|
|
1308
|
-
else {
|
|
1309
|
-
sql = sql_constants_1.MYSQL.FOREIGN_KEY_CHECKS.disable;
|
|
1310
|
-
}
|
|
1311
|
-
await this.query(sql, { raw: true });
|
|
1132
|
+
await this.query(this.getDisableForeignKeyChecksSQL(), { raw: true });
|
|
1312
1133
|
}
|
|
1313
1134
|
/**
|
|
1314
1135
|
* Enable foreign key checks for the current session
|
|
1136
|
+
*
|
|
1137
|
+
* The inverse of {@link disableForeignKeyChecks}, with the same dialect
|
|
1138
|
+
* support and the same PostgreSQL privilege requirement.
|
|
1139
|
+
*
|
|
1140
|
+
* @throws {UnsupportedForeignKeyChecksError} on dialects with no
|
|
1141
|
+
* session-level switch
|
|
1315
1142
|
* @returns Promise that resolves when foreign key checks are enabled
|
|
1316
1143
|
*/
|
|
1317
1144
|
async enableForeignKeyChecks() {
|
|
1318
|
-
|
|
1319
|
-
let sql;
|
|
1320
|
-
if (dialect === 'sqlite' || dialect === 'turso') {
|
|
1321
|
-
sql = sql_constants_1.SQLITE.PRAGMA.foreign_keys.on;
|
|
1322
|
-
}
|
|
1323
|
-
else if (dialect === 'postgres') {
|
|
1324
|
-
sql = 'SET FOREIGN_KEY_CHECKS = 1'; // Fallback
|
|
1325
|
-
}
|
|
1326
|
-
else {
|
|
1327
|
-
sql = sql_constants_1.MYSQL.FOREIGN_KEY_CHECKS.enable;
|
|
1328
|
-
}
|
|
1329
|
-
await this.query(sql, { raw: true });
|
|
1145
|
+
await this.query(this.getEnableForeignKeyChecksSQL(), { raw: true });
|
|
1330
1146
|
}
|
|
1331
1147
|
/**
|
|
1332
1148
|
* Set the transaction isolation level
|
|
@@ -1349,31 +1165,30 @@ class Prorm extends events_1.EventEmitter {
|
|
|
1349
1165
|
}
|
|
1350
1166
|
/**
|
|
1351
1167
|
* Get SQL for disabling foreign key checks for the current dialect
|
|
1168
|
+
*
|
|
1169
|
+
* Routing lives in `sql-constants.getForeignKeyChecksSQL` so that the three
|
|
1170
|
+
* mutually incompatible statements (SQLite's PRAGMA, MySQL's session
|
|
1171
|
+
* variable, PostgreSQL's `session_replication_role`) are chosen from one
|
|
1172
|
+
* table. This used to fall through to MySQL syntax for every dialect it did
|
|
1173
|
+
* not name - PostgreSQL included - so `disableForeignKeyChecks()` threw a
|
|
1174
|
+
* syntax error at the server on most of the supported databases.
|
|
1175
|
+
*
|
|
1176
|
+
* @throws {UnsupportedForeignKeyChecksError} when the dialect has no
|
|
1177
|
+
* session-level equivalent
|
|
1352
1178
|
* @returns SQL string for disabling foreign key checks
|
|
1353
1179
|
*/
|
|
1354
1180
|
getDisableForeignKeyChecksSQL() {
|
|
1355
|
-
|
|
1356
|
-
if (dialect === 'sqlite' || dialect === 'turso') {
|
|
1357
|
-
return sql_constants_1.SQLITE.PRAGMA.foreign_keys.off;
|
|
1358
|
-
}
|
|
1359
|
-
else if (dialect === 'postgres') {
|
|
1360
|
-
return 'SET FOREIGN_KEY_CHECKS = 0';
|
|
1361
|
-
}
|
|
1362
|
-
return sql_constants_1.MYSQL.FOREIGN_KEY_CHECKS.disable;
|
|
1181
|
+
return (0, sql_constants_1.getForeignKeyChecksSQL)(this.getDialect(), 'disable');
|
|
1363
1182
|
}
|
|
1364
1183
|
/**
|
|
1365
1184
|
* Get SQL for enabling foreign key checks for the current dialect
|
|
1185
|
+
*
|
|
1186
|
+
* @throws {UnsupportedForeignKeyChecksError} when the dialect has no
|
|
1187
|
+
* session-level equivalent
|
|
1366
1188
|
* @returns SQL string for enabling foreign key checks
|
|
1367
1189
|
*/
|
|
1368
1190
|
getEnableForeignKeyChecksSQL() {
|
|
1369
|
-
|
|
1370
|
-
if (dialect === 'sqlite' || dialect === 'turso') {
|
|
1371
|
-
return sql_constants_1.SQLITE.PRAGMA.foreign_keys.on;
|
|
1372
|
-
}
|
|
1373
|
-
else if (dialect === 'postgres') {
|
|
1374
|
-
return 'SET FOREIGN_KEY_CHECKS = 1';
|
|
1375
|
-
}
|
|
1376
|
-
return sql_constants_1.MYSQL.FOREIGN_KEY_CHECKS.enable;
|
|
1191
|
+
return (0, sql_constants_1.getForeignKeyChecksSQL)(this.getDialect(), 'enable');
|
|
1377
1192
|
}
|
|
1378
1193
|
/**
|
|
1379
1194
|
* Get SQL for setting transaction isolation level for the current dialect
|
|
@@ -1392,6 +1207,19 @@ class Prorm extends events_1.EventEmitter {
|
|
|
1392
1207
|
}
|
|
1393
1208
|
/**
|
|
1394
1209
|
* Disable unique key checks for the current session (MySQL/MariaDB only)
|
|
1210
|
+
*
|
|
1211
|
+
* The no-op on other dialects is deliberate, and is not the same situation as
|
|
1212
|
+
* the foreign key switch above. `SET UNIQUE_CHECKS` is a MySQL/MariaDB
|
|
1213
|
+
* *optimisation* hint: it defers uniqueness verification on InnoDB secondary
|
|
1214
|
+
* indexes during bulk loads. No other supported database has a session
|
|
1215
|
+
* setting that suspends unique constraint checking - PostgreSQL's
|
|
1216
|
+
* `session_replication_role = 'replica'` suppresses triggers and foreign
|
|
1217
|
+
* keys but still enforces unique indexes - so there is nothing to translate
|
|
1218
|
+
* it to. Skipping the statement leaves the database in exactly the state the
|
|
1219
|
+
* caller already had (uniqueness enforced), which is safe; throwing would
|
|
1220
|
+
* break the common `disableUniqueKeyChecks(); bulkCreate(); enable...()`
|
|
1221
|
+
* bulk-load pattern on every non-MySQL dialect for no benefit.
|
|
1222
|
+
*
|
|
1395
1223
|
* @returns Promise that resolves when unique key checks are disabled
|
|
1396
1224
|
*/
|
|
1397
1225
|
async disableUniqueKeyChecks() {
|
|
@@ -1402,6 +1230,9 @@ class Prorm extends events_1.EventEmitter {
|
|
|
1402
1230
|
}
|
|
1403
1231
|
/**
|
|
1404
1232
|
* Enable unique key checks for the current session (MySQL/MariaDB only)
|
|
1233
|
+
*
|
|
1234
|
+
* A no-op elsewhere, for the reason given on {@link disableUniqueKeyChecks}.
|
|
1235
|
+
*
|
|
1405
1236
|
* @returns Promise that resolves when unique key checks are enabled
|
|
1406
1237
|
*/
|
|
1407
1238
|
async enableUniqueKeyChecks() {
|
|
@@ -1746,6 +1577,70 @@ class Prorm extends events_1.EventEmitter {
|
|
|
1746
1577
|
}
|
|
1747
1578
|
return val;
|
|
1748
1579
|
}
|
|
1580
|
+
if (typeKey === 'JSON' || typeKey === 'JSONB') {
|
|
1581
|
+
// Dialects disagree about the wire format of a JSON column: SQLite
|
|
1582
|
+
// stores it as TEXT and hands back the raw string, while the
|
|
1583
|
+
// PostgreSQL and MySQL drivers parse it and hand back an object. That
|
|
1584
|
+
// made the same model code dialect-specific - `row.attrs.a` worked on
|
|
1585
|
+
// Postgres and threw on SQLite. Parse the string form here so every
|
|
1586
|
+
// dialect yields the same JS value.
|
|
1587
|
+
//
|
|
1588
|
+
// Parse defensively, exactly as `castValuesFromDatabase` in
|
|
1589
|
+
// models/model.ts does: a value that is already an object is left
|
|
1590
|
+
// alone, and a string that isn't valid JSON is returned untouched
|
|
1591
|
+
// rather than throwing or collapsing to undefined (a column that
|
|
1592
|
+
// predates the JSON type, or hand-written data, should still read
|
|
1593
|
+
// back as whatever it actually holds).
|
|
1594
|
+
if (typeof val === 'string') {
|
|
1595
|
+
try {
|
|
1596
|
+
return JSON.parse(val);
|
|
1597
|
+
}
|
|
1598
|
+
catch {
|
|
1599
|
+
// Not valid JSON. Rows written before the encoding fix above stored
|
|
1600
|
+
// strings raw, so this is the path that still reads them back
|
|
1601
|
+
// correctly; hand-written data and pre-JSON-type columns land here
|
|
1602
|
+
// too. Returning the raw string beats throwing or yielding
|
|
1603
|
+
// undefined.
|
|
1604
|
+
return val;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
return val;
|
|
1608
|
+
}
|
|
1609
|
+
return val;
|
|
1610
|
+
};
|
|
1611
|
+
// Helper: prepare a JS value for the wire format its column expects.
|
|
1612
|
+
//
|
|
1613
|
+
// Only JSON/JSONB needs this today, and it is the write-side mirror of
|
|
1614
|
+
// the JSON branch in `_coerceValue`. The base dialect's
|
|
1615
|
+
// `escapeReplacement()` does stringify a plain object, but it expands an
|
|
1616
|
+
// *array* into the `(a, b, c)` IN-list form, so `{ tags: ['a', 'b'] }` on
|
|
1617
|
+
// a JSON column produced invalid SQL instead of a JSON literal.
|
|
1618
|
+
// Serializing here settles the format before it reaches the dialect and
|
|
1619
|
+
// keeps write and read symmetric. Strings are passed through so a caller
|
|
1620
|
+
// that already did their own `JSON.stringify` isn't double-encoded.
|
|
1621
|
+
const _serializeValue = (key, val) => {
|
|
1622
|
+
const attr = attributes[key];
|
|
1623
|
+
if (attr === undefined || val === null || val === undefined)
|
|
1624
|
+
return val;
|
|
1625
|
+
const typeObj = (attr.type || attr);
|
|
1626
|
+
const typeKey = typeObj?.key || (typeof typeObj === 'string' ? typeObj : undefined);
|
|
1627
|
+
if (typeKey === 'JSON' || typeKey === 'JSONB') {
|
|
1628
|
+
// A Literal/Col is raw SQL the caller wants inlined, not data - the
|
|
1629
|
+
// insert/update builders detect it by class, so stringifying it here
|
|
1630
|
+
// would turn `literal("'{}'::jsonb")` into a quoted string.
|
|
1631
|
+
if (val instanceof expressions_1.Literal || val instanceof expressions_1.Col)
|
|
1632
|
+
return val;
|
|
1633
|
+
// Encode EVERY value, strings included. Returning a string unencoded
|
|
1634
|
+
// here is what made the round trip lossy: the value went to the column
|
|
1635
|
+
// raw, and the read side then JSON.parse'd it unconditionally, so any
|
|
1636
|
+
// string that happened to be valid JSON came back as something else -
|
|
1637
|
+
// '123' as the number 123, 'true' as a boolean, '{"a":1}' as an object,
|
|
1638
|
+
// and '12345678901234567890' silently rounded to 12345678901234567000.
|
|
1639
|
+
// It also produced invalid JSON on PostgreSQL, which rejects a bare
|
|
1640
|
+
// `hello` for a json column. Encoding strings makes write and read
|
|
1641
|
+
// symmetric, so a value round-trips as the type it was stored as.
|
|
1642
|
+
return JSON.stringify(val);
|
|
1643
|
+
}
|
|
1749
1644
|
return val;
|
|
1750
1645
|
};
|
|
1751
1646
|
// Helper: wrap a plain data object with full instance methods (get/set/update/save/destroy/toJSON)
|
|
@@ -1960,15 +1855,26 @@ class Prorm extends events_1.EventEmitter {
|
|
|
1960
1855
|
if (typeof modelHooksRef.beforeUpdate === 'function') {
|
|
1961
1856
|
await modelHooksRef.beforeUpdate(rowInstance, {});
|
|
1962
1857
|
}
|
|
1858
|
+
// beforeSave covers both halves of persistence, so a single hook can
|
|
1859
|
+
// normalize a record whether it is being created or updated.
|
|
1860
|
+
if (typeof modelHooksRef.beforeSave === 'function') {
|
|
1861
|
+
await modelHooksRef.beforeSave(rowInstance, {});
|
|
1862
|
+
}
|
|
1963
1863
|
const pk = dataValues.id;
|
|
1964
1864
|
if (pk !== undefined) {
|
|
1965
|
-
|
|
1865
|
+
// This path has already fired the update hooks for this record;
|
|
1866
|
+
// without the marker the static update() fires them a second time,
|
|
1867
|
+
// with a different argument shape.
|
|
1868
|
+
await modelRef.update(vals, { where: { id: pk }, _hooksAlreadyRun: true });
|
|
1966
1869
|
}
|
|
1967
1870
|
Object.assign(dataValues, vals);
|
|
1968
1871
|
Object.assign(rowInstance, vals);
|
|
1969
1872
|
if (typeof modelHooksRef.afterUpdate === 'function') {
|
|
1970
1873
|
await modelHooksRef.afterUpdate(rowInstance, {});
|
|
1971
1874
|
}
|
|
1875
|
+
if (typeof modelHooksRef.afterSave === 'function') {
|
|
1876
|
+
await modelHooksRef.afterSave(rowInstance, {});
|
|
1877
|
+
}
|
|
1972
1878
|
return rowInstance;
|
|
1973
1879
|
},
|
|
1974
1880
|
save: async () => {
|
|
@@ -2024,7 +1930,24 @@ class Prorm extends events_1.EventEmitter {
|
|
|
2024
1930
|
saveVals[k] = v;
|
|
2025
1931
|
}
|
|
2026
1932
|
}
|
|
2027
|
-
|
|
1933
|
+
// Fire the persistence hooks once, here, rather than letting the
|
|
1934
|
+
// delegated static update() fire a different-shaped pair.
|
|
1935
|
+
if (typeof modelHooksRef.beforeUpdate === 'function') {
|
|
1936
|
+
await modelHooksRef.beforeUpdate(rowInstance, {});
|
|
1937
|
+
}
|
|
1938
|
+
if (typeof modelHooksRef.beforeSave === 'function') {
|
|
1939
|
+
await modelHooksRef.beforeSave(rowInstance, {});
|
|
1940
|
+
}
|
|
1941
|
+
await modelRef.update(saveVals, {
|
|
1942
|
+
where: { id: pk },
|
|
1943
|
+
_hooksAlreadyRun: true,
|
|
1944
|
+
});
|
|
1945
|
+
if (typeof modelHooksRef.afterUpdate === 'function') {
|
|
1946
|
+
await modelHooksRef.afterUpdate(rowInstance, {});
|
|
1947
|
+
}
|
|
1948
|
+
if (typeof modelHooksRef.afterSave === 'function') {
|
|
1949
|
+
await modelHooksRef.afterSave(rowInstance, {});
|
|
1950
|
+
}
|
|
2028
1951
|
}
|
|
2029
1952
|
else {
|
|
2030
1953
|
// New record - do an INSERT via create
|
|
@@ -2161,7 +2084,11 @@ class Prorm extends events_1.EventEmitter {
|
|
|
2161
2084
|
// Validation methods
|
|
2162
2085
|
validate: async function (options) {
|
|
2163
2086
|
const modelOptions = modelRef.options || {};
|
|
2164
|
-
|
|
2087
|
+
// `validate({ hooks: true })` used to read `_hooksManager`, a property
|
|
2088
|
+
// nothing ever assigned - so the flag was accepted and the hooks never
|
|
2089
|
+
// ran. Use the model's declared hooks, the same source every other
|
|
2090
|
+
// lifecycle hook comes from.
|
|
2091
|
+
const validateHooks = modelOptions?.hooks || {};
|
|
2165
2092
|
// Parse options
|
|
2166
2093
|
let fieldsToValidate;
|
|
2167
2094
|
let runHooks = false;
|
|
@@ -2177,8 +2104,8 @@ class Prorm extends events_1.EventEmitter {
|
|
|
2177
2104
|
return { valid: true, errors: [] };
|
|
2178
2105
|
}
|
|
2179
2106
|
// Run beforeValidate hook if requested
|
|
2180
|
-
if (runHooks &&
|
|
2181
|
-
await
|
|
2107
|
+
if (runHooks && typeof validateHooks.beforeValidate === 'function') {
|
|
2108
|
+
await validateHooks.beforeValidate(rowInstance, { model: modelRef });
|
|
2182
2109
|
}
|
|
2183
2110
|
const validations = {};
|
|
2184
2111
|
// Collect validations from attributes
|
|
@@ -2216,8 +2143,10 @@ class Prorm extends events_1.EventEmitter {
|
|
|
2216
2143
|
// Use the validator
|
|
2217
2144
|
const result = (0, validator_1.validate)(dataToValidate, validations);
|
|
2218
2145
|
// Run afterValidate hook if requested
|
|
2219
|
-
if (runHooks &&
|
|
2220
|
-
|
|
2146
|
+
if (runHooks &&
|
|
2147
|
+
result.valid &&
|
|
2148
|
+
typeof validateHooks.afterValidate === 'function') {
|
|
2149
|
+
await validateHooks.afterValidate(rowInstance, { model: modelRef });
|
|
2221
2150
|
}
|
|
2222
2151
|
return {
|
|
2223
2152
|
valid: result.valid,
|
|
@@ -2369,11 +2298,23 @@ class Prorm extends events_1.EventEmitter {
|
|
|
2369
2298
|
return assoc.target.findOne({ where: { id: fkVal }, ...(opts || {}) });
|
|
2370
2299
|
};
|
|
2371
2300
|
rowInstance[`set${capitalize(assocAlias)}`] = async (instance) => {
|
|
2372
|
-
|
|
2301
|
+
// Passing null clears the association, matching `unset`.
|
|
2302
|
+
const targetId = instance === null ? null : (instance?.id ?? instance);
|
|
2373
2303
|
dataValues[fk] = targetId;
|
|
2374
2304
|
rowInstance[fk] = targetId;
|
|
2375
2305
|
await modelRef.update({ [fk]: targetId }, { where: { id: dataValues.id } });
|
|
2376
2306
|
};
|
|
2307
|
+
/**
|
|
2308
|
+
* Clear the association: null the foreign key and persist.
|
|
2309
|
+
*
|
|
2310
|
+
* `setX(null)` does the same thing, but reads as an assignment; this
|
|
2311
|
+
* says what it does at the call site.
|
|
2312
|
+
*/
|
|
2313
|
+
rowInstance[`unset${capitalize(assocAlias)}`] = async () => {
|
|
2314
|
+
dataValues[fk] = null;
|
|
2315
|
+
rowInstance[fk] = null;
|
|
2316
|
+
await modelRef.update({ [fk]: null }, { where: { id: dataValues.id } });
|
|
2317
|
+
};
|
|
2377
2318
|
// create/has/count were missing here while the equivalent mixins in
|
|
2378
2319
|
// models/associations.ts defined them, so which helpers existed on an
|
|
2379
2320
|
// instance depended on how the model had been defined.
|
|
@@ -2878,10 +2819,25 @@ class Prorm extends events_1.EventEmitter {
|
|
|
2878
2819
|
* Find all records
|
|
2879
2820
|
*/
|
|
2880
2821
|
async findAll(findOptions = {}) {
|
|
2822
|
+
// `using: 'name'` runs this query against a sibling connection
|
|
2823
|
+
// registered on the same ConnectionManager, rather than this one.
|
|
2824
|
+
// Resolve it before anything else touches `self.dialect` - the whole
|
|
2825
|
+
// point is that the query never runs here.
|
|
2826
|
+
if (findOptions.using) {
|
|
2827
|
+
return self._findAllUsingConnection(modelName, findOptions);
|
|
2828
|
+
}
|
|
2881
2829
|
if (!self.dialect) {
|
|
2882
2830
|
throw new Error('Database not connected');
|
|
2883
2831
|
}
|
|
2884
2832
|
const dialect = self.dialect;
|
|
2833
|
+
// beforeFind runs before scopes are merged so a hook sees - and can
|
|
2834
|
+
// adjust - the options the caller actually passed.
|
|
2835
|
+
const findHooks = this.options?.hooks || {};
|
|
2836
|
+
if (typeof findHooks.beforeFind === 'function') {
|
|
2837
|
+
const replaced = await findHooks.beforeFind(findOptions);
|
|
2838
|
+
if (replaced)
|
|
2839
|
+
findOptions = replaced;
|
|
2840
|
+
}
|
|
2885
2841
|
// Apply scopes (defaultScope + named scopes)
|
|
2886
2842
|
findOptions = this._mergeScopes(findOptions);
|
|
2887
2843
|
// Get model options for paranoid configuration
|
|
@@ -2900,6 +2856,19 @@ class Prorm extends events_1.EventEmitter {
|
|
|
2900
2856
|
[deletedAt]: { $isNull: true },
|
|
2901
2857
|
};
|
|
2902
2858
|
}
|
|
2859
|
+
// Resolve `{ all: true }` / `{ all: 'alias' }` into one concrete include
|
|
2860
|
+
// per association *before* anything reads the include list. This used to
|
|
2861
|
+
// happen inside `loadIncludes()`, at the very end of the query - which
|
|
2862
|
+
// is after the two passes below have already run over the raw list, so
|
|
2863
|
+
// `{ all: true, required: true }` reached `resolveRequiredIncludeFilter`
|
|
2864
|
+
// as an entry with no `model` and was skipped: the associations loaded
|
|
2865
|
+
// but the parents were never narrowed.
|
|
2866
|
+
if (findOptions.include && findOptions.include.length > 0) {
|
|
2867
|
+
findOptions = {
|
|
2868
|
+
...findOptions,
|
|
2869
|
+
include: self._expandIncludes(model, modelName, findOptions.include),
|
|
2870
|
+
};
|
|
2871
|
+
}
|
|
2903
2872
|
// Apply INNER-JOIN semantics for filtering includes.
|
|
2904
2873
|
//
|
|
2905
2874
|
// `include: [{ model: Post, required: true }]` and an include carrying
|
|
@@ -2929,24 +2898,11 @@ class Prorm extends events_1.EventEmitter {
|
|
|
2929
2898
|
}
|
|
2930
2899
|
findOptions = { ...findOptions, include: includeList };
|
|
2931
2900
|
}
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
if (filter.values.length === 0) {
|
|
2938
|
-
// No parent can satisfy a required include with no matches.
|
|
2939
|
-
return [];
|
|
2940
|
-
}
|
|
2941
|
-
const existing = where?.[filter.parentAttr];
|
|
2942
|
-
where = {
|
|
2943
|
-
...where,
|
|
2944
|
-
[filter.parentAttr]: existing
|
|
2945
|
-
? { $and: [existing, { $in: filter.values }] }
|
|
2946
|
-
: { $in: filter.values },
|
|
2947
|
-
};
|
|
2948
|
-
}
|
|
2949
|
-
}
|
|
2901
|
+
const filteredWhere = await self._applyRequiredIncludeFilters(model, modelName, findOptions.include, where);
|
|
2902
|
+
// null means some required include matches nothing, so nothing matches.
|
|
2903
|
+
if (filteredWhere === null)
|
|
2904
|
+
return [];
|
|
2905
|
+
where = filteredWhere;
|
|
2950
2906
|
// Translate attributes.exclude to explicit list (SQLite doesn't support "* EXCEPT(...)")
|
|
2951
2907
|
let resolvedAttributes = findOptions.attributes;
|
|
2952
2908
|
if (resolvedAttributes &&
|
|
@@ -2976,6 +2932,26 @@ class Prorm extends events_1.EventEmitter {
|
|
|
2976
2932
|
subQuery: findOptions.subquery,
|
|
2977
2933
|
benchmark: findOptions.benchmark,
|
|
2978
2934
|
cte: findOptions.cte,
|
|
2935
|
+
// Row-level locking. `lock` was declared on FindOptions, documented,
|
|
2936
|
+
// and compiled by the dialects (PostgreSQL emits FOR UPDATE / FOR
|
|
2937
|
+
// SHARE / FOR KEY SHARE, MySQL LOCK IN SHARE MODE), but never made
|
|
2938
|
+
// it out of here - so `findAll({ lock: 'UPDATE', transaction: t })`
|
|
2939
|
+
// silently took no lock at all and code written to serialize
|
|
2940
|
+
// concurrent updates didn't.
|
|
2941
|
+
//
|
|
2942
|
+
// Dialects with no row-level locking (SQLite above all) drop the
|
|
2943
|
+
// clause rather than throwing. That is deliberate: SQLite already
|
|
2944
|
+
// serializes writers at the database level, so the transaction the
|
|
2945
|
+
// lock was protecting is serialized there anyway, and rejecting the
|
|
2946
|
+
// option would make otherwise-portable model code dialect-specific.
|
|
2947
|
+
lock: findOptions.lock,
|
|
2948
|
+
// UNION / UNION ALL / EXCEPT / INTERSECT branches appended after the
|
|
2949
|
+
// main query. Each dialect pushes the branches' bound values onto
|
|
2950
|
+
// the same `values` array in the order they are emitted, so the
|
|
2951
|
+
// single replaceReplacements() call below binds them correctly
|
|
2952
|
+
// alongside the main query's.
|
|
2953
|
+
unionType: findOptions.unionType,
|
|
2954
|
+
union: findOptions.union,
|
|
2979
2955
|
});
|
|
2980
2956
|
// Handle benchmark option
|
|
2981
2957
|
const startTime = Date.now();
|
|
@@ -2984,44 +2960,15 @@ class Prorm extends events_1.EventEmitter {
|
|
|
2984
2960
|
// Calculate query duration
|
|
2985
2961
|
const duration = Date.now() - startTime;
|
|
2986
2962
|
// Determine logging function
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
: typeof self.loggingOptions === 'function'
|
|
2991
|
-
? self.loggingOptions
|
|
2992
|
-
: null;
|
|
2993
|
-
// Log benchmark/timing if enabled
|
|
2994
|
-
const benchmarkEnabled = findOptions.benchmark === true;
|
|
2995
|
-
if (shouldLog || benchmarkEnabled) {
|
|
2996
|
-
if (logFn) {
|
|
2997
|
-
logFn(query.sql, duration);
|
|
2998
|
-
}
|
|
2999
|
-
else if (findOptions.logging === true ||
|
|
3000
|
-
self.loggingOptions === true ||
|
|
3001
|
-
benchmarkEnabled) {
|
|
3002
|
-
console.log(`Executed (${duration}ms): ${query.sql}`);
|
|
3003
|
-
}
|
|
3004
|
-
// Log to internal logger for slow query detection
|
|
3005
|
-
self.logger.logQuery(query.sql, duration);
|
|
3006
|
-
}
|
|
3007
|
-
// Emit query event for external listeners
|
|
3008
|
-
self.emit('query', {
|
|
2963
|
+
// Query logging, slow-query detection and the query/slowQuery events all
|
|
2964
|
+
// go through one path so every operation behaves the same way.
|
|
2965
|
+
self._logQuery({
|
|
3009
2966
|
sql: query.sql,
|
|
3010
2967
|
duration,
|
|
3011
|
-
|
|
2968
|
+
parameters: query.values,
|
|
3012
2969
|
model: modelName,
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
const slowQueryThreshold = self.config.slowQueryThreshold || 1000;
|
|
3016
|
-
if (duration > slowQueryThreshold) {
|
|
3017
|
-
self.emit('slowQuery', {
|
|
3018
|
-
sql: query.sql,
|
|
3019
|
-
duration,
|
|
3020
|
-
threshold: slowQueryThreshold,
|
|
3021
|
-
timestamp: new Date(),
|
|
3022
|
-
model: modelName,
|
|
3023
|
-
});
|
|
3024
|
-
}
|
|
2970
|
+
type: 'SELECT',
|
|
2971
|
+
}, findOptions.logging);
|
|
3025
2972
|
// Wrap each row with instance methods.
|
|
3026
2973
|
//
|
|
3027
2974
|
// `raw: true` asks for plain rows and was accepted but ignored - every
|
|
@@ -3034,6 +2981,12 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3034
2981
|
const wrappedRows = (findOptions.raw === true
|
|
3035
2982
|
? result.rows.map((row) => ({ ...row }))
|
|
3036
2983
|
: result.rows.map((row) => _wrapInstance(row, modelRef, modelHooksRef)));
|
|
2984
|
+
// Related-row counts, without loading the rows. One grouped query per
|
|
2985
|
+
// association rather than one per parent.
|
|
2986
|
+
const withCount = findOptions.withCount;
|
|
2987
|
+
if (withCount?.length) {
|
|
2988
|
+
await (0, eager_load_1.loadCounts)(self.eagerLoadContext(), wrappedRows, model, modelName, withCount);
|
|
2989
|
+
}
|
|
3037
2990
|
// Eager-load associations. The implementation is shared with the
|
|
3038
2991
|
// class-based Model API (see models/eager-load.ts) - it used to be
|
|
3039
2992
|
// duplicated here and in models/model.ts, and the two copies had
|
|
@@ -3041,6 +2994,11 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3041
2994
|
if (findOptions.include && findOptions.include.length > 0) {
|
|
3042
2995
|
await (0, eager_load_1.loadIncludes)(self.eagerLoadContext(), wrappedRows, model, modelName, findOptions.include);
|
|
3043
2996
|
}
|
|
2997
|
+
if (typeof findHooks.afterFind === 'function') {
|
|
2998
|
+
const replaced = await findHooks.afterFind(wrappedRows, findOptions);
|
|
2999
|
+
if (replaced)
|
|
3000
|
+
return replaced;
|
|
3001
|
+
}
|
|
3044
3002
|
return wrappedRows;
|
|
3045
3003
|
},
|
|
3046
3004
|
/**
|
|
@@ -3051,7 +3009,8 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3051
3009
|
// Count with an aggregate rather than by re-running the full query
|
|
3052
3010
|
// without a limit and measuring the array. `limit`/`offset` are dropped
|
|
3053
3011
|
// (the count describes the whole matching set), everything else that
|
|
3054
|
-
// narrows the set -
|
|
3012
|
+
// narrows the set - filtering includes and `union` branches included -
|
|
3013
|
+
// is preserved, so `count` and `rows` always describe the same set.
|
|
3055
3014
|
const { limit: _limit, offset: _offset, ...countable } = this._mergeScopes(findOptions);
|
|
3056
3015
|
const count = await self._countRows(model, modelName, countable);
|
|
3057
3016
|
return { rows, count };
|
|
@@ -3313,6 +3272,11 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3313
3272
|
if (typeof modelHooks.beforeCreate === 'function') {
|
|
3314
3273
|
await modelHooks.beforeCreate(hookInstance, createOptions);
|
|
3315
3274
|
}
|
|
3275
|
+
// beforeSave fires for both create and update, so a single hook can
|
|
3276
|
+
// normalize a record however it is being persisted.
|
|
3277
|
+
if (typeof modelHooks.beforeSave === 'function') {
|
|
3278
|
+
await modelHooks.beforeSave(hookInstance, createOptions);
|
|
3279
|
+
}
|
|
3316
3280
|
// Build dbValues from hookInstance (after hooks may have modified it), filtering virtual fields and respecting 'fields' option
|
|
3317
3281
|
const allowedFields = createOptions.fields;
|
|
3318
3282
|
const dbValues = {};
|
|
@@ -3325,7 +3289,7 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3325
3289
|
if (typeObj.key === 'VIRTUAL' || typeObj.key === 'VIRTUALSTRING')
|
|
3326
3290
|
continue;
|
|
3327
3291
|
}
|
|
3328
|
-
dbValues[key] = value;
|
|
3292
|
+
dbValues[key] = _serializeValue(key, value);
|
|
3329
3293
|
}
|
|
3330
3294
|
const { sql, values: insertValues } = dialect.buildInsertQuery(this.tableName, dbValues, {
|
|
3331
3295
|
returning: createOptions.returning,
|
|
@@ -3420,6 +3384,9 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3420
3384
|
if (typeof modelHooks.afterCreate === 'function') {
|
|
3421
3385
|
await modelHooks.afterCreate(createdInstance, createOptions);
|
|
3422
3386
|
}
|
|
3387
|
+
if (typeof modelHooks.afterSave === 'function') {
|
|
3388
|
+
await modelHooks.afterSave(createdInstance, createOptions);
|
|
3389
|
+
}
|
|
3423
3390
|
return createdInstance;
|
|
3424
3391
|
},
|
|
3425
3392
|
/**
|
|
@@ -3432,11 +3399,27 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3432
3399
|
throw new Error('Database not connected');
|
|
3433
3400
|
}
|
|
3434
3401
|
const bulkModelHooks = this.options?.hooks || {};
|
|
3402
|
+
// `instance.update()` fires the update hooks itself before delegating
|
|
3403
|
+
// here; the marker stops them running twice per call.
|
|
3404
|
+
const hooksAlreadyRun = updateOptions?._hooksAlreadyRun === true;
|
|
3405
|
+
// beforeUpdate fires for the statement as a whole, alongside the bulk
|
|
3406
|
+
// hook; it previously only ran for `instance.update()`, so a model-level
|
|
3407
|
+
// update bypassed it entirely.
|
|
3408
|
+
if (!hooksAlreadyRun && typeof bulkModelHooks.beforeUpdate === 'function') {
|
|
3409
|
+
await bulkModelHooks.beforeUpdate(values, updateOptions);
|
|
3410
|
+
}
|
|
3435
3411
|
if (typeof bulkModelHooks.beforeBulkUpdate === 'function') {
|
|
3436
3412
|
await bulkModelHooks.beforeBulkUpdate({ ...updateOptions, attributes: values });
|
|
3437
3413
|
}
|
|
3438
3414
|
const dialect = self.dialect;
|
|
3439
|
-
|
|
3415
|
+
// Serialize JSON/JSONB columns on the way to the dialect (see
|
|
3416
|
+
// `_serializeValue`). `values` itself is left untouched so the
|
|
3417
|
+
// after-hooks below still see what the caller passed.
|
|
3418
|
+
const dbValues = {};
|
|
3419
|
+
for (const [key, value] of Object.entries(values)) {
|
|
3420
|
+
dbValues[key] = _serializeValue(key, value);
|
|
3421
|
+
}
|
|
3422
|
+
const { sql: updateSql, values: updateVals } = dialect.buildUpdateQuery(this.tableName, dbValues, updateOptions.where, { limit: updateOptions.limit, returning: updateOptions.returning });
|
|
3440
3423
|
const sql = dialect.replaceReplacements(updateSql, updateVals);
|
|
3441
3424
|
// Handle logging
|
|
3442
3425
|
const startTime = Date.now();
|
|
@@ -3466,6 +3449,9 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3466
3449
|
model: modelName,
|
|
3467
3450
|
type: 'UPDATE',
|
|
3468
3451
|
});
|
|
3452
|
+
if (!hooksAlreadyRun && typeof bulkModelHooks.afterUpdate === 'function') {
|
|
3453
|
+
await bulkModelHooks.afterUpdate(values, updateOptions);
|
|
3454
|
+
}
|
|
3469
3455
|
if (typeof bulkModelHooks.afterBulkUpdate === 'function') {
|
|
3470
3456
|
await bulkModelHooks.afterBulkUpdate({ ...updateOptions, attributes: values });
|
|
3471
3457
|
}
|
|
@@ -3482,6 +3468,11 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3482
3468
|
if (!destroyOptions.where) {
|
|
3483
3469
|
throw new Error('Missing where option in destroy');
|
|
3484
3470
|
}
|
|
3471
|
+
// As with update, this previously only fired for `instance.destroy()`.
|
|
3472
|
+
const destroyHooks = this.options?.hooks || {};
|
|
3473
|
+
if (typeof destroyHooks.beforeDestroy === 'function') {
|
|
3474
|
+
await destroyHooks.beforeDestroy(destroyOptions);
|
|
3475
|
+
}
|
|
3485
3476
|
const dialect = self.dialect;
|
|
3486
3477
|
const modelOptions = this.options;
|
|
3487
3478
|
const bulkDestroyHooks = modelOptions?.hooks || {};
|
|
@@ -3555,6 +3546,9 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3555
3546
|
model: modelName,
|
|
3556
3547
|
type: isParanoid && !forceDelete ? 'UPDATE' : 'DELETE',
|
|
3557
3548
|
});
|
|
3549
|
+
if (typeof destroyHooks.afterDestroy === 'function') {
|
|
3550
|
+
await destroyHooks.afterDestroy(destroyOptions);
|
|
3551
|
+
}
|
|
3558
3552
|
if (typeof bulkDestroyHooks.afterBulkDestroy === 'function') {
|
|
3559
3553
|
await bulkDestroyHooks.afterBulkDestroy(destroyOptions);
|
|
3560
3554
|
}
|
|
@@ -3615,6 +3609,11 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3615
3609
|
const dialect = self.dialect;
|
|
3616
3610
|
for (const record of records) {
|
|
3617
3611
|
const dbValues = {};
|
|
3612
|
+
// The values as the caller supplied them, used to build the
|
|
3613
|
+
// returned record: `dbValues` holds the wire format (a JSON column
|
|
3614
|
+
// is serialized on the way to the dialect), which is not what a
|
|
3615
|
+
// caller should get back from bulkCreate.
|
|
3616
|
+
const jsValues = {};
|
|
3618
3617
|
for (const [key, value] of Object.entries(record)) {
|
|
3619
3618
|
const attr = attributes[key];
|
|
3620
3619
|
if (attr && attr.type) {
|
|
@@ -3622,14 +3621,23 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3622
3621
|
if (typeObj.key === 'VIRTUAL' || typeObj.key === 'VIRTUALSTRING')
|
|
3623
3622
|
continue;
|
|
3624
3623
|
}
|
|
3625
|
-
dbValues[key] = value;
|
|
3624
|
+
dbValues[key] = _serializeValue(key, value);
|
|
3625
|
+
jsValues[key] = value;
|
|
3626
3626
|
}
|
|
3627
3627
|
const { sql, values: insertValues } = dialect.buildInsertQuery(this.tableName, dbValues, {});
|
|
3628
3628
|
const finalSql = dialect.replaceReplacements(sql, insertValues);
|
|
3629
|
+
const bcStarted = Date.now();
|
|
3629
3630
|
const insertResult = await dialect.query(finalSql);
|
|
3631
|
+
self._logQuery({
|
|
3632
|
+
sql: finalSql,
|
|
3633
|
+
duration: Date.now() - bcStarted,
|
|
3634
|
+
parameters: insertValues,
|
|
3635
|
+
model: modelName,
|
|
3636
|
+
type: 'INSERT',
|
|
3637
|
+
}, bulkCreateOptions?.logging);
|
|
3630
3638
|
const lastId = insertResult.lastInsertRowid;
|
|
3631
3639
|
results.push({
|
|
3632
|
-
...
|
|
3640
|
+
...jsValues,
|
|
3633
3641
|
...(lastId !== undefined && !record.id ? { id: lastId } : {}),
|
|
3634
3642
|
isNewRecord: false,
|
|
3635
3643
|
});
|
|
@@ -3653,6 +3661,12 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3653
3661
|
* and `distinct`, `col` and `group` were silently discarded.
|
|
3654
3662
|
*/
|
|
3655
3663
|
async count(countOptions = {}) {
|
|
3664
|
+
const countHooks = this.options?.hooks || {};
|
|
3665
|
+
if (typeof countHooks.beforeCount === 'function') {
|
|
3666
|
+
const replaced = await countHooks.beforeCount(countOptions);
|
|
3667
|
+
if (replaced)
|
|
3668
|
+
countOptions = replaced;
|
|
3669
|
+
}
|
|
3656
3670
|
// Apply defaultScope / named scopes exactly as findAll does, so
|
|
3657
3671
|
// `User.scope('admin').count()` counts the scoped set.
|
|
3658
3672
|
return self._countRows(model, modelName, this._mergeScopes(countOptions));
|
|
@@ -3926,7 +3940,148 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3926
3940
|
async avg(attribute, aggOptions = {}) {
|
|
3927
3941
|
return self._aggregate(model, modelName, 'AVG', attribute, this._mergeScopes(aggOptions));
|
|
3928
3942
|
},
|
|
3943
|
+
/**
|
|
3944
|
+
* Like `findOne`, but throws `EmptyResultError` when nothing matches.
|
|
3945
|
+
*
|
|
3946
|
+
* Saves the `if (!row) throw ...` that otherwise follows every lookup
|
|
3947
|
+
* whose absence is a genuine error rather than a case to handle.
|
|
3948
|
+
*/
|
|
3949
|
+
async findOneOrFail(findOptions = {}) {
|
|
3950
|
+
const row = await this.findOne(findOptions);
|
|
3951
|
+
if (!row) {
|
|
3952
|
+
throw new errors_1.EmptyResultError(`No ${modelName} matched the given conditions`, {
|
|
3953
|
+
model: modelName,
|
|
3954
|
+
});
|
|
3955
|
+
}
|
|
3956
|
+
return row;
|
|
3957
|
+
},
|
|
3958
|
+
/** Like `findByPk`, but throws `EmptyResultError` when the row is absent. */
|
|
3959
|
+
async findByPkOrFail(pk, findOptions = {}) {
|
|
3960
|
+
const row = await this.findByPk(pk, findOptions);
|
|
3961
|
+
if (!row) {
|
|
3962
|
+
throw new errors_1.EmptyResultError(`${modelName} ${String(pk)} not found`, {
|
|
3963
|
+
model: modelName,
|
|
3964
|
+
primaryKey: pk,
|
|
3965
|
+
});
|
|
3966
|
+
}
|
|
3967
|
+
return row;
|
|
3968
|
+
},
|
|
3969
|
+
/** The last row in `order`, or null. Inverts the ordering and takes one. */
|
|
3970
|
+
async findLast(findOptions = {}) {
|
|
3971
|
+
const pkAttr = resolveModelPrimaryKeyAttr(model);
|
|
3972
|
+
const order = findOptions.order ?? [[pkAttr, 'ASC']];
|
|
3973
|
+
const inverted = order.map((entry) => {
|
|
3974
|
+
if (!Array.isArray(entry))
|
|
3975
|
+
return [entry, 'DESC'];
|
|
3976
|
+
const [col, dir] = entry;
|
|
3977
|
+
return [col, String(dir ?? 'ASC').toUpperCase() === 'DESC' ? 'ASC' : 'DESC'];
|
|
3978
|
+
});
|
|
3979
|
+
const rows = await this.findAll({ ...findOptions, order: inverted, limit: 1 });
|
|
3980
|
+
return rows[0] ?? null;
|
|
3981
|
+
},
|
|
3982
|
+
/**
|
|
3983
|
+
* Whether any row matches, without fetching one.
|
|
3984
|
+
*
|
|
3985
|
+
* `SELECT COUNT(*) ... LIMIT 1` rather than loading a row and testing it
|
|
3986
|
+
* for null.
|
|
3987
|
+
*/
|
|
3988
|
+
async exists(where = {}) {
|
|
3989
|
+
return (await self._countRows(model, modelName, this._mergeScopes({ where }))) > 0;
|
|
3990
|
+
},
|
|
3991
|
+
/**
|
|
3992
|
+
* Walk the whole matching set in batches, without holding it in memory.
|
|
3993
|
+
*
|
|
3994
|
+
* Pages by primary key rather than OFFSET, so the cost per batch stays
|
|
3995
|
+
* flat instead of degrading as the offset grows, and rows written during
|
|
3996
|
+
* the walk cannot cause a page to be skipped.
|
|
3997
|
+
*
|
|
3998
|
+
* ```typescript
|
|
3999
|
+
* await User.findInBatches({ where: { active: true } }, async (batch) => {
|
|
4000
|
+
* await sendDigest(batch);
|
|
4001
|
+
* }, { batchSize: 500 });
|
|
4002
|
+
* ```
|
|
4003
|
+
*/
|
|
4004
|
+
async findInBatches(findOptions, handler, options = {}) {
|
|
4005
|
+
const size = options.batchSize ?? 100;
|
|
4006
|
+
const pkAttr = resolveModelPrimaryKeyAttr(model);
|
|
4007
|
+
let cursor = undefined;
|
|
4008
|
+
let seen = 0;
|
|
4009
|
+
for (;;) {
|
|
4010
|
+
const where = cursor === undefined
|
|
4011
|
+
? findOptions.where
|
|
4012
|
+
: {
|
|
4013
|
+
...findOptions.where,
|
|
4014
|
+
[pkAttr]: { $gt: cursor },
|
|
4015
|
+
};
|
|
4016
|
+
const batch = await this.findAll({
|
|
4017
|
+
...findOptions,
|
|
4018
|
+
where,
|
|
4019
|
+
order: [[pkAttr, 'ASC']],
|
|
4020
|
+
limit: size,
|
|
4021
|
+
});
|
|
4022
|
+
if (batch.length === 0)
|
|
4023
|
+
break;
|
|
4024
|
+
await handler(batch);
|
|
4025
|
+
seen += batch.length;
|
|
4026
|
+
cursor = batch[batch.length - 1][pkAttr];
|
|
4027
|
+
if (batch.length < size)
|
|
4028
|
+
break;
|
|
4029
|
+
}
|
|
4030
|
+
return seen;
|
|
4031
|
+
},
|
|
4032
|
+
/** Call `handler` once per matching row, in batches. See `findInBatches`. */
|
|
4033
|
+
async findEach(findOptions, handler, options = {}) {
|
|
4034
|
+
return this.findInBatches(findOptions, async (batch) => {
|
|
4035
|
+
for (const row of batch)
|
|
4036
|
+
await handler(row);
|
|
4037
|
+
}, options);
|
|
4038
|
+
},
|
|
4039
|
+
/**
|
|
4040
|
+
* Add to one or more numeric columns in the database.
|
|
4041
|
+
*
|
|
4042
|
+
* `SET n = n + 1` in a single statement, so concurrent callers each apply
|
|
4043
|
+
* their delta. Reading a value, adding to it in JS and writing it back
|
|
4044
|
+
* loses one of two simultaneous increments.
|
|
4045
|
+
*
|
|
4046
|
+
* ```typescript
|
|
4047
|
+
* await Post.increment('views', { where: { id: 1 } });
|
|
4048
|
+
* await Post.increment(['views', 'score'], { by: 2, where: { id: 1 } });
|
|
4049
|
+
* await Post.increment({ views: 1, score: -1 }, { where: { id: 1 } });
|
|
4050
|
+
* ```
|
|
4051
|
+
*
|
|
4052
|
+
* @returns the number of rows affected
|
|
4053
|
+
*/
|
|
4054
|
+
async increment(fields, options = {}) {
|
|
4055
|
+
return self._applyDelta(model, modelName, fields, options, 1);
|
|
4056
|
+
},
|
|
4057
|
+
/** Subtract from one or more numeric columns. See `increment`. */
|
|
4058
|
+
async decrement(fields, options = {}) {
|
|
4059
|
+
return self._applyDelta(model, modelName, fields, options, -1);
|
|
4060
|
+
},
|
|
4061
|
+
/**
|
|
4062
|
+
* Update many rows to *different* values in one statement.
|
|
4063
|
+
*
|
|
4064
|
+
* Looping `update()` costs one round trip per row. This compiles a single
|
|
4065
|
+
* UPDATE with a CASE per column, which every supported dialect accepts:
|
|
4066
|
+
*
|
|
4067
|
+
* ```sql
|
|
4068
|
+
* UPDATE users SET name = CASE id WHEN 1 THEN 'a' WHEN 2 THEN 'b' END
|
|
4069
|
+
* WHERE id IN (1, 2)
|
|
4070
|
+
* ```
|
|
4071
|
+
*
|
|
4072
|
+
* Each row must carry the primary key. Columns absent from a given row
|
|
4073
|
+
* keep their current value.
|
|
4074
|
+
*
|
|
4075
|
+
* @returns the number of rows affected
|
|
4076
|
+
*/
|
|
4077
|
+
async bulkUpdate(rows, options = {}) {
|
|
4078
|
+
return self._bulkUpdate(model, modelName, rows, options);
|
|
4079
|
+
},
|
|
3929
4080
|
async upsert(values, upsertOptions) {
|
|
4081
|
+
const upsertHooks = this.options?.hooks || {};
|
|
4082
|
+
if (typeof upsertHooks.beforeUpsert === 'function') {
|
|
4083
|
+
await upsertHooks.beforeUpsert(values, upsertOptions);
|
|
4084
|
+
}
|
|
3930
4085
|
if (!self.dialect) {
|
|
3931
4086
|
throw new Error('Database not connected');
|
|
3932
4087
|
}
|
|
@@ -3949,7 +4104,9 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3949
4104
|
if (typeObj.key === 'VIRTUAL' || typeObj.key === 'VIRTUALSTRING')
|
|
3950
4105
|
continue;
|
|
3951
4106
|
}
|
|
3952
|
-
|
|
4107
|
+
// Serialized on the way to the dialect (JSON/JSONB) - see
|
|
4108
|
+
// `_serializeValue`.
|
|
4109
|
+
dbValues[key] = _serializeValue(key, value);
|
|
3953
4110
|
}
|
|
3954
4111
|
const columns = Object.keys(dbValues);
|
|
3955
4112
|
// Default the conflict target: prefer an explicit primary key value, but
|
|
@@ -4035,6 +4192,9 @@ class Prorm extends events_1.EventEmitter {
|
|
|
4035
4192
|
if (!instance) {
|
|
4036
4193
|
instance = _wrapInstance({ ...dbValues }, model, options.hooks || {});
|
|
4037
4194
|
}
|
|
4195
|
+
if (typeof upsertHooks.afterUpsert === 'function') {
|
|
4196
|
+
await upsertHooks.afterUpsert([instance, created], upsertOptions);
|
|
4197
|
+
}
|
|
4038
4198
|
return [instance, created];
|
|
4039
4199
|
},
|
|
4040
4200
|
// ============ Model-level Hooks ============
|
|
@@ -4050,7 +4210,7 @@ class Prorm extends events_1.EventEmitter {
|
|
|
4050
4210
|
beforeSync(callback) {
|
|
4051
4211
|
let modelHook = self.modelHooks.get(modelName);
|
|
4052
4212
|
if (!modelHook) {
|
|
4053
|
-
modelHook = new ModelHooks();
|
|
4213
|
+
modelHook = new hooks_manager_1.ModelHooks();
|
|
4054
4214
|
self.modelHooks.set(modelName, modelHook);
|
|
4055
4215
|
}
|
|
4056
4216
|
modelHook.beforeSync(callback);
|
|
@@ -4067,7 +4227,7 @@ class Prorm extends events_1.EventEmitter {
|
|
|
4067
4227
|
afterSync(callback) {
|
|
4068
4228
|
let modelHook = self.modelHooks.get(modelName);
|
|
4069
4229
|
if (!modelHook) {
|
|
4070
|
-
modelHook = new ModelHooks();
|
|
4230
|
+
modelHook = new hooks_manager_1.ModelHooks();
|
|
4071
4231
|
self.modelHooks.set(modelName, modelHook);
|
|
4072
4232
|
}
|
|
4073
4233
|
modelHook.afterSync(callback);
|
|
@@ -4182,9 +4342,16 @@ class Prorm extends events_1.EventEmitter {
|
|
|
4182
4342
|
*/
|
|
4183
4343
|
addModel(modelClass) {
|
|
4184
4344
|
const modelName = modelClass.name;
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4345
|
+
if ((0, decorators_1.isAbstractModel)(modelClass)) {
|
|
4346
|
+
throw new Error(`${modelName} is declared @AbstractModel and has no table of its own. ` +
|
|
4347
|
+
`Register the concrete models that extend it instead.`);
|
|
4348
|
+
}
|
|
4349
|
+
// Get model metadata from decorators. Attributes are collected up the
|
|
4350
|
+
// prototype chain so columns declared on an @AbstractModel base are
|
|
4351
|
+
// inherited; metadata is keyed by constructor name, so without this a
|
|
4352
|
+
// subclass would only see the columns declared directly on it.
|
|
4353
|
+
const modelMeta = (0, decorators_1.getModelMetadata)(modelName) ?? inheritedModelMetadata(modelClass);
|
|
4354
|
+
const attrMeta = (0, decorators_1.getInheritedAttributeMetadata)(modelClass);
|
|
4188
4355
|
// A plain class with no @Table/@Attribute decorators is still valid — register it
|
|
4189
4356
|
// with no attributes rather than failing, matching a bare `class Foo extends Model {}`.
|
|
4190
4357
|
// Convert attribute metadata to Prorm format
|
|
@@ -4523,9 +4690,29 @@ class Prorm extends events_1.EventEmitter {
|
|
|
4523
4690
|
: undefined,
|
|
4524
4691
|
check: c.check,
|
|
4525
4692
|
}));
|
|
4693
|
+
// Composite unique keys, from all three declaration forms. They are turned
|
|
4694
|
+
// into ordinary table-level UNIQUE constraints - and unique indexes, for
|
|
4695
|
+
// the entries that asked for one - rather than being forwarded as the
|
|
4696
|
+
// dialects' `uniqueKeys` option, because that option is honored
|
|
4697
|
+
// inconsistently: ClickHouse warns and drops it, and roughly half the
|
|
4698
|
+
// dialects have no handling for it at all, while every dialect renders
|
|
4699
|
+
// `constraints`. (The SQLite dialect built its list and then never used it,
|
|
4700
|
+
// which is fixed there too, for callers who reach `createTable` directly.)
|
|
4701
|
+
// Forwarding both would emit the constraint twice on Postgres/MySQL/MariaDB,
|
|
4702
|
+
// which do implement it.
|
|
4703
|
+
const uniqueKeys = collectUniqueKeys(tableName, attributes, options, columns);
|
|
4526
4704
|
const allTableConstraints = [
|
|
4527
4705
|
...(tableConstraints || []),
|
|
4528
4706
|
...synthesizedFkConstraints,
|
|
4707
|
+
...uniqueKeys
|
|
4708
|
+
.filter((uk) => !uk.asIndex)
|
|
4709
|
+
.map((uk) => ({ name: uk.name, type: 'UNIQUE', fields: uk.fields })),
|
|
4710
|
+
];
|
|
4711
|
+
const allTableIndexes = [
|
|
4712
|
+
...(tableIndexes || []),
|
|
4713
|
+
...uniqueKeys
|
|
4714
|
+
.filter((uk) => uk.asIndex)
|
|
4715
|
+
.map((uk) => ({ name: uk.name, unique: true, fields: uk.fields })),
|
|
4529
4716
|
];
|
|
4530
4717
|
await this.dialect.createTable(tableName, columns, {
|
|
4531
4718
|
ifNotExists: true,
|
|
@@ -4535,8 +4722,7 @@ class Prorm extends events_1.EventEmitter {
|
|
|
4535
4722
|
comment: options.comment,
|
|
4536
4723
|
rowFormat: options.rowFormat,
|
|
4537
4724
|
initialAutoIncrement: options.initialAutoIncrement,
|
|
4538
|
-
|
|
4539
|
-
indexes: tableIndexes,
|
|
4725
|
+
indexes: allTableIndexes.length ? allTableIndexes : undefined,
|
|
4540
4726
|
constraints: allTableConstraints,
|
|
4541
4727
|
});
|
|
4542
4728
|
}
|
|
@@ -4621,30 +4807,30 @@ class Prorm extends events_1.EventEmitter {
|
|
|
4621
4807
|
}
|
|
4622
4808
|
const trimmedSql = sql.trim().toUpperCase();
|
|
4623
4809
|
if (trimmedSql.startsWith('SELECT')) {
|
|
4624
|
-
return QueryTypes.SELECT;
|
|
4810
|
+
return query_types_1.QueryTypes.SELECT;
|
|
4625
4811
|
}
|
|
4626
4812
|
if (trimmedSql.startsWith('INSERT')) {
|
|
4627
4813
|
// Check for UPSERT patterns: ON CONFLICT, ON DUPLICATE KEY, INSERT OR REPLACE
|
|
4628
4814
|
if (trimmedSql.includes('ON CONFLICT') ||
|
|
4629
4815
|
trimmedSql.includes('ON DUPLICATE KEY') ||
|
|
4630
4816
|
trimmedSql.includes('INSERT OR REPLACE')) {
|
|
4631
|
-
return QueryTypes.UPSERT;
|
|
4817
|
+
return query_types_1.QueryTypes.UPSERT;
|
|
4632
4818
|
}
|
|
4633
|
-
return QueryTypes.INSERT;
|
|
4819
|
+
return query_types_1.QueryTypes.INSERT;
|
|
4634
4820
|
}
|
|
4635
4821
|
if (trimmedSql.startsWith('UPDATE')) {
|
|
4636
|
-
return QueryTypes.UPDATE;
|
|
4822
|
+
return query_types_1.QueryTypes.UPDATE;
|
|
4637
4823
|
}
|
|
4638
4824
|
if (trimmedSql.startsWith('DELETE')) {
|
|
4639
|
-
return QueryTypes.DELETE;
|
|
4825
|
+
return query_types_1.QueryTypes.DELETE;
|
|
4640
4826
|
}
|
|
4641
4827
|
if (trimmedSql.startsWith('UPSERT')) {
|
|
4642
|
-
return QueryTypes.UPSERT;
|
|
4828
|
+
return query_types_1.QueryTypes.UPSERT;
|
|
4643
4829
|
}
|
|
4644
4830
|
if (trimmedSql.startsWith('PRAGMA') || trimmedSql.startsWith('EXPLAIN')) {
|
|
4645
|
-
return QueryTypes.RAW;
|
|
4831
|
+
return query_types_1.QueryTypes.RAW;
|
|
4646
4832
|
}
|
|
4647
|
-
return QueryTypes.RAW;
|
|
4833
|
+
return query_types_1.QueryTypes.RAW;
|
|
4648
4834
|
}
|
|
4649
4835
|
/**
|
|
4650
4836
|
* Execute a raw query
|
|
@@ -4755,10 +4941,10 @@ class Prorm extends events_1.EventEmitter {
|
|
|
4755
4941
|
}
|
|
4756
4942
|
// Process results based on query type
|
|
4757
4943
|
switch (queryType) {
|
|
4758
|
-
case QueryTypes.SELECT:
|
|
4759
|
-
case QueryTypes.SHOWTABLES:
|
|
4760
|
-
case QueryTypes.DESCRIBE:
|
|
4761
|
-
case QueryTypes.VERSION: {
|
|
4944
|
+
case query_types_1.QueryTypes.SELECT:
|
|
4945
|
+
case query_types_1.QueryTypes.SHOWTABLES:
|
|
4946
|
+
case query_types_1.QueryTypes.DESCRIBE:
|
|
4947
|
+
case query_types_1.QueryTypes.VERSION: {
|
|
4762
4948
|
// Map raw results to model instances if requested
|
|
4763
4949
|
if (options?.mapToModel && options?.model) {
|
|
4764
4950
|
const ModelStatic = options.model;
|
|
@@ -4768,22 +4954,22 @@ class Prorm extends events_1.EventEmitter {
|
|
|
4768
4954
|
// Return array of results
|
|
4769
4955
|
return result.rows;
|
|
4770
4956
|
}
|
|
4771
|
-
case QueryTypes.INSERT:
|
|
4957
|
+
case query_types_1.QueryTypes.INSERT:
|
|
4772
4958
|
// For INSERT, return [results, created]
|
|
4773
4959
|
// Check if a row was inserted by examining the last insert rowid
|
|
4774
4960
|
const insertId = result.rowid || result.insertId || 0;
|
|
4775
4961
|
const created = insertId > 0;
|
|
4776
4962
|
return [result.rows, created];
|
|
4777
|
-
case QueryTypes.UPDATE:
|
|
4778
|
-
case QueryTypes.DELETE:
|
|
4779
|
-
case QueryTypes.BULKDELETE:
|
|
4780
|
-
case QueryTypes.UPSERT:
|
|
4963
|
+
case query_types_1.QueryTypes.UPDATE:
|
|
4964
|
+
case query_types_1.QueryTypes.DELETE:
|
|
4965
|
+
case query_types_1.QueryTypes.BULKDELETE:
|
|
4966
|
+
case query_types_1.QueryTypes.UPSERT:
|
|
4781
4967
|
// Return affected count
|
|
4782
4968
|
return result.rowCount;
|
|
4783
|
-
case QueryTypes.BULKINSERT:
|
|
4969
|
+
case query_types_1.QueryTypes.BULKINSERT:
|
|
4784
4970
|
// Return number of inserted rows
|
|
4785
4971
|
return result.rowCount;
|
|
4786
|
-
case QueryTypes.RAW:
|
|
4972
|
+
case query_types_1.QueryTypes.RAW:
|
|
4787
4973
|
default:
|
|
4788
4974
|
// For RAW queries, return rows
|
|
4789
4975
|
return result.rows;
|
|
@@ -4843,10 +5029,10 @@ class Prorm extends events_1.EventEmitter {
|
|
|
4843
5029
|
// Determine query type
|
|
4844
5030
|
const queryType = this.determineQueryType(processedSql, options?.type);
|
|
4845
5031
|
const isSelect = [
|
|
4846
|
-
QueryTypes.SELECT,
|
|
4847
|
-
QueryTypes.SHOWTABLES,
|
|
4848
|
-
QueryTypes.DESCRIBE,
|
|
4849
|
-
QueryTypes.VERSION,
|
|
5032
|
+
query_types_1.QueryTypes.SELECT,
|
|
5033
|
+
query_types_1.QueryTypes.SHOWTABLES,
|
|
5034
|
+
query_types_1.QueryTypes.DESCRIBE,
|
|
5035
|
+
query_types_1.QueryTypes.VERSION,
|
|
4850
5036
|
].includes(queryType);
|
|
4851
5037
|
// Execute the query
|
|
4852
5038
|
const result = await this.dialect.query(processedSql, { raw: options?.raw });
|
|
@@ -6321,6 +6507,103 @@ $$ LANGUAGE plpgsql;`;
|
|
|
6321
6507
|
}
|
|
6322
6508
|
return Array.from(names);
|
|
6323
6509
|
}
|
|
6510
|
+
/**
|
|
6511
|
+
* Run a `findAll` against the connection named by `FindOptions.using`.
|
|
6512
|
+
*
|
|
6513
|
+
* `using` lets one query hop to a sibling database registered on the same
|
|
6514
|
+
* `ConnectionManager` (`User.findAll({ using: 'analytics' })`). The target
|
|
6515
|
+
* connection has its own models, so this re-dispatches to *that*
|
|
6516
|
+
* connection's copy of the model by name and drops `using` from the options
|
|
6517
|
+
* so the delegate runs the query normally instead of bouncing forever.
|
|
6518
|
+
*
|
|
6519
|
+
* Both failure modes report what is actually available, since a typo in a
|
|
6520
|
+
* connection name and a model that was never defined on the target
|
|
6521
|
+
* connection are otherwise indistinguishable from "the query returned
|
|
6522
|
+
* nothing".
|
|
6523
|
+
*/
|
|
6524
|
+
async _findAllUsingConnection(modelName, findOptions) {
|
|
6525
|
+
const name = findOptions.using;
|
|
6526
|
+
const registry = this.connectionManager;
|
|
6527
|
+
const connection = registry?.getConnection(name);
|
|
6528
|
+
if (!connection) {
|
|
6529
|
+
const available = registry?.getConnectionNames().join(', ') || 'none';
|
|
6530
|
+
throw new Error(`Connection '${name}' not found. Available: ${available}`);
|
|
6531
|
+
}
|
|
6532
|
+
const target = connection.models[modelName];
|
|
6533
|
+
if (!target) {
|
|
6534
|
+
const defined = Object.keys(connection.models).join(', ') || 'none';
|
|
6535
|
+
throw new Error(`Model '${modelName}' is not defined on connection '${name}'. Defined there: ${defined}`);
|
|
6536
|
+
}
|
|
6537
|
+
const { using: _using, ...queryOptions } = findOptions;
|
|
6538
|
+
return target.findAll(queryOptions);
|
|
6539
|
+
}
|
|
6540
|
+
/**
|
|
6541
|
+
* Resolve an `include` list into concrete, per-association entries.
|
|
6542
|
+
*
|
|
6543
|
+
* Every pass that reads an include - the cross-table `$assoc.col$` rewrite,
|
|
6544
|
+
* the parent-filtering (`required`) pass, and the eager loader itself - is
|
|
6545
|
+
* written against an entry that carries a `model`. `{ all: true }` carries
|
|
6546
|
+
* none, so each of those passes has to see the *expanded* list or it silently
|
|
6547
|
+
* does nothing for `all`: that is how `include: [{ all: true, required: true }]`
|
|
6548
|
+
* came to load the associations without narrowing the parents, while the
|
|
6549
|
+
* equivalent explicit include filtered correctly.
|
|
6550
|
+
*
|
|
6551
|
+
* Expansion is idempotent - a list with no `all` entry comes back as the same
|
|
6552
|
+
* entries - so running it here as well as inside `loadIncludes()` is safe.
|
|
6553
|
+
*/
|
|
6554
|
+
_expandIncludes(model, modelName, include) {
|
|
6555
|
+
if (!Array.isArray(include) || include.length === 0)
|
|
6556
|
+
return [];
|
|
6557
|
+
return (0, eager_load_1.expandAllIncludes)(this.eagerLoadContext(), model, modelName, include);
|
|
6558
|
+
}
|
|
6559
|
+
/**
|
|
6560
|
+
* Narrow a WHERE clause by the query's filtering (`required`) includes.
|
|
6561
|
+
*
|
|
6562
|
+
* Shared by `findAll`, `_countRows` and `_aggregate` so that a filtering
|
|
6563
|
+
* include restricts the rows, the count and an aggregate to the same set.
|
|
6564
|
+
* Returns `null` when some include can match no parent at all - the whole
|
|
6565
|
+
* query is then empty, and each caller spells "empty" its own way (no rows,
|
|
6566
|
+
* a count of 0, a null aggregate).
|
|
6567
|
+
*
|
|
6568
|
+
* Several includes can constrain the *same* parent column, and since
|
|
6569
|
+
* `{ all: true, required: true }` expands to one include per association that
|
|
6570
|
+
* is now the common case rather than a corner. Their key sets are intersected
|
|
6571
|
+
* here. They must not be emitted as two conditions on one key: a
|
|
6572
|
+
* `{ id: { $and: [{ $in: [...] }, { $in: [...] }] } }` shape is not something
|
|
6573
|
+
* the where-builders recognize, and it collapsed to a single `"id" = ?` -
|
|
6574
|
+
* which is why two `required` includes used to return the wrong rows.
|
|
6575
|
+
*/
|
|
6576
|
+
async _applyRequiredIncludeFilters(model, modelName, include, where) {
|
|
6577
|
+
const byParentAttr = new Map();
|
|
6578
|
+
for (const rawInclude of this._expandIncludes(model, modelName, include)) {
|
|
6579
|
+
const filter = await (0, eager_load_1.resolveRequiredIncludeFilter)(this.eagerLoadContext(), model, modelName, rawInclude);
|
|
6580
|
+
if (!filter)
|
|
6581
|
+
continue;
|
|
6582
|
+
// No parent can satisfy a required include with no matches.
|
|
6583
|
+
if (filter.values.length === 0)
|
|
6584
|
+
return null;
|
|
6585
|
+
const previous = byParentAttr.get(filter.parentAttr);
|
|
6586
|
+
if (!previous) {
|
|
6587
|
+
byParentAttr.set(filter.parentAttr, filter.values);
|
|
6588
|
+
continue;
|
|
6589
|
+
}
|
|
6590
|
+
const keep = new Set(filter.values);
|
|
6591
|
+
const intersection = previous.filter((v) => keep.has(v));
|
|
6592
|
+
if (intersection.length === 0)
|
|
6593
|
+
return null;
|
|
6594
|
+
byParentAttr.set(filter.parentAttr, intersection);
|
|
6595
|
+
}
|
|
6596
|
+
let narrowed = where;
|
|
6597
|
+
for (const [parentAttr, values] of byParentAttr) {
|
|
6598
|
+
// A condition the caller wrote on the same column still applies on top.
|
|
6599
|
+
const existing = narrowed?.[parentAttr];
|
|
6600
|
+
narrowed = {
|
|
6601
|
+
...narrowed,
|
|
6602
|
+
[parentAttr]: existing ? { $and: [existing, { $in: values }] } : { $in: values },
|
|
6603
|
+
};
|
|
6604
|
+
}
|
|
6605
|
+
return narrowed;
|
|
6606
|
+
}
|
|
6324
6607
|
/**
|
|
6325
6608
|
* Issue a real `SELECT COUNT(...)` for a model.
|
|
6326
6609
|
*
|
|
@@ -6329,6 +6612,9 @@ $$ LANGUAGE plpgsql;`;
|
|
|
6329
6612
|
* - `col` picks the counted expression (`COUNT("col")`)
|
|
6330
6613
|
* - `distinct: true` counts distinct values of `col`, or of the primary key
|
|
6331
6614
|
* - `group` returns the number of groups
|
|
6615
|
+
*
|
|
6616
|
+
* A query carrying `union` branches is counted over the combined query rather
|
|
6617
|
+
* than this model's table alone - see the derived-table note below.
|
|
6332
6618
|
*/
|
|
6333
6619
|
async _countRows(model, modelName, options = {}) {
|
|
6334
6620
|
if (!this.dialect) {
|
|
@@ -6344,22 +6630,10 @@ $$ LANGUAGE plpgsql;`;
|
|
|
6344
6630
|
where = { ...where, [deletedAt]: { $isNull: true } };
|
|
6345
6631
|
}
|
|
6346
6632
|
// Filtering includes restrict the counted set exactly as they restrict findAll.
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
continue;
|
|
6352
|
-
if (filter.values.length === 0)
|
|
6353
|
-
return 0;
|
|
6354
|
-
const existing = where?.[filter.parentAttr];
|
|
6355
|
-
where = {
|
|
6356
|
-
...where,
|
|
6357
|
-
[filter.parentAttr]: existing
|
|
6358
|
-
? { $and: [existing, { $in: filter.values }] }
|
|
6359
|
-
: { $in: filter.values },
|
|
6360
|
-
};
|
|
6361
|
-
}
|
|
6362
|
-
}
|
|
6633
|
+
const countableWhere = await this._applyRequiredIncludeFilters(model, modelName, options.include, where);
|
|
6634
|
+
if (countableWhere === null)
|
|
6635
|
+
return 0;
|
|
6636
|
+
where = countableWhere;
|
|
6363
6637
|
const pkAttr = resolveModelPrimaryKeyAttr(model);
|
|
6364
6638
|
const countCol = opts.col;
|
|
6365
6639
|
const distinct = opts.distinct === true;
|
|
@@ -6380,18 +6654,89 @@ $$ LANGUAGE plpgsql;`;
|
|
|
6380
6654
|
const groupFields = opts.group
|
|
6381
6655
|
? (Array.isArray(opts.group) ? opts.group : [opts.group]).map((g) => typeof g === 'string' ? g : String(g))
|
|
6382
6656
|
: undefined;
|
|
6383
|
-
|
|
6657
|
+
// `union` branches widen the set the query returns, so counting this
|
|
6658
|
+
// model's table alone leaves `findAndCountAll` contradicting itself: `rows`
|
|
6659
|
+
// spans every branch while `count` describes only the first. When the query
|
|
6660
|
+
// carries branches, count over the combined query instead:
|
|
6661
|
+
//
|
|
6662
|
+
// SELECT COUNT(*) AS "count" FROM (
|
|
6663
|
+
// SELECT * FROM "active" WHERE ... UNION ALL SELECT * FROM "archived" WHERE ...
|
|
6664
|
+
// ) AS "prorm_count"
|
|
6665
|
+
//
|
|
6666
|
+
// The set operator does its work *inside* the derived table, so `UNION` and
|
|
6667
|
+
// `UNION ALL` count differently with no special-casing here: the distinct
|
|
6668
|
+
// form collapses duplicate rows before they reach COUNT, the ALL form does
|
|
6669
|
+
// not, and EXCEPT/INTERSECT likewise count exactly what they produce.
|
|
6670
|
+
//
|
|
6671
|
+
// Left out on purpose: `order` and the outer `limit`/`offset`. Neither can
|
|
6672
|
+
// change a count, and several dialects reject ORDER BY inside a derived
|
|
6673
|
+
// table. Every other narrowing option is kept, each branch's own `where`
|
|
6674
|
+
// and `limit` included. The projection is resolved the way findAll resolves
|
|
6675
|
+
// it, because a distinct UNION dedupes on the selected columns.
|
|
6676
|
+
//
|
|
6677
|
+
// Not every dialect compiles `union` - the ones that don't drop it from
|
|
6678
|
+
// findAll too, so their rows come from this table alone and the plain count
|
|
6679
|
+
// is already correct for them. Rather than keep a list of which do, compile
|
|
6680
|
+
// the query both with and without the branches and compare: identical SQL
|
|
6681
|
+
// means the dialect ignored them.
|
|
6682
|
+
const unionBranches = opts.union ?? [];
|
|
6683
|
+
let unionSource = null;
|
|
6684
|
+
if (unionBranches.length > 0) {
|
|
6685
|
+
let projection = opts.attributes;
|
|
6686
|
+
if (projection && !Array.isArray(projection) && projection.exclude) {
|
|
6687
|
+
const excluded = new Set(projection.exclude);
|
|
6688
|
+
projection = Array.from(modelColumnNames(model)).filter((k) => !excluded.has(k));
|
|
6689
|
+
}
|
|
6690
|
+
const shared = {
|
|
6691
|
+
tableName,
|
|
6692
|
+
schema: model.schema,
|
|
6693
|
+
attributes: projection,
|
|
6694
|
+
where,
|
|
6695
|
+
distinct: opts.distinct,
|
|
6696
|
+
distinctOn: opts.distinctOn,
|
|
6697
|
+
// Associations are resolved with their own queries, never joined in.
|
|
6698
|
+
include: undefined,
|
|
6699
|
+
};
|
|
6700
|
+
const withoutBranches = dialect.buildSelectQuery({ ...shared });
|
|
6701
|
+
const withBranches = dialect.buildSelectQuery({
|
|
6702
|
+
...shared,
|
|
6703
|
+
unionType: opts.unionType,
|
|
6704
|
+
union: unionBranches,
|
|
6705
|
+
});
|
|
6706
|
+
if (withBranches.sql !== withoutBranches.sql) {
|
|
6707
|
+
unionSource = { sql: withBranches.sql, values: withBranches.values };
|
|
6708
|
+
}
|
|
6709
|
+
}
|
|
6710
|
+
let sql;
|
|
6384
6711
|
let values = [];
|
|
6385
|
-
if (
|
|
6386
|
-
|
|
6387
|
-
sql
|
|
6388
|
-
|
|
6712
|
+
if (unionSource) {
|
|
6713
|
+
// `where` is already compiled into the derived table; no outer WHERE.
|
|
6714
|
+
sql =
|
|
6715
|
+
`SELECT ${countExpr} AS ${dialect.escapeId('count')} ` +
|
|
6716
|
+
`FROM (${unionSource.sql}) AS ${dialect.escapeId('prorm_count')}`;
|
|
6717
|
+
values = unionSource.values;
|
|
6718
|
+
}
|
|
6719
|
+
else {
|
|
6720
|
+
sql = `SELECT ${countExpr} AS ${dialect.escapeId('count')} FROM ${qualified}`;
|
|
6721
|
+
if (where && Object.keys(where).length > 0) {
|
|
6722
|
+
const built = dialect.buildWhereClause(where);
|
|
6723
|
+
sql += ` WHERE ${built.sql}`;
|
|
6724
|
+
values = built.values;
|
|
6725
|
+
}
|
|
6389
6726
|
}
|
|
6390
6727
|
if (groupFields?.length) {
|
|
6391
6728
|
sql += ` GROUP BY ${groupFields.map((g) => dialect.escapeId(g)).join(', ')}`;
|
|
6392
6729
|
}
|
|
6393
6730
|
const finalSql = dialect.replaceReplacements(sql, values);
|
|
6731
|
+
const started = Date.now();
|
|
6394
6732
|
const result = await dialect.query(finalSql);
|
|
6733
|
+
this._logQuery({
|
|
6734
|
+
sql: finalSql,
|
|
6735
|
+
duration: Date.now() - started,
|
|
6736
|
+
parameters: values,
|
|
6737
|
+
model: modelName,
|
|
6738
|
+
type: 'SELECT',
|
|
6739
|
+
}, options.logging);
|
|
6395
6740
|
const rows = result.rows || [];
|
|
6396
6741
|
// With GROUP BY the aggregate returns one row per group; the count is the
|
|
6397
6742
|
// number of groups, matching how callers use `count({ group })`.
|
|
@@ -6417,10 +6762,10 @@ $$ LANGUAGE plpgsql;`;
|
|
|
6417
6762
|
throw new Error('Database not connected');
|
|
6418
6763
|
}
|
|
6419
6764
|
const dialect = this.dialect;
|
|
6420
|
-
|
|
6421
|
-
|
|
6765
|
+
const columns = modelColumnNames(model);
|
|
6766
|
+
if (!columns.has(attribute)) {
|
|
6422
6767
|
throw new Error(`${modelName} has no attribute '${attribute}' to ${aggregate.toLowerCase()}.` +
|
|
6423
|
-
|
|
6768
|
+
` Known attributes: ${Array.from(columns).join(', ')}.`);
|
|
6424
6769
|
}
|
|
6425
6770
|
const modelOptions = model.options;
|
|
6426
6771
|
const deletedAt = modelOptions?.deletedAt || 'deletedAt';
|
|
@@ -6431,22 +6776,10 @@ $$ LANGUAGE plpgsql;`;
|
|
|
6431
6776
|
where = { ...where, [deletedAt]: { $isNull: true } };
|
|
6432
6777
|
}
|
|
6433
6778
|
// Filtering includes restrict the aggregated set, as they do for findAll.
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
continue;
|
|
6439
|
-
if (filter.values.length === 0)
|
|
6440
|
-
return aggregate === 'SUM' ? 0 : null;
|
|
6441
|
-
const existing = where?.[filter.parentAttr];
|
|
6442
|
-
where = {
|
|
6443
|
-
...where,
|
|
6444
|
-
[filter.parentAttr]: existing
|
|
6445
|
-
? { $and: [existing, { $in: filter.values }] }
|
|
6446
|
-
: { $in: filter.values },
|
|
6447
|
-
};
|
|
6448
|
-
}
|
|
6449
|
-
}
|
|
6779
|
+
const aggregateWhere = await this._applyRequiredIncludeFilters(model, modelName, options.include, where);
|
|
6780
|
+
if (aggregateWhere === null)
|
|
6781
|
+
return aggregate === 'SUM' ? 0 : null;
|
|
6782
|
+
where = aggregateWhere;
|
|
6450
6783
|
const qualified = model.schema
|
|
6451
6784
|
? `${dialect.escapeId(model.schema)}.${dialect.escapeId(model.tableName)}`
|
|
6452
6785
|
: dialect.escapeId(model.tableName);
|
|
@@ -6457,12 +6790,191 @@ $$ LANGUAGE plpgsql;`;
|
|
|
6457
6790
|
sql += ` WHERE ${built.sql}`;
|
|
6458
6791
|
values = built.values;
|
|
6459
6792
|
}
|
|
6460
|
-
const
|
|
6793
|
+
const finalAggSql = dialect.replaceReplacements(sql, values);
|
|
6794
|
+
const aggStarted = Date.now();
|
|
6795
|
+
const result = await dialect.query(finalAggSql);
|
|
6796
|
+
this._logQuery({
|
|
6797
|
+
sql: finalAggSql,
|
|
6798
|
+
duration: Date.now() - aggStarted,
|
|
6799
|
+
parameters: values,
|
|
6800
|
+
model: modelName,
|
|
6801
|
+
type: 'SELECT',
|
|
6802
|
+
}, options.logging);
|
|
6461
6803
|
const val = result.rows?.[0]?.__agg;
|
|
6462
6804
|
if (val === null || val === undefined)
|
|
6463
6805
|
return aggregate === 'SUM' ? 0 : null;
|
|
6464
6806
|
return Number(val);
|
|
6465
6807
|
}
|
|
6808
|
+
/**
|
|
6809
|
+
* Log one executed query, and emit the events that hang off it.
|
|
6810
|
+
*
|
|
6811
|
+
* Every statement the ORM issues should route through here. Query logging was
|
|
6812
|
+
* previously inlined at four call sites and only some ran it, so `count`,
|
|
6813
|
+
* `bulkCreate`, `increment` and the aggregates emitted nothing - turning
|
|
6814
|
+
* logging on showed an incomplete picture of what the ORM was doing.
|
|
6815
|
+
*/
|
|
6816
|
+
_logQuery(context, queryLogging) {
|
|
6817
|
+
(0, logging_1.logQuery)(context, {
|
|
6818
|
+
instanceLogging: this.loggingOptions,
|
|
6819
|
+
queryLogging,
|
|
6820
|
+
benchmark: this.config.benchmark === true,
|
|
6821
|
+
logQueryParameters: this.config.logQueryParameters === true,
|
|
6822
|
+
});
|
|
6823
|
+
// Slow-query detection and the internal logger's own listeners.
|
|
6824
|
+
this.logger.logQuery(context.sql, context.duration);
|
|
6825
|
+
this.emit('query', {
|
|
6826
|
+
sql: context.sql,
|
|
6827
|
+
duration: context.duration,
|
|
6828
|
+
timestamp: new Date(),
|
|
6829
|
+
model: context.model,
|
|
6830
|
+
});
|
|
6831
|
+
const threshold = this.config.slowQueryThreshold || 1000;
|
|
6832
|
+
if (context.duration > threshold) {
|
|
6833
|
+
this.emit('slowQuery', {
|
|
6834
|
+
sql: context.sql,
|
|
6835
|
+
duration: context.duration,
|
|
6836
|
+
threshold,
|
|
6837
|
+
timestamp: new Date(),
|
|
6838
|
+
model: context.model,
|
|
6839
|
+
});
|
|
6840
|
+
}
|
|
6841
|
+
}
|
|
6842
|
+
/**
|
|
6843
|
+
* Apply `+/- delta` to numeric columns in a single UPDATE.
|
|
6844
|
+
*
|
|
6845
|
+
* Backs `Model.increment` / `Model.decrement`. The arithmetic happens in the
|
|
6846
|
+
* database, so two concurrent callers both land - a read-modify-write in
|
|
6847
|
+
* application code silently drops one of them.
|
|
6848
|
+
*/
|
|
6849
|
+
async _applyDelta(model, modelName, fields, options = {}, sign) {
|
|
6850
|
+
if (!this.dialect)
|
|
6851
|
+
throw new Error('Database not connected');
|
|
6852
|
+
const dialect = this.dialect;
|
|
6853
|
+
// Normalize the three accepted shapes to column -> delta.
|
|
6854
|
+
const by = options.by ?? 1;
|
|
6855
|
+
let deltas;
|
|
6856
|
+
if (typeof fields === 'string') {
|
|
6857
|
+
deltas = { [fields]: by };
|
|
6858
|
+
}
|
|
6859
|
+
else if (Array.isArray(fields)) {
|
|
6860
|
+
deltas = Object.fromEntries(fields.map((f) => [f, by]));
|
|
6861
|
+
}
|
|
6862
|
+
else {
|
|
6863
|
+
deltas = fields;
|
|
6864
|
+
}
|
|
6865
|
+
const names = Object.keys(deltas);
|
|
6866
|
+
if (names.length === 0)
|
|
6867
|
+
return 0;
|
|
6868
|
+
const columns = modelColumnNames(model);
|
|
6869
|
+
for (const name of names) {
|
|
6870
|
+
if (!columns.has(name)) {
|
|
6871
|
+
throw new Error(`${modelName} has no attribute '${name}' to increment. ` +
|
|
6872
|
+
`Known attributes: ${Array.from(columns).join(', ')}.`);
|
|
6873
|
+
}
|
|
6874
|
+
}
|
|
6875
|
+
const qualified = model.schema
|
|
6876
|
+
? `${dialect.escapeId(model.schema)}.${dialect.escapeId(model.tableName)}`
|
|
6877
|
+
: dialect.escapeId(model.tableName);
|
|
6878
|
+
const assignments = names.map((name) => {
|
|
6879
|
+
const col = dialect.escapeId(name);
|
|
6880
|
+
const amount = deltas[name] * sign;
|
|
6881
|
+
// Emit `col = col + n` / `col = col - n` rather than a bound parameter
|
|
6882
|
+
// for the operator, since the sign is structural.
|
|
6883
|
+
return `${col} = ${col} ${amount < 0 ? '-' : '+'} ${Math.abs(amount)}`;
|
|
6884
|
+
});
|
|
6885
|
+
let sql = `UPDATE ${qualified} SET ${assignments.join(', ')}`;
|
|
6886
|
+
let values = [];
|
|
6887
|
+
// Respect paranoid filtering, as update() does.
|
|
6888
|
+
const modelOptions = model.options;
|
|
6889
|
+
const deletedAt = modelOptions?.deletedAt || 'deletedAt';
|
|
6890
|
+
let where = options.where;
|
|
6891
|
+
if (modelOptions?.paranoid === true && model.rawAttributes?.[deletedAt]) {
|
|
6892
|
+
where = { ...where, [deletedAt]: { $isNull: true } };
|
|
6893
|
+
}
|
|
6894
|
+
if (where && Object.keys(where).length > 0) {
|
|
6895
|
+
const built = dialect.buildWhereClause((0, operators_1.normalizeOperatorKeys)(where));
|
|
6896
|
+
sql += ` WHERE ${built.sql}`;
|
|
6897
|
+
values = built.values;
|
|
6898
|
+
}
|
|
6899
|
+
const finalDeltaSql = dialect.replaceReplacements(sql, values);
|
|
6900
|
+
const deltaStarted = Date.now();
|
|
6901
|
+
const result = await dialect.query(finalDeltaSql);
|
|
6902
|
+
this._logQuery({
|
|
6903
|
+
sql: finalDeltaSql,
|
|
6904
|
+
duration: Date.now() - deltaStarted,
|
|
6905
|
+
parameters: values,
|
|
6906
|
+
model: modelName,
|
|
6907
|
+
type: 'UPDATE',
|
|
6908
|
+
}, options.logging);
|
|
6909
|
+
return result.affectedRows ?? result.rowCount ?? 0;
|
|
6910
|
+
}
|
|
6911
|
+
/**
|
|
6912
|
+
* Update many rows to different values in one statement.
|
|
6913
|
+
*
|
|
6914
|
+
* Backs `Model.bulkUpdate`. Compiles one UPDATE with a CASE expression per
|
|
6915
|
+
* column - portable across every supported dialect - instead of N round
|
|
6916
|
+
* trips.
|
|
6917
|
+
*/
|
|
6918
|
+
async _bulkUpdate(model, modelName, rows, options = {}) {
|
|
6919
|
+
if (!this.dialect)
|
|
6920
|
+
throw new Error('Database not connected');
|
|
6921
|
+
if (!rows || rows.length === 0)
|
|
6922
|
+
return 0;
|
|
6923
|
+
const dialect = this.dialect;
|
|
6924
|
+
const keyAttr = options.key ?? resolveModelPrimaryKeyAttr(model);
|
|
6925
|
+
const keys = [];
|
|
6926
|
+
for (const row of rows) {
|
|
6927
|
+
if (row[keyAttr] === undefined || row[keyAttr] === null) {
|
|
6928
|
+
throw new Error(`bulkUpdate on ${modelName}: every row must carry '${keyAttr}'. ` +
|
|
6929
|
+
`Pass { key: '<column>' } if the rows are identified by something else.`);
|
|
6930
|
+
}
|
|
6931
|
+
keys.push(row[keyAttr]);
|
|
6932
|
+
}
|
|
6933
|
+
// Columns to write: every key seen across the rows, minus the identifier.
|
|
6934
|
+
const columns = Array.from(new Set(rows.flatMap((r) => Object.keys(r)))).filter((c) => c !== keyAttr);
|
|
6935
|
+
if (columns.length === 0)
|
|
6936
|
+
return 0;
|
|
6937
|
+
const knownColumns = modelColumnNames(model);
|
|
6938
|
+
for (const col of columns) {
|
|
6939
|
+
if (!knownColumns.has(col)) {
|
|
6940
|
+
throw new Error(`bulkUpdate on ${modelName}: unknown attribute '${col}'. ` +
|
|
6941
|
+
`Known attributes: ${Array.from(knownColumns).join(', ')}.`);
|
|
6942
|
+
}
|
|
6943
|
+
}
|
|
6944
|
+
const qualified = model.schema
|
|
6945
|
+
? `${dialect.escapeId(model.schema)}.${dialect.escapeId(model.tableName)}`
|
|
6946
|
+
: dialect.escapeId(model.tableName);
|
|
6947
|
+
const keyCol = dialect.escapeId(keyAttr);
|
|
6948
|
+
const values = [];
|
|
6949
|
+
const assignments = columns.map((col) => {
|
|
6950
|
+
const escaped = dialect.escapeId(col);
|
|
6951
|
+
const branches = [];
|
|
6952
|
+
for (const row of rows) {
|
|
6953
|
+
// A row that omits this column keeps its current value, so it gets no
|
|
6954
|
+
// WHEN branch and falls through to the ELSE.
|
|
6955
|
+
if (!(col in row))
|
|
6956
|
+
continue;
|
|
6957
|
+
branches.push('WHEN ? THEN ?');
|
|
6958
|
+
values.push(row[keyAttr], row[col]);
|
|
6959
|
+
}
|
|
6960
|
+
return `${escaped} = CASE ${keyCol} ${branches.join(' ')} ELSE ${escaped} END`;
|
|
6961
|
+
});
|
|
6962
|
+
const placeholders = keys.map(() => '?').join(', ');
|
|
6963
|
+
values.push(...keys);
|
|
6964
|
+
const sql = `UPDATE ${qualified} SET ${assignments.join(', ')} ` +
|
|
6965
|
+
`WHERE ${keyCol} IN (${placeholders})`;
|
|
6966
|
+
const finalBulkSql = dialect.replaceReplacements(sql, values);
|
|
6967
|
+
const bulkStarted = Date.now();
|
|
6968
|
+
const result = await dialect.query(finalBulkSql);
|
|
6969
|
+
this._logQuery({
|
|
6970
|
+
sql: finalBulkSql,
|
|
6971
|
+
duration: Date.now() - bulkStarted,
|
|
6972
|
+
parameters: values,
|
|
6973
|
+
model: modelName,
|
|
6974
|
+
type: 'UPDATE',
|
|
6975
|
+
}, options.logging);
|
|
6976
|
+
return result.affectedRows ?? result.rowCount ?? rows.length;
|
|
6977
|
+
}
|
|
6466
6978
|
/**
|
|
6467
6979
|
* The view of this instance that the shared eager-loader needs.
|
|
6468
6980
|
*/
|
|
@@ -6630,6 +7142,7 @@ $$ LANGUAGE plpgsql;`;
|
|
|
6630
7142
|
}
|
|
6631
7143
|
// Table exists - let's alter it to match the model
|
|
6632
7144
|
await this.alterTable(tableName, modelAttributes, modelOptions, { drop });
|
|
7145
|
+
await this.reconcileUniqueKeys(tableName, modelAttributes, modelOptions);
|
|
6633
7146
|
}
|
|
6634
7147
|
catch (error) {
|
|
6635
7148
|
this.logger.error(`Failed to alter table ${tableName}`, { error });
|
|
@@ -6637,8 +7150,74 @@ $$ LANGUAGE plpgsql;`;
|
|
|
6637
7150
|
}
|
|
6638
7151
|
}
|
|
6639
7152
|
}
|
|
7153
|
+
/**
|
|
7154
|
+
* Make sure the model's declared composite unique keys are enforced on an
|
|
7155
|
+
* existing table, as a unique index.
|
|
7156
|
+
*
|
|
7157
|
+
* `alterTable` reconciles columns only, and on SQLite reconciling a column
|
|
7158
|
+
* means rebuilding the table - which drops the table-level UNIQUE constraint
|
|
7159
|
+
* that `createTableFromModel` put in the CREATE TABLE text. Without this step
|
|
7160
|
+
* a single `sync({ alter: true })` would silently un-enforce a unique key the
|
|
7161
|
+
* model still declares, which is the same silent-data-integrity failure the
|
|
7162
|
+
* unique key support was added to fix.
|
|
7163
|
+
*
|
|
7164
|
+
* A unique index enforces exactly the same rule as a unique constraint and,
|
|
7165
|
+
* unlike `ADD CONSTRAINT`, every dialect here can create one. Existing
|
|
7166
|
+
* indexes are listed first so re-syncing is idempotent on dialects whose
|
|
7167
|
+
* `addIndex` has no IF NOT EXISTS.
|
|
7168
|
+
*
|
|
7169
|
+
* This deliberately does not remove unique keys the model no longer declares
|
|
7170
|
+
* (dropping a constraint is not a change to make implicitly), and it will
|
|
7171
|
+
* fail loudly if the table already holds rows that violate a newly declared
|
|
7172
|
+
* key - which is the honest outcome, not something to swallow.
|
|
7173
|
+
*/
|
|
7174
|
+
async reconcileUniqueKeys(tableName, attributes, options) {
|
|
7175
|
+
if (!this.dialect)
|
|
7176
|
+
return;
|
|
7177
|
+
const columns = this.buildColumnDefinitions(attributes, options);
|
|
7178
|
+
// `buildColumnDefinitions` omits the implicit `id` that
|
|
7179
|
+
// `createTableFromModel` synthesizes for a model with no declared primary
|
|
7180
|
+
// key, so add it back before validating the declared field names against
|
|
7181
|
+
// the column list - otherwise a unique key over `id` looks bogus here.
|
|
7182
|
+
if (!Object.values(columns).some((c) => c.primaryKey) && !columns['id']) {
|
|
7183
|
+
columns['id'] = {
|
|
7184
|
+
type: 'INTEGER',
|
|
7185
|
+
allowNull: false,
|
|
7186
|
+
primaryKey: true,
|
|
7187
|
+
autoIncrement: true,
|
|
7188
|
+
};
|
|
7189
|
+
}
|
|
7190
|
+
const uniqueKeys = collectUniqueKeys(tableName, attributes, options, columns);
|
|
7191
|
+
if (uniqueKeys.length === 0)
|
|
7192
|
+
return;
|
|
7193
|
+
const existing = new Set();
|
|
7194
|
+
try {
|
|
7195
|
+
for (const index of await this.dialect.showIndexes(tableName)) {
|
|
7196
|
+
const name = index?.name ?? index?.Key_name ?? index?.index_name;
|
|
7197
|
+
if (typeof name === 'string')
|
|
7198
|
+
existing.add(name);
|
|
7199
|
+
}
|
|
7200
|
+
}
|
|
7201
|
+
catch {
|
|
7202
|
+
// A dialect that cannot list indexes just means we may re-issue a CREATE
|
|
7203
|
+
// that already exists; that is the addIndex implementation's problem, not
|
|
7204
|
+
// a reason to skip enforcing the key.
|
|
7205
|
+
}
|
|
7206
|
+
for (const uk of uniqueKeys) {
|
|
7207
|
+
if (existing.has(uk.name))
|
|
7208
|
+
continue;
|
|
7209
|
+
await this.dialect.addIndex(tableName, uk.name, uk.fields, { unique: true });
|
|
7210
|
+
this.logger.info(`Applied unique key ${uk.name} to table ${tableName}`);
|
|
7211
|
+
}
|
|
7212
|
+
}
|
|
6640
7213
|
/**
|
|
6641
7214
|
* Alter a table to match model definition
|
|
7215
|
+
*
|
|
7216
|
+
* Scope note: this reconciles *columns* only - it adds, changes and (with
|
|
7217
|
+
* `drop`) removes them. Table-level constraints are not diffed against the
|
|
7218
|
+
* database. The composite unique keys collected by `collectUniqueKeys` are
|
|
7219
|
+
* the exception, and they are handled just after this runs, by
|
|
7220
|
+
* `reconcileUniqueKeys`, because a column rebuild would otherwise drop them.
|
|
6642
7221
|
*/
|
|
6643
7222
|
async alterTable(tableName, modelAttributes, modelOptions, options = {}) {
|
|
6644
7223
|
if (!this.dialect) {
|
|
@@ -6916,7 +7495,7 @@ $$ LANGUAGE plpgsql;`;
|
|
|
6916
7495
|
* })
|
|
6917
7496
|
*/
|
|
6918
7497
|
col(tableOrColumn, column) {
|
|
6919
|
-
return new Col(tableOrColumn, column);
|
|
7498
|
+
return new expressions_1.Col(tableOrColumn, column);
|
|
6920
7499
|
}
|
|
6921
7500
|
/**
|
|
6922
7501
|
* Create a literal SQL expression
|
|
@@ -6979,7 +7558,7 @@ $$ LANGUAGE plpgsql;`;
|
|
|
6979
7558
|
* });
|
|
6980
7559
|
*/
|
|
6981
7560
|
get UUIDV4() {
|
|
6982
|
-
return new Literal(data_types_1.UUIDV4.generate());
|
|
7561
|
+
return new expressions_1.Literal(data_types_1.UUIDV4.generate());
|
|
6983
7562
|
}
|
|
6984
7563
|
/**
|
|
6985
7564
|
* Create a where condition for complex queries
|
|
@@ -7265,10 +7844,10 @@ exports.DataTypes = {
|
|
|
7265
7844
|
// Static Prorm operators - these can be used without creating a Prorm instance (e.g., Prorm.literal('NOW()'))
|
|
7266
7845
|
// Using type assertion to avoid TypeScript errors since these are added after class definition
|
|
7267
7846
|
Prorm.literal = function (sql) {
|
|
7268
|
-
return new Literal(sql);
|
|
7847
|
+
return new expressions_1.Literal(sql);
|
|
7269
7848
|
};
|
|
7270
7849
|
Prorm.col = function (tableOrColumn, column) {
|
|
7271
|
-
return new Col(tableOrColumn, column);
|
|
7850
|
+
return new expressions_1.Col(tableOrColumn, column);
|
|
7272
7851
|
};
|
|
7273
7852
|
Prorm.fn = function (fnName, ...args) {
|
|
7274
7853
|
return {
|
|
@@ -7285,7 +7864,7 @@ Prorm.cast = function (value, type) {
|
|
|
7285
7864
|
};
|
|
7286
7865
|
Object.defineProperty(Prorm, 'UUIDV4', {
|
|
7287
7866
|
get() {
|
|
7288
|
-
return new Literal('lower(hex(randomblob(16)))');
|
|
7867
|
+
return new expressions_1.Literal('lower(hex(randomblob(16)))');
|
|
7289
7868
|
},
|
|
7290
7869
|
});
|
|
7291
7870
|
Prorm.where = function (col, value) {
|