velocious 1.0.577 → 1.0.578

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 (42) hide show
  1. package/README.md +11 -5
  2. package/build/background-jobs/main.js +2 -2
  3. package/build/configuration-types.js +1 -0
  4. package/build/configuration.js +52 -10
  5. package/build/database/drivers/mysql/structure-sql.js +70 -9
  6. package/build/database/query/with-count.js +8 -8
  7. package/build/database/record/record-not-found-error.js +1 -0
  8. package/build/frontend-model-controller.js +71 -69
  9. package/build/frontend-models/base.js +10 -1
  10. package/build/src/background-jobs/main.js +3 -3
  11. package/build/src/configuration-types.d.ts +5 -0
  12. package/build/src/configuration-types.d.ts.map +1 -1
  13. package/build/src/configuration-types.js +2 -1
  14. package/build/src/configuration.d.ts +13 -1
  15. package/build/src/configuration.d.ts.map +1 -1
  16. package/build/src/configuration.js +52 -11
  17. package/build/src/database/drivers/mysql/structure-sql.d.ts +11 -0
  18. package/build/src/database/drivers/mysql/structure-sql.d.ts.map +1 -1
  19. package/build/src/database/drivers/mysql/structure-sql.js +61 -10
  20. package/build/src/database/query/with-count.js +8 -8
  21. package/build/src/database/record/record-not-found-error.d.ts +1 -0
  22. package/build/src/database/record/record-not-found-error.d.ts.map +1 -1
  23. package/build/src/database/record/record-not-found-error.js +2 -1
  24. package/build/src/frontend-model-controller.d.ts +10 -10
  25. package/build/src/frontend-model-controller.d.ts.map +1 -1
  26. package/build/src/frontend-model-controller.js +73 -67
  27. package/build/src/frontend-models/base.d.ts.map +1 -1
  28. package/build/src/frontend-models/base.js +11 -2
  29. package/build/src/velocious-error.d.ts +10 -0
  30. package/build/src/velocious-error.d.ts.map +1 -1
  31. package/build/src/velocious-error.js +8 -2
  32. package/build/velocious-error.js +7 -1
  33. package/package.json +2 -2
  34. package/src/background-jobs/main.js +2 -2
  35. package/src/configuration-types.js +1 -0
  36. package/src/configuration.js +52 -10
  37. package/src/database/drivers/mysql/structure-sql.js +70 -9
  38. package/src/database/query/with-count.js +8 -8
  39. package/src/database/record/record-not-found-error.js +1 -0
  40. package/src/frontend-model-controller.js +71 -69
  41. package/src/frontend-models/base.js +10 -1
  42. package/src/velocious-error.js +7 -1
package/README.md CHANGED
@@ -22,7 +22,7 @@
22
22
  * Gap-less positional lists with automatic reordering via `actsAsList`, including models with numeric, string, or UUID primary keys (see [docs/acts-as-list.md](docs/acts-as-list.md))
23
23
  * Rails-style nested-attribute writes on frontend-model `save()` (see [docs/nested-attributes.md](docs/nested-attributes.md))
24
24
  * Async-aware test-data factories with inherited traits, graph-first native association autosave, metadata-aware override precedence, callbacks, sequences, and linting (see [docs/factories.md](docs/factories.md))
25
- * Per-row association counts via `.withCount(...)`, including safe batching of structurally identical aggregates, on frontend and backend queries (see [docs/with-count.md](docs/with-count.md))
25
+ * Per-row association counts via `.withCount(...)`, including cohort-safe intersected filters and safe batching of structurally identical aggregates, on frontend and backend queries (see [docs/with-count.md](docs/with-count.md))
26
26
  * Consumer-defined per-row SQL aggregates/computations via `.queryData(...)`, with compatible projections sharing a roundtrip while preserving declared alias-overwrite order, on frontend and backend queries (see [docs/query-data.md](docs/query-data.md))
27
27
  * Per-record ability checks via `.abilities(...)` on frontend queries + `record.can(action)` (see [docs/abilities.md](docs/abilities.md))
28
28
  * Translated model attributes with current-locale relationship sorting (see [docs/translations.md](docs/translations.md))
@@ -741,7 +741,7 @@ Use `await FrontendModelBase.waitForIdle()` when a test harness or app lifecycle
741
741
 
742
742
  Frontend-model HTTP requests always use `credentials: "include"` so shared custom commands can set session cookies without app-level transport overrides.
