velocious 1.0.443 → 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.
@@ -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
@@ -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>} */
@@ -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
 
@@ -2512,7 +2520,7 @@ class VelociousDatabaseRecord {
2512
2520
 
2513
2521
  /**
2514
2522
  * Resolves a relationship foreign-key column to this model's public attribute name.
2515
- * @param {import("./instance-relationships/base.js").default<?, ?>} instanceRelationship - Relationship instance.
2523
+ * @param {import("./instance-relationships/base.js").default<typeof VelociousDatabaseRecord, typeof VelociousDatabaseRecord>} instanceRelationship - Relationship instance.
2516
2524
  * @returns {string} Attribute name accepted by setAttribute/assign.
2517
2525
  */
2518
2526
  _relationshipForeignKeyAttribute(instanceRelationship) {
@@ -3374,7 +3382,9 @@ class VelociousDatabaseRecord {
3374
3382
  * @returns {void} - No return value.
3375
3383
  */
3376
3384
  assign(attributesToAssign) {
3385
+ this._assignedAttributeNames ||= new Set()
3377
3386
  for (const attributeToAssign in attributesToAssign) {
3387
+ this._assignedAttributeNames.add(attributeToAssign)
3378
3388
  this.setAttribute(attributeToAssign, attributesToAssign[attributeToAssign])
3379
3389
  }
3380
3390
  }
@@ -4178,6 +4188,7 @@ class VelociousDatabaseRecord {
4178
4188
 
4179
4189
  this._attributes = reloadedModel.rawAttributes()
4180
4190
  this._changes = {}
4191
+ this._assignedAttributeNames = undefined
4181
4192
  }
4182
4193
 
4183
4194
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"acts-as-list.d.ts","sourceRoot":"","sources":["../../../../src/database/record/acts-as-list.js"],"names":[],"mappings":"AA8BA;;;;;;;;;;;;;;GAcG;AACH,gEANW,cAAc,YAAY,EAAE,OAAO,kBACnC,MAAM,aAEd;IAAwB,KAAK,EAArB,MAAM;CACd,GAAU,IAAI,CAuGhB"}
1
+ {"version":3,"file":"acts-as-list.d.ts","sourceRoot":"","sources":["../../../../src/database/record/acts-as-list.js"],"names":[],"mappings":"AA8BA;;;;;;;;;;;;;;GAcG;AACH,gEANW,cAAc,YAAY,EAAE,OAAO,kBACnC,MAAM,aAEd;IAAwB,KAAK,EAArB,MAAM;CACd,GAAU,IAAI,CAyGhB"}