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
|
@@ -12,12 +12,24 @@
|
|
|
12
12
|
* bugs you hit depended on which API you had used to define the model.
|
|
13
13
|
*
|
|
14
14
|
* Both paths now call `loadIncludes()` below.
|
|
15
|
+
*
|
|
16
|
+
* Two things about this loader are worth knowing before reading it:
|
|
17
|
+
*
|
|
18
|
+
* 1. An include is resolved with ONE query per association level, never a JOIN:
|
|
19
|
+
* all parents are satisfied by a single `WHERE fk IN (...)`. That is
|
|
20
|
+
* deliberate - it keeps the statement count constant in the number of rows -
|
|
21
|
+
* but it is also why a per-include `limit` cannot simply be forwarded to that
|
|
22
|
+
* query; see the per-parent window notes on `loadIncludes()`.
|
|
23
|
+
* 2. `{ all: true }` entries are expanded to one concrete include per
|
|
24
|
+
* association before any of that happens; see `expandAllIncludes()`.
|
|
15
25
|
*/
|
|
16
26
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
27
|
exports.resolvePrimaryKeyAttr = resolvePrimaryKeyAttr;
|
|
18
28
|
exports.resolveIncludeAssociation = resolveIncludeAssociation;
|
|
29
|
+
exports.expandAllIncludes = expandAllIncludes;
|
|
19
30
|
exports.includeFiltersParents = includeFiltersParents;
|
|
20
31
|
exports.resolveRequiredIncludeFilter = resolveRequiredIncludeFilter;
|
|
32
|
+
exports.loadCounts = loadCounts;
|
|
21
33
|
exports.loadIncludes = loadIncludes;
|
|
22
34
|
const errors_1 = require("../errors");
|
|
23
35
|
/** Resolve a model's primary key attribute name, defaulting to `id`. */
|
|
@@ -127,6 +139,140 @@ function resolveIncludeAssociation(ctx, parentModel, parentModelName, includeOpt
|
|
|
127
139
|
}
|
|
128
140
|
return matches[0];
|
|
129
141
|
}
|
|
142
|
+
/** A stable identity for a model, for cycle detection during `all` expansion. */
|
|
143
|
+
function modelKey(m) {
|
|
144
|
+
return m?.name || m?.tableName || String(m);
|
|
145
|
+
}
|
|
146
|
+
/** Every association declared *from* `parentModel`, in declaration order. */
|
|
147
|
+
function associationsFrom(ctx, parentModel, parentModelName) {
|
|
148
|
+
const out = [];
|
|
149
|
+
for (const assoc of ctx.associations) {
|
|
150
|
+
if (assoc.source === parentModel || assoc.source?.name === parentModelName) {
|
|
151
|
+
out.push(assoc);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Depth cap for automatic `{ all: true, nested: true }` expansion.
|
|
158
|
+
*
|
|
159
|
+
* Same value the class-based path uses (`expandNestedIncludes()` in
|
|
160
|
+
* models/model.ts), so `{ all: true, nested: true }` expands to the same shape
|
|
161
|
+
* whichever API defined the model.
|
|
162
|
+
*/
|
|
163
|
+
const MAX_ALL_EXPANSION_DEPTH = 10;
|
|
164
|
+
/**
|
|
165
|
+
* Expand `{ all: true }` / `{ all: 'alias' }` include entries into one concrete
|
|
166
|
+
* include per association.
|
|
167
|
+
*
|
|
168
|
+
* `loadIncludes()` used to open each iteration with `if (!includeOpt.model)
|
|
169
|
+
* continue`, so an entry carrying no `model` - which is exactly what
|
|
170
|
+
* `{ all: true }` is - was skipped without a word: `include: [{ all: true }]`
|
|
171
|
+
* emitted the parent SELECT and attached nothing. The class-based path expanded
|
|
172
|
+
* it (models/model.ts) and the `prorm.define()` path did not, so which of the
|
|
173
|
+
* two APIs you had used decided whether the option existed.
|
|
174
|
+
*
|
|
175
|
+
* Bounding the recursion. `nested: true` walks the association graph, and that
|
|
176
|
+
* graph routinely contains cycles (`User.hasMany(Post)` + `Post.belongsTo(User)`)
|
|
177
|
+
* and self-references (`Category.hasMany(Category)`). Depth alone does not bound
|
|
178
|
+
* those usefully here: unlike the class-based expander - which builds one SQL
|
|
179
|
+
* statement - every expanded include is another round trip, so a cycle across a
|
|
180
|
+
* model with three associations would reach 3^10 queries before the depth cap
|
|
181
|
+
* bit. So expansion is bounded twice: by `MAX_ALL_EXPANSION_DEPTH`, and by the
|
|
182
|
+
* chain of models already expanded on the current branch - a model is never
|
|
183
|
+
* auto-expanded twice on one path, which terminates cycles and self-references
|
|
184
|
+
* at their first repeat.
|
|
185
|
+
*
|
|
186
|
+
* The chain stops *re-expansion*, not the association itself: `User` with
|
|
187
|
+
* `{ all: true, nested: true }` still loads `posts` and each post's `user` (that
|
|
188
|
+
* is one of Post's associations, and `all` means all), but stops there rather
|
|
189
|
+
* than walking back into User's associations again. So a two-model cycle costs
|
|
190
|
+
* three queries, not ten levels of ping-pong.
|
|
191
|
+
*
|
|
192
|
+
* Two deliberate differences from the class-based expander:
|
|
193
|
+
* - the depth cap only ever stops *automatic* expansion; a hand-written include
|
|
194
|
+
* is passed through at any depth rather than silently dropped.
|
|
195
|
+
* - an unknown `{ all: 'alias' }` is an error rather than a console warning.
|
|
196
|
+
*
|
|
197
|
+
* `required` carried onto an expanded include filters parents like any other
|
|
198
|
+
* required include. That needs expansion to happen before the parent-filtering
|
|
199
|
+
* pass rather than inside `loadIncludes()` at the end of the query - the
|
|
200
|
+
* filtering pass calls `resolveRequiredIncludeFilter`, which returns null for an
|
|
201
|
+
* entry with no `model`, so while `{ all: true }` was still un-expanded at that
|
|
202
|
+
* point `{ all: true, required: true }` loaded the associations and narrowed
|
|
203
|
+
* nothing. Callers therefore expand once, up front; expansion is idempotent, so
|
|
204
|
+
* `loadIncludes()` re-running it on an already-expanded list is a no-op.
|
|
205
|
+
*/
|
|
206
|
+
function expandAllIncludes(ctx, parentModel, parentModelName, includeList, depth = 0, chain = []) {
|
|
207
|
+
if (!includeList?.length)
|
|
208
|
+
return [];
|
|
209
|
+
const expanded = [];
|
|
210
|
+
for (const rawIncludeOpt of includeList) {
|
|
211
|
+
const includeOpt = normalizeInclude(rawIncludeOpt);
|
|
212
|
+
if (!includeOpt)
|
|
213
|
+
continue;
|
|
214
|
+
const all = includeOpt.all;
|
|
215
|
+
if (all === undefined || all === null || all === false) {
|
|
216
|
+
// An ordinary include. Its own nested list is expanded relative to the
|
|
217
|
+
// included model, so `{ model: Post, include: [{ all: true }] }` works.
|
|
218
|
+
const nestedList = includeOpt.include;
|
|
219
|
+
if (nestedList?.length && includeOpt.model) {
|
|
220
|
+
expanded.push({
|
|
221
|
+
...includeOpt,
|
|
222
|
+
include: expandAllIncludes(ctx, includeOpt.model, includeOpt.model.name, nestedList, depth, chain),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
expanded.push(includeOpt);
|
|
227
|
+
}
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const candidates = associationsFrom(ctx, parentModel, parentModelName);
|
|
231
|
+
let selected;
|
|
232
|
+
if (all === true) {
|
|
233
|
+
selected = candidates;
|
|
234
|
+
}
|
|
235
|
+
else if (typeof all === 'string') {
|
|
236
|
+
selected = candidates.filter((a) => (a.as ?? a.options?.as) === all);
|
|
237
|
+
if (selected.length === 0) {
|
|
238
|
+
// The class-based expander only warns here. This loader reports bad
|
|
239
|
+
// includes rather than resolving them to nothing - the same rule that
|
|
240
|
+
// makes a mistyped `as` an AssociationError instead of an empty array.
|
|
241
|
+
const available = candidates.map((a) => a.as ?? a.options?.as).filter(Boolean);
|
|
242
|
+
throw new errors_1.AssociationError(`include { all: '${all}' }: ${parentModelName} has no association aliased '${all}'` +
|
|
243
|
+
(available.length ? `. Available: ${available.join(', ')}` : ''), { association: all });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
throw new errors_1.AssociationError(`include \`all\` must be true or an association alias, got ${typeof all}`, { association: String(all) });
|
|
248
|
+
}
|
|
249
|
+
// Anything else the caller put on the `{ all: true }` entry - `where`,
|
|
250
|
+
// `attributes`, `order`, `limit`, `required`, ... - applies to every
|
|
251
|
+
// association it expands to. `include` is the one exception: a single
|
|
252
|
+
// hand-written nested list cannot be meaningful for every association at
|
|
253
|
+
// once, and `nested: true` is the option that expands nested levels.
|
|
254
|
+
const { all: _all, nested: _nested, include: _include, model: _model, as: _as, ...carried } = includeOpt;
|
|
255
|
+
const nested = includeOpt.nested === true;
|
|
256
|
+
const parentKey = modelKey(parentModel);
|
|
257
|
+
const branch = chain.includes(parentKey) ? chain : [...chain, parentKey];
|
|
258
|
+
for (const assoc of selected) {
|
|
259
|
+
const entry = {
|
|
260
|
+
...carried,
|
|
261
|
+
model: assoc.target,
|
|
262
|
+
as: assoc.as ?? assoc.options?.as,
|
|
263
|
+
};
|
|
264
|
+
if (nested &&
|
|
265
|
+
depth + 1 < MAX_ALL_EXPANSION_DEPTH &&
|
|
266
|
+
!branch.includes(modelKey(assoc.target))) {
|
|
267
|
+
const nestedEntries = expandAllIncludes(ctx, assoc.target, assoc.target?.name, [{ all: true, nested: true }], depth + 1, branch);
|
|
268
|
+
if (nestedEntries.length)
|
|
269
|
+
entry.include = nestedEntries;
|
|
270
|
+
}
|
|
271
|
+
expanded.push(entry);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return expanded;
|
|
275
|
+
}
|
|
130
276
|
/** Resolve a `through` given as a string against the model registry. */
|
|
131
277
|
function resolveThroughModel(ctx, assoc) {
|
|
132
278
|
if (assoc.throughModel && typeof assoc.throughModel.findAll === 'function') {
|
|
@@ -226,6 +372,110 @@ async function resolveRequiredIncludeFilter(ctx, parentModel, parentModelName, r
|
|
|
226
372
|
.filter((v) => v !== undefined && v !== null)));
|
|
227
373
|
return { parentAttr: sourcePkAttr, values: parentIds };
|
|
228
374
|
}
|
|
375
|
+
/**
|
|
376
|
+
* Attach a count of related rows to each parent, without loading them.
|
|
377
|
+
*
|
|
378
|
+
* `findAll({ withCount: ['posts'] })` sets `row.postsCount` on every parent.
|
|
379
|
+
* The counts are resolved with one grouped query per association, so this stays
|
|
380
|
+
* a constant number of statements no matter how many parents there are - the
|
|
381
|
+
* naive alternative is a count query per row.
|
|
382
|
+
*
|
|
383
|
+
* Use this when you need "how many" but not the rows themselves; a full
|
|
384
|
+
* `include` fetches every child just to measure the array.
|
|
385
|
+
*/
|
|
386
|
+
async function loadCounts(ctx, parentRows, parentModel, parentModelName, aliases) {
|
|
387
|
+
if (!parentRows.length || !aliases?.length)
|
|
388
|
+
return;
|
|
389
|
+
const sourcePkAttr = resolvePrimaryKeyAttr(parentModel);
|
|
390
|
+
const parentIds = parentRows
|
|
391
|
+
.map((r) => readField(r, sourcePkAttr))
|
|
392
|
+
.filter((v) => v !== undefined && v !== null);
|
|
393
|
+
for (const alias of aliases) {
|
|
394
|
+
const assoc = findAssociationByAlias(ctx, parentModel, parentModelName, alias);
|
|
395
|
+
const field = `${alias}Count`;
|
|
396
|
+
if (parentIds.length === 0) {
|
|
397
|
+
for (const row of parentRows)
|
|
398
|
+
attachField(row, field, 0);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
try {
|
|
402
|
+
const counts = new Map();
|
|
403
|
+
if (assoc.type === 'belongsToMany') {
|
|
404
|
+
const throughModel = resolveThroughModel(ctx, assoc);
|
|
405
|
+
if (!throughModel || typeof throughModel.findAll !== 'function') {
|
|
406
|
+
throw new Error(`Cannot resolve through model for '${alias}'`);
|
|
407
|
+
}
|
|
408
|
+
const junction = await throughModel.findAll({
|
|
409
|
+
where: { [assoc.foreignKey]: { $in: parentIds } },
|
|
410
|
+
});
|
|
411
|
+
for (const jr of junction) {
|
|
412
|
+
const k = readField(jr, assoc.foreignKey);
|
|
413
|
+
counts.set(k, (counts.get(k) ?? 0) + 1);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
else if (assoc.type === 'belongsTo') {
|
|
417
|
+
// A belongsTo points at one row, so the count is 0 or 1 per parent.
|
|
418
|
+
for (const row of parentRows) {
|
|
419
|
+
const fk = readField(row, assoc.foreignKey);
|
|
420
|
+
attachField(row, field, fk === undefined || fk === null ? 0 : 1);
|
|
421
|
+
}
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
const children = await assoc.target.findAll({
|
|
426
|
+
where: {
|
|
427
|
+
...(assoc.options?.scope || {}),
|
|
428
|
+
[assoc.foreignKey]: { $in: parentIds },
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
for (const c of children) {
|
|
432
|
+
const k = readField(c, assoc.foreignKey);
|
|
433
|
+
counts.set(k, (counts.get(k) ?? 0) + 1);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
for (const row of parentRows) {
|
|
437
|
+
attachField(row, field, counts.get(readField(row, sourcePkAttr)) ?? 0);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
if (error instanceof errors_1.EagerLoadError || error instanceof errors_1.AssociationError)
|
|
442
|
+
throw error;
|
|
443
|
+
throw new errors_1.EagerLoadError(parentModelName, alias, assoc.type, error);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/** Resolve an association by its alias alone, for `withCount`. */
|
|
448
|
+
function findAssociationByAlias(ctx, parentModel, parentModelName, alias) {
|
|
449
|
+
const candidates = associationsFrom(ctx, parentModel, parentModelName);
|
|
450
|
+
const match = candidates.find((a) => (a.as ?? a.options?.as) === alias);
|
|
451
|
+
if (!match) {
|
|
452
|
+
const available = candidates.map((a) => a.as).filter(Boolean);
|
|
453
|
+
throw new errors_1.AssociationError(`withCount: ${parentModelName} has no association aliased '${alias}'` +
|
|
454
|
+
(available.length ? `. Available: ${available.join(', ')}` : ''), { association: alias });
|
|
455
|
+
}
|
|
456
|
+
return match;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* A per-parent `limit`/`offset` requested by one include, normalized.
|
|
460
|
+
*
|
|
461
|
+
* Anything that is not a non-negative integer is rejected rather than rounded or
|
|
462
|
+
* ignored: silently dropping a `limit` is the bug this whole path exists to fix,
|
|
463
|
+
* so a nonsensical one should not be dropped either.
|
|
464
|
+
*/
|
|
465
|
+
function readIncludeWindow(includeOpt, includeAlias) {
|
|
466
|
+
const window = {};
|
|
467
|
+
for (const key of ['limit', 'offset']) {
|
|
468
|
+
const raw = includeOpt[key];
|
|
469
|
+
if (raw === undefined || raw === null)
|
|
470
|
+
continue;
|
|
471
|
+
const value = typeof raw === 'string' ? Number(raw) : raw;
|
|
472
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
|
|
473
|
+
throw new errors_1.AssociationError(`include '${includeAlias}': \`${key}\` must be a non-negative integer, got ${JSON.stringify(raw)}`, { association: includeAlias });
|
|
474
|
+
}
|
|
475
|
+
window[key] = value;
|
|
476
|
+
}
|
|
477
|
+
return window;
|
|
478
|
+
}
|
|
229
479
|
/**
|
|
230
480
|
* Eager-load `includeList` onto `parentRows`, recursing into nested includes.
|
|
231
481
|
*
|
|
@@ -233,11 +483,48 @@ async function resolveRequiredIncludeFilter(ctx, parentModel, parentModelName, r
|
|
|
233
483
|
* caught and reported as an empty result, which made a typo'd alias, a missing
|
|
234
484
|
* foreign key column, an unresolvable through model and a genuinely childless
|
|
235
485
|
* parent all look identical - and hid most of the bugs this module now fixes.
|
|
486
|
+
*
|
|
487
|
+
* ## Per-include `limit` / `offset`
|
|
488
|
+
*
|
|
489
|
+
* `include: [{ model: Post, order: [['id', 'DESC']], limit: 5 }]` reads as "the
|
|
490
|
+
* 5 most recent posts *per user*", and that is what it now means. Getting there
|
|
491
|
+
* needs care, because one query serves every parent:
|
|
492
|
+
*
|
|
493
|
+
* SELECT ... FROM "posts" WHERE "userId" IN (1, 2, 3) ORDER BY "id" DESC
|
|
494
|
+
*
|
|
495
|
+
* Forwarding `LIMIT 5` into that statement would cap the *whole result set* at 5
|
|
496
|
+
* rows - user 1 would get five posts and users 2 and 3 none - which is worse
|
|
497
|
+
* than the old behaviour of dropping the option, because the answer looks
|
|
498
|
+
* plausible while being wrong for every parent but the first. So the limit is
|
|
499
|
+
* never forwarded into a multi-parent `IN (...)` query. Instead:
|
|
500
|
+
*
|
|
501
|
+
* - `order` is still applied *in SQL*, so the database decides which rows come
|
|
502
|
+
* first - the window is a window over the ordering the caller asked for, not
|
|
503
|
+
* over insertion order.
|
|
504
|
+
* - the window is then applied per parent, in memory, when the fetched rows are
|
|
505
|
+
* grouped onto their parents.
|
|
506
|
+
* - the one case where the shared query and a per-parent window coincide is a
|
|
507
|
+
* single parent row (`findByPk(...)` with an include, the common case): there
|
|
508
|
+
* `LIMIT`/`OFFSET` *are* pushed down to SQL, for `hasMany` only - see below.
|
|
509
|
+
*
|
|
510
|
+
* What this does NOT do: reduce what crosses the wire for multi-parent queries.
|
|
511
|
+
* All matching children are fetched and most of them are then discarded.
|
|
512
|
+
* `limit` here bounds the arrays you get back, it is not a transfer
|
|
513
|
+
* optimization. Doing better needs per-parent SQL windows (`ROW_NUMBER() OVER
|
|
514
|
+
* (PARTITION BY ...)` or `LATERAL`), which is a dialect-level feature this
|
|
515
|
+
* loader - which only ever calls `Model.findAll()` - cannot express today.
|
|
516
|
+
*
|
|
517
|
+
* `limit`/`offset` are only meaningful on a to-many include. On `belongsTo` and
|
|
518
|
+
* `hasOne` the result is a single row rather than a list, so they are reported
|
|
519
|
+
* and ignored rather than quietly accepted.
|
|
236
520
|
*/
|
|
237
521
|
async function loadIncludes(ctx, parentRows, parentModel, parentModelName, includeList) {
|
|
238
522
|
if (!parentRows.length || !includeList?.length)
|
|
239
523
|
return;
|
|
240
|
-
|
|
524
|
+
// Resolve `{ all: true }` / `{ all: 'alias' }` into concrete includes first,
|
|
525
|
+
// so everything below only ever sees an entry with a `model`.
|
|
526
|
+
const resolvedIncludes = expandAllIncludes(ctx, parentModel, parentModelName, includeList);
|
|
527
|
+
for (const rawIncludeOpt of resolvedIncludes) {
|
|
241
528
|
const includeOpt = normalizeInclude(rawIncludeOpt);
|
|
242
529
|
const includeModel = includeOpt?.model;
|
|
243
530
|
if (!includeModel)
|
|
@@ -251,6 +538,32 @@ async function loadIncludes(ctx, parentRows, parentModel, parentModelName, inclu
|
|
|
251
538
|
const foreignKey = assoc.foreignKey;
|
|
252
539
|
const sourcePkAttr = resolvePrimaryKeyAttr(parentModel);
|
|
253
540
|
const targetPkAttr = resolvePrimaryKeyAttr(includeModel);
|
|
541
|
+
// Per-parent window. See the `limit`/`offset` section on this function for
|
|
542
|
+
// why this is resolved here rather than forwarded to the child query.
|
|
543
|
+
const perParent = readIncludeWindow(includeOpt, includeAlias);
|
|
544
|
+
const hasWindow = perParent.limit !== undefined || perParent.offset !== undefined;
|
|
545
|
+
const isToMany = associationType === 'hasMany' || associationType === 'belongsToMany';
|
|
546
|
+
if (hasWindow && !isToMany) {
|
|
547
|
+
// eslint-disable-next-line no-console
|
|
548
|
+
console.warn(`include '${includeAlias}' on ${parentModelName} is a ${associationType}, which ` +
|
|
549
|
+
`resolves to a single row: \`limit\`/\`offset\` have no meaning here and are ignored.`);
|
|
550
|
+
}
|
|
551
|
+
// The only shape where the shared query's LIMIT *is* the per-parent limit is
|
|
552
|
+
// a single parent, and only for hasMany - there one fetched row is one
|
|
553
|
+
// element of one parent's array. belongsToMany goes through a junction, so
|
|
554
|
+
// fetched target rows and array elements are not in one-to-one
|
|
555
|
+
// correspondence (a repeated junction pair yields two elements from one
|
|
556
|
+
// row); its window always stays in memory, where it counts the right things.
|
|
557
|
+
const pushWindowDown = hasWindow && associationType === 'hasMany' && parentRows.length === 1;
|
|
558
|
+
/** Apply the per-parent window to one parent's list of related rows. */
|
|
559
|
+
const applyWindow = (rows) => {
|
|
560
|
+
if (!hasWindow || pushWindowDown)
|
|
561
|
+
return rows;
|
|
562
|
+
const start = perParent.offset ?? 0;
|
|
563
|
+
return perParent.limit === undefined
|
|
564
|
+
? rows.slice(start)
|
|
565
|
+
: rows.slice(start, start + perParent.limit);
|
|
566
|
+
};
|
|
254
567
|
let fetchedRows = [];
|
|
255
568
|
try {
|
|
256
569
|
// An association `scope` is a fixed set of conditions applied every time
|
|
@@ -264,6 +577,16 @@ async function loadIncludes(ctx, parentRows, parentModel, parentModelName, inclu
|
|
|
264
577
|
const passThrough = {
|
|
265
578
|
...(includeOpt.attributes ? { attributes: includeOpt.attributes } : {}),
|
|
266
579
|
...(includeOpt.order ? { order: includeOpt.order } : {}),
|
|
580
|
+
// `limit`/`offset` reach SQL only in the single-parent hasMany case,
|
|
581
|
+
// where the shared query's window and the per-parent window are the same
|
|
582
|
+
// window. Every other shape applies it per parent below - forwarding it
|
|
583
|
+
// here would bound the combined result set instead.
|
|
584
|
+
...(pushWindowDown
|
|
585
|
+
? {
|
|
586
|
+
...(perParent.limit !== undefined ? { limit: perParent.limit } : {}),
|
|
587
|
+
...(perParent.offset !== undefined ? { offset: perParent.offset } : {}),
|
|
588
|
+
}
|
|
589
|
+
: {}),
|
|
267
590
|
};
|
|
268
591
|
if (associationType === 'belongsTo') {
|
|
269
592
|
// The foreign key is a column on the parent, referencing the target's key.
|
|
@@ -325,11 +648,28 @@ async function loadIncludes(ctx, parentRows, parentModel, parentModelName, inclu
|
|
|
325
648
|
});
|
|
326
649
|
fetchedRows = targetRows;
|
|
327
650
|
const targetMap = new Map();
|
|
328
|
-
|
|
329
|
-
|
|
651
|
+
// Position of each target row in the order the database returned them.
|
|
652
|
+
// A parent's array is otherwise built in *junction* row order, which
|
|
653
|
+
// means an `order` on the include never reached the array the caller
|
|
654
|
+
// sees - and a per-parent `limit` would then slice an arbitrary window
|
|
655
|
+
// rather than the one that was ordered. Only reordered when an `order`
|
|
656
|
+
// was actually asked for, so the junction ordering stays the default.
|
|
657
|
+
const targetPos = new Map();
|
|
658
|
+
targetRows.forEach((tr, i) => {
|
|
659
|
+
const pk = readField(tr, targetPkAttr);
|
|
660
|
+
targetMap.set(pk, tr);
|
|
661
|
+
targetPos.set(pk, i);
|
|
662
|
+
});
|
|
330
663
|
for (const row of parentRows) {
|
|
331
664
|
const targetIds = targetIdsBySource.get(readField(row, sourcePkAttr)) || [];
|
|
332
|
-
|
|
665
|
+
const related = targetIds
|
|
666
|
+
.map((tid) => targetMap.get(tid))
|
|
667
|
+
.filter((v) => v !== undefined);
|
|
668
|
+
if (includeOpt.order) {
|
|
669
|
+
related.sort((a, b) => (targetPos.get(readField(a, targetPkAttr)) ?? 0) -
|
|
670
|
+
(targetPos.get(readField(b, targetPkAttr)) ?? 0));
|
|
671
|
+
}
|
|
672
|
+
attachField(row, includeAlias, applyWindow(related));
|
|
333
673
|
}
|
|
334
674
|
}
|
|
335
675
|
else {
|
|
@@ -351,7 +691,10 @@ async function loadIncludes(ctx, parentRows, parentModel, parentModelName, inclu
|
|
|
351
691
|
}
|
|
352
692
|
for (const row of parentRows) {
|
|
353
693
|
const associated = groupedRows.get(readField(row, sourcePkAttr)) || [];
|
|
354
|
-
attachField(row, includeAlias,
|
|
694
|
+
attachField(row, includeAlias,
|
|
695
|
+
// hasOne is a single row, so the window (already reported as ignored
|
|
696
|
+
// above) does not apply to it.
|
|
697
|
+
associationType === 'hasOne' ? associated[0] ?? null : applyWindow(associated));
|
|
355
698
|
}
|
|
356
699
|
}
|
|
357
700
|
// Recurse, treating the rows just fetched as the parents for the next level.
|
package/dist/models/indexes.d.ts
CHANGED
|
@@ -23,7 +23,8 @@ export interface IndexOptions {
|
|
|
23
23
|
using?: string;
|
|
24
24
|
algorithm?: 'btree' | 'hash' | 'gist' | 'spgist' | 'gin' | 'brin' | 'hstore' | 'rum' | 'zombodb' | 'spatial';
|
|
25
25
|
fields?: (string | IndexField)[];
|
|
26
|
-
where
|
|
26
|
+
/** Partial-index predicate; see `IndexOptions.where` in `src/dialects/dialect.ts`. */
|
|
27
|
+
where?: WhereOptions | string;
|
|
27
28
|
/** Include columns for covering index (PostgreSQL 11+, MySQL 8.0.17+) */
|
|
28
29
|
include?: string[];
|
|
29
30
|
/** Expression for expression-based indexes (e.g., LOWER(column)) */
|
|
@@ -80,7 +81,7 @@ export declare class Index {
|
|
|
80
81
|
readonly using?: string;
|
|
81
82
|
readonly algorithm?: 'btree' | 'hash' | 'gist' | 'spgist' | 'gin' | 'brin' | 'hstore' | 'rum' | 'zombodb' | 'spatial';
|
|
82
83
|
readonly fields: IndexField[];
|
|
83
|
-
readonly where?: WhereOptions;
|
|
84
|
+
readonly where?: WhereOptions | string;
|
|
84
85
|
readonly include?: string[];
|
|
85
86
|
readonly expression?: string;
|
|
86
87
|
readonly tableName?: string;
|
package/dist/models/indexes.js
CHANGED
|
@@ -64,7 +64,13 @@ class Index {
|
|
|
64
64
|
* Check if this is a partial index
|
|
65
65
|
*/
|
|
66
66
|
isPartial() {
|
|
67
|
-
|
|
67
|
+
if (!this.where)
|
|
68
|
+
return false;
|
|
69
|
+
// A predicate can be a hand-written SQL string as well as an object.
|
|
70
|
+
if (typeof this.where === 'string')
|
|
71
|
+
return this.where.trim().length > 0;
|
|
72
|
+
// Symbol operator keys (`{ [Op.or]: [...] }`) do not show up in Object.keys.
|
|
73
|
+
return (Object.keys(this.where).length > 0 || Object.getOwnPropertySymbols(this.where).length > 0);
|
|
68
74
|
}
|
|
69
75
|
/**
|
|
70
76
|
* Check if this is an expression index
|
package/dist/models/model.js
CHANGED
|
@@ -48,6 +48,25 @@ const validators_1 = require("../validators");
|
|
|
48
48
|
const sql_compiler_1 = require("../query-builders/sql-compiler");
|
|
49
49
|
const history_query_1 = require("../audit/history-query");
|
|
50
50
|
// ==================== Nested Include Helpers ====================
|
|
51
|
+
/**
|
|
52
|
+
* Normalize the two accepted include shapes: `{ model: Post, as: 'posts' }` and
|
|
53
|
+
* the bare-model shorthand `Post`.
|
|
54
|
+
*
|
|
55
|
+
* Mirrors `normalizeInclude()` in `./eager-load` - a bare model is recognised by
|
|
56
|
+
* carrying `rawAttributes` while carrying no `model` of its own. Keep the two in
|
|
57
|
+
* step: the loader skips any entry without a `model`, so an entry that is not
|
|
58
|
+
* normalized here silently loads nothing (and the SQL builder, which reads
|
|
59
|
+
* `include.model.tableName` when it emits the JOIN, throws on it outright).
|
|
60
|
+
*
|
|
61
|
+
* @param include - A raw include entry, either shape
|
|
62
|
+
* @returns The entry as `IncludeOptions`, wrapping a bare model if needed
|
|
63
|
+
*/
|
|
64
|
+
function normalizeIncludeEntry(include) {
|
|
65
|
+
const raw = include;
|
|
66
|
+
return raw && !raw.model && raw.rawAttributes
|
|
67
|
+
? { model: raw }
|
|
68
|
+
: raw;
|
|
69
|
+
}
|
|
51
70
|
/**
|
|
52
71
|
* Expand nested includes from { all: true } or { nested: true } options
|
|
53
72
|
* This processes the include options to expand all associations recursively
|
|
@@ -70,7 +89,9 @@ function expandNestedIncludes(includes, model, depth = 0, maxDepth = 10) {
|
|
|
70
89
|
}
|
|
71
90
|
const expanded = [];
|
|
72
91
|
for (const include of includes) {
|
|
73
|
-
|
|
92
|
+
// Accept the bare-model shorthand (`include: [Post]`) at every level, not
|
|
93
|
+
// just `{ model: Post }`.
|
|
94
|
+
const includeOpts = normalizeIncludeEntry(include);
|
|
74
95
|
// Handle { all: true } or { all: 'associationName' } - include all or specific associations
|
|
75
96
|
if (includeOpts.all !== undefined) {
|
|
76
97
|
const modelAssociations = model.associations || {};
|
|
@@ -93,19 +114,16 @@ function expandNestedIncludes(includes, model, depth = 0, maxDepth = 10) {
|
|
|
93
114
|
console.warn(`Association '${allValue}' not found on model '${model.name}'`);
|
|
94
115
|
}
|
|
95
116
|
}
|
|
117
|
+
// Everything the caller put on the `{ all }` entry is copied onto each
|
|
118
|
+
// generated entry. `all`/`nested` are consumed by this branch and must not
|
|
119
|
+
// leak through, and `include` belongs to the entry being expanded, not to
|
|
120
|
+
// the associations it expands into.
|
|
121
|
+
const { all: _all, nested: _nested, include: _include, ...carriedOptions } = includeOpts;
|
|
96
122
|
for (const [assocName, association] of associationsToInclude) {
|
|
97
123
|
const assocInclude = {
|
|
124
|
+
...carriedOptions,
|
|
98
125
|
model: association.target,
|
|
99
126
|
as: association.as,
|
|
100
|
-
where: includeOpts.where,
|
|
101
|
-
attributes: includeOpts.attributes,
|
|
102
|
-
required: includeOpts.required,
|
|
103
|
-
through: includeOpts.through,
|
|
104
|
-
on: includeOpts.on,
|
|
105
|
-
order: includeOpts.order,
|
|
106
|
-
limit: includeOpts.limit,
|
|
107
|
-
offset: includeOpts.offset,
|
|
108
|
-
paranoid: includeOpts.paranoid,
|
|
109
127
|
duplicate,
|
|
110
128
|
};
|
|
111
129
|
// If nested is true, recursively expand all nested associations
|
|
@@ -116,22 +134,12 @@ function expandNestedIncludes(includes, model, depth = 0, maxDepth = 10) {
|
|
|
116
134
|
}
|
|
117
135
|
continue;
|
|
118
136
|
}
|
|
119
|
-
// Handle regular include (no { all } or { nested } special handling)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
required: includeOpts.required,
|
|
126
|
-
through: includeOpts.through,
|
|
127
|
-
on: includeOpts.on,
|
|
128
|
-
order: includeOpts.order,
|
|
129
|
-
limit: includeOpts.limit,
|
|
130
|
-
offset: includeOpts.offset,
|
|
131
|
-
paranoid: includeOpts.paranoid,
|
|
132
|
-
duplicate: includeOpts.duplicate,
|
|
133
|
-
targetKey: includeOpts.targetKey,
|
|
134
|
-
};
|
|
137
|
+
// Handle regular include (no { all } or { nested } special handling).
|
|
138
|
+
// Copy the whole entry: this used to rebuild it from a fixed field list,
|
|
139
|
+
// which dropped every option that list did not name (`schema`, `foreignKey`,
|
|
140
|
+
// `joinType`, `lateral`, `antiJoin`/`antiJoinType`, `apply`,
|
|
141
|
+
// `lateralSubquery`, `duplicating`, `includeIgnoreAttributes`, `nested`).
|
|
142
|
+
const expandedInclude = { ...includeOpts };
|
|
135
143
|
// Recursively expand nested includes
|
|
136
144
|
if (includeOpts.include) {
|
|
137
145
|
expandedInclude.include = expandNestedIncludes(includeOpts.include, includeOpts.model, depth + 1, maxDepth);
|
|
@@ -774,10 +782,15 @@ class ModelInstance {
|
|
|
774
782
|
if (value === null || value === undefined) {
|
|
775
783
|
casted[key] = null;
|
|
776
784
|
}
|
|
777
|
-
else if (typeof value === 'string') {
|
|
778
|
-
casted[key] = value;
|
|
779
|
-
}
|
|
780
785
|
else {
|
|
786
|
+
// Encode every value, strings included. Passing a string through
|
|
787
|
+
// unencoded is what made the round trip lossy: it reached the column
|
|
788
|
+
// raw, and castValuesFromDatabase then JSON.parse'd it
|
|
789
|
+
// unconditionally, so any string that was itself valid JSON came back
|
|
790
|
+
// as something else - '123' as a number, 'true' as a boolean,
|
|
791
|
+
// '12345678901234567890' silently rounded. It also wrote invalid JSON
|
|
792
|
+
// on PostgreSQL, which rejects a bare `hello` for a json column.
|
|
793
|
+
// (Mirrors the same fix in `_serializeValue` in src/prorm.ts.)
|
|
781
794
|
casted[key] = JSON.stringify(value);
|
|
782
795
|
}
|
|
783
796
|
continue;
|
|
@@ -1405,7 +1418,9 @@ class ModelInstance {
|
|
|
1405
1418
|
*/
|
|
1406
1419
|
async validate(options) {
|
|
1407
1420
|
const modelOptions = this.model.options;
|
|
1408
|
-
|
|
1421
|
+
// `_hooksManager` was read here but assigned nowhere, so `{ hooks: true }`
|
|
1422
|
+
// silently ran no hooks. Use the model's declared hooks instead.
|
|
1423
|
+
const validateHooks = modelOptions?.hooks || {};
|
|
1409
1424
|
// Parse options
|
|
1410
1425
|
let fieldsToValidate;
|
|
1411
1426
|
let runHooks = false;
|
|
@@ -1421,8 +1436,8 @@ class ModelInstance {
|
|
|
1421
1436
|
return { valid: true, errors: [] };
|
|
1422
1437
|
}
|
|
1423
1438
|
// Run beforeValidate hook if requested
|
|
1424
|
-
if (runHooks &&
|
|
1425
|
-
await
|
|
1439
|
+
if (runHooks && typeof validateHooks.beforeValidate === 'function') {
|
|
1440
|
+
await validateHooks.beforeValidate(this, { model: this.model });
|
|
1426
1441
|
}
|
|
1427
1442
|
const validations = {};
|
|
1428
1443
|
// Collect validations from rawAttributes
|
|
@@ -1465,8 +1480,8 @@ class ModelInstance {
|
|
|
1465
1480
|
exclude: !this.isNewRecord && pkField ? { [pkField]: this.dataValues[pkField] } : undefined,
|
|
1466
1481
|
});
|
|
1467
1482
|
// Run afterValidate hook if requested
|
|
1468
|
-
if (runHooks &&
|
|
1469
|
-
await
|
|
1483
|
+
if (runHooks && result.valid && typeof validateHooks.afterValidate === 'function') {
|
|
1484
|
+
await validateHooks.afterValidate(this, { model: this.model });
|
|
1470
1485
|
}
|
|
1471
1486
|
// Store validation errors for later retrieval
|
|
1472
1487
|
this._validationErrors = result.errors;
|
|
@@ -2805,18 +2820,26 @@ function createModel(prorm, modelName, attributes, options = {}) {
|
|
|
2805
2820
|
* });
|
|
2806
2821
|
*/
|
|
2807
2822
|
async findAll(findOptions = {}) {
|
|
2808
|
-
// Handle connection switching via 'using' option
|
|
2823
|
+
// Handle connection switching via 'using' option: the query runs against
|
|
2824
|
+
// the named sibling connection's copy of this model, not this one.
|
|
2809
2825
|
if (findOptions.using) {
|
|
2810
|
-
const
|
|
2826
|
+
const registry = prorm.connectionManager;
|
|
2827
|
+
const connection = registry?.getConnection(findOptions.using);
|
|
2811
2828
|
if (!connection) {
|
|
2812
|
-
|
|
2829
|
+
const available = registry?.getConnectionNames()?.join(', ') || 'none';
|
|
2830
|
+
throw new Error(`Connection '${findOptions.using}' not found. Available: ${available}`);
|
|
2813
2831
|
}
|
|
2814
|
-
//
|
|
2815
|
-
|
|
2832
|
+
// `connection.models` is a plain object keyed by model name (the
|
|
2833
|
+
// getter builds it from the internal map); this used to call `.get()`
|
|
2834
|
+
// on it, which threw a TypeError before the option could ever work.
|
|
2835
|
+
const modelName = this.name;
|
|
2836
|
+
const model = connection.models[modelName];
|
|
2816
2837
|
if (!model) {
|
|
2817
|
-
|
|
2838
|
+
const defined = Object.keys(connection.models).join(', ') || 'none';
|
|
2839
|
+
throw new Error(`Model '${modelName}' is not defined on connection '${findOptions.using}'. ` +
|
|
2840
|
+
`Defined there: ${defined}`);
|
|
2818
2841
|
}
|
|
2819
|
-
const { using, ...queryOptions } = findOptions;
|
|
2842
|
+
const { using: _using, ...queryOptions } = findOptions;
|
|
2820
2843
|
return model.findAll(queryOptions);
|
|
2821
2844
|
}
|
|
2822
2845
|
const dialect = prorm.getDialectInstance();
|
|
@@ -2881,10 +2904,13 @@ function createModel(prorm, modelName, attributes, options = {}) {
|
|
|
2881
2904
|
lock: findOptions.lock,
|
|
2882
2905
|
};
|
|
2883
2906
|
const { sql, values } = dialect.buildSelectQuery(selectOptions);
|
|
2884
|
-
// Execute query
|
|
2907
|
+
// Execute query. The bind values come from buildSelectQuery - passing
|
|
2908
|
+
// `selectOptions` here instead meant every query carrying a placeholder
|
|
2909
|
+
// (any `where`, including one on an include) bound nothing and failed with
|
|
2910
|
+
// "Too few parameter values were provided".
|
|
2885
2911
|
const result = await dialect.query(sql, {
|
|
2886
2912
|
raw: findOptions.raw,
|
|
2887
|
-
replacements: values?.length ?
|
|
2913
|
+
replacements: values?.length ? values : undefined,
|
|
2888
2914
|
});
|
|
2889
2915
|
// Create model instances
|
|
2890
2916
|
const instances = result.rows.map((row) => new ModelInstance(this, prorm, row, { isNewRecord: false }));
|