743
743
 
744
- Unexpected frontend-model endpoint failures stay client-safe in production with `errorMessage: "Request failed."`.
744
+ Unexpected frontend-model endpoint failures stay client-safe in production with `errorType: "internal_error"`, `errorMessage: "Request failed."`, and a server-generated `correlationId` shared with the matching framework-error report. Expected application failures can use `VelociousError.safe(message, {errorType, details, code})`; generated frontend-model callers preserve the server's safe error fields. See [docs/frontend-models.md](docs/frontend-models.md#error-payloads).
745
745
  Invalid client query descriptors, such as unknown `select`, `where`, `search`, `joins`, `preload`, `group`, `sort`, `pluck`, or Ransack attributes, return the specific frontend-model query error message with `velocious.code: "frontend-model-query-error"` and are not emitted as framework errors.
746
746
  Invalid frontend-model write attributes and attachment names, including attributes rejected by `permittedParams()`, return the specific safe error message with `velocious.code: "frontend-model-attribute-error"` and are not emitted as framework errors.
747
747
  In `development` and `test`, Velocious also includes `debugErrorClass`, `debugErrorMessage`, and `debugBacktrace` fields so browser/system-test failures are easier to diagnose without exposing those details in production.
@@ -934,7 +934,8 @@ Translated models also get a `currentTranslation` `hasOne` relationship scoped t
934
934
 
935
935
  Async class APIs initialize record metadata on first use when a model has not
936
936
  already been initialized eagerly. See [docs/model-initialization.md](docs/model-initialization.md)
937
- for the eager and lazy initialization behavior.
937
+ for the eager and lazy initialization behavior, including atomic shared bootstrap
938
+ and complete recovery after an eager initialization failure.
938
939
 
939
940
  ## Lifecycle callbacks
940
941
 
@@ -1347,7 +1348,7 @@ If you need to regenerate missing structure files without rerunning migrations,
1347
1348
  npx velocious db:schema:dump
1348
1349
  ```
1349
1350
 
1350
- `db:schema:dump` generates a structure SQL file for each configured database identifier under `db/structure-<identifier>.sql`. It only writes files when one or more expected files are missing. The generated file includes the full DDL (tables, indexes, views, triggers, etc.) followed by `INSERT INTO schema_migrations (version) VALUES (...)` for every currently applied migration version. This preserves the migration ledger in the checked-in snapshot so fresh databases loaded from it do not re-run migrations that already shaped the schemas in the file.
1351
+ `db:schema:dump` generates a structure SQL file for each configured database identifier under `db/structure-<identifier>.sql`. It only writes files when one or more expected files are missing. The generated file includes the full DDL (tables, indexes, views, triggers, etc.) followed by `INSERT INTO schema_migrations (version) VALUES (...)` for every currently applied migration version. MySQL and MariaDB dumps place same-schema referenced base tables before their dependent tables. The migration ledger preserves applied versions in the checked-in snapshot so fresh databases loaded from it do not re-run migrations that already shaped the schemas in the file.
1351
1352
 
1352
1353
  If you need to load the checked-in structure files for each configured database, use:
1353
1354
 
@@ -1857,7 +1858,7 @@ configuration.getErrorEvents().on("all-error", ({error, errorType}) => {
1857
1858
  })
1858
1859
  ```
1859
1860
 
1860
- Genuinely unexpected frontend-model command failures reach this bus too. The frontend-model controller catches them to return a client-safe `Request failed.` response, but it also emits them as `framework-error`/`all-error` (with `context.frontendModelEndpoint === true`) so they are reported instead of being silently swallowed. Expected user-flow errors are excluded: validation failures are forwarded with their real message (for example `Name can't be blank`), invalid client query descriptors are returned as frontend-model query errors, and `error.velocious`-annotated / `safeToExpose` / `errorType`-marked errors keep their expected-error status none of these reach the error bus.
1861
+ Genuinely unexpected frontend-model command failures reach this bus too. The frontend-model controller catches them to return a client-safe `internal_error` response with `Request failed.` and a correlation ID, then emits them as `framework-error`/`all-error` with the same correlation ID and `context.frontendModelEndpoint === true`. Expected user-flow errors are excluded: validation failures are forwarded with their real message (for example `Name can't be blank`), invalid client query descriptors are returned as frontend-model query errors, and `error.velocious`-annotated / `safeToExpose` errors keep their expected-error status. A raw `errorType` property alone is not considered safe and does not suppress reporting.
1861
1862
 
