ts-prorm-orm 1.2.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +209 -0
- package/README.md +32 -24
- package/dist/compliance/index.d.ts +2 -0
- package/dist/compliance/index.js +13 -1
- 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/index.d.ts +2 -0
- package/dist/decorators/index.js +11 -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 +29 -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/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 +13 -4
- package/dist/index.js +54 -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/decorators.d.ts +39 -3
- package/dist/models/decorators.js +74 -0
- 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 +1142 -557
- 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/sql-constants.d.ts +44 -0
- package/dist/sql-constants.js +138 -1
- package/dist/types/index.d.ts +53 -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 +32 -5
- 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,50 +2960,33 @@ 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
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
model: modelName,
|
|
3023
|
-
});
|
|
3024
|
-
}
|
|
3025
|
-
// Wrap each row with instance methods
|
|
2970
|
+
type: 'SELECT',
|
|
2971
|
+
}, findOptions.logging);
|
|
2972
|
+
// Wrap each row with instance methods.
|
|
2973
|
+
//
|
|
2974
|
+
// `raw: true` asks for plain rows and was accepted but ignored - every
|
|
2975
|
+
// row came back as a full instance carrying save/update/destroy and the
|
|
2976
|
+
// internal bookkeeping fields, which is both slower than asked for and
|
|
2977
|
+
// surprising when the result is a grouped aggregate that maps to no
|
|
2978
|
+
// model instance at all.
|
|
3026
2979
|
const modelRef = this;
|
|
3027
2980
|
const modelHooksRef = options.hooks || {};
|
|
3028
|
-
const wrappedRows =
|
|
3029
|
-
|
|
3030
|
-
|
|
2981
|
+
const wrappedRows = (findOptions.raw === true
|
|
2982
|
+
? result.rows.map((row) => ({ ...row }))
|
|
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
|
+
}
|
|
3031
2990
|
// Eager-load associations. The implementation is shared with the
|
|
3032
2991
|
// class-based Model API (see models/eager-load.ts) - it used to be
|
|
3033
2992
|
// duplicated here and in models/model.ts, and the two copies had
|
|
@@ -3035,6 +2994,11 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3035
2994
|
if (findOptions.include && findOptions.include.length > 0) {
|
|
3036
2995
|
await (0, eager_load_1.loadIncludes)(self.eagerLoadContext(), wrappedRows, model, modelName, findOptions.include);
|
|
3037
2996
|
}
|
|
2997
|
+
if (typeof findHooks.afterFind === 'function') {
|
|
2998
|
+
const replaced = await findHooks.afterFind(wrappedRows, findOptions);
|
|
2999
|
+
if (replaced)
|
|
3000
|
+
return replaced;
|
|
3001
|
+
}
|
|
3038
3002
|
return wrappedRows;
|
|
3039
3003
|
},
|
|
3040
3004
|
/**
|
|
@@ -3045,7 +3009,8 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3045
3009
|
// Count with an aggregate rather than by re-running the full query
|
|
3046
3010
|
// without a limit and measuring the array. `limit`/`offset` are dropped
|
|
3047
3011
|
// (the count describes the whole matching set), everything else that
|
|
3048
|
-
// 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.
|
|
3049
3014
|
const { limit: _limit, offset: _offset, ...countable } = this._mergeScopes(findOptions);
|
|
3050
3015
|
const count = await self._countRows(model, modelName, countable);
|
|
3051
3016
|
return { rows, count };
|
|
@@ -3307,6 +3272,11 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3307
3272
|
if (typeof modelHooks.beforeCreate === 'function') {
|
|
3308
3273
|
await modelHooks.beforeCreate(hookInstance, createOptions);
|
|
3309
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
|
+
}
|
|
3310
3280
|
// Build dbValues from hookInstance (after hooks may have modified it), filtering virtual fields and respecting 'fields' option
|
|
3311
3281
|
const allowedFields = createOptions.fields;
|
|
3312
3282
|
const dbValues = {};
|
|
@@ -3319,7 +3289,7 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3319
3289
|
if (typeObj.key === 'VIRTUAL' || typeObj.key === 'VIRTUALSTRING')
|
|
3320
3290
|
continue;
|
|
3321
3291
|
}
|
|
3322
|
-
dbValues[key] = value;
|
|
3292
|
+
dbValues[key] = _serializeValue(key, value);
|
|
3323
3293
|
}
|
|
3324
3294
|
const { sql, values: insertValues } = dialect.buildInsertQuery(this.tableName, dbValues, {
|
|
3325
3295
|
returning: createOptions.returning,
|
|
@@ -3414,6 +3384,9 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3414
3384
|
if (typeof modelHooks.afterCreate === 'function') {
|
|
3415
3385
|
await modelHooks.afterCreate(createdInstance, createOptions);
|
|
3416
3386
|
}
|
|
3387
|
+
if (typeof modelHooks.afterSave === 'function') {
|
|
3388
|
+
await modelHooks.afterSave(createdInstance, createOptions);
|
|
3389
|
+
}
|
|
3417
3390
|
return createdInstance;
|
|
3418
3391
|
},
|
|
3419
3392
|
/**
|
|
@@ -3426,11 +3399,27 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3426
3399
|
throw new Error('Database not connected');
|
|
3427
3400
|
}
|
|
3428
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
|
+
}
|
|
3429
3411
|
if (typeof bulkModelHooks.beforeBulkUpdate === 'function') {
|
|
3430
3412
|
await bulkModelHooks.beforeBulkUpdate({ ...updateOptions, attributes: values });
|
|
3431
3413
|
}
|
|
3432
3414
|
const dialect = self.dialect;
|
|
3433
|
-
|
|
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 });
|
|
3434
3423
|
const sql = dialect.replaceReplacements(updateSql, updateVals);
|
|
3435
3424
|
// Handle logging
|
|
3436
3425
|
const startTime = Date.now();
|
|
@@ -3460,6 +3449,9 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3460
3449
|
model: modelName,
|
|
3461
3450
|
type: 'UPDATE',
|
|
3462
3451
|
});
|
|
3452
|
+
if (!hooksAlreadyRun && typeof bulkModelHooks.afterUpdate === 'function') {
|
|
3453
|
+
await bulkModelHooks.afterUpdate(values, updateOptions);
|
|
3454
|
+
}
|
|
3463
3455
|
if (typeof bulkModelHooks.afterBulkUpdate === 'function') {
|
|
3464
3456
|
await bulkModelHooks.afterBulkUpdate({ ...updateOptions, attributes: values });
|
|
3465
3457
|
}
|
|
@@ -3476,6 +3468,11 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3476
3468
|
if (!destroyOptions.where) {
|
|
3477
3469
|
throw new Error('Missing where option in destroy');
|
|
3478
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
|
+
}
|
|
3479
3476
|
const dialect = self.dialect;
|
|
3480
3477
|
const modelOptions = this.options;
|
|
3481
3478
|
const bulkDestroyHooks = modelOptions?.hooks || {};
|
|
@@ -3549,6 +3546,9 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3549
3546
|
model: modelName,
|
|
3550
3547
|
type: isParanoid && !forceDelete ? 'UPDATE' : 'DELETE',
|
|
3551
3548
|
});
|
|
3549
|
+
if (typeof destroyHooks.afterDestroy === 'function') {
|
|
3550
|
+
await destroyHooks.afterDestroy(destroyOptions);
|
|
3551
|
+
}
|
|
3552
3552
|
if (typeof bulkDestroyHooks.afterBulkDestroy === 'function') {
|
|
3553
3553
|
await bulkDestroyHooks.afterBulkDestroy(destroyOptions);
|
|
3554
3554
|
}
|
|
@@ -3609,6 +3609,11 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3609
3609
|
const dialect = self.dialect;
|
|
3610
3610
|
for (const record of records) {
|
|
3611
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 = {};
|
|
3612
3617
|
for (const [key, value] of Object.entries(record)) {
|
|
3613
3618
|
const attr = attributes[key];
|
|
3614
3619
|
if (attr && attr.type) {
|
|
@@ -3616,14 +3621,23 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3616
3621
|
if (typeObj.key === 'VIRTUAL' || typeObj.key === 'VIRTUALSTRING')
|
|
3617
3622
|
continue;
|
|
3618
3623
|
}
|
|
3619
|
-
dbValues[key] = value;
|
|
3624
|
+
dbValues[key] = _serializeValue(key, value);
|
|
3625
|
+
jsValues[key] = value;
|
|
3620
3626
|
}
|
|
3621
3627
|
const { sql, values: insertValues } = dialect.buildInsertQuery(this.tableName, dbValues, {});
|
|
3622
3628
|
const finalSql = dialect.replaceReplacements(sql, insertValues);
|
|
3629
|
+
const bcStarted = Date.now();
|
|
3623
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);
|
|
3624
3638
|
const lastId = insertResult.lastInsertRowid;
|
|
3625
3639
|
results.push({
|
|
3626
|
-
...
|
|
3640
|
+
...jsValues,
|
|
3627
3641
|
...(lastId !== undefined && !record.id ? { id: lastId } : {}),
|
|
3628
3642
|
isNewRecord: false,
|
|
3629
3643
|
});
|
|
@@ -3647,6 +3661,12 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3647
3661
|
* and `distinct`, `col` and `group` were silently discarded.
|
|
3648
3662
|
*/
|
|
3649
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
|
+
}
|
|
3650
3670
|
// Apply defaultScope / named scopes exactly as findAll does, so
|
|
3651
3671
|
// `User.scope('admin').count()` counts the scoped set.
|
|
3652
3672
|
return self._countRows(model, modelName, this._mergeScopes(countOptions));
|
|
@@ -3920,7 +3940,148 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3920
3940
|
async avg(attribute, aggOptions = {}) {
|
|
3921
3941
|
return self._aggregate(model, modelName, 'AVG', attribute, this._mergeScopes(aggOptions));
|
|
3922
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
|
+
},
|
|
3923
4080
|
async upsert(values, upsertOptions) {
|
|
4081
|
+
const upsertHooks = this.options?.hooks || {};
|
|
4082
|
+
if (typeof upsertHooks.beforeUpsert === 'function') {
|
|
4083
|
+
await upsertHooks.beforeUpsert(values, upsertOptions);
|
|
4084
|
+
}
|
|
3924
4085
|
if (!self.dialect) {
|
|
3925
4086
|
throw new Error('Database not connected');
|
|
3926
4087
|
}
|
|
@@ -3943,7 +4104,9 @@ class Prorm extends events_1.EventEmitter {
|
|
|
3943
4104
|
if (typeObj.key === 'VIRTUAL' || typeObj.key === 'VIRTUALSTRING')
|
|
3944
4105
|
continue;
|
|
3945
4106
|
}
|
|
3946
|
-
|
|
4107
|
+
// Serialized on the way to the dialect (JSON/JSONB) - see
|
|
4108
|
+
// `_serializeValue`.
|
|
4109
|
+
dbValues[key] = _serializeValue(key, value);
|
|
3947
4110
|
}
|
|
3948
4111
|
const columns = Object.keys(dbValues);
|
|
3949
4112
|
// Default the conflict target: prefer an explicit primary key value, but
|
|
@@ -4029,6 +4192,9 @@ class Prorm extends events_1.EventEmitter {
|
|
|
4029
4192
|
if (!instance) {
|
|
4030
4193
|
instance = _wrapInstance({ ...dbValues }, model, options.hooks || {});
|
|
4031
4194
|
}
|
|
4195
|
+
if (typeof upsertHooks.afterUpsert === 'function') {
|
|
4196
|
+
await upsertHooks.afterUpsert([instance, created], upsertOptions);
|
|
4197
|
+
}
|
|
4032
4198
|
return [instance, created];
|
|
4033
4199
|
},
|
|
4034
4200
|
// ============ Model-level Hooks ============
|
|
@@ -4044,7 +4210,7 @@ class Prorm extends events_1.EventEmitter {
|
|
|
4044
4210
|
beforeSync(callback) {
|
|
4045
4211
|
let modelHook = self.modelHooks.get(modelName);
|
|
4046
4212
|
if (!modelHook) {
|
|
4047
|
-
modelHook = new ModelHooks();
|
|
4213
|
+
modelHook = new hooks_manager_1.ModelHooks();
|
|
4048
4214
|
self.modelHooks.set(modelName, modelHook);
|
|
4049
4215
|
}
|
|
4050
4216
|
modelHook.beforeSync(callback);
|
|
@@ -4061,7 +4227,7 @@ class Prorm extends events_1.EventEmitter {
|
|
|
4061
4227
|
afterSync(callback) {
|
|
4062
4228
|
let modelHook = self.modelHooks.get(modelName);
|
|
4063
4229
|
if (!modelHook) {
|
|
4064
|
-
modelHook = new ModelHooks();
|
|
4230
|
+
modelHook = new hooks_manager_1.ModelHooks();
|
|
4065
4231
|
self.modelHooks.set(modelName, modelHook);
|
|
4066
4232
|
}
|
|
4067
4233
|
modelHook.afterSync(callback);
|
|
@@ -4176,9 +4342,16 @@ class Prorm extends events_1.EventEmitter {
|
|
|
4176
4342
|
*/
|
|
4177
4343
|
addModel(modelClass) {
|
|
4178
4344
|
const modelName = modelClass.name;
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
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);
|
|
4182
4355
|
// A plain class with no @Table/@Attribute decorators is still valid — register it
|
|
4183
4356
|
// with no attributes rather than failing, matching a bare `class Foo extends Model {}`.
|
|
4184
4357
|
// Convert attribute metadata to Prorm format
|
|
@@ -4517,9 +4690,29 @@ class Prorm extends events_1.EventEmitter {
|
|
|
4517
4690
|
: undefined,
|
|
4518
4691
|
check: c.check,
|
|
4519
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);
|
|
4520
4704
|
const allTableConstraints = [
|
|
4521
4705
|
...(tableConstraints || []),
|
|
4522
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 })),
|
|
4523
4716
|
];
|
|
4524
4717
|
await this.dialect.createTable(tableName, columns, {
|
|
4525
4718
|
ifNotExists: true,
|
|
@@ -4529,8 +4722,7 @@ class Prorm extends events_1.EventEmitter {
|
|
|
4529
4722
|
comment: options.comment,
|
|
4530
4723
|
rowFormat: options.rowFormat,
|
|
4531
4724
|
initialAutoIncrement: options.initialAutoIncrement,
|
|
4532
|
-
|
|
4533
|
-
indexes: tableIndexes,
|
|
4725
|
+
indexes: allTableIndexes.length ? allTableIndexes : undefined,
|
|
4534
4726
|
constraints: allTableConstraints,
|
|
4535
4727
|
});
|
|
4536
4728
|
}
|
|
@@ -4615,30 +4807,30 @@ class Prorm extends events_1.EventEmitter {
|
|
|
4615
4807
|
}
|
|
4616
4808
|
const trimmedSql = sql.trim().toUpperCase();
|
|
4617
4809
|
if (trimmedSql.startsWith('SELECT')) {
|
|
4618
|
-
return QueryTypes.SELECT;
|
|
4810
|
+
return query_types_1.QueryTypes.SELECT;
|
|
4619
4811
|
}
|
|
4620
4812
|
if (trimmedSql.startsWith('INSERT')) {
|
|
4621
4813
|
// Check for UPSERT patterns: ON CONFLICT, ON DUPLICATE KEY, INSERT OR REPLACE
|
|
4622
4814
|
if (trimmedSql.includes('ON CONFLICT') ||
|
|
4623
4815
|
trimmedSql.includes('ON DUPLICATE KEY') ||
|
|
4624
4816
|
trimmedSql.includes('INSERT OR REPLACE')) {
|
|
4625
|
-
return QueryTypes.UPSERT;
|
|
4817
|
+
return query_types_1.QueryTypes.UPSERT;
|
|
4626
4818
|
}
|
|
4627
|
-
return QueryTypes.INSERT;
|
|
4819
|
+
return query_types_1.QueryTypes.INSERT;
|
|
4628
4820
|
}
|
|
4629
4821
|
if (trimmedSql.startsWith('UPDATE')) {
|
|
4630
|
-
return QueryTypes.UPDATE;
|
|
4822
|
+
return query_types_1.QueryTypes.UPDATE;
|
|
4631
4823
|
}
|
|
4632
4824
|
if (trimmedSql.startsWith('DELETE')) {
|
|
4633
|
-
return QueryTypes.DELETE;
|
|
4825
|
+
return query_types_1.QueryTypes.DELETE;
|
|
4634
4826
|
}
|
|
4635
4827
|
if (trimmedSql.startsWith('UPSERT')) {
|
|
4636
|
-
return QueryTypes.UPSERT;
|
|
4828
|
+
return query_types_1.QueryTypes.UPSERT;
|
|
4637
4829
|
}
|
|
4638
4830
|
if (trimmedSql.startsWith('PRAGMA') || trimmedSql.startsWith('EXPLAIN')) {
|
|
4639
|
-
return QueryTypes.RAW;
|
|
4831
|
+
return query_types_1.QueryTypes.RAW;
|
|
4640
4832
|
}
|
|
4641
|
-
return QueryTypes.RAW;
|
|
4833
|
+
return query_types_1.QueryTypes.RAW;
|
|
4642
4834
|
}
|
|
4643
4835
|
/**
|
|
4644
4836
|
* Execute a raw query
|
|
@@ -4749,10 +4941,10 @@ class Prorm extends events_1.EventEmitter {
|
|
|
4749
4941
|
}
|
|
4750
4942
|
// Process results based on query type
|
|
4751
4943
|
switch (queryType) {
|
|
4752
|
-
case QueryTypes.SELECT:
|
|
4753
|
-
case QueryTypes.SHOWTABLES:
|
|
4754
|
-
case QueryTypes.DESCRIBE:
|
|
4755
|
-
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: {
|
|
4756
4948
|
// Map raw results to model instances if requested
|
|
4757
4949
|
if (options?.mapToModel && options?.model) {
|
|
4758
4950
|
const ModelStatic = options.model;
|
|
@@ -4762,22 +4954,22 @@ class Prorm extends events_1.EventEmitter {
|
|
|
4762
4954
|
// Return array of results
|
|
4763
4955
|
return result.rows;
|
|
4764
4956
|
}
|
|
4765
|
-
case QueryTypes.INSERT:
|
|
4957
|
+
case query_types_1.QueryTypes.INSERT:
|
|
4766
4958
|
// For INSERT, return [results, created]
|
|
4767
4959
|
// Check if a row was inserted by examining the last insert rowid
|
|
4768
4960
|
const insertId = result.rowid || result.insertId || 0;
|
|
4769
4961
|
const created = insertId > 0;
|
|
4770
4962
|
return [result.rows, created];
|
|
4771
|
-
case QueryTypes.UPDATE:
|
|
4772
|
-
case QueryTypes.DELETE:
|
|
4773
|
-
case QueryTypes.BULKDELETE:
|
|
4774
|
-
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:
|
|
4775
4967
|
// Return affected count
|
|
4776
4968
|
return result.rowCount;
|
|
4777
|
-
case QueryTypes.BULKINSERT:
|
|
4969
|
+
case query_types_1.QueryTypes.BULKINSERT:
|
|
4778
4970
|
// Return number of inserted rows
|
|
4779
4971
|
return result.rowCount;
|
|
4780
|
-
case QueryTypes.RAW:
|
|
4972
|
+
case query_types_1.QueryTypes.RAW:
|
|
4781
4973
|
default:
|
|
4782
4974
|
// For RAW queries, return rows
|
|
4783
4975
|
return result.rows;
|
|
@@ -4837,10 +5029,10 @@ class Prorm extends events_1.EventEmitter {
|
|
|
4837
5029
|
// Determine query type
|
|
4838
5030
|
const queryType = this.determineQueryType(processedSql, options?.type);
|
|
4839
5031
|
const isSelect = [
|
|
4840
|
-
QueryTypes.SELECT,
|
|
4841
|
-
QueryTypes.SHOWTABLES,
|
|
4842
|
-
QueryTypes.DESCRIBE,
|
|
4843
|
-
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,
|
|
4844
5036
|
].includes(queryType);
|
|
4845
5037
|
// Execute the query
|
|
4846
5038
|
const result = await this.dialect.query(processedSql, { raw: options?.raw });
|
|
@@ -6315,6 +6507,103 @@ $$ LANGUAGE plpgsql;`;
|
|
|
6315
6507
|
}
|
|
6316
6508
|
return Array.from(names);
|
|
6317
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
|
+
}
|
|
6318
6607
|
/**
|
|
6319
6608
|
* Issue a real `SELECT COUNT(...)` for a model.
|
|
6320
6609
|
*
|
|
@@ -6323,6 +6612,9 @@ $$ LANGUAGE plpgsql;`;
|
|
|
6323
6612
|
* - `col` picks the counted expression (`COUNT("col")`)
|
|
6324
6613
|
* - `distinct: true` counts distinct values of `col`, or of the primary key
|
|
6325
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.
|
|
6326
6618
|
*/
|
|
6327
6619
|
async _countRows(model, modelName, options = {}) {
|
|
6328
6620
|
if (!this.dialect) {
|
|
@@ -6338,22 +6630,10 @@ $$ LANGUAGE plpgsql;`;
|
|
|
6338
6630
|
where = { ...where, [deletedAt]: { $isNull: true } };
|
|
6339
6631
|
}
|
|
6340
6632
|
// Filtering includes restrict the counted set exactly as they restrict findAll.
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
continue;
|
|
6346
|
-
if (filter.values.length === 0)
|
|
6347
|
-
return 0;
|
|
6348
|
-
const existing = where?.[filter.parentAttr];
|
|
6349
|
-
where = {
|
|
6350
|
-
...where,
|
|
6351
|
-
[filter.parentAttr]: existing
|
|
6352
|
-
? { $and: [existing, { $in: filter.values }] }
|
|
6353
|
-
: { $in: filter.values },
|
|
6354
|
-
};
|
|
6355
|
-
}
|
|
6356
|
-
}
|
|
6633
|
+
const countableWhere = await this._applyRequiredIncludeFilters(model, modelName, options.include, where);
|
|
6634
|
+
if (countableWhere === null)
|
|
6635
|
+
return 0;
|
|
6636
|
+
where = countableWhere;
|
|
6357
6637
|
const pkAttr = resolveModelPrimaryKeyAttr(model);
|
|
6358
6638
|
const countCol = opts.col;
|
|
6359
6639
|
const distinct = opts.distinct === true;
|
|
@@ -6374,18 +6654,89 @@ $$ LANGUAGE plpgsql;`;
|
|
|
6374
6654
|
const groupFields = opts.group
|
|
6375
6655
|
? (Array.isArray(opts.group) ? opts.group : [opts.group]).map((g) => typeof g === 'string' ? g : String(g))
|
|
6376
6656
|
: undefined;
|
|
6377
|
-
|
|
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;
|
|
6378
6711
|
let values = [];
|
|
6379
|
-
if (
|
|
6380
|
-
|
|
6381
|
-
sql
|
|
6382
|
-
|
|
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
|
+
}
|
|
6383
6726
|
}
|
|
6384
6727
|
if (groupFields?.length) {
|
|
6385
6728
|
sql += ` GROUP BY ${groupFields.map((g) => dialect.escapeId(g)).join(', ')}`;
|
|
6386
6729
|
}
|
|
6387
6730
|
const finalSql = dialect.replaceReplacements(sql, values);
|
|
6731
|
+
const started = Date.now();
|
|
6388
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);
|
|
6389
6740
|
const rows = result.rows || [];
|
|
6390
6741
|
// With GROUP BY the aggregate returns one row per group; the count is the
|
|
6391
6742
|
// number of groups, matching how callers use `count({ group })`.
|
|
@@ -6411,10 +6762,10 @@ $$ LANGUAGE plpgsql;`;
|
|
|
6411
6762
|
throw new Error('Database not connected');
|
|
6412
6763
|
}
|
|
6413
6764
|
const dialect = this.dialect;
|
|
6414
|
-
|
|
6415
|
-
|
|
6765
|
+
const columns = modelColumnNames(model);
|
|
6766
|
+
if (!columns.has(attribute)) {
|
|
6416
6767
|
throw new Error(`${modelName} has no attribute '${attribute}' to ${aggregate.toLowerCase()}.` +
|
|
6417
|
-
|
|
6768
|
+
` Known attributes: ${Array.from(columns).join(', ')}.`);
|
|
6418
6769
|
}
|
|
6419
6770
|
const modelOptions = model.options;
|
|
6420
6771
|
const deletedAt = modelOptions?.deletedAt || 'deletedAt';
|
|
@@ -6425,22 +6776,10 @@ $$ LANGUAGE plpgsql;`;
|
|
|
6425
6776
|
where = { ...where, [deletedAt]: { $isNull: true } };
|
|
6426
6777
|
}
|
|
6427
6778
|
// Filtering includes restrict the aggregated set, as they do for findAll.
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
continue;
|
|
6433
|
-
if (filter.values.length === 0)
|
|
6434
|
-
return aggregate === 'SUM' ? 0 : null;
|
|
6435
|
-
const existing = where?.[filter.parentAttr];
|
|
6436
|
-
where = {
|
|
6437
|
-
...where,
|
|
6438
|
-
[filter.parentAttr]: existing
|
|
6439
|
-
? { $and: [existing, { $in: filter.values }] }
|
|
6440
|
-
: { $in: filter.values },
|
|
6441
|
-
};
|
|
6442
|
-
}
|
|
6443
|
-
}
|
|
6779
|
+
const aggregateWhere = await this._applyRequiredIncludeFilters(model, modelName, options.include, where);
|
|
6780
|
+
if (aggregateWhere === null)
|
|
6781
|
+
return aggregate === 'SUM' ? 0 : null;
|
|
6782
|
+
where = aggregateWhere;
|
|
6444
6783
|
const qualified = model.schema
|
|
6445
6784
|
? `${dialect.escapeId(model.schema)}.${dialect.escapeId(model.tableName)}`
|
|
6446
6785
|
: dialect.escapeId(model.tableName);
|
|
@@ -6451,12 +6790,191 @@ $$ LANGUAGE plpgsql;`;
|
|
|
6451
6790
|
sql += ` WHERE ${built.sql}`;
|
|
6452
6791
|
values = built.values;
|
|
6453
6792
|
}
|
|
6454
|
-
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);
|
|
6455
6803
|
const val = result.rows?.[0]?.__agg;
|
|
6456
6804
|
if (val === null || val === undefined)
|
|
6457
6805
|
return aggregate === 'SUM' ? 0 : null;
|
|
6458
6806
|
return Number(val);
|
|
6459
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
|
+
}
|
|
6460
6978
|
/**
|
|
6461
6979
|
* The view of this instance that the shared eager-loader needs.
|
|
6462
6980
|
*/
|
|
@@ -6624,6 +7142,7 @@ $$ LANGUAGE plpgsql;`;
|
|
|
6624
7142
|
}
|
|
6625
7143
|
// Table exists - let's alter it to match the model
|
|
6626
7144
|
await this.alterTable(tableName, modelAttributes, modelOptions, { drop });
|
|
7145
|
+
await this.reconcileUniqueKeys(tableName, modelAttributes, modelOptions);
|
|
6627
7146
|
}
|
|
6628
7147
|
catch (error) {
|
|
6629
7148
|
this.logger.error(`Failed to alter table ${tableName}`, { error });
|
|
@@ -6631,8 +7150,74 @@ $$ LANGUAGE plpgsql;`;
|
|
|
6631
7150
|
}
|
|
6632
7151
|
}
|
|
6633
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
|
+
}
|
|
6634
7213
|
/**
|
|
6635
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.
|
|
6636
7221
|
*/
|
|
6637
7222
|
async alterTable(tableName, modelAttributes, modelOptions, options = {}) {
|
|
6638
7223
|
if (!this.dialect) {
|
|
@@ -6910,7 +7495,7 @@ $$ LANGUAGE plpgsql;`;
|
|
|
6910
7495
|
* })
|
|
6911
7496
|
*/
|
|
6912
7497
|
col(tableOrColumn, column) {
|
|
6913
|
-
return new Col(tableOrColumn, column);
|
|
7498
|
+
return new expressions_1.Col(tableOrColumn, column);
|
|
6914
7499
|
}
|
|
6915
7500
|
/**
|
|
6916
7501
|
* Create a literal SQL expression
|
|
@@ -6973,7 +7558,7 @@ $$ LANGUAGE plpgsql;`;
|
|
|
6973
7558
|
* });
|
|
6974
7559
|
*/
|
|
6975
7560
|
get UUIDV4() {
|
|
6976
|
-
return new Literal(data_types_1.UUIDV4.generate());
|
|
7561
|
+
return new expressions_1.Literal(data_types_1.UUIDV4.generate());
|
|
6977
7562
|
}
|
|
6978
7563
|
/**
|
|
6979
7564
|
* Create a where condition for complex queries
|
|
@@ -7259,10 +7844,10 @@ exports.DataTypes = {
|
|
|
7259
7844
|
// Static Prorm operators - these can be used without creating a Prorm instance (e.g., Prorm.literal('NOW()'))
|
|
7260
7845
|
// Using type assertion to avoid TypeScript errors since these are added after class definition
|
|
7261
7846
|
Prorm.literal = function (sql) {
|
|
7262
|
-
return new Literal(sql);
|
|
7847
|
+
return new expressions_1.Literal(sql);
|
|
7263
7848
|
};
|
|
7264
7849
|
Prorm.col = function (tableOrColumn, column) {
|
|
7265
|
-
return new Col(tableOrColumn, column);
|
|
7850
|
+
return new expressions_1.Col(tableOrColumn, column);
|
|
7266
7851
|
};
|
|
7267
7852
|
Prorm.fn = function (fnName, ...args) {
|
|
7268
7853
|
return {
|
|
@@ -7279,7 +7864,7 @@ Prorm.cast = function (value, type) {
|
|
|
7279
7864
|
};
|
|
7280
7865
|
Object.defineProperty(Prorm, 'UUIDV4', {
|
|
7281
7866
|
get() {
|
|
7282
|
-
return new Literal('lower(hex(randomblob(16)))');
|
|
7867
|
+
return new expressions_1.Literal('lower(hex(randomblob(16)))');
|
|
7283
7868
|
},
|
|
7284
7869
|
});
|
|
7285
7870
|
Prorm.where = function (col, value) {
|