velocious 1.0.442 → 1.0.444

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 (63) hide show
  1. package/build/authorization/base-resource.js +2 -2
  2. package/build/beacon/client.js +13 -7
  3. package/build/beacon/server.js +11 -0
  4. package/build/configuration-types.js +5 -1
  5. package/build/controller.js +1 -1
  6. package/build/database/record/acts-as-list.js +89 -24
  7. package/build/database/record/index.js +53 -31
  8. package/build/database/record/relationships/belongs-to.js +1 -1
  9. package/build/database/record/relationships/has-many.js +3 -1
  10. package/build/database/record/relationships/has-one.js +3 -1
  11. package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +2 -2
  12. package/build/frontend-model-resource/base-resource.js +9 -10
  13. package/build/frontend-models/base.js +6 -6
  14. package/build/frontend-models/query.js +2 -2
  15. package/build/src/authorization/base-resource.d.ts +4 -4
  16. package/build/src/authorization/base-resource.d.ts.map +1 -1
  17. package/build/src/authorization/base-resource.js +3 -3
  18. package/build/src/beacon/client.d.ts.map +1 -1
  19. package/build/src/beacon/client.js +13 -8
  20. package/build/src/beacon/server.d.ts +5 -0
  21. package/build/src/beacon/server.d.ts.map +1 -1
  22. package/build/src/beacon/server.js +11 -1
  23. package/build/src/configuration-types.d.ts +7 -3
  24. package/build/src/configuration-types.d.ts.map +1 -1
  25. package/build/src/configuration-types.js +5 -2
  26. package/build/src/controller.d.ts +3 -3
  27. package/build/src/controller.d.ts.map +1 -1
  28. package/build/src/controller.js +2 -2
  29. package/build/src/database/record/acts-as-list.d.ts.map +1 -1
  30. package/build/src/database/record/acts-as-list.js +80 -23
  31. package/build/src/database/record/index.d.ts +48 -37
  32. package/build/src/database/record/index.d.ts.map +1 -1
  33. package/build/src/database/record/index.js +50 -32
  34. package/build/src/database/record/relationships/belongs-to.js +2 -2
  35. package/build/src/database/record/relationships/has-many.d.ts.map +1 -1
  36. package/build/src/database/record/relationships/has-many.js +3 -2
  37. package/build/src/database/record/relationships/has-one.d.ts.map +1 -1
  38. package/build/src/database/record/relationships/has-one.js +3 -2
  39. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +3 -3
  40. package/build/src/frontend-model-resource/base-resource.d.ts +15 -16
  41. package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
  42. package/build/src/frontend-model-resource/base-resource.js +10 -11
  43. package/build/src/frontend-models/base.d.ts +18 -12
  44. package/build/src/frontend-models/base.d.ts.map +1 -1
  45. package/build/src/frontend-models/base.js +7 -7
  46. package/build/src/frontend-models/query.d.ts +4 -4
  47. package/build/src/frontend-models/query.d.ts.map +1 -1
  48. package/build/src/frontend-models/query.js +3 -3
  49. package/package.json +1 -1
  50. package/src/authorization/base-resource.js +2 -2
  51. package/src/beacon/client.js +13 -7
  52. package/src/beacon/server.js +11 -0
  53. package/src/configuration-types.js +5 -1
  54. package/src/controller.js +1 -1
  55. package/src/database/record/acts-as-list.js +89 -24
  56. package/src/database/record/index.js +53 -31
  57. package/src/database/record/relationships/belongs-to.js +1 -1
  58. package/src/database/record/relationships/has-many.js +3 -1
  59. package/src/database/record/relationships/has-one.js +3 -1
  60. package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +2 -2
  61. package/src/frontend-model-resource/base-resource.js +9 -10
  62. package/src/frontend-models/base.js +6 -6
  63. package/src/frontend-models/query.js +2 -2
