velocious 1.0.475 → 1.0.477

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 (116) hide show
  1. package/README.md +3 -0
  2. package/build/configuration-types.js +4 -0
  3. package/build/configuration.js +29 -0
  4. package/build/database/drivers/base.js +16 -0
  5. package/build/database/drivers/mssql/index.js +13 -0
  6. package/build/database/drivers/mssql/sql/remove-index.js +6 -0
  7. package/build/database/drivers/mysql/index.js +13 -0
  8. package/build/database/drivers/mysql/sql/remove-index.js +6 -0
  9. package/build/database/drivers/pgsql/index.js +13 -0
  10. package/build/database/drivers/pgsql/sql/remove-index.js +6 -0
  11. package/build/database/drivers/sqlite/base.js +13 -0
  12. package/build/database/drivers/sqlite/sql/remove-index.js +6 -0
  13. package/build/database/migration/index.js +44 -0
  14. package/build/database/pool/base-methods-forward.js +1 -0
  15. package/build/database/query/remove-index-base.js +39 -0
  16. package/build/database/record/index.js +25 -9
  17. package/build/environment-handlers/node/cli/commands/generate/base-models.js +36 -0
  18. package/build/frontend-model-controller.js +632 -1
  19. package/build/frontend-models/base.js +5 -1
  20. package/build/frontend-models/resource-definition.js +20 -3
  21. package/build/http-server/client/websocket-session.js +93 -0
  22. package/build/routes/hooks/frontend-model-command-route-hook.js +24 -0
  23. package/build/src/configuration-types.d.ts +20 -0
  24. package/build/src/configuration-types.d.ts.map +1 -1
  25. package/build/src/configuration-types.js +5 -1
  26. package/build/src/configuration.d.ts +13 -0
  27. package/build/src/configuration.d.ts.map +1 -1
  28. package/build/src/configuration.js +28 -1
  29. package/build/src/database/drivers/base.d.ts +20 -0
  30. package/build/src/database/drivers/base.d.ts.map +1 -1
  31. package/build/src/database/drivers/base.js +16 -1
  32. package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
  33. package/build/src/database/drivers/mssql/index.js +12 -1
  34. package/build/src/database/drivers/mssql/sql/remove-index.d.ts +4 -0
  35. package/build/src/database/drivers/mssql/sql/remove-index.d.ts.map +1 -0
  36. package/build/src/database/drivers/mssql/sql/remove-index.js +5 -0
  37. package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
  38. package/build/src/database/drivers/mysql/index.js +12 -1
  39. package/build/src/database/drivers/mysql/sql/remove-index.d.ts +4 -0
  40. package/build/src/database/drivers/mysql/sql/remove-index.d.ts.map +1 -0
  41. package/build/src/database/drivers/mysql/sql/remove-index.js +5 -0
  42. package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
  43. package/build/src/database/drivers/pgsql/index.js +12 -1
  44. package/build/src/database/drivers/pgsql/sql/remove-index.d.ts +4 -0
  45. package/build/src/database/drivers/pgsql/sql/remove-index.d.ts.map +1 -0
  46. package/build/src/database/drivers/pgsql/sql/remove-index.js +5 -0
  47. package/build/src/database/drivers/sqlite/base.d.ts.map +1 -1
  48. package/build/src/database/drivers/sqlite/base.js +12 -1
  49. package/build/src/database/drivers/sqlite/sql/remove-index.d.ts +4 -0
  50. package/build/src/database/drivers/sqlite/sql/remove-index.d.ts.map +1 -0
  51. package/build/src/database/drivers/sqlite/sql/remove-index.js +5 -0
  52. package/build/src/database/migration/index.d.ts +25 -0
  53. package/build/src/database/migration/index.d.ts.map +1 -1
  54. package/build/src/database/migration/index.js +39 -1
  55. package/build/src/database/pool/base-methods-forward.d.ts.map +1 -1
  56. package/build/src/database/pool/base-methods-forward.js +2 -1
  57. package/build/src/database/query/remove-index-base.d.ts +35 -0
  58. package/build/src/database/query/remove-index-base.d.ts.map +1 -0
  59. package/build/src/database/query/remove-index-base.js +34 -0
  60. package/build/src/database/record/index.d.ts +11 -0
  61. package/build/src/database/record/index.d.ts.map +1 -1
  62. package/build/src/database/record/index.js +26 -11
  63. package/build/src/environment-handlers/node/cli/commands/generate/base-models.d.ts +15 -0
  64. package/build/src/environment-handlers/node/cli/commands/generate/base-models.d.ts.map +1 -1
  65. package/build/src/environment-handlers/node/cli/commands/generate/base-models.js +33 -1
  66. package/build/src/frontend-model-controller.d.ts +156 -0
  67. package/build/src/frontend-model-controller.d.ts.map +1 -1
  68. package/build/src/frontend-model-controller.js +555 -2
  69. package/build/src/frontend-models/base.d.ts +5 -0
  70. package/build/src/frontend-models/base.d.ts.map +1 -1
  71. package/build/src/frontend-models/base.js +6 -2
  72. package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
  73. package/build/src/frontend-models/resource-definition.js +20 -4
  74. package/build/src/http-server/client/websocket-session.d.ts +39 -0
  75. package/build/src/http-server/client/websocket-session.d.ts.map +1 -1
  76. package/build/src/http-server/client/websocket-session.js +85 -1
  77. package/build/src/routes/hooks/frontend-model-command-route-hook.d.ts.map +1 -1
  78. package/build/src/routes/hooks/frontend-model-command-route-hook.js +22 -1
  79. package/build/src/sync/conflict-strategy.d.ts +83 -0
  80. package/build/src/sync/conflict-strategy.d.ts.map +1 -0
  81. package/build/src/sync/conflict-strategy.js +215 -0
  82. package/build/src/sync/peer-mutation-bundle.d.ts +97 -0
  83. package/build/src/sync/peer-mutation-bundle.d.ts.map +1 -0
  84. package/build/src/sync/peer-mutation-bundle.js +215 -0
  85. package/build/src/sync/server-change-feed.d.ts +265 -0
  86. package/build/src/sync/server-change-feed.d.ts.map +1 -0
  87. package/build/src/sync/server-change-feed.js +475 -0
  88. package/build/sync/conflict-strategy.js +225 -0
  89. package/build/sync/peer-mutation-bundle.js +232 -0
  90. package/build/sync/server-change-feed.js +524 -0
  91. package/build/tsconfig.tsbuildinfo +1 -1
  92. package/package.json +1 -1
  93. package/src/configuration-types.js +4 -0
  94. package/src/configuration.js +29 -0
  95. package/src/database/drivers/base.js +16 -0
  96. package/src/database/drivers/mssql/index.js +13 -0
  97. package/src/database/drivers/mssql/sql/remove-index.js +6 -0
  98. package/src/database/drivers/mysql/index.js +13 -0
  99. package/src/database/drivers/mysql/sql/remove-index.js +6 -0
  100. package/src/database/drivers/pgsql/index.js +13 -0
  101. package/src/database/drivers/pgsql/sql/remove-index.js +6 -0
  102. package/src/database/drivers/sqlite/base.js +13 -0
  103. package/src/database/drivers/sqlite/sql/remove-index.js +6 -0
  104. package/src/database/migration/index.js +44 -0
  105. package/src/database/pool/base-methods-forward.js +1 -0
  106. package/src/database/query/remove-index-base.js +39 -0
  107. package/src/database/record/index.js +25 -9
  108. package/src/environment-handlers/node/cli/commands/generate/base-models.js +36 -0
  109. package/src/frontend-model-controller.js +632 -1
  110. package/src/frontend-models/base.js +5 -1
  111. package/src/frontend-models/resource-definition.js +20 -3
  112. package/src/http-server/client/websocket-session.js +93 -0
  113. package/src/routes/hooks/frontend-model-command-route-hook.js +24 -0
  114. package/src/sync/conflict-strategy.js +225 -0
  115. package/src/sync/peer-mutation-bundle.js +232 -0
  116. package/src/sync/server-change-feed.js +524 -0
