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.
Files changed (129) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/README.md +32 -24
  3. package/dist/compliance/index.d.ts +2 -0
  4. package/dist/compliance/index.js +13 -1
  5. package/dist/connection-manager.js +13 -1
  6. package/dist/core/expressions.d.ts +58 -0
  7. package/dist/core/expressions.js +83 -0
  8. package/dist/core/hooks-manager.d.ts +179 -0
  9. package/dist/core/hooks-manager.js +291 -0
  10. package/dist/core/types.d.ts +235 -0
  11. package/dist/core/types.js +9 -0
  12. package/dist/decorators/index.d.ts +2 -0
  13. package/dist/decorators/index.js +11 -1
  14. package/dist/diagrams/chen-diagram.d.ts +15 -12
  15. package/dist/diagrams/chen-diagram.js +76 -85
  16. package/dist/diagrams/class-diagram.d.ts +9 -8
  17. package/dist/diagrams/class-diagram.js +60 -82
  18. package/dist/diagrams/core/index.d.ts +13 -0
  19. package/dist/diagrams/core/index.js +33 -0
  20. package/dist/diagrams/core/layout.d.ts +104 -0
  21. package/dist/diagrams/core/layout.js +223 -0
  22. package/dist/diagrams/{svg-dom.d.ts → core/svg.d.ts} +8 -20
  23. package/dist/diagrams/{svg-dom.js → core/svg.js} +21 -29
  24. package/dist/diagrams/core/theme.d.ts +84 -0
  25. package/dist/diagrams/core/theme.js +62 -0
  26. package/dist/diagrams/dependency-diagram.d.ts +15 -14
  27. package/dist/diagrams/dependency-diagram.js +71 -105
  28. package/dist/diagrams/er-diagram.d.ts +13 -26
  29. package/dist/diagrams/er-diagram.js +180 -259
  30. package/dist/diagrams/flow-diagram.d.ts +16 -11
  31. package/dist/diagrams/flow-diagram.js +96 -91
  32. package/dist/diagrams/gantt-diagram.d.ts +10 -5
  33. package/dist/diagrams/gantt-diagram.js +77 -103
  34. package/dist/diagrams/index-diagram.d.ts +10 -10
  35. package/dist/diagrams/index-diagram.js +106 -116
  36. package/dist/diagrams/index.d.ts +6 -9
  37. package/dist/diagrams/index.js +9 -24
  38. package/dist/diagrams/migration-diagram.d.ts +8 -4
  39. package/dist/diagrams/migration-diagram.js +189 -161
  40. package/dist/diagrams/model-diagram.d.ts +34 -34
  41. package/dist/diagrams/model-diagram.js +106 -485
  42. package/dist/diagrams/package-diagram.d.ts +9 -6
  43. package/dist/diagrams/package-diagram.js +64 -101
  44. package/dist/diagrams/relational-diagram.d.ts +12 -10
  45. package/dist/diagrams/relational-diagram.js +87 -114
  46. package/dist/diagrams/schemadoc-diagram.d.ts +8 -8
  47. package/dist/diagrams/schemadoc-diagram.js +71 -93
  48. package/dist/diagrams/sequence-diagram.d.ts +10 -10
  49. package/dist/diagrams/sequence-diagram.js +64 -89
  50. package/dist/diagrams/state-diagram.d.ts +10 -9
  51. package/dist/diagrams/state-diagram.js +69 -92
  52. package/dist/diagrams/tree-diagram.d.ts +11 -5
  53. package/dist/diagrams/tree-diagram.js +41 -65
  54. package/dist/dialects/clickhouse/index.js +15 -0
  55. package/dist/dialects/cockroachdb/index.d.ts +1 -1
  56. package/dist/dialects/cockroachdb/index.js +32 -63
  57. package/dist/dialects/db2/index.js +23 -8
  58. package/dist/dialects/dialect.d.ts +29 -5
  59. package/dist/dialects/duckdb/index.js +9 -2
  60. package/dist/dialects/hana/index.js +22 -5
  61. package/dist/dialects/lock-clause-helper.d.ts +167 -0
  62. package/dist/dialects/lock-clause-helper.js +316 -0
  63. package/dist/dialects/mariadb/index.d.ts +7 -4
  64. package/dist/dialects/mariadb/index.js +36 -60
  65. package/dist/dialects/mssql/index.js +34 -3
  66. package/dist/dialects/mysql/index.d.ts +1 -1
  67. package/dist/dialects/mysql/index.js +17 -45
  68. package/dist/dialects/oracle/index.d.ts +14 -3
  69. package/dist/dialects/oracle/index.js +66 -36
  70. package/dist/dialects/order-expression-helper.d.ts +72 -0
  71. package/dist/dialects/order-expression-helper.js +154 -0
  72. package/dist/dialects/partial-index.d.ts +52 -0
  73. package/dist/dialects/partial-index.js +251 -0
  74. package/dist/dialects/postgres/index.d.ts +1 -1
  75. package/dist/dialects/postgres/index.js +43 -68
  76. package/dist/dialects/redshift/index.js +9 -2
  77. package/dist/dialects/snowflake/index.js +17 -0
  78. package/dist/dialects/spanner/index.js +7 -2
  79. package/dist/dialects/sqlite/driver.d.ts +55 -0
  80. package/dist/dialects/sqlite/driver.js +102 -0
  81. package/dist/dialects/sqlite/index.d.ts +1 -1
  82. package/dist/dialects/sqlite/index.js +49 -27
  83. package/dist/graph/base-graph-dialect.d.ts +24 -3
  84. package/dist/graph/base-graph-dialect.js +43 -2
  85. package/dist/graph/dgraph/index.js +6 -0
  86. package/dist/graph/gremlin/index.d.ts +6 -0
  87. package/dist/graph/gremlin/index.js +12 -1
  88. package/dist/graph/index.d.ts +1 -1
  89. package/dist/graph/neo4j/index.d.ts +2 -0
  90. package/dist/graph/neo4j/index.js +6 -1
  91. package/dist/graph/types.d.ts +12 -1
  92. package/dist/index.d.ts +13 -4
  93. package/dist/index.js +54 -7
  94. package/dist/logging/index.d.ts +10 -0
  95. package/dist/logging/index.js +23 -0
  96. package/dist/logging/query-logging.d.ts +82 -0
  97. package/dist/logging/query-logging.js +102 -0
  98. package/dist/models/decorators.d.ts +39 -3
  99. package/dist/models/decorators.js +74 -0
  100. package/dist/models/eager-load.d.ts +99 -0
  101. package/dist/models/eager-load.js +348 -5
  102. package/dist/models/indexes.d.ts +3 -2
  103. package/dist/models/indexes.js +7 -1
  104. package/dist/models/model.js +69 -43
  105. package/dist/prorm.d.ts +175 -409
  106. package/dist/prorm.js +1142 -557
  107. package/dist/query-builders/order-limit-builder.js +12 -10
  108. package/dist/query-builders/sql-compiler.d.ts +10 -3
  109. package/dist/query-builders/sql-compiler.js +14 -43
  110. package/dist/query-interface.d.ts +2 -1
  111. package/dist/sql-constants.d.ts +44 -0
  112. package/dist/sql-constants.js +138 -1
  113. package/dist/types/index.d.ts +53 -17
  114. package/dist/types/index.js +10 -15
  115. package/dist/types/query-types.d.ts +26 -0
  116. package/dist/types/query-types.js +30 -0
  117. package/package.json +32 -5
  118. package/dist/decorators/belongs-to-many.js +0 -115
  119. package/dist/decorators/belongs-to.js +0 -115
  120. package/dist/decorators/has-many.js +0 -127
  121. package/dist/decorators/has-one.js +0 -116
  122. package/dist/diagrams/palette.d.ts +0 -138
  123. package/dist/diagrams/palette.js +0 -194
  124. package/dist/hooks/hooks-manager.d.ts +0 -189
  125. package/dist/hooks/hooks-manager.js +0 -350
  126. package/dist/hooks/index.d.ts +0 -8
  127. package/dist/hooks/index.js +0 -37
  128. /package/dist/{logging.d.ts → logging/logger.d.ts} +0 -0
  129. /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