1862
1863
  Unexpected inbound decoded WebSocket dispatch failures emit one `framework-error` and one matching `all-error`. Established expected client-flow errors remain excluded from both events.
1863
1864
 
@@ -2226,6 +2227,11 @@ VELOCIOUS_BACKGROUND_JOBS_JOB_TIMEOUT_MS=5400000
2226
2227
 
2227
2228
  New jobs default to `executionMode: "pooled"`: a worker runs them in warm, reusable Node child runners. `pooledRunnerCount` (default: `4`) bounds this independent per-worker pool, and `pooledRunnerConcurrency` (default: `1`) sets how many jobs each child runs at once on its own event loop, so total pooled capacity is `pooledRunnerCount × pooledRunnerConcurrency` — raise concurrency for I/O-bound jobs to get high throughput from a bounded, isolated set of processes. `pooledRunnerCount`, `pooledRunnerConcurrency`, and `pooledRunnerMaxJobs` must be finite positive integers; the RSS and lifetime limits must be finite positive numbers. A child is recycled after an acknowledged job when it reaches `pooledRunnerMaxJobs` (default: `100`), `pooledRunnerMaxRssBytes` (default: `536870912`, or 512 MiB), or `pooledRunnerMaxLifetimeMs` (default: `3600000`, or one hour). `execution_mode` is the single source of truth for a job's runtime — pooled rows persist as `execution_mode = "pooled"` directly. See [execution modes and pooled runners](docs/background-jobs.md#execution-modes-and-pooled-runners).
2228
2229
 
2230
+ Cold pooled jobs share one atomic model-bootstrap phase. If that phase fails, all
2231
+ waiting jobs receive the failure; a later job in the surviving child cannot run
2232
+ until a complete model-initialization phase succeeds. The pool's configured
2233
+ concurrency and per-job connection scopes are unchanged.
2234
+
2229
2235
  `maxConcurrentForkedJobs` (default: `4`) caps how many out-of-process `executionMode: "forked"` or `executionMode: "spawned"` jobs one worker may keep in flight. Forked jobs use `child_process.fork()` with an attached IPC channel. After the main process acknowledges their durable status report, forked and spawned one-shot runners exit without waiting for graceful Beacon/database teardown; the OS closes their process-owned resources. A missing or rejected status acknowledgement makes the runner exit as failed instead of reporting clean success. Spawned jobs use the legacy `background-jobs-runner` CLI process via `child_process.spawn()` and are only for callers that intentionally want that spawned behavior.
2230
2236
 
2231
2237
  `jobTimeoutMs` (or `VELOCIOUS_BACKGROUND_JOBS_JOB_TIMEOUT_MS`, milliseconds; default: disabled) is a wall-clock backstop for `"forked"` and `"pooled"` jobs. A job still running after the timeout is terminated (`SIGTERM`, then `SIGKILL` after the reaping grace) and reported `failed`, so a genuinely-hung job can't pin a worker's capacity — and its whole-app boot and DB connections — indefinitely (notably a retired-release worker draining after a deploy). For a **pooled** job the whole child running it is killed, so its concurrent in-flight siblings on that child are also reported `failed` and requeued — a hung JS job can't be cancelled any other way — before a replacement child is spawned. It's a coarse safety net, not per-job tuning: it applies to every forked and pooled job, so set it well above the longest legitimate job. Omit it, or set `null`/`<= 0`, to disable. `"inline"` jobs are not covered — they share the worker's process and can't be killed without killing the worker. See [docs/background-jobs.md](docs/background-jobs.md#job-timeout-hung-runner-backstop).
@@ -786,9 +786,9 @@ export default class BackgroundJobsMain {
786
786
  options: message.options || {}
787
787
  })
788
788
 
789
- jsonSocket.send({type: "schedule-replaced", ...result})
790
789
  this._notifyEnqueued()
791
790
  await this._drain()