package/README.md CHANGED
@@ -723,6 +723,8 @@ Task.validates("name", {presence: true, uniqueness: true})
723
723
  export default Task
724
724
  ```
725
725
 
726
+ Generated belongs-to setters synchronize the loaded relationship and foreign key before save, so `task.setProject(project)` updates `task.projectId()`, `task.changes()`, callbacks, and scoped features such as `actsAsList`. Custom relationship primary keys are also used after autosaving an assigned new or dirty related record. Generated backend write attributes accept belongs-to relationship names for `create` and `update` payloads. See [docs/relationships.md](docs/relationships.md#assigning-belongs-to-relationships).
727
+
726
728
  Translated models also get a `currentTranslation` `hasOne` relationship scoped to the first available row in the current locale fallback order. See [docs/translations.md](docs/translations.md) for preloading and frontend-model sorting behavior.
727
729
 
728
730
  Async class APIs initialize record metadata on first use when a model has not
@@ -1076,6 +1078,7 @@ export default class CreateEvents extends Migration {
1076
1078
  }
1077
1079
 
1078
1080
  async down() {
1081
+ await this.removeIndex("task_translations", ["task_id", "locale"])
1079
1082
  await this.dropTable("task_translations")
1080
1083
  await this.dropTable("examples")
1081
1084
  await this.dropTable("tasks")
@@ -348,6 +348,7 @@
348
348
  * frontends and peers; `policy` is hashed but intentionally omitted from
349
349
  * frontend-safe manifests.
350
350
  * @typedef {object} FrontendModelResourceSyncConfiguration
351
+ * @property {"optimisticVersion" | "serverWins" | "lastWriterWins" | "fieldThreeWay" | "appendOnly"} [conflictStrategy] - Strategy used when replay detects server/client divergence. Defaults to `optimisticVersion`.
351
352
  * @property {boolean} [enabled] - Whether the resource is sync-enabled. Defaults to true when `sync` is configured.
352
353
  * @property {string[]} [operations] - Sync operation names such as `index`, `find`, `create`, `update`, custom domain commands, etc.
353
354
  * @property {string | number} [policyVersion] - App-controlled policy version used as a stable change signal.
@@ -358,6 +359,8 @@
358
359
  /**
359
360
  * Velocious sync configuration.
360
361
  * @typedef {object} VelociousSyncConfiguration
362
+ * @property {import("./sync/device-identity.js").SyncJsonWebKey | null} [deviceCertificateBackendPublicKey] - Public backend key used to verify offline device certificates for sync replay.
363
+ * @property {number} [changeFeedRetentionSize] - Number of accepted server changes retained before clients must refresh from snapshot.
361
364
  * @property {Array<import("./sync/offline-grant.js").OfflineGrantSigningKey>} offlineGrantSigningKeys - Signing keys used to issue and verify offline grants. Secrets must never be exposed to clients.
362
365
  * @property {number} [offlineGrantTtlMs] - Default offline grant TTL in milliseconds. Defaults to 24 hours.
363
366
  */
@@ -365,6 +368,7 @@
365
368
  /**
366
369
  * Frontend-safe normalized sync metadata.
367
370
  * @typedef {object} NormalizedFrontendModelResourceSyncConfiguration
371
+ * @property {"optimisticVersion" | "serverWins" | "lastWriterWins" | "fieldThreeWay" | "appendOnly"} conflictStrategy - Normalized replay conflict strategy.
368
372
  * @property {boolean} enabled - Whether the resource is sync-enabled.
369
373
  * @property {string[]} operations - Sorted, duplicate-free sync operation names.
370
374
  * @property {string | null} policyVersion - App-controlled policy version, or null.
@@ -232,6 +232,9 @@ export default class VelociousConfiguration {
232
232
  /** Grace period for paused WebSocket sessions before permanent teardown. */
233
233
  this._websocketSessionGraceSeconds = 300
234
234
 
235
+ /** Interval (seconds) between server→client heartbeat pings; 0 disables reaping of silent sockets. */
236
+ this._websocketSessionHeartbeatSeconds = 30
237
+
235
238
  /**
236
239
  * Optional wrapper called around every WebSocket-borne request /
237
240
  * connection message / channel dispatch. Apps register it here
@@ -406,15 +409,25 @@ export default class VelociousConfiguration {
406
409
  * @returns {import("./configuration-types.js").VelociousSyncConfiguration} - Normalized sync configuration.
407
410
  */
408
411
  _normalizeSyncConfiguration(sync) {
412
+ const deviceCertificateBackendPublicKey = sync?.deviceCertificateBackendPublicKey || null
413
+ const changeFeedRetentionSize = sync?.changeFeedRetentionSize
409
414
  const offlineGrantSigningKeys = sync?.offlineGrantSigningKeys || []
410
415
  const offlineGrantTtlMs = sync?.offlineGrantTtlMs
411
416
 
417
+ if (deviceCertificateBackendPublicKey !== null && (typeof deviceCertificateBackendPublicKey !== "object" || Array.isArray(deviceCertificateBackendPublicKey))) {
418
+ throw new Error("sync.deviceCertificateBackendPublicKey must be a public JSON Web Key object")
419
+ }
420
+ if (changeFeedRetentionSize !== undefined && (!Number.isInteger(changeFeedRetentionSize) || changeFeedRetentionSize <= 0)) {
421
+ throw new Error("sync.changeFeedRetentionSize must be a positive integer")
422
+ }
412
423
  if (!Array.isArray(offlineGrantSigningKeys)) throw new Error("sync.offlineGrantSigningKeys must be an array")
413
424
  if (offlineGrantTtlMs !== undefined && (!Number.isInteger(offlineGrantTtlMs) || offlineGrantTtlMs <= 0)) {
414
425
  throw new Error("sync.offlineGrantTtlMs must be a positive integer number of milliseconds")
415
426
  }
416
427
 
417
428
  return {
429
+ changeFeedRetentionSize: changeFeedRetentionSize || 10000,
430
+ deviceCertificateBackendPublicKey,
418
431
  offlineGrantSigningKeys: offlineGrantSigningKeys.map((key) => normalizeOfflineGrantSigningKey(key)),
419
432
  offlineGrantTtlMs: offlineGrantTtlMs || 24 * 60 * 60 * 1000
420
433
  }
@@ -2042,6 +2055,12 @@ export default class VelociousConfiguration {
2042
2055
  */
2043
2056
  getWebsocketSessionGraceSeconds() { return this._websocketSessionGraceSeconds }
2044
2057
 
2058
+ /**
2059
+ * Runs get websocket session heartbeat seconds.
2060
+ * @returns {number} - Interval (seconds) between server→client heartbeat pings; 0 disables reaping.
2061
+ */
2062
+ getWebsocketSessionHeartbeatSeconds() { return this._websocketSessionHeartbeatSeconds }
2063
+
2045
2064
  /**
2046
2065
  * Registers a wrapper invoked around every WS-borne request /
2047
2066
  * connection message / channel dispatch. The wrapper receives the
@@ -2119,6 +2138,16 @@ export default class VelociousConfiguration {
2119
2138
  this._websocketSessionGraceSeconds = seconds
2120
2139
  }
2121
2140
 
2141
+ /**
2142
+ * Runs set websocket session heartbeat seconds.
2143
+ * @param {number} seconds
2144
+ * @returns {void}
2145
+ */
2146
+ setWebsocketSessionHeartbeatSeconds(seconds) {
2147
+ if (!Number.isFinite(seconds) || seconds < 0) throw new Error(`Invalid heartbeat seconds: ${seconds}`)
2148
+ this._websocketSessionHeartbeatSeconds = seconds
2149
+ }
2150
+
2122
2151
  /**
2123
2152
  * Moves a session into the paused registry and starts the grace
2124
2153
  * timer. When the timer fires, the session's permanent teardown
@@ -9,6 +9,12 @@
9
9
  * @property {boolean} [unique] - Whether the index should enforce uniqueness.
10
10
  * @property {string} tableName - Name of the table to add the index to.
11
11
  */
12
+ /**
13
+ * RemoveIndexSqlArgs type.
14
+ * @typedef {object} RemoveIndexSqlArgs
15
+ * @property {string} name - Index name to drop.
16
+ * @property {string} tableName - Name of the table the index belongs to.
17
+ */
12
18
  /**
13
19
  * DropTableSqlArgsType type.
14
20
  * @typedef {object} DropTableSqlArgsType
@@ -307,6 +313,16 @@ export default class VelociousDatabaseDriversBase {
307
313
  throw new Error("'createIndexSQLs' not implemented")
308
314
  }
309
315
 
316
+ /**
317
+ * Runs remove index sqls.
318
+ * @abstract
319
+ * @param {RemoveIndexSqlArgs} indexData - Index data.
320
+ * @returns {Promise<string[]>} - Resolves with SQL statements.
321
+ */
322
+ async removeIndexSQLs(indexData) { // eslint-disable-line no-unused-vars
323
+ throw new Error("'removeIndexSQLs' not implemented")
324
+ }
325
+
310
326
  /**
311
327
  * Runs create table.
312
328
  * @param {import("../table-data/index.js").default} tableData - Table data.
@@ -15,6 +15,7 @@ import Options from "./options.js"
15
15
  import mssql from "mssql"
16
16
  import net from "node:net"
17
17
  import QueryParser from "./query-parser.js"
18
+ import RemoveIndex from "./sql/remove-index.js"
18
19
  import Table from "./table.js"
19
20
  import StructureSql from "./structure-sql.js"
20
21
  import timeout from "awaitery/build/timeout.js"
@@ -113,6 +114,18 @@ export default class VelociousDatabaseDriversMssql extends Base{
113
114
  return await createIndex.toSQLs()
114
115
  }
115
116
 
117
+ /**
118
+ * Runs remove index sqls.
119
+ * @param {import("../base.js").RemoveIndexSqlArgs} indexData - Index data.
120
+ * @returns {Promise<string[]>} - Resolves with SQL statements.
121
+ */
122
+ async removeIndexSQLs(indexData) {
123
+ const removeArgs = Object.assign({driver: this}, indexData)
124
+ const removeIndex = new RemoveIndex(removeArgs)
125
+
126
+ return await removeIndex.toSQLs()
127
+ }
128
+
116
129
  /**
117
130
  * Runs create table sql.
118
131
  * @param {import("../../table-data/index.js").default} tableData - Table data.
@@ -0,0 +1,6 @@
1
+ // @ts-check
2
+
3
+ import RemoveIndexBase from "../../../query/remove-index-base.js"
4
+
5
+ export default class VelociousDatabaseConnectionDriversMssqlSqlRemoveIndex extends RemoveIndexBase {
6
+ }
@@ -14,6 +14,7 @@ import Options from "./options.js"
14
14
  import mysql from "mysql"
15
15
  import query from "./query.js"
16
16
  import QueryParser from "./query-parser.js"
17
+ import RemoveIndex from "./sql/remove-index.js"
17
18
  import Table from "./table.js"
18
19
  import StructureSql from "./structure-sql.js"
19
20
  import Upsert from "./sql/upsert.js"
@@ -227,6 +228,18 @@ export default class VelociousDatabaseDriversMysql extends Base{
227
228
  return await createIndex.toSQLs()
228
229
  }
229
230
 
231
+ /**
232
+ * Runs remove index sqls.
233
+ * @param {import("../base.js").RemoveIndexSqlArgs} indexData - Index data.
234
+ * @returns {Promise<string[]>} - Resolves with SQL statements.
235
+ */
236
+ async removeIndexSQLs(indexData) {
237
+ const removeArgs = Object.assign({driver: this}, indexData)
238
+ const removeIndex = new RemoveIndex(removeArgs)
239
+
240
+ return await removeIndex.toSQLs()
241
+ }
242
+
230
243
  /**
231
244
  * Runs create table sql.
232
245
  * @param {import("../../table-data/index.js").default} tableData - Table data.
@@ -0,0 +1,6 @@
1
+ // @ts-check
2
+
3
+ import RemoveIndexBase from "../../../query/remove-index-base.js"
4
+
5
+ export default class VelociousDatabaseConnectionDriversMysqlSqlRemoveIndex extends RemoveIndexBase {
6
+ }
@@ -14,6 +14,7 @@ import DropTable from "./sql/drop-table.js"
14
14
  import Insert from "./sql/insert.js"
15
15
  import Options from "./options.js"
16
16
  import QueryParser from "./query-parser.js"
17
+ import RemoveIndex from "./sql/remove-index.js"
17
18
  import Table from "./table.js"
18
19
  import StructureSql from "./structure-sql.js"
19
20
  import Upsert from "./sql/upsert.js"
@@ -149,6 +150,18 @@ export default class VelociousDatabaseDriversPgsql extends Base{
149
150
  return await createIndex.toSQLs()
150
151
  }
151
152
 
153
+ /**
154
+ * Runs remove index sqls.
155
+ * @param {import("../base.js").RemoveIndexSqlArgs} indexData - Index data.
156
+ * @returns {Promise<string[]>} - Resolves with SQL statements.
157
+ */
158
+ async removeIndexSQLs(indexData) {
159
+ const removeArgs = Object.assign({driver: this}, indexData)
160
+ const removeIndex = new RemoveIndex(removeArgs)
161
+
162
+ return await removeIndex.toSQLs()
163
+ }
164
+
152
165
  /**
153
166
  * Runs create table sql.
154
167
  * @param {import("../../table-data/index.js").default} tableData - Table data.
@@ -0,0 +1,6 @@
1
+ // @ts-check
2
+
3
+ import RemoveIndexBase from "../../../query/remove-index-base.js"
4
+
5
+ export default class VelociousDatabaseConnectionDriversPgsqlSqlRemoveIndex extends RemoveIndexBase {
6
+ }
@@ -12,6 +12,7 @@ import escapeString from "sql-escape-string"
12
12
  import Insert from "./sql/insert.js"
13
13
  import Options from "./options.js"
14
14
  import QueryParser from "./query-parser.js"
15
+ import RemoveIndex from "./sql/remove-index.js"
15
16
  import Table from "./table.js"
16
17
  import StructureSql from "./structure-sql.js"
17
18
  import Upsert from "./sql/upsert.js"
@@ -55,6 +56,18 @@ export default class VelociousDatabaseDriversSqliteBase extends Base {
55
56
  return await createIndex.toSQLs()
56
57
  }
57
58
 
59
+ /**
60
+ * Runs remove index sqls.
61
+ * @param {import("../base.js").RemoveIndexSqlArgs} indexData - Index data.
62
+ * @returns {Promise<string[]>} - Resolves with SQL statements.
63
+ */
64
+ async removeIndexSQLs(indexData) {
65
+ const removeArgs = Object.assign({driver: this}, indexData)
66
+ const removeIndex = new RemoveIndex(removeArgs)
67
+
68
+ return await removeIndex.toSQLs()
69
+ }
70
+
58
71
  /**
59
72
  * Runs create table sql.
60
73
  * @abstract
@@ -0,0 +1,6 @@
1
+ // @ts-check
2
+
3
+ import RemoveIndexBase from "../../../query/remove-index-base.js"
4
+
5
+ export default class VelociousDatabaseConnectionDriversSqliteSqlRemoveIndex extends RemoveIndexBase {
6
+ }
@@ -39,6 +39,7 @@
39
39
  import { convertLegacyDateValueToUtcStorage } from "../datetime-storage.js"
40
40
  import * as inflection from "inflection"
41
41
  import restArgsError from "../../utils/rest-args-error.js"
42
+ import CreateIndexBase from "../query/create-index-base.js"
42
43
  import TableData from "../table-data/index.js"
43
44
  class NotImplementedError extends Error {}
44
45
 
@@ -184,6 +185,49 @@ export default class VelociousDatabaseMigration {
184
185
  }
185
186
  }
186
187
 
188
+ /**
189
+ * RemoveIndexArgsType type.
190
+ * @typedef {object} RemoveIndexArgsType
191
+ * @property {string} [name] - Explicit index name to remove.
192
+ */
193
+ /**
194
+ * Runs remove index.
195
+ * @param {string} tableName - Table name.
196
+ * @param {string | Array<string | import("../table-data/table-column.js").default>} nameOrColumns - Index name or columns whose default addIndex name should be removed.
197
+ * @param {RemoveIndexArgsType} [args] - Options object.
198
+ * @returns {Promise<void>} - Resolves when complete.
199
+ */
200
+ async removeIndex(tableName, nameOrColumns, args = {}) {
201
+ const {name, ...restArgs} = args
202
+
203
+ restArgsError(restArgs)
204
+
205
+ const removeIndexName = name || this._removeIndexName(tableName, nameOrColumns)
206
+ const sqls = await this.getDriver().removeIndexSQLs({name: removeIndexName, tableName})
207
+
208
+ for (const sql of sqls) {
209
+ await this.getDriver().query(sql)
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Runs remove index name.
215
+ * @param {string} tableName - Table name.
216
+ * @param {string | Array<string | import("../table-data/table-column.js").default>} nameOrColumns - Index name or columns.
217
+ * @returns {string} - The index name.
218
+ */
219
+ _removeIndexName(tableName, nameOrColumns) {
220
+ if (typeof nameOrColumns === "string") return nameOrColumns
221
+
222
+ const createIndex = new CreateIndexBase({
223
+ columns: nameOrColumns,
224
+ driver: this.getDriver(),
225
+ tableName
226
+ })
227
+
228
+ return createIndex.generateIndexName()
229
+ }
230
+
187
231
  /**
188
232
  * AddForeignKeyArgsType type.
189
233
  * @typedef {object} AddForeignKeyArgsType
@@ -23,6 +23,7 @@ export default function baseMethodsForward(PoolBase) {
23
23
  "quote",
24
24
  "quoteColumn",
25
25
  "quoteTable",
26
+ "removeIndexSQLs",
26
27
  "select",
27
28
  "update",
28
29
  "updateSql"
@@ -0,0 +1,39 @@
1
+ // @ts-check
2
+
3
+ import QueryBase from "./base.js"
4
+
5
+ /**
6
+ * RemoveIndexBaseArgsType type.
7
+ * @typedef {object} RemoveIndexBaseArgsType
8
+ * @property {import("../drivers/base.js").default} driver - Database driver used to generate SQL.
9
+ * @property {string} name - Index name to drop.
10
+ * @property {string} tableName - Name of the table the index belongs to.
11
+ */
12
+
13
+ export default class VelociousDatabaseQueryRemoveIndexBase extends QueryBase {
14
+ /**
15
+ * Runs constructor.
16
+ * @param {RemoveIndexBaseArgsType} args - Options object.
17
+ */
18
+ constructor({driver, name, tableName}) {
19
+ super({driver})
20
+ this.name = name
21
+ this.tableName = tableName
22
+ }
23
+
24
+ /**
25
+ * Runs to sqls.
26
+ * @returns {Promise<string[]>} - Resolves with SQL statements.
27
+ */
28
+ async toSQLs() {
29
+ const databaseType = this.getDriver().getType()
30
+ const options = this.getOptions()
31
+ let sql = `DROP INDEX ${options.quoteIndexName(this.name)}`
32
+
33
+ if (databaseType == "mssql" || databaseType == "mysql") {
34
+ sql += ` ON ${options.quoteTableName(this.tableName)}`
35
+ }
36
+
37
+ return [sql]
38
+ }
39
+ }
@@ -690,11 +690,14 @@ class VelociousDatabaseRecord {
690
690
  return await this.relationshipOrLoad(relationshipName, {preloadTranslations: true})
691
691
  }
692
692
 
693
- prototype[`set${inflection.camelize(relationshipName)}`] = function(/** @type {?} */ model) {
693
+ prototype[`set${inflection.camelize(relationshipName)}`] = function(/** @type {VelociousDatabaseRecord | null | undefined} */ model) {
694
694
  const relationship = this.getRelationshipByName(relationshipName)
695
+ const foreignKeyValue = this._belongsToForeignKeyValue({model, relationship})
695
696
 
696
- relationship.setLoaded(model)
697
+ relationship.setLoaded(model || undefined)
698
+ relationship.setPreloaded(true)
697
699
  relationship.setDirty(true)
700
+ this._setColumnAttribute(relationship.getForeignKey(), foreignKeyValue)
698
701
  }
699
702
  } else if (actualData.type == "hasMany") {
700
703
  relationship = new HasManyRelationship(actualData)
@@ -1783,6 +1786,20 @@ class VelociousDatabaseRecord {
1783
1786
  return loaded.readColumn(relationship.getPrimaryKey()) == normalizedValue
1784
1787
  }
1785
1788
 
1789
+ /**
1790
+ * Returns the foreign key value for a belongs-to relationship assignment.
1791
+ * @param {object} args - Relationship assignment arguments.
1792
+ * @param {VelociousDatabaseRecord | null | undefined} args.model - Assigned model.
1793
+ * @param {import("./instance-relationships/base.js").default} args.relationship - Belongs-to relationship instance.
1794
+ * @returns {string | number | null | undefined} - Foreign key value for the assignment.
1795
+ */
1796
+ _belongsToForeignKeyValue({model, relationship}) {
1797
+ if (model == null) return null
1798
+ if (!(model instanceof VelociousDatabaseRecord)) throw new Error(`Unexpected model type: ${typeof model}`)
1799
+
1800
+ return /** @type {string | number | null | undefined} */ (model.readColumn(relationship.getPrimaryKey()))
1801
+ }
1802
+
1786
1803
  /**
1787
1804
  * Runs clear loaded belongs to relationship.
1788
1805
  * @param {?} relationship - Relationship instance.
@@ -2364,8 +2381,9 @@ class VelociousDatabaseRecord {
2364
2381
  await model.save()
2365
2382
 
2366
2383
  const foreignKey = this._relationshipForeignKeyAttribute(instanceRelationship)
2384
+ const foreignKeyValue = this._belongsToForeignKeyValue({model, relationship: instanceRelationship})
2367
2385
 
2368
- this.setAttribute(foreignKey, model.id())
2386
+ this.setAttribute(foreignKey, foreignKeyValue)
2369
2387
 
2370
2388
  instanceRelationship.setPreloaded(true)
2371
2389
  instanceRelationship.setDirty(false)
@@ -3801,14 +3819,12 @@ class VelociousDatabaseRecord {
3801
3819
  const relationship = this._instanceRelationships[relationshipName]
3802
3820
 
3803
3821
  if (relationship.getType() == "belongsTo" && relationship.getDirty()) {
3804
- const model = relationship.loaded()
3822
+ const model = relationship.getLoadedOrUndefined()
3805
3823
 
3806
3824
  if (model) {
3807
- if (model instanceof VelociousDatabaseRecord) {
3808
- belongsToChanges[relationship.getForeignKey()] = model?.id()
3809
- } else {
3810
- throw new Error(`Unexpected model type: ${typeof model}`)
3811
- }
3825
+ if (Array.isArray(model)) throw new Error("Unexpected belongs-to model array")
3826
+
3827
+ belongsToChanges[relationship.getForeignKey()] = this._belongsToForeignKeyValue({model, relationship})
3812
3828
  }
3813
3829
  }
3814
3830
  }
@@ -120,6 +120,7 @@ export default class DbGenerateModel extends BaseCommand {
120
120
 
121
121
  const columns = await table.getColumns()
122
122
  const writeAttributeTypeName = `${modelNameCamelized}WriteAttributes`
123
+ const belongsToWriteAttributes = await this.belongsToWriteAttributesForModel({modelClass, modelsDir})
123
124
  const nestedWriteAttributes = this.nestedWriteAttributesForModel({modelClass})
124
125
 
125
126
  fileContent += `import DatabaseRecord from "${velociousPath}/database/record/index.js"\n\n`
@@ -135,6 +136,9 @@ export default class DbGenerateModel extends BaseCommand {
135
136
  fileContent += ` * @property {${setterJsdocType}${column.getNull() ? " | null" : ""}} [${camelizedColumnName}] - Value for the ${camelizedColumnName} attribute.\n`
136
137
  }
137
138
  }
139
+ for (const belongsToWriteAttribute of belongsToWriteAttributes) {
140
+ fileContent += ` * @property {${belongsToWriteAttribute.propertyType}} [${belongsToWriteAttribute.propertyName}] - Related ${belongsToWriteAttribute.relationshipName} record.\n`
141
+ }
138
142
  for (const nestedWriteAttribute of nestedWriteAttributes) {
139
143
  fileContent += ` * @property {${nestedWriteAttribute.propertyType}} [${nestedWriteAttribute.propertyName}] - Nested ${nestedWriteAttribute.relationshipName} attributes.\n`
140
144
  }
@@ -440,6 +444,38 @@ export default class DbGenerateModel extends BaseCommand {
440
444
  return this.jsDocTypeFromColumn(column, modelClass)
441
445
  }
442
446
 
447
+ /**
448
+ * Runs belongs to write attributes for model.
449
+ * @param {object} args - Arguments.
450
+ * @param {typeof import("../../../../../database/record/index.js").default} args.modelClass - Model class.
451
+ * @param {string} args.modelsDir - Source models directory.
452
+ * @returns {Promise<Array<{propertyName: string, propertyType: string, relationshipName: string}>>} - Belongs-to write attributes.
453
+ */
454
+ async belongsToWriteAttributesForModel({modelClass, modelsDir}) {
455
+ const writeAttributes = []
456
+
457
+ for (const relationship of modelClass.getRelationships()) {
458
+ if (relationship.getType() !== "belongsTo") continue
459
+ if (relationship.getPolymorphic()) continue
460
+
461
+ const targetModelClass = relationship.getTargetModelClass()
462
+
463
+ if (!targetModelClass) throw new Error(`Relationship '${relationship.getRelationshipName()}' on '${modelClass.getModelName()}' has no target model class`)
464
+
465
+ const targetModelFileName = inflection.dasherize(inflection.underscore(targetModelClass.getModelName()))
466
+ const targetModelPath = `${modelsDir}/${targetModelFileName}.js`
467
+ const targetImportPath = await fileExists(targetModelPath) ? `../models/${targetModelFileName}.js` : `./${targetModelFileName}.js`
468
+
469
+ writeAttributes.push({
470
+ propertyName: relationship.getRelationshipName(),
471
+ propertyType: `import("${targetImportPath}").default`,
472
+ relationshipName: relationship.getRelationshipName()
473
+ })
474
+ }
475
+
476
+ return writeAttributes
477
+ }
478
+
443
479
  /**
444
480
  * Runs nested write attributes for model.
445
481
  * @param {object} args - Arguments.