@@ -111,7 +111,7 @@ export default class AuthorizationBaseResource {
111
111
 
112
112
  /**
113
113
  * Runs current user.
114
- * @returns {import("../configuration-types.js").VelociousLooseObject | null | undefined} - Current user from context.
114
+ * @returns {unknown} - Current user from context.
115
115
  */
116
116
  currentUser() {
117
117
  return this.context.currentUser
@@ -127,7 +127,7 @@ export default class AuthorizationBaseResource {
127
127
 
128
128
  /**
129
129
  * Runs params.
130
- * @returns {import("../configuration-types.js").VelociousLooseObject | undefined} - Params from context.
130
+ * @returns {import("../configuration-types.js").VelociousParams | undefined} - Params from context.
131
131
  */
132
132
  params() {
133
133
  return this.context.params
@@ -225,14 +225,20 @@ export default class BeaconClient extends EventEmitter {
225
225
  this._reconnectTimer = undefined
226
226
  }
227
227
 
228
- if (this._socket) {
229
- const socket = this._socket
228
+ const socket = this._socket
230
229
 
231
- await new Promise((resolve) => {
232
- socket.once("close", () => resolve(undefined))
233
- socket.end()
234
- })
235
- }
230
+ if (!socket) return
231
+
232
+ this._socket = undefined
233
+ this._jsonSocket = undefined
234
+
235
+ if (socket.destroyed) return
236
+
237
+ await new Promise((resolve) => {
238
+ socket.once("close", () => resolve(undefined))
239
+ socket.end()
240
+ socket.destroySoon()
241
+ })
236
242
  }
237
243
 
238
244
  /**
@@ -37,6 +37,11 @@ export default class BeaconServer {
37
37
  * Narrows the runtime value to the documented type.
38
38
  @type {net.Server | undefined} */
39
39
  this.server = undefined
40
+ /**
41
+ * Accepted sockets, including connections that have not completed the hello handshake yet.
42
+ * @type {Set<net.Socket>}
43
+ */
44
+ this.sockets = new Set()
40
45
  }
41
46
 
42
47
  /**
@@ -68,6 +73,10 @@ export default class BeaconServer {
68
73
  peer.close()
69
74
  }
70
75
 
76
+ for (const socket of this.sockets) {
77
+ socket.destroy()
78
+ }
79
+
71
80
  if (!this.server) return
72
81
 
73
82
  const {server} = this
@@ -97,6 +106,7 @@ export default class BeaconServer {
97
106
  * @returns {void}
98
107
  */
99
108
  _handleConnection(socket) {
109
+ this.sockets.add(socket)
100
110
  const jsonSocket = new JsonSocket(socket)
101
111
  /**
102
112
  * Defines peerId.
@@ -104,6 +114,7 @@ export default class BeaconServer {
104
114
  let peerId
105
115
 
106
116
  const cleanup = () => {
117
+ this.sockets.delete(socket)
107
118
  this.peers.delete(jsonSocket)
108
119
  }
109
120
 
@@ -202,7 +202,11 @@
202
202
  */
203
203
 
204
204
  /**
205
- * @typedef {Record<string, unknown> & {configuration?: import("./configuration.js").default, currentUser?: Record<string, unknown> | null, params?: Record<string, unknown>, request?: import("./http-server/client/request.js").default | import("./http-server/client/websocket-request.js").default}} VelociousLooseObject
205
+ * @typedef {Record<string, string>} VelociousParams
206
+ */
207
+
208
+ /**
209
+ * @typedef {Record<string, unknown> & {configuration?: import("./configuration.js").default, currentUser?: unknown, params?: VelociousParams, request?: import("./http-server/client/request.js").default | import("./http-server/client/websocket-request.js").default}} VelociousLooseObject
206
210
  */
207
211
 
208
212
  /**
@@ -34,7 +34,7 @@ export default class VelociousController {
34
34
  * @param {string} args.action - Action.
35
35
  * @param {import("./configuration.js").default} args.configuration - Configuration instance.
36
36
  * @param {string} args.controller - Controller.
37
- * @param {object} args.params - Parameters object.
37
+ * @param {Record<string, ?>} args.params - Parameters object.
38
38
  * @param {import("./http-server/client/request.js").default} args.request - Request object.
39
39
  * @param {import("./http-server/client/response.js").default} args.response - Response object.
40
40
  * @param {string} args.viewPath - View path.
@@ -67,14 +67,15 @@ export default function registerActsAsListCallbacks(modelClass, positionColumn,
67
67
  @type {typeof import("./index.js").default} */ (record.constructor)
68
68
  const posColumn = modelClass.getColumnNameForAttributeName(positionColumn)
69
69
  const scopeCol = modelClass.getColumnNameForAttributeName(scope)
70
- const rawAttributes = /**
71
- * Narrows the runtime value to the documented type.
72
- @type {Record<string, ?>} */ (record._attributes || {})
73
- const changes = /**
74
- * Narrows the runtime value to the documented type.
75
- @type {Record<string, ?>} */ (record._changes || {})
70
+ /** @type {Record<string, ?>} */
71
+ const rawAttributes = record._attributes || {}
72
+ /** @type {Record<string, ?>} */
73
+ const changes = record._changes || {}
74
+ /** @type {Set<string>} */
75
+ const assignedAttributeNames = record._assignedAttributeNames || new Set()
76
76
  const posChanged = posColumn in changes
77
77
  const scopeChanged = scopeCol in changes
78
+ const posAssigned = assignedAttributeNames.has(positionColumn)
78
79
 
79
80
  if (!posChanged && !scopeChanged) return
80
81
 
@@ -102,18 +103,13 @@ export default function registerActsAsListCallbacks(modelClass, positionColumn,
102
103
  if (oldPosition == null || newPosition == null) return
103
104
  if (newPosition === oldPosition && newScopeValue === oldScopeValue) return
104
105
 
105
- // Move the record out of the way before shifting others to avoid
106
- // intermediate UNIQUE constraint violations. Use the old scope value
107
- // for the move-out-of-way because the record is still in the old scope.
108
- await moveOutOfWay({record, positionColumn, scope, scopeValue: oldScopeValue})
109
- setShiftingFlag(record, false)
110
-
111
106
  if (scopeChanged && oldScopeValue !== newScopeValue) {
112
- let targetPosition = newPosition
113
-
114
107
  // When only the scope changes without a new position, append to the end
115
108
  // of the new scope. There is no target-scope row to shift out of the way.
116
- if (!posChanged) {
109
+ if (!posAssigned) {
110
+ await moveOutOfWay({record, positionColumn, scope, scopeValue: oldScopeValue})
111
+ setShiftingFlag(record, false)
112
+
117
113
  const highestNew = await highestPositionInScope({record, positionColumn, scope, scopeValue: newScopeValue})
118
114
  const nextPos = highestNew + 1
119
115
 
@@ -122,11 +118,17 @@ export default function registerActsAsListCallbacks(modelClass, positionColumn,
122
118
  return
123
119
  }
124
120
 
121
+ await moveOutOfWay({record, positionColumn, scope, scopeValue: oldScopeValue, targetScopeValue: newScopeValue})
122
+ setShiftingFlag(record, false)
125
123
  await shiftPositionsDown({record, positionColumn, scope, scopeValue: oldScopeValue, fromPosition: oldPosition + 1})
126
- await shiftPositionsUp({record, positionColumn, scope, scopeValue: newScopeValue, fromPosition: targetPosition})
124
+ await shiftPositionsUp({record, positionColumn, scope, scopeValue: newScopeValue, fromPosition: newPosition, excludeRecordId: record.id()})
125
+ await placeMovedRecord({record, positionColumn, scope, scopeValue: newScopeValue, position: newPosition})
127
126
  return
128
127
  }
129
128
 
129
+ await moveOutOfWay({record, positionColumn, scope, scopeValue: oldScopeValue})
130
+ setShiftingFlag(record, false)
131
+
130
132
  if (newPosition < oldPosition) {
131
133
  await shiftPositionsUp({record, positionColumn, scope, fromPosition: newPosition, toPosition: oldPosition})
132
134
  } else if (newPosition > oldPosition) {
@@ -146,6 +148,53 @@ export default function registerActsAsListCallbacks(modelClass, positionColumn,
146
148
  })
147
149
  }
148
150
 
151
+ /**
152
+ * Places a moved row after surrounding rows have shifted.
153
+ * @param {object} args - Arguments.
154
+ * @param {import("./index.js").default} args.record - Model instance.
155
+ * @param {string} args.positionColumn - Position attribute name.
156
+ * @param {string} args.scope - Scope attribute name.
157
+ * @param {string | number} args.scopeValue - Destination scope value.
158
+ * @param {number} args.position - Destination position.
159
+ * @returns {Promise<void>} Resolves after placement.
160
+ */
161
+ async function placeMovedRecord({record, positionColumn, scope, scopeValue, position}) {
162
+ const modelClass = /** @type {typeof import("./index.js").default} */ (record.constructor)
163
+ const connection = modelClass.connection()
164
+ const tableSql = connection.quoteTable(modelClass._getTable().getName())
165
+ const scopeCol = modelClass.getColumnNameForAttributeName(scope)
166
+ const posCol = modelClass.getColumnNameForAttributeName(positionColumn)
167
+ const preservedChanges = {...record._changes}
168
+ const scopeColumnSql = connection.quoteColumn(scopeCol)
169
+ const positionColumnSql = connection.quoteColumn(posCol)
170
+ const primaryKeySql = connection.quoteColumn(modelClass.primaryKey())
171
+
172
+ delete preservedChanges[scopeCol]
173
+ delete preservedChanges[posCol]
174
+
175
+ await connection.query(
176
+ `UPDATE ${tableSql} SET ${scopeColumnSql} = ${connection.quote(scopeValue)}, ${positionColumnSql} = ${connection.quote(position)} WHERE ${primaryKeySql} = ${connection.quote(record.id())}`
177
+ )
178
+ await record._reloadWithId(record.id())
179
+ record._changes = preservedChanges
180
+ clearBelongsToChangeForScope(record)
181
+ }
182
+
183
+ /**
184
+ * Clears dirty belongs-to state for the scope FK after direct placement.
185
+ * @param {import("./index.js").default} record - Model instance.
186
+ * @returns {void} Nothing.
187
+ */
188
+ function clearBelongsToChangeForScope(record) {
189
+ for (const relationshipName in record._instanceRelationships || {}) {
190
+ const relationship = record._instanceRelationships[relationshipName]
191
+
192
+ if (relationship.getType() !== "belongsTo") continue
193
+
194
+ relationship.setDirty(false)
195
+ }
196
+ }
197
+
149
198
  /**
150
199
  * Bumps positions UP by 1 in the range [fromPosition, toPosition) within the
151
200
  * same scope. Updates in descending order to avoid intermediate UNIQUE
@@ -157,9 +206,10 @@ export default function registerActsAsListCallbacks(modelClass, positionColumn,
157
206
  * @param {number} args.fromPosition - Starting position (inclusive).
158
207
  * @param {number} [args.toPosition] - Ending position (exclusive).
159
208
  * @param {string | number} [args.scopeValue] - Explicit scope value.
209
+ * @param {string | number} [args.excludeRecordId] - Record id to exclude from shifts.
160
210
  * @returns {Promise<void>}
161
211
  */
162
- async function shiftPositionsUp({record, positionColumn, scope, fromPosition, toPosition, scopeValue}) {
212
+ async function shiftPositionsUp({record, positionColumn, scope, fromPosition, toPosition, scopeValue, excludeRecordId}) {
163
213
  const modelClass = /**
164
214
  * Narrows the runtime value to the documented type.
165
215
  @type {typeof import("./index.js").default} */ (record.constructor)
@@ -173,16 +223,25 @@ async function shiftPositionsUp({record, positionColumn, scope, fromPosition, to
173
223
  const positionColumnName = modelClass.getColumnNameForAttributeName(positionColumn)
174
224
  const positionColumnSql = connection.quoteColumn(positionColumnName)
175
225
  const scopeColumnSql = connection.quoteColumn(scopeColumnName)
226
+ const primaryKeySql = connection.quoteColumn(modelClass.primaryKey())
176
227
  const tableSql = connection.quoteTable(tableName)
177
228
  const quotedScope = connection.quote(resolvedScopeValue)
178
229
 
179
230
  // Load rows in descending order so we bump the highest first
180
231
  let query = modelClass
232
+ .select(modelClass.primaryKey())
181
233
  .select(positionColumn)
182
234
  .where({[scopeColumnName]: resolvedScopeValue})
183
235
  .where(`${positionColumnSql} >= ${connection.quote(fromPosition)}`)
236
+ .where(`${positionColumnSql} > 0`)
184
237
  .order(`${positionColumnSql} DESC`)
185
238
 
239
+ const recordIdToExclude = excludeRecordId || (record.isPersisted() ? record.id() : null)
240
+
241
+ if (recordIdToExclude != null) {
242
+ query = query.where(`${primaryKeySql} != ${connection.quote(recordIdToExclude)}`)
243
+ }
244
+
186
245
  if (toPosition != null) {
187
246
  query = query.where(`${positionColumnSql} < ${connection.quote(toPosition)}`)
188
247
  }
@@ -196,7 +255,7 @@ async function shiftPositionsUp({record, positionColumn, scope, fromPosition, to
196
255
  const currentPos = Number(row.readAttribute(positionColumn))
197
256
 
198
257
  await connection.query(
199
- `UPDATE ${tableSql} SET ${positionColumnSql} = ${positionColumnSql} + 1 WHERE ${scopeColumnSql} = ${quotedScope} AND ${positionColumnSql} = ${connection.quote(currentPos)}`
258
+ `UPDATE ${tableSql} SET ${positionColumnSql} = ${positionColumnSql} + 1 WHERE ${primaryKeySql} = ${connection.quote(row.id())} AND ${scopeColumnSql} = ${quotedScope} AND ${positionColumnSql} = ${connection.quote(currentPos)}`
200
259
  )
201
260
  }
202
261
  } finally {
@@ -231,14 +290,18 @@ async function shiftPositionsDown({record, positionColumn, scope, fromPosition,
231
290
  const positionColumnName = modelClass.getColumnNameForAttributeName(positionColumn)
232
291
  const positionColumnSql = connection.quoteColumn(positionColumnName)
233
292
  const scopeColumnSql = connection.quoteColumn(scopeColumnName)
293
+ const primaryKeySql = connection.quoteColumn(modelClass.primaryKey())
234
294
  const tableSql = connection.quoteTable(tableName)
235
295
  const quotedScope = connection.quote(resolvedScopeValue)
236
296
 
237
297
  // Load rows in ascending order so we shift the lowest gap first
238
298
  let query = modelClass
299
+ .select(modelClass.primaryKey())
239
300
  .select(positionColumn)
240
301
  .where({[scopeColumnName]: resolvedScopeValue})
241
302
  .where(`${positionColumnSql} >= ${connection.quote(fromPosition)}`)
303
+ .where(`${positionColumnSql} > 0`)
304
+ .where(`${primaryKeySql} != ${connection.quote(record.id())}`)
242
305
  .order({column: positionColumnName, direction: "ASC"})
243
306
 
244
307
  if (toPosition != null) {
@@ -254,7 +317,7 @@ async function shiftPositionsDown({record, positionColumn, scope, fromPosition,
254
317
  const currentPos = Number(row.readAttribute(positionColumn))
255
318
 
256
319
  await connection.query(
257
- `UPDATE ${tableSql} SET ${positionColumnSql} = ${positionColumnSql} - 1 WHERE ${scopeColumnSql} = ${quotedScope} AND ${positionColumnSql} = ${connection.quote(currentPos)}`
320
+ `UPDATE ${tableSql} SET ${positionColumnSql} = ${positionColumnSql} - 1 WHERE ${primaryKeySql} = ${connection.quote(row.id())} AND ${scopeColumnSql} = ${quotedScope} AND ${positionColumnSql} = ${connection.quote(currentPos)}`
258
321
  )
259
322
  }
260
323
  } finally {
@@ -342,21 +405,23 @@ function resolveScopeValue(record, scope) {
342
405
  * @param {import("./index.js").default} args.record - Model instance.
343
406
  * @param {string} args.positionColumn - camelCase position attribute.
344
407
  * @param {string} args.scope - camelCase scope attribute.
345
- * @param {string | number | null} [args.scopeValue] - Explicit scope value (defaults to resolveScopeValue).
408
+ * @param {string | number | null} [args.scopeValue] - Scope containing the record before move-out.
409
+ * @param {string | number | null} [args.targetScopeValue] - Temporary scope value to assign.
346
410
  * @returns {Promise<void>}
347
411
  */
348
- async function moveOutOfWay({record, positionColumn, scope, scopeValue}) {
412
+ async function moveOutOfWay({record, positionColumn, scope, scopeValue, targetScopeValue}) {
349
413
  const modelClass = /**
350
414
  * Narrows the runtime value to the documented type.
351
415
  @type {typeof import("./index.js").default} */ (record.constructor)
352
416
  const connection = modelClass.connection()
353
417
  const tableName = modelClass._getTable().getName()
354
418
  const resolvedScopeValue = scopeValue != null ? scopeValue : resolveScopeValue(record, scope)
419
+ const resolvedTargetScopeValue = targetScopeValue != null ? targetScopeValue : resolvedScopeValue
355
420
 
356
421
  if (resolvedScopeValue == null) return
422
+ if (resolvedTargetScopeValue == null) return
357
423
 
358
- const highest = await highestPositionInScope({record, positionColumn, scope, scopeValue: resolvedScopeValue})
359
- const tempPosition = highest + 10000
424
+ const tempPosition = -record.id()
360
425
  const positionColumnSql = connection.quoteColumn(modelClass.getColumnNameForAttributeName(positionColumn))
361
426
  const scopeColumnSql = connection.quoteColumn(modelClass.getColumnNameForAttributeName(scope))
362
427
  const tableSql = connection.quoteTable(tableName)
@@ -366,7 +431,7 @@ async function moveOutOfWay({record, positionColumn, scope, scopeValue}) {
366
431
 
367
432
  try {
368
433
  await connection.query(
369
- `UPDATE ${tableSql} SET ${positionColumnSql} = ${connection.quote(tempPosition)} WHERE ${scopeColumnSql} = ${connection.quote(resolvedScopeValue)} AND ${pkSql} = ${connection.quote(record.id())}`
434
+ `UPDATE ${tableSql} SET ${scopeColumnSql} = ${connection.quote(resolvedTargetScopeValue)}, ${positionColumnSql} = ${connection.quote(tempPosition)} WHERE ${scopeColumnSql} = ${connection.quote(resolvedScopeValue)} AND ${pkSql} = ${connection.quote(record.id())}`
370
435
  )
371
436
  } finally {
372
437
  // Don't clear the flag here — the caller will do that after shifts
@@ -14,7 +14,7 @@
14
14
  /**
15
15
  * Model class constructor type used for static `this` typing.
16
16
  * @template {VelociousDatabaseRecord} T
17
- * @typedef {{new (...args: Array<never>): T}} ModelConstructor
17
+ * @typedef {{new (changes?: Record<string, unknown>): T}} ModelConstructor
18
18
  */
19
19
 
20
20
  import timeout from "awaitery/build/timeout.js"
@@ -435,6 +435,12 @@ class VelociousDatabaseRecord {
435
435
  @type {Record<string, ?>} */
436
436
  _changes = {}
437
437
 
438
+ /**
439
+ * Attribute names explicitly assigned in the current update call.
440
+ @type {Set<string> | undefined}
441
+ */
442
+ _assignedAttributeNames = undefined
443
+
438
444
  /**
439
445
  * Columns as hash.
440
446
  @type {Record<string, import("../drivers/base-column.js").default>} */
@@ -501,9 +507,9 @@ class VelociousDatabaseRecord {
501
507
 
502
508
  /**
503
509
  * Runs before validation.
504
- * @template {VelociousDatabaseRecord} T
505
- * @this {ModelConstructor<T> & typeof VelociousDatabaseRecord}
506
- * @param {LifecycleCallbackType<T>} callback - Callback function or instance method name.
510
+ * @template {typeof VelociousDatabaseRecord} MC
511
+ * @this {MC}
512
+ * @param {LifecycleCallbackType<InstanceType<MC>>} callback - Callback function or instance method name.
507
513
  * @returns {void}
508
514
  */
509
515
  static beforeValidation(callback) {
@@ -512,9 +518,9 @@ class VelociousDatabaseRecord {
512
518
 
513
519
  /**
514
520
  * Runs before save.
515
- * @template {VelociousDatabaseRecord} T
516
- * @this {ModelConstructor<T> & typeof VelociousDatabaseRecord}
517
- * @param {LifecycleCallbackType<T>} callback - Callback function or instance method name.
521
+ * @template {typeof VelociousDatabaseRecord} MC
522
+ * @this {MC}
523
+ * @param {LifecycleCallbackType<InstanceType<MC>>} callback - Callback function or instance method name.
518
524
  * @returns {void}
519
525
  */
520
526
  static beforeSave(callback) {
@@ -523,9 +529,9 @@ class VelociousDatabaseRecord {
523
529
 
524
530
  /**
525
531
  * Runs before create.
526
- * @template {VelociousDatabaseRecord} T
527
- * @this {ModelConstructor<T> & typeof VelociousDatabaseRecord}
528
- * @param {LifecycleCallbackType<T>} callback - Callback function or instance method name.
532
+ * @template {typeof VelociousDatabaseRecord} MC
533
+ * @this {MC}
534
+ * @param {LifecycleCallbackType<InstanceType<MC>>} callback - Callback function or instance method name.
529
535
  * @returns {void}
530
536
  */
531
537
  static beforeCreate(callback) {
@@ -534,9 +540,9 @@ class VelociousDatabaseRecord {
534
540
 
535
541
  /**
536
542
  * Runs before update.
537
- * @template {VelociousDatabaseRecord} T
538
- * @this {ModelConstructor<T> & typeof VelociousDatabaseRecord}
539
- * @param {LifecycleCallbackType<T>} callback - Callback function or instance method name.
543
+ * @template {typeof VelociousDatabaseRecord} MC
544
+ * @this {MC}
545
+ * @param {LifecycleCallbackType<InstanceType<MC>>} callback - Callback function or instance method name.
540
546
  * @returns {void}
541
547
  */
542
548
  static beforeUpdate(callback) {
@@ -545,9 +551,9 @@ class VelociousDatabaseRecord {
545
551
 
546
552
  /**
547
553
  * Runs before destroy.
548
- * @template {VelociousDatabaseRecord} T
549
- * @this {ModelConstructor<T> & typeof VelociousDatabaseRecord}
550
- * @param {LifecycleCallbackType<T>} callback - Callback function or instance method name.
554
+ * @template {typeof VelociousDatabaseRecord} MC
555
+ * @this {MC}
556
+ * @param {LifecycleCallbackType<InstanceType<MC>>} callback - Callback function or instance method name.
551
557
  * @returns {void}
552
558
  */
553
559
  static beforeDestroy(callback) {
@@ -556,9 +562,9 @@ class VelociousDatabaseRecord {
556
562
 
557
563
  /**
558
564
  * Runs after save.
559
- * @template {VelociousDatabaseRecord} T
560
- * @this {ModelConstructor<T> & typeof VelociousDatabaseRecord}
561
- * @param {LifecycleCallbackType<T>} callback - Callback function or instance method name.
565
+ * @template {typeof VelociousDatabaseRecord} MC
566
+ * @this {MC}
567
+ * @param {LifecycleCallbackType<InstanceType<MC>>} callback - Callback function or instance method name.
562
568
  * @returns {void}
563
569
  */
564
570
  static afterSave(callback) {
@@ -567,9 +573,9 @@ class VelociousDatabaseRecord {
567
573
 
568
574
  /**
569
575
  * Runs after create.
570
- * @template {VelociousDatabaseRecord} T
571
- * @this {ModelConstructor<T> & typeof VelociousDatabaseRecord}
572
- * @param {LifecycleCallbackType<T>} callback - Callback function or instance method name.
576
+ * @template {typeof VelociousDatabaseRecord} MC
577
+ * @this {MC}
578
+ * @param {LifecycleCallbackType<InstanceType<MC>>} callback - Callback function or instance method name.
573
579
  * @returns {void}
574
580
  */
575
581
  static afterCreate(callback) {
@@ -578,9 +584,9 @@ class VelociousDatabaseRecord {
578
584
 
579
585
  /**
580
586
  * Runs after update.
581
- * @template {VelociousDatabaseRecord} T
582
- * @this {ModelConstructor<T> & typeof VelociousDatabaseRecord}
583
- * @param {LifecycleCallbackType<T>} callback - Callback function or instance method name.
587
+ * @template {typeof VelociousDatabaseRecord} MC
588
+ * @this {MC}
589
+ * @param {LifecycleCallbackType<InstanceType<MC>>} callback - Callback function or instance method name.
584
590
  * @returns {void}
585
591
  */
586
592
  static afterUpdate(callback) {
@@ -589,9 +595,9 @@ class VelociousDatabaseRecord {
589
595
 
590
596
  /**
591
597
  * Runs after destroy.
592
- * @template {VelociousDatabaseRecord} T
593
- * @this {ModelConstructor<T> & typeof VelociousDatabaseRecord}
594
- * @param {LifecycleCallbackType<T>} callback - Callback function or instance method name.
598
+ * @template {typeof VelociousDatabaseRecord} MC
599
+ * @this {MC}
600
+ * @param {LifecycleCallbackType<InstanceType<MC>>} callback - Callback function or instance method name.
595
601
  * @returns {void}
596
602
  */
597
603
  static afterDestroy(callback) {
@@ -2414,6 +2420,8 @@ class VelociousDatabaseRecord {
2414
2420
  })
2415
2421
  })
2416
2422
 
2423
+ this._assignedAttributeNames = undefined
2424
+
2417
2425
  return result
2418
2426
  }
2419
2427
 
@@ -2438,7 +2446,7 @@ class VelociousDatabaseRecord {
2438
2446
  if (model.isChanged()) {
2439
2447
  await model.save()
2440
2448
 
2441
- const foreignKey = instanceRelationship.getForeignKey()
2449
+ const foreignKey = this._relationshipForeignKeyAttribute(instanceRelationship)
2442
2450
 
2443
2451
  this.setAttribute(foreignKey, model.id())
2444
2452
 
@@ -2493,7 +2501,7 @@ class VelociousDatabaseRecord {
2493
2501
 
2494
2502
  if (loaded) {
2495
2503
  for (const model of loaded) {
2496
- const foreignKey = instanceRelationship.getForeignKey()
2504
+ const foreignKey = model._relationshipForeignKeyAttribute(instanceRelationship)
2497
2505
 
2498
2506
  model.setAttribute(foreignKey, this.id())
2499
2507
 
@@ -2510,6 +2518,17 @@ class VelociousDatabaseRecord {
2510
2518
  return relationships
2511
2519
  }
2512
2520
 
2521
+ /**
2522
+ * Resolves a relationship foreign-key column to this model's public attribute name.
2523
+ * @param {import("./instance-relationships/base.js").default<typeof VelociousDatabaseRecord, typeof VelociousDatabaseRecord>} instanceRelationship - Relationship instance.
2524
+ * @returns {string} Attribute name accepted by setAttribute/assign.
2525
+ */
2526
+ _relationshipForeignKeyAttribute(instanceRelationship) {
2527
+ const foreignKey = instanceRelationship.getForeignKey()
2528
+
2529
+ return this.getModelClass().getColumnNameToAttributeNameMap()[foreignKey] || foreignKey
2530
+ }
2531
+
2513
2532
  /**
2514
2533
  * Runs auto save has many and has one relationships.
2515
2534
  * @param {object} args - Options object.
@@ -2535,7 +2554,7 @@ class VelociousDatabaseRecord {
2535
2554
  }
2536
2555
 
2537
2556
  for (const model of loaded) {
2538
- const foreignKey = instanceRelationship.getForeignKey()
2557
+ const foreignKey = model._relationshipForeignKeyAttribute(instanceRelationship)
2539
2558
 
2540
2559
  model.setAttribute(foreignKey, this.id())
2541
2560
 
@@ -3363,7 +3382,9 @@ class VelociousDatabaseRecord {
3363
3382
  * @returns {void} - No return value.
3364
3383
  */
3365
3384
  assign(attributesToAssign) {
3385
+ this._assignedAttributeNames ||= new Set()
3366
3386
  for (const attributeToAssign in attributesToAssign) {
3387
+ this._assignedAttributeNames.add(attributeToAssign)
3367
3388
  this.setAttribute(attributeToAssign, attributesToAssign[attributeToAssign])
3368
3389
  }
3369
3390
  }
@@ -4167,6 +4188,7 @@ class VelociousDatabaseRecord {
4167
4188
 
4168
4189
  this._attributes = reloadedModel.rawAttributes()
4169
4190
  this._changes = {}
4191
+ this._assignedAttributeNames = undefined
4170
4192
  }
4171
4193
 
4172
4194
  /**
@@ -21,7 +21,7 @@ export default class VelociousDatabaseRecordBelongsToRelationship extends BaseRe
21
21
  }
22
22
  }
23
23
 
24
- return this.foreignKey
24
+ return this.modelClass.getAttributeNameToColumnNameMap()[this.foreignKey] || this.foreignKey
25
25
  }
26
26
 
27
27
  /**
@@ -13,7 +13,9 @@ export default class VelociousDatabaseRecordHasManyRelationship extends BaseRela
13
13
  this.foreignKey = `${inflection.underscore(this.modelClass.getModelName())}_id`
14
14
  }
15
15
 
16
- return this.foreignKey
16
+ const targetModelClass = this.className || this.klass ? this.getTargetModelClass() : undefined
17
+
18
+ return targetModelClass?.getAttributeNameToColumnNameMap()[this.foreignKey] || this.foreignKey
17
19
  }
18
20
 
19
21
  /**
@@ -13,7 +13,9 @@ export default class VelociousDatabaseRecordHasOneRelationship extends BaseRelat
13
13
  this.foreignKey = `${inflection.underscore(this.modelClass.getModelName())}_id`
14
14
  }
15
15
 
16
- return this.foreignKey
16
+ const targetModelClass = this.className || this.klass ? this.getTargetModelClass() : undefined
17
+
18
+ return targetModelClass?.getAttributeNameToColumnNameMap()[this.foreignKey] || this.foreignKey
17
19
  }
18
20
 
19
21
  /**
@@ -373,14 +373,14 @@ export default class DbGenerateFrontendModels extends BaseCommand {
373
373
 
374
374
  fileContent += "\n"
375
375
  fileContent += ` /** @returns {${attributesTypeName}[${JSON.stringify(attribute.name)}]} - Attribute value. */\n`
376
- fileContent += ` ${camelizedAttribute}() { return this.readAttribute(${JSON.stringify(attribute.name)}) }\n`
376
+ fileContent += ` ${camelizedAttribute}() { return /** @type {${attributesTypeName}[${JSON.stringify(attribute.name)}]} */ (this.readAttribute(${JSON.stringify(attribute.name)})) }\n`
377
377
 
378
378
  fileContent += "\n"
379
379
  fileContent += " /**\n"
380
380
  fileContent += ` * @param {${attributesTypeName}[${JSON.stringify(attribute.name)}]} newValue - New attribute value.\n`
381
381
  fileContent += ` * @returns {${attributesTypeName}[${JSON.stringify(attribute.name)}]} - Assigned value.\n`
382
382
  fileContent += " */\n"
383
- fileContent += ` set${camelizedAttributeUpper}(newValue) { return this.setAttribute(${JSON.stringify(attribute.name)}, newValue) }\n`
383
+ fileContent += ` set${camelizedAttributeUpper}(newValue) { return /** @type {${attributesTypeName}[${JSON.stringify(attribute.name)}]} */ (this.setAttribute(${JSON.stringify(attribute.name)}, newValue)) }\n`
384
384
  }
385
385
 
386
386
  for (const methodName of Object.keys(collectionCommands)) {