791
+ jsonSocket.send({type: "schedule-replaced", ...result})
792
792
  } catch (error) {
793
793
  this._handleClientMutationError({
794
794
  context: {jobName: message.jobName, scheduleKey: message.scheduleKey, stage: "background-job-replace-scheduled"},
@@ -812,9 +812,9 @@ export default class BackgroundJobsMain {
812
812
  try {
813
813
  const result = await this.store.cancelScheduled(message.scheduleKey)
814
814
 
815
- jsonSocket.send({type: "schedule-cancelled", ...result})
816
815
  this._notifyEnqueued()
817
816
  await this._drain()
817
+ jsonSocket.send({type: "schedule-cancelled", ...result})
818
818
  } catch (error) {
819
819
  this._handleClientMutationError({
820
820
  context: {scheduleKey: message.scheduleKey, stage: "background-job-cancel-scheduled"},
@@ -312,6 +312,7 @@
312
312
  * @property {string} controller - Controller class name.
313
313
  * @property {string} [action] - Controller action or endpoint label.
314
314
  * @property {"index" | "find" | "create" | "update" | "destroy" | "attach" | "attachmentList" | "download" | "url" | "custom-command"} [commandType] - Frontend-model command type.
315
+ * @property {string} [correlationId] - Server-generated identifier shared by an unexpected client error and framework reports.
315
316
  * @property {boolean} [expectedError] - Whether the error is an expected user-flow failure.
316
317
  * @property {boolean} [frontendModelEndpoint] - Whether the error came from the frontend-model endpoint.
317
318
  * @property {string} [model] - Frontend-model name from the failed request.
@@ -286,6 +286,19 @@ export default class VelociousConfiguration {
286
286
  }
287
287
 
288
288
  this._isInitialized = false
289
+ this._modelsInitialized = false
290
+ /**
291
+ * Invalidates model phases that started before database connections closed.
292
+ * @type {number}
293
+ */
294
+ this._modelInitializationGeneration = 0
295
+ /**
296
+ * In-progress `initializeModels()` promise. Model initialization is an
297
+ * atomic bootstrap phase: concurrent callers share it, and a rejection
298
+ * leaves the phase eligible for a later complete attempt.
299
+ * @type {Promise<void> | undefined}
300
+ */
301
+ this._initializeModelsPromise = undefined
289
302
  /**
290
303
  * In-progress `initialize()` promise, memoized so concurrent callers await
291
304
  * the same bootstrap. Reset to undefined if initialization fails.
@@ -2020,25 +2033,39 @@ export default class VelociousConfiguration {
2020
2033
  * @returns {Promise<void>} - Resolves when complete.
2021
2034
  */
2022
2035
  async initializeModels(args = {type: "server"}) {
2023
- if (!this._modelsInitialized) {
2024
- this._modelsInitialized = true
2036
+ if (this._modelsInitialized) return
2037
+ if (this._initializeModelsPromise) return await this._initializeModelsPromise
2025
2038
 
2039
+ const modelInitializationGeneration = this._modelInitializationGeneration
2040
+ const initializeModelsPromise = (async () => {
2026
2041
  const shouldSkipDummyModelInitialization = process.env.VELOCIOUS_SKIP_DUMMY_MODEL_INITIALIZATION === "1"
2027
2042
  && process.env.VELOCIOUS_BROWSER_TESTS === "true"
2028
2043
  && this.getEnvironment() === "test"
2029
2044
 
2030
- if (shouldSkipDummyModelInitialization) {
2031
- return
2045
+ if (!shouldSkipDummyModelInitialization) {
2046
+ if (this._initializeModels) {
2047
+ await this._initializeModels({configuration: this, type: args.type})
2048
+ }
2049
+
2050
+ await this.getEnvironmentHandler().initializePackageModels(this)
2051
+ await initializeAuditedModelRelationships(this)
2052
+
2053
+ await this.getEnvironmentHandler().initializeFrontendModelWebsocketPublishers(this)
2032
2054
  }
2033
2055
 
2034
- if (this._initializeModels) {
2035
- await this._initializeModels({configuration: this, type: args.type})
2056
+ if (this._modelInitializationGeneration === modelInitializationGeneration) {
2057
+ this._modelsInitialized = true
2036
2058
  }
2059
+ })()
2037
2060
 
2038
- await this.getEnvironmentHandler().initializePackageModels(this)
2039
- await initializeAuditedModelRelationships(this)
2061
+ this._initializeModelsPromise = initializeModelsPromise
2040
2062
 
2041
- await this.getEnvironmentHandler().initializeFrontendModelWebsocketPublishers(this)
2063
+ try {
2064
+ await initializeModelsPromise
2065
+ } finally {
2066
+ if (this._initializeModelsPromise === initializeModelsPromise) {
2067
+ this._initializeModelsPromise = undefined
2068
+ }
2042
2069
  }
2043
2070
  }
2044
2071
 
@@ -2073,6 +2100,12 @@ export default class VelociousConfiguration {
2073
2100
 
2074
2101
  this._initializePromise = (async () => {
2075
2102
  await this.initializeModels({type})
2103
+
2104
+ // Model initialization can be invalidated by a concurrent connection close.
2105
+ // If models are not ready, stop without marking the configuration initialized
2106
+ // so the next caller retries a full bootstrap.
2107
+ if (!this._modelsInitialized) return
2108
+
2076
2109
  await this.getEnvironmentHandler().autoDiscoverResources(this)
2077
2110
  this._mergeDiscoveredAbilityResources()
2078
2111
  this._validateResourceRelationshipsOnModels()
@@ -2104,6 +2137,13 @@ export default class VelociousConfiguration {
2104
2137
  this._initializePromise = undefined
2105
2138
  throw error
2106
2139
  }
2140
+
2141
+ // If the inner IIFE returned without marking the configuration initialized
2142
+ // (e.g. because models were invalidated mid-bootstrap), clear the promise so
2143
+ // a later call retries a full bootstrap.
2144
+ if (!this._isInitialized) {
2145
+ this._initializePromise = undefined
2146
+ }
2107
2147
  }
2108
2148
 
2109
2149
  /**
@@ -3244,8 +3284,10 @@ export default class VelociousConfiguration {
3244
3284
  PoolClass.clearGlobalConnections(this)
3245
3285
  }
3246
3286
 
3247
- // Allow models to be re-initialized after connections are closed.
3287
+ // Allow full re-initialization after connections are closed.
3288
+ this._modelInitializationGeneration += 1
3248
3289
  this._modelsInitialized = false
3290
+ this._isInitialized = false
3249
3291
  }
3250
3292
  })()
3251
3293
 
@@ -20,6 +20,9 @@ export default class VelociousDatabaseDriversMysqlStructureSql {
20
20
  const {driver} = this
21
21
  const isMariaDb = await this._isMariaDb()
22
22
  const rows = await driver.query("SELECT table_name, table_type FROM information_schema.tables WHERE table_schema = DATABASE() ORDER BY table_type, table_name")
23
+ const foreignKeyRows = await driver.query("SELECT table_name, referenced_table_name FROM information_schema.key_column_usage WHERE table_schema = DATABASE() AND referenced_table_schema = DATABASE() AND referenced_table_name IS NOT NULL")
24
+ const baseTableNames = []
25
+ const views = []
23
26
  const statements = []
24
27
 
25
28
  for (const row of rows) {
@@ -31,24 +34,82 @@ export default class VelociousDatabaseDriversMysqlStructureSql {
31
34
  if (!tableName || !tableType) continue
32
35
 
33
36
  if (tableType == "BASE TABLE") {
34
- const createRows = await driver.query(`SHOW CREATE TABLE ${driver.quoteTable(tableName)}`)
35
- const rawCreateStatement = this._mysqlCreateStatement(createRows?.[0])
36
- const createStatement = rawCreateStatement ? this._stripAutoIncrement(rawCreateStatement) : null
37
-
38
- if (createStatement) statements.push(normalizeSqlStatement(createStatement))
37
+ baseTableNames.push(tableName)
39
38
  } else if (tableType == "VIEW" || (isMariaDb && tableType == "SYSTEM VIEW")) {
40
- const createRows = await driver.query(`SHOW CREATE VIEW ${driver.quoteTable(tableName)}`)
41
- const createStatement = this._mysqlCreateStatement(createRows?.[0])
42
-
43
- if (createStatement) statements.push(normalizeSqlStatement(createStatement))
39
+ views.push(tableName)
44
40
  }
45
41
  }
46
42
 
43
+ for (const tableName of this._orderBaseTables({foreignKeyRows, tableNames: baseTableNames})) {
44
+ const createRows = await driver.query(`SHOW CREATE TABLE ${driver.quoteTable(tableName)}`)
45
+ const rawCreateStatement = this._mysqlCreateStatement(createRows?.[0])
46
+ const createStatement = rawCreateStatement ? this._stripAutoIncrement(rawCreateStatement) : null
47
+
48
+ if (createStatement) statements.push(normalizeSqlStatement(createStatement))
49
+ }
50
+
51
+ for (const tableName of views) {
52
+ const createRows = await driver.query(`SHOW CREATE VIEW ${driver.quoteTable(tableName)}`)
53
+ const createStatement = this._mysqlCreateStatement(createRows?.[0])
54
+
55
+ if (createStatement) statements.push(normalizeSqlStatement(createStatement))
56
+ }
57
+
47
58
  if (statements.length == 0) return null
48
59
 
49
60
  return `${statements.join("\n\n")}\n`
50
61
  }
51
62
 
63
+ /**
64
+ * Orders tables so referenced tables are created before their dependents.
65
+ * @param {object} args - Options object.
66
+ * @param {Array<Record<string, ?>>} args.foreignKeyRows - Foreign key metadata rows.
67
+ * @param {string[]} args.tableNames - Base table names in their existing order.
68
+ * @returns {string[]} - Ordered table names.
69
+ */
70
+ _orderBaseTables({foreignKeyRows, tableNames}) {
71
+ const pendingTableNames = new Set(tableNames)
72
+ /** @type {Record<string, Set<string>>} */
73
+ const dependenciesByTableName = {}
74
+ const orderedTableNames = []
75
+
76
+ for (const tableName of tableNames) {
77
+ dependenciesByTableName[tableName] = new Set()
78
+ }
79
+
80
+ for (const row of foreignKeyRows) {
81
+ const tableNameValue = row.table_name || row.TABLE_NAME
82
+ const referencedTableNameValue = row.referenced_table_name || row.REFERENCED_TABLE_NAME
83
+ const tableName = tableNameValue ? String(tableNameValue) : ""
84
+ const referencedTableName = referencedTableNameValue ? String(referencedTableNameValue) : ""
85
+
86
+ if (tableName == referencedTableName || !pendingTableNames.has(tableName) || !pendingTableNames.has(referencedTableName)) continue
87
+
88
+ dependenciesByTableName[tableName].add(referencedTableName)
89
+ }
90
+
91
+ while (pendingTableNames.size > 0) {
92
+ const nextTableName = tableNames.find((tableName) => {
93
+ if (!pendingTableNames.has(tableName)) return false
94
+
95
+ return Array.from(dependenciesByTableName[tableName]).every((dependencyTableName) => !pendingTableNames.has(dependencyTableName))
96
+ })
97
+
98
+ if (!nextTableName) {
99
+ for (const tableName of tableNames) {
100
+ if (pendingTableNames.has(tableName)) orderedTableNames.push(tableName)
101
+ }
102
+
103
+ break
104
+ }
105
+
106
+ orderedTableNames.push(nextTableName)
107
+ pendingTableNames.delete(nextTableName)
108
+ }
109
+
110
+ return orderedTableNames
111
+ }
112
+
52
113
  /**
53
114
  * Runs is maria db.
54
115
  * @returns {Promise<boolean>} - Resolves with Whether maria db.
@@ -161,22 +161,22 @@ function queryForEntry({entry, modelClass, parentIds, sourceModel}) {
161
161
 
162
162
  const foreignKey = relationship.getForeignKey()
163
163
  /**
164
- * Where conditions.
164
+ * Mandatory cohort conditions.
165
165
  * @type {Record<string, ?>} */
166
- const whereConditions = {[foreignKey]: parentIds}
166
+ const mandatoryWhereConditions = {[foreignKey]: parentIds}
167
167
 
168
168
  if (relationship.getPolymorphic && relationship.getPolymorphic()) {
169
169
  const typeColumn = relationship.getPolymorphicTypeColumn()
170
- whereConditions[typeColumn] = modelClass.getModelName()
171
- }
172
-
173
- if (entry.where) {
174
- Object.assign(whereConditions, entry.where)
170
+ mandatoryWhereConditions[typeColumn] = modelClass.getModelName()
175
171
  }
176
172
 
177
173
  const baseQuery = sourceModel.queryForModel(targetModelClass)
178
174
  baseQuery._forceQualifyBaseTable = true
179
- baseQuery.where(whereConditions)
175
+ baseQuery.where(mandatoryWhereConditions)
176
+
177
+ if (entry.where) {
178
+ baseQuery.where(entry.where)
179
+ }
180
180
 
181
181
  const countQuery = relationship.applyScope(baseQuery)
182
182
 
@@ -1,3 +1,4 @@
1
1
  // @ts-check
2
2
 
3
+ /** Backend missing-record error whose diagnostic message stays server-side. */
3
4
  export default class RecordNotFoundError extends Error {}