- for (const rawIncludeOpt of includeList) {
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
- for (const tr of targetRows)
329
- targetMap.set(readField(tr, targetPkAttr), tr);
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
- attachField(row, includeAlias, targetIds.map((tid) => targetMap.get(tid)).filter((v) => v !== undefined));
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, associationType === 'hasOne' ? associated[0] ?? null : associated);
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.
@@ -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?: WhereOptions;
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;
@@ -64,7 +64,13 @@ class Index {
64
64
  * Check if this is a partial index
65
65
  */
66
66
  isPartial() {
67
- return !!this.where && Object.keys(this.where).length > 0;
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
@@ -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
- const includeOpts = include;
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
- const expandedInclude = {
121
- model: includeOpts.model,
122
- as: includeOpts.as,
123
- where: includeOpts.where,
124
- attributes: includeOpts.attributes,
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
- const hooksManager = this.model._hooksManager;
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 && hooksManager) {
1425
- await hooksManager.runHook('beforeValidate', this, { model: this.model });
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 && hooksManager && result.valid) {
1469
- await hooksManager.runHook('afterValidate', this, { model: this.model });
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 connection = prorm.connectionManager?.getConnection(findOptions.using);
2826
+ const registry = prorm.connectionManager;
2827
+ const connection = registry?.getConnection(findOptions.using);
2811
2828
  if (!connection) {
2812
- throw new Error(`Connection '${findOptions.using}' not found. Available: ${prorm.connectionManager?.getConnectionNames()?.join(', ') || 'none'}`);
2829
+ const available = registry?.getConnectionNames()?.join(', ') || 'none';
2830
+ throw new Error(`Connection '${findOptions.using}' not found. Available: ${available}`);
2813
2831
  }
2814
- // Delegate to the specified connection's model
2815
- const model = connection.models.get(this.name);
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
- throw new Error(`Model '${this.name}' not found in connection '${findOptions.using}'`);
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 ? selectOptions : undefined,
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 }));