@rvoh/dream 2.30.1 → 2.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/dist/cjs/src/Dream.js +9 -5
  2. package/dist/cjs/src/db/errors.js +2 -0
  3. package/dist/cjs/src/decorators/Decorators.js +121 -6
  4. package/dist/cjs/src/decorators/field/sortable/Sortable.js +4 -31
  5. package/dist/cjs/src/decorators/field/sortable/helpers/cascadeReachesEveryRowInSortScope.js +51 -0
  6. package/dist/cjs/src/decorators/field/sortable/helpers/performSortableDestroyWork.js +5 -2
  7. package/dist/cjs/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.js +39 -25
  8. package/dist/cjs/src/decorators/field/sortable/helpers/sortableCascadeEdge.js +32 -0
  9. package/dist/cjs/src/dream/Query.js +37 -13
  10. package/dist/cjs/src/dream/QueryDriver/Base.js +26 -0
  11. package/dist/cjs/src/dream/QueryDriver/Kysely.js +4 -1
  12. package/dist/cjs/src/dream/QueryDriver/Postgres.js +7 -1
  13. package/dist/cjs/src/dream/internal/destroyAssociatedRecords.js +7 -0
  14. package/dist/cjs/src/dream/internal/destroyDream.js +25 -14
  15. package/dist/cjs/src/dream/internal/saveDream.js +9 -0
  16. package/dist/cjs/src/dream/internal/undestroyDream.js +55 -32
  17. package/dist/cjs/src/dream/internal/withConcurrentWriterRetry.js +64 -0
  18. package/dist/cjs/src/errors/CannotSaveMissingDream.js +24 -0
  19. package/dist/cjs/src/errors/SortableBatchRequiresTooManyScopeLocks.js +7 -0
  20. package/dist/cjs/src/errors/associations/MissingRequiredBelongsToAssociation.js +8 -3
  21. package/dist/cjs/src/package-exports/errors.js +10 -1
  22. package/dist/esm/src/Dream.js +9 -5
  23. package/dist/esm/src/db/errors.js +2 -0
  24. package/dist/esm/src/decorators/Decorators.js +121 -6
  25. package/dist/esm/src/decorators/field/sortable/Sortable.js +4 -31
  26. package/dist/esm/src/decorators/field/sortable/helpers/cascadeReachesEveryRowInSortScope.js +51 -0
  27. package/dist/esm/src/decorators/field/sortable/helpers/performSortableDestroyWork.js +5 -2
  28. package/dist/esm/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.js +39 -25
  29. package/dist/esm/src/decorators/field/sortable/helpers/sortableCascadeEdge.js +32 -0
  30. package/dist/esm/src/dream/Query.js +37 -13
  31. package/dist/esm/src/dream/QueryDriver/Base.js +26 -0
  32. package/dist/esm/src/dream/QueryDriver/Kysely.js +4 -1
  33. package/dist/esm/src/dream/QueryDriver/Postgres.js +7 -1
  34. package/dist/esm/src/dream/internal/destroyAssociatedRecords.js +7 -0
  35. package/dist/esm/src/dream/internal/destroyDream.js +25 -14
  36. package/dist/esm/src/dream/internal/saveDream.js +9 -0
  37. package/dist/esm/src/dream/internal/undestroyDream.js +55 -32
  38. package/dist/esm/src/dream/internal/withConcurrentWriterRetry.js +64 -0
  39. package/dist/esm/src/errors/CannotSaveMissingDream.js +24 -0
  40. package/dist/esm/src/errors/SortableBatchRequiresTooManyScopeLocks.js +7 -0
  41. package/dist/esm/src/errors/associations/MissingRequiredBelongsToAssociation.js +8 -3
  42. package/dist/esm/src/package-exports/errors.js +10 -1
  43. package/dist/types/src/Dream.d.ts +9 -5
  44. package/dist/types/src/db/errors.d.ts +2 -0
  45. package/dist/types/src/decorators/Decorators.d.ts +121 -6
  46. package/dist/types/src/decorators/field/sortable/Sortable.d.ts +4 -31
  47. package/dist/types/src/decorators/field/sortable/helpers/cascadeReachesEveryRowInSortScope.d.ts +39 -0
  48. package/dist/types/src/decorators/field/sortable/helpers/performSortableDestroyWork.d.ts +6 -1
  49. package/dist/types/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.d.ts +37 -20
  50. package/dist/types/src/decorators/field/sortable/helpers/sortableCascadeEdge.d.ts +20 -0
  51. package/dist/types/src/dream/Query.d.ts +37 -13
  52. package/dist/types/src/dream/QueryDriver/Base.d.ts +20 -0
  53. package/dist/types/src/dream/QueryDriver/Postgres.d.ts +2 -0
  54. package/dist/types/src/dream/internal/destroyDream.d.ts +3 -1
  55. package/dist/types/src/dream/internal/undestroyDream.d.ts +6 -4
  56. package/dist/types/src/dream/internal/withConcurrentWriterRetry.d.ts +47 -0
  57. package/dist/types/src/errors/CannotSaveMissingDream.d.ts +23 -0
  58. package/dist/types/src/errors/SortableBatchRequiresTooManyScopeLocks.d.ts +7 -0
  59. package/dist/types/src/errors/associations/MissingRequiredBelongsToAssociation.d.ts +1 -0
  60. package/dist/types/src/package-exports/errors.d.ts +1 -1
  61. package/docs/assets/hierarchy.js +1 -1
  62. package/docs/assets/navigation.js +1 -1
  63. package/docs/assets/search.js +1 -1
  64. package/docs/classes/db.DreamMigrationHelpers.html +11 -11
  65. package/docs/classes/db.KyselyQueryDriver.html +55 -43
  66. package/docs/classes/db.PostgresQueryDriver.html +56 -44
  67. package/docs/classes/db.QueryDriverBase.html +54 -42
  68. package/docs/classes/errors.CannotSaveMissingDream.html +47 -0
  69. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  70. package/docs/classes/errors.ColumnOverflow.html +3 -3
  71. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  72. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  73. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  74. package/docs/classes/errors.DecryptionError.html +2 -2
  75. package/docs/classes/errors.DecryptionParseError.html +2 -2
  76. package/docs/classes/errors.DecryptionRotationError.html +3 -3
  77. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  78. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  79. package/docs/classes/errors.InvalidClockTime.html +2 -2
  80. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  81. package/docs/classes/errors.InvalidDateTime.html +2 -2
  82. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  83. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  84. package/docs/classes/errors.NotNullViolation.html +3 -3
  85. package/docs/classes/errors.RecordNotFound.html +3 -3
  86. package/docs/classes/errors.SortableScopeDidNotStabilize.html +3 -3
  87. package/docs/classes/errors.SortableScopeLockWaitTimedOut.html +3 -3
  88. package/docs/classes/errors.ValidationError.html +3 -3
  89. package/docs/classes/index.CalendarDate.html +33 -33
  90. package/docs/classes/index.ClockTime.html +32 -32
  91. package/docs/classes/index.ClockTimeTz.html +35 -35
  92. package/docs/classes/index.DateTime.html +86 -86
  93. package/docs/classes/index.Decorators.html +101 -22
  94. package/docs/classes/index.Dream.html +133 -129
  95. package/docs/classes/index.DreamApp.html +10 -10
  96. package/docs/classes/index.DreamTransaction.html +2 -2
  97. package/docs/classes/index.Env.html +2 -2
  98. package/docs/classes/index.Query.html +88 -67
  99. package/docs/classes/system.CliFileWriter.html +4 -4
  100. package/docs/classes/system.DreamBin.html +2 -2
  101. package/docs/classes/system.DreamCLI.html +7 -7
  102. package/docs/classes/system.DreamImporter.html +2 -2
  103. package/docs/classes/system.DreamLogos.html +2 -2
  104. package/docs/classes/system.DreamSerializerBuilder.html +11 -11
  105. package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
  106. package/docs/classes/system.PathHelpers.html +3 -3
  107. package/docs/classes/utils.Encrypt.html +3 -3
  108. package/docs/classes/utils.Range.html +2 -2
  109. package/docs/functions/db.closeAllDbConnections.html +1 -1
  110. package/docs/functions/db.dreamDbConnections.html +1 -1
  111. package/docs/functions/db.untypedDb.html +1 -1
  112. package/docs/functions/db.validateColumn.html +1 -1
  113. package/docs/functions/db.validateTable.html +1 -1
  114. package/docs/functions/errors.pgErrorType.html +1 -1
  115. package/docs/functions/index.DreamSerializer.html +1 -1
  116. package/docs/functions/index.ObjectSerializer.html +1 -1
  117. package/docs/functions/index.ReplicaSafe.html +1 -1
  118. package/docs/functions/index.STI.html +1 -1
  119. package/docs/functions/index.SoftDelete.html +1 -1
  120. package/docs/functions/utils.camelize.html +1 -1
  121. package/docs/functions/utils.capitalize.html +1 -1
  122. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  123. package/docs/functions/utils.compact.html +1 -1
  124. package/docs/functions/utils.groupBy.html +1 -1
  125. package/docs/functions/utils.hyphenize.html +1 -1
  126. package/docs/functions/utils.intersection.html +1 -1
  127. package/docs/functions/utils.isEmpty.html +1 -1
  128. package/docs/functions/utils.normalizeUnicode.html +1 -1
  129. package/docs/functions/utils.pascalize.html +1 -1
  130. package/docs/functions/utils.percent.html +1 -1
  131. package/docs/functions/utils.range.html +1 -1
  132. package/docs/functions/utils.round.html +1 -1
  133. package/docs/functions/utils.sanitizeString.html +1 -1
  134. package/docs/functions/utils.snakeify.html +1 -1
  135. package/docs/functions/utils.sort.html +1 -1
  136. package/docs/functions/utils.sortBy.html +1 -1
  137. package/docs/functions/utils.sortObjectByKey.html +1 -1
  138. package/docs/functions/utils.sortObjectByValue.html +1 -1
  139. package/docs/functions/utils.uncapitalize.html +1 -1
  140. package/docs/functions/utils.uniq.html +1 -1
  141. package/docs/hierarchy.html +1 -1
  142. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  143. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  144. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  145. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  146. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  147. package/docs/interfaces/types.DecoratorContext.html +2 -2
  148. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  149. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  150. package/docs/interfaces/types.DreamDbConfig.html +5 -5
  151. package/docs/interfaces/types.DurationObject.html +2 -2
  152. package/docs/interfaces/types.EncryptOptions.html +2 -2
  153. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  154. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  155. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  156. package/docs/modules/errors.html +1 -1
  157. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  158. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  159. package/docs/types/openapi.OpenapiFormats.html +1 -1
  160. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  161. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  162. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  163. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  164. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  165. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  166. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  167. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  168. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  169. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
  170. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
  171. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
  172. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  173. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  174. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  175. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  176. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  177. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  178. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  179. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  180. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
  181. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
  182. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  183. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  184. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
  185. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
  186. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  187. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  188. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
  189. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
  190. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
  191. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  192. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  193. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  194. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  195. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  196. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  197. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  198. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  199. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  200. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  201. package/docs/types/types.CalendarDateObject.html +1 -1
  202. package/docs/types/types.Camelized.html +1 -1
  203. package/docs/types/types.ClockTimeObject.html +1 -1
  204. package/docs/types/types.DbConnectionType.html +1 -1
  205. package/docs/types/types.DbTypes.html +1 -1
  206. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  207. package/docs/types/types.DreamAttributes.html +1 -1
  208. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  209. package/docs/types/types.DreamClassColumn.html +1 -1
  210. package/docs/types/types.DreamColumn.html +1 -1
  211. package/docs/types/types.DreamColumnNames.html +1 -1
  212. package/docs/types/types.DreamLogLevel.html +1 -1
  213. package/docs/types/types.DreamLogger.html +2 -2
  214. package/docs/types/types.DreamModelSerializerType.html +1 -1
  215. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  216. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  217. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  218. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  219. package/docs/types/types.DreamSerializable.html +1 -1
  220. package/docs/types/types.DreamSerializableArray.html +1 -1
  221. package/docs/types/types.DreamSerializerKey.html +1 -1
  222. package/docs/types/types.DreamSerializers.html +1 -1
  223. package/docs/types/types.DreamVirtualColumns.html +1 -1
  224. package/docs/types/types.DurationUnit.html +1 -1
  225. package/docs/types/types.EncryptAlgorithm.html +1 -1
  226. package/docs/types/types.HasManyStatement.html +1 -1
  227. package/docs/types/types.HasOneStatement.html +1 -1
  228. package/docs/types/types.Hyphenized.html +1 -1
  229. package/docs/types/types.Pascalized.html +1 -1
  230. package/docs/types/types.PrimaryKeyType.html +1 -1
  231. package/docs/types/types.RoundingPrecision.html +1 -1
  232. package/docs/types/types.SerializerCasing.html +1 -1
  233. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  234. package/docs/types/types.Snakeified.html +1 -1
  235. package/docs/types/types.StrictInterface.html +1 -1
  236. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  237. package/docs/types/types.UpdateableProperties.html +1 -1
  238. package/docs/types/types.ValidationType.html +1 -1
  239. package/docs/types/types.ViewModel.html +2 -2
  240. package/docs/types/types.ViewModelClass.html +1 -1
  241. package/docs/types/types.WeekdayName.html +1 -1
  242. package/docs/types/types.WhereStatementForDream.html +1 -1
  243. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  244. package/docs/variables/index.DreamConst.html +1 -1
  245. package/docs/variables/index.ops.html +1 -1
  246. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  247. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  248. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  249. package/docs/variables/system.primaryKeyTypes.html +1 -1
  250. package/package.json +2 -2
  251. package/docs/classes/errors.SortableBatchRequiresTooManyScopeLocks.html +0 -61
@@ -1,15 +1,21 @@
1
+ import { SOFT_DELETE_SCOPE_NAME } from '../../decorators/class/SoftDelete.js';
1
2
  import acquireStabilizedSortableScopeLocks from '../../decorators/field/sortable/helpers/acquireStabilizedSortableScopeLocks.js';
2
3
  import filterQueryToScopeValues from '../../decorators/field/sortable/helpers/filterQueryToScopeValues.js';
3
- import { snapshotScopeValue } from '../../decorators/field/sortable/helpers/sortableSnapshot.js';
4
+ import { consumeSortableCascadeEdge, markSortableCascadeEdge, } from '../../decorators/field/sortable/helpers/sortableCascadeEdge.js';
5
+ import { readSortableSnapshots, snapshotScopeValue, } from '../../decorators/field/sortable/helpers/sortableSnapshot.js';
6
+ import associationUpdateQuery from './associations/associationUpdateQuery.js';
4
7
  import runHooksFor from './runHooksFor.js';
8
+ import withConcurrentWriterRetry from './withConcurrentWriterRetry.js';
5
9
  /**
6
10
  * @internal
7
11
  *
8
- * Destroys the Dream and any `dependent: 'destroy'` associations
12
+ * Undestroys the Dream and any `dependent: 'destroy'` associations
9
13
  * within a transaction. If a transaction is passed, it will be used.
10
- * Otherwise, a new transaction will be created automatically.
11
- * If any of the nested associations fails to destroy, then this
12
- * record will also fail to destroy. If skipHooks is true, model hooks
14
+ * Otherwise, a new transaction will be created automatically, and the
15
+ * whole undestroy is run again if that transaction is undone by a
16
+ * concurrent writer (see `withConcurrentWriterRetry`).
17
+ * If any of the nested associations fails to undestroy, then this
18
+ * record will also fail to undestroy. If skipHooks is true, model hooks
13
19
  * will be bypassed.
14
20
  */
15
21
  export default async function undestroyDream(dream, txn = null, options) {
@@ -18,33 +24,35 @@ export default async function undestroyDream(dream, txn = null, options) {
18
24
  }
19
25
  else {
20
26
  const dreamClass = dream.constructor;
21
- return await dreamClass.transaction(async (txn) => await undestroyDreamWithTransaction(dream, txn, options));
27
+ return await withConcurrentWriterRetry(dreamClass, async (txn) => await undestroyDreamWithTransaction(dream, txn, options));
22
28
  }
23
29
  }
24
30
  /**
25
31
  * @internal
26
32
  *
27
- * Given a transaction, applies the destroy query,
33
+ * Given a transaction, applies the undestroy query,
28
34
  * including cascading to child associations and
29
35
  * model hooks.
30
36
  */
31
37
  async function undestroyDreamWithTransaction(dream, txn, options) {
32
38
  const { cascade, skipHooks } = options;
39
+ // Whether a `dependent: 'destroy'` cascade reached this record, which decides
40
+ // whether its restore takes the scope lock. Consumed first, before a hook or
41
+ // the cascade below can re-enter this function for the same instance.
42
+ const cascaded = consumeSortableCascadeEdge(dream) !== null;
33
43
  if (!skipHooks) {
34
44
  await runHooksFor('beforeUpdate', dream, true, null, txn);
35
45
  }
36
- // The cascade runs ahead of the restore, matching `destroyDream`: this
37
- // record's own scope lock is taken inside `doUndestroyDream`, so a cascade
38
- // seated after it would hold that lock across every descendant's undestroy,
39
- // and a parent and its children would take their advisory keys in the
40
- // opposite order to the one the destroy side takes them in. The nested select
41
- // the cascade builds bypasses the soft-delete scope on this record's own
42
- // class (`undestroyOptions`), so it reaches the children while this record is
43
- // still deleted.
46
+ // The cascade runs ahead of the restore, matching `destroyDream`: a direct
47
+ // undestroy takes this record's own scope lock inside `doUndestroyDream`, so
48
+ // a cascade seated after it would hold that lock across every descendant's
49
+ // undestroy. The nested select the cascade builds bypasses the soft-delete
50
+ // scope on this record's own class (`undestroyOptions`), so it reaches the
51
+ // children while this record is still deleted.
44
52
  if (cascade) {
45
53
  await undestroyAssociatedRecords(dream, txn, options);
46
54
  }
47
- const restoredRowCount = await doUndestroyDream(dream, txn);
55
+ const restoredRowCount = await doUndestroyDream(dream, txn, { lock: !cascaded });
48
56
  // A restore of a row that is not deleted — a second `undestroy()`, a job
49
57
  // retry, or a record that was never destroyed, which `Query#undestroy` reaches
50
58
  // routinely since it lifts the soft-delete scope and walks every match —
@@ -68,9 +76,16 @@ async function undestroyDreamWithTransaction(dream, txn, options) {
68
76
  * is computed from, and would move one past itself on every repeat — a
69
77
  * permanent gap in the scope.
70
78
  *
79
+ * @param lock - whether to take each sortable field's scope lock around the
80
+ * position computation. A direct undestroy does; a cascaded one does not,
81
+ * since a cascade would otherwise hold one lock per distinct sort scope it
82
+ * restores into until the root transaction commits. Without the lock the
83
+ * `max(position) + 1` below can race a concurrent writer of the scope onto the
84
+ * same position, which the scope's deferrable unique constraint refuses at
85
+ * COMMIT; see `withConcurrentWriterRetry`.
71
86
  * @returns the number of rows restored: 1, or 0 when the row was not deleted
72
87
  */
73
- async function doUndestroyDream(dream, txn) {
88
+ async function doUndestroyDream(dream, txn, { lock }) {
74
89
  const updateStatement = txn.kyselyTransaction.updateTable(dream.table);
75
90
  let query = updateStatement
76
91
  .where(dream['_primaryKey'], '=', dream.primaryKeyValue())
@@ -79,16 +94,16 @@ async function doUndestroyDream(dream, txn) {
79
94
  const dreamClass = dream.constructor;
80
95
  const sortableFields = (dreamClass['sortableFields'] ?? []);
81
96
  // Undestroy has no sortable hook of its own — the position is recomputed
82
- // inline, below, before `afterUpdate` ever runs — so the scope lock is taken
83
- // here, around that recomputation. Without it the `max(position) + 1`
84
- // subquery races every other writer of the scope the record is being restored
85
- // into.
97
+ // inline, below, before `afterUpdate` ever runs — so a direct undestroy takes
98
+ // the scope lock here, around that recomputation.
86
99
  //
87
100
  // The scope this record is being restored into is the one its row physically
88
101
  // carries, not the one the instance was loaded with: another writer may have
89
- // moved it between scopes while it was soft deleted, which is what the
90
- // stabilized acquisition converges on.
91
- const { snapshots } = await acquireStabilizedSortableScopeLocks(dream, txn, sortableFields);
102
+ // moved it between scopes while it was soft deleted. The stabilized
103
+ // acquisition converges on that scope; the unlocked read simply reads it.
104
+ const { snapshots } = lock
105
+ ? await acquireStabilizedSortableScopeLocks(dream, txn, sortableFields)
106
+ : await readSortableSnapshots(dream, txn, sortableFields);
92
107
  sortableFields.forEach(sortableFieldMetadata => {
93
108
  const positionColumn = sortableFieldMetadata.positionField;
94
109
  const snapshot = snapshots.get(positionColumn);
@@ -113,22 +128,30 @@ async function doUndestroyDream(dream, txn) {
113
128
  /**
114
129
  * @internal
115
130
  *
116
- * Destroys all HasOne/HasMany associations on this
117
- * dream that are marked as `dependent: 'destroy'`
131
+ * Undestroys all HasOne/HasMany associations on this dream that are marked as
132
+ * `dependent: 'destroy'`: the walk `Query#undestroy` makes, with each record
133
+ * marked as reached by this cascade before its own undestroy runs, so that its
134
+ * restore takes no scope lock.
118
135
  */
119
136
  async function undestroyAssociatedRecords(dream, txn, options) {
120
137
  const dreamClass = dream.constructor;
121
138
  for (const associationName of dreamClass['dependentDestroyAssociationNames']()) {
122
- const associationMetadata = dreamClass['associationMetadataMap']()[associationName];
123
- const associatedClass = associationMetadata?.modelCB?.();
139
+ const association = dreamClass['associationMetadataMap']()[associationName];
140
+ const associatedClass = association?.modelCB?.();
124
141
  if (Array.isArray(associatedClass)) {
125
142
  // TODO: decide how to handle polymorphic associations with dependent: destroy
126
143
  // raise?
127
144
  }
128
- else {
129
- if (associatedClass?.['softDelete']) {
130
- await dream.txn(txn).undestroyAssociation(associationName, options);
131
- }
145
+ else if (association && associatedClass?.['softDelete']) {
146
+ const query = associationUpdateQuery(dream, txn, associationName, {
147
+ joinAndStatements: {},
148
+ bypassAllDefaultScopes: options.bypassAllDefaultScopes ?? false,
149
+ defaultScopesToBypass: (options.defaultScopesToBypass ?? []),
150
+ });
151
+ await query.removeDefaultScope(SOFT_DELETE_SCOPE_NAME).findEach(async (record) => {
152
+ markSortableCascadeEdge(record, association);
153
+ await record.txn(txn).undestroy(options);
154
+ });
132
155
  }
133
156
  }
134
157
  }
@@ -0,0 +1,64 @@
1
+ import DreamApp from '../../dream-app/index.js';
2
+ /**
3
+ * How many times a destroy or undestroy that opened its own transaction runs
4
+ * before a failure caused by a concurrent writer escapes. One such failure
5
+ * needs another writer of the same rows or sort scope during the operation;
6
+ * three in a row is contention past the point of retrying.
7
+ */
8
+ export const MAX_CONCURRENT_WRITER_ATTEMPTS = 3;
9
+ /**
10
+ * @internal
11
+ *
12
+ * Runs `callback` in a transaction of the model's own, and runs it again when
13
+ * that transaction is undone by a concurrent writer: a deadlock, or a unique
14
+ * constraint refusing the COMMIT.
15
+ *
16
+ * A cascaded destroy or undestroy does its sortable position work without the
17
+ * scope lock a direct one takes (see `@deco.Sortable`), so a concurrent writer
18
+ * of the same sort scope can leave two live rows sharing a position, which the
19
+ * scope's deferrable unique constraint refuses at COMMIT — or, when both
20
+ * writers reach COMMIT together, as a deadlock between their two checks. A
21
+ * cascade holding row locks can deadlock against another writer of the same
22
+ * rows mid-statement as well. Either way the database has undone the whole
23
+ * transaction, nothing has committed and no commit hook has run, so running
24
+ * the operation again reads the rows as the other writer left them. The last
25
+ * attempt's error is the adapter's own.
26
+ *
27
+ * A unique violation raised by a statement inside the callback is not a
28
+ * collision but a write that violates a constraint outright, and is not
29
+ * retried; only a refusal of the COMMIT itself is, which is why the callback
30
+ * having resolved is part of the test.
31
+ *
32
+ * Only a transaction Dream opens is retried. Inside a caller's transaction the
33
+ * failure is the caller's, and the caller retries the whole transaction.
34
+ *
35
+ * Saves are deliberately not run through this. When a direct save and a
36
+ * cascade collide, the loser is whichever transaction waits first at COMMIT,
37
+ * and that is usually the save, which is short and reaches COMMIT while the
38
+ * cascade is still working: it fails with the adapter's deadlock error after
39
+ * `deadlock_timeout`. That bounded, occasional failure is the accepted price of
40
+ * cascades holding no scope locks, and the save path's state restore
41
+ * (`saveDream`) already makes calling `save` again correct. The collision needs
42
+ * a position write into the same sort scope inside the cascade's window that
43
+ * picks the same position, which a cascade destroying a scope's owner never
44
+ * exposes since it writes no position at all.
45
+ */
46
+ export default async function withConcurrentWriterRetry(dreamClass, callback) {
47
+ const queryDriverClass = DreamApp.getOrFail().dbConnectionQueryDriverClass(dreamClass.prototype.connectionName);
48
+ for (let attempt = 1;; attempt++) {
49
+ let callbackResolved = false;
50
+ try {
51
+ return await dreamClass.transaction(async (txn) => {
52
+ const result = await callback(txn);
53
+ callbackResolved = true;
54
+ return result;
55
+ });
56
+ }
57
+ catch (error) {
58
+ const undoneByConcurrentWriter = queryDriverClass.isDeadlock(error) ||
59
+ (callbackResolved && queryDriverClass.isUniqueConstraintViolation(error));
60
+ if (!undoneByConcurrentWriter || attempt === MAX_CONCURRENT_WRITER_ATTEMPTS)
61
+ throw error;
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Raised when saving a persisted instance whose database row no longer exists —
3
+ * the row was deleted between the moment the instance was loaded and the moment
4
+ * the save reached the database.
5
+ *
6
+ * This is the write-side counterpart of `RecordNotFound`, and applications catch
7
+ * it for the same reasons: a request updating a record another request has since
8
+ * deleted should answer 404 or 409 rather than 500, and a background job holding
9
+ * a stale instance should usually drop the work rather than fail loudly. Nothing
10
+ * is written when it is raised.
11
+ *
12
+ * It does not mean the save was rejected — a save that fails validation raises
13
+ * `ValidationError`, and a constraint violation raises its own error. It means
14
+ * the row that the instance stands for is gone.
15
+ *
16
+ * Dream raises it from the query driver, so a custom driver overriding
17
+ * `saveDream` translates its own adapter's empty-result error the same way, as
18
+ * the test-app MySQL driver does.
19
+ */
20
+ export default class CannotSaveMissingDream extends Error {
21
+ constructor(dream) {
22
+ super(`Cannot save ${dream.sanitizedConstructorName} with primary key ${String(dream.primaryKeyValue())} because its persisted database row no longer exists.`);
23
+ }
24
+ }
@@ -1,4 +1,6 @@
1
1
  /**
2
+ * @internal
3
+ *
2
4
  * Raised when a batch of a locked `Query#destroy` or `Query#update` on a
3
5
  * sortable model would carry its transaction past the number of advisory
4
6
  * sort-scope locks Dream is willing to hold at once.
@@ -27,6 +29,11 @@
27
29
  * — a long multi-batch run inside a transaction you opened — either let Dream
28
30
  * open a transaction per batch (do not pass a transaction of your own) or
29
31
  * commit between runs, which is what releases the locks as it goes.
32
+ *
33
+ * Not exported from `@rvoh/dream/errors`. Exceeding the bound is a sizing
34
+ * mistake in the calling code, not a runtime condition an application can
35
+ * recover from, so there is nothing for a consumer to catch: the fix is a
36
+ * smaller batch or a narrower transaction, never a retry.
30
37
  */
31
38
  export default class SortableBatchRequiresTooManyScopeLocks extends Error {
32
39
  dreamClassName;
@@ -48,10 +48,15 @@ export default class MissingRequiredBelongsToAssociation extends Error {
48
48
  Attempting to access required BelongsTo association \`${this.associationName}\` on an instance of \`${this.dreamClass.sanitizedName}\`,
49
49
  but the loaded association is null.
50
50
 
51
- ${this.foreignKeyMessage}
52
-
53
- If the associated record was deleted, check whether the inverse HasOne/HasMany association should specify dependent: 'destroy'.
51
+ ${this.foreignKeyMessage}${this.populatedForeignKeyDiagnosticMessage}
54
52
  `;
53
+ }
54
+ get populatedForeignKeyDiagnosticMessage() {
55
+ if (this.foreignKeyValue === null || this.foreignKeyValue === undefined)
56
+ return '';
57
+ return `
58
+
59
+ The associated record may have been deleted (including soft-deleted), or it may not match this association’s target—for example, its STI type may differ from the targeted STI child. If the record was deleted unexpectedly, check whether the inverse HasOne/HasMany association should specify \`dependent: 'destroy'\`.`;
55
60
  }
56
61
  get foreignKeyMessage() {
57
62
  if (this.foreignKeyValue === null || this.foreignKeyValue === undefined) {
@@ -1,18 +1,27 @@
1
1
  export { pgErrorType } from '../db/errors.js';
2
2
  export { default as NonLoadedAssociation } from '../errors/associations/NonLoadedAssociation.js';
3
+ export { default as CannotSaveMissingDream } from '../errors/CannotSaveMissingDream.js';
3
4
  export { default as CreateOrFindByFailedToCreateAndFind } from '../errors/CreateOrFindByFailedToCreateAndFind.js';
4
5
  export { default as CheckConstraintViolation } from '../errors/db/CheckConstraintViolation.js';
5
6
  export { default as ColumnOverflow } from '../errors/db/ColumnOverflow.js';
6
7
  export { default as DataIncompatibleWithDatabaseField } from '../errors/db/DataIncompatibleWithDatabaseField.js';
7
8
  export { default as DataTypeColumnTypeMismatch } from '../errors/db/DataTypeColumnTypeMismatch.js';
8
9
  export { default as NotNullViolation } from '../errors/db/NotNullViolation.js';
10
+ // Raised by the framework packages layered on Dream, not only by Dream: psychic
11
+ // (`src/controller/index.ts`) raises it for a controller's global name, and
12
+ // psychic-workers (`src/background/BaseBackgroundedService.ts`,
13
+ // `src/background/BaseScheduledService.ts`) for service global names. Both reach it
14
+ // through this entry point, so unexporting it breaks the build of every application
15
+ // that installs either package.
9
16
  export { default as GlobalNameNotSet } from '../errors/dream-app/GlobalNameNotSet.js';
10
17
  export { default as DecryptionError } from '../errors/encrypt/DecryptionError.js';
11
18
  export { default as DecryptionParseError } from '../errors/encrypt/DecryptionParseError.js';
12
19
  export { default as DecryptionRotationError } from '../errors/encrypt/DecryptionRotationError.js';
13
20
  export { default as RecordNotFound } from '../errors/RecordNotFound.js';
21
+ // Caught through this entry point by psychic
22
+ // (`test-app/src/app/controllers/SerializerTestsController.ts`), which distinguishes it
23
+ // to prove a controller can render a missing-serializers failure as a response.
14
24
  export { default as MissingSerializersDefinition } from '../errors/serializers/MissingSerializersDefinition.js';
15
- export { default as SortableBatchRequiresTooManyScopeLocks } from '../errors/SortableBatchRequiresTooManyScopeLocks.js';
16
25
  export { default as SortableScopeDidNotStabilize } from '../errors/SortableScopeDidNotStabilize.js';
17
26
  export { default as SortableScopeLockWaitTimedOut } from '../errors/SortableScopeLockWaitTimedOut.js';
18
27
  export { default as ValidationError } from '../errors/ValidationError.js';
@@ -1488,16 +1488,20 @@ export default class Dream {
1488
1488
  * with no gaps, accounting for the scopes specified in the
1489
1489
  * corresponding Sortable decorator.
1490
1490
  *
1491
- * Each sort scope is renumbered in its own transaction, under the same
1492
- * advisory lock every other position-mutating path takes on that scope, from
1493
- * a read of the scope taken inside that lock — so a concurrent create or
1494
- * destroy cannot make the renumbering land on positions it never read.
1491
+ * Each sort scope is renumbered in its own transaction, under the advisory
1492
+ * lock every direct position-mutating path takes on that scope, from a read
1493
+ * of the scope taken inside that lock — so a concurrent create or direct
1494
+ * destroy cannot make the renumbering land on positions it never read. A
1495
+ * cascaded destroy or undestroy takes no scope lock (see `@deco.Sortable`),
1496
+ * so one running at the same moment can still change a scope under `resort`;
1497
+ * a later `resort` repairs whatever that leaves.
1495
1498
  *
1496
1499
  * **Do not call `resort` from inside a transaction you own.** It opens those
1497
1500
  * transactions itself, on connections of its own, so it can neither join
1498
1501
  * yours nor roll back with it: its writes commit on their own. Worse, if your
1499
1502
  * transaction already holds the scope lock for a scope being renumbered — any
1500
- * position-mutating write to that scope earlier in the transaction takes it —
1503
+ * direct position-mutating write to that scope earlier in the transaction
1504
+ * takes it; a cascaded destroy or undestroy takes none —
1501
1505
  * `resort` waits on that lock from the outside, where no deadlock detector
1502
1506
  * can see the cycle, until the wait bound expires and it throws
1503
1507
  * `SortableScopeLockWaitTimedOut`.
@@ -1,4 +1,5 @@
1
1
  export declare const CHECK_VIOLATION = "CHECK_VIOLATION";
2
+ export declare const DEADLOCK_DETECTED = "DEADLOCK_DETECTED";
2
3
  export declare const FOREIGN_KEY_VIOLATION = "FOREIGN_KEY_VIOLATION";
3
4
  export declare const INTEGRITY_CONSTRAINT_VIOLATION = "INTEGRITY_CONSTRAINT_VIOLATION";
4
5
  export declare const INVALID_INPUT_SYNTAX = "INVALID_INPUT_SYNTAX";
@@ -19,6 +20,7 @@ export declare const PG_ERRORS: {
19
20
  readonly '23001': "RESTRICT_VIOLATION";
20
21
  readonly '23503': "FOREIGN_KEY_VIOLATION";
21
22
  readonly '55P03': "LOCK_NOT_AVAILABLE";
23
+ readonly '40P01': "DEADLOCK_DETECTED";
22
24
  };
23
25
  type PgErrorType = (typeof PG_ERRORS)[keyof typeof PG_ERRORS];
24
26
  export declare function pgErrorType(error: any): PgErrorType | null;
@@ -63,18 +63,133 @@ export default class Decorators<TD extends typeof Dream, T extends Dream = Insta
63
63
  default?: boolean;
64
64
  }): any;
65
65
  /**
66
- * The Sortable decorator automatically adjusts the value of the columns
67
- * corresponding to the decorated field.
68
- *
69
- * NOTE: the Sortable decorator may not be used in STI child models (it may be used in the STI base class)
66
+ * Marks an integer column as a sortable position: Dream keeps the positions of
67
+ * every record in a sort scope contiguous, starting at 1, as records are
68
+ * created, moved, destroyed and undestroyed. A concurrent write racing a
69
+ * cascaded destroy or undestroy, under **Cascaded destroy and undestroy**
70
+ * below, is the one thing that can leave a scope with a gap;
71
+ * `Model.resort('position')` closes it.
70
72
  *
71
73
  * ```ts
72
- * class Balloon {
74
+ * class Post extends ApplicationModel {
73
75
  * @deco.Sortable({ scope: 'user' })
74
- * public position: DreamColumn<Balloon, 'position'>
76
+ * public position: number
77
+ * }
78
+ *
79
+ * await post.update({ position: 2 }) // the records at 2 and above shift up
80
+ * ```
81
+ *
82
+ * A position past the end of the scope is clamped to the end, and a position
83
+ * below 1 — or none at all — lands the record at the end.
84
+ *
85
+ * **A save that changes the sort scope ignores a position given alongside it.**
86
+ * The record lands at the end of the scope it moves into, whatever position the
87
+ * same `update` supplied:
88
+ *
89
+ * ```ts
90
+ * await post.update({ user: otherUser, position: 1 })
91
+ * // post is now the last record in otherUser's scope, not the first
92
+ * ```
93
+ *
94
+ * Move it in two saves to place it: `await post.update({ user: otherUser })`,
95
+ * then `await post.update({ position: 1 })`.
96
+ *
97
+ * Sortable requires a query driver that supports advisory transaction locks —
98
+ * the `PostgresQueryDriver` does — since a position write serializes the
99
+ * writers of its sort scope on one, apart from the cascades described below.
100
+ * This concurrency guarantee first shipped
101
+ * in Dream 2.28.0 and begins only after every writer is running a lock-aware
102
+ * release. During the first rolling deployment, older processes take no
103
+ * advisory locks and can still race the upgraded processes.
104
+ *
105
+ * All participating Dream writers of one hot scope serialize. Ordinary saves
106
+ * and destroys with `skipHooks`, direct query writes, raw SQL, and older
107
+ * pre-lock Dream processes bypass Sortable maintenance and do not participate
108
+ * in its locking protocol; a cascaded destroy or undestroy performs its
109
+ * maintenance but does not participate either. A direct undestroy still
110
+ * performs stabilized Sortable maintenance and acquires scope locks with
111
+ * `skipHooks: true`; locked query
112
+ * batches likewise acquire their scope locks during preflight, before any
113
+ * per-record callbacks, even when those callbacks skip hooks. A waiter that
114
+ * enters the protocol holds a pooled connection until the holder finishes or
115
+ * `sortableScopeLockTimeout` expires; sustained contention can therefore
116
+ * produce latency waves, timeouts, and pool starvation. Keep database work
117
+ * inside the lock window short, and avoid cross-region database latency for
118
+ * hot scopes.
119
+ *
120
+ * **Cascaded destroy and undestroy take no scope lock.** A record destroyed or
121
+ * undestroyed because its owner was — reached through a `dependent: 'destroy'`
122
+ * association, or through the undestroy that restores one — does its position
123
+ * work without the advisory lock, so a cascade holds no lock per sort scope it
124
+ * touches, however many scopes that is. Calling `destroy()` or `undestroy()`
125
+ * on a record yourself is unchanged. Where the cascade is destroying the owner
126
+ * of a sort scope — a `HasMany` whose foreign key is one of the field's scope
127
+ * columns, with no condition and not `through`, not polymorphic, whose target
128
+ * is not an STI child and declares no default scope other than
129
+ * `@deco.SoftDelete` — every row of that scope goes with it, and the cascade
130
+ * neither reads nor compacts it. Any other cascaded destroy compacts the
131
+ * scope it vacates, and every cascaded undestroy appends at the end of its
132
+ * scope, exactly as the direct operations do, only without the lock.
133
+ *
134
+ * Without the lock, a writer of the same sort scope during the cascade — a
135
+ * create, a move, a destroy, a `resort` — is not kept out, which costs one of
136
+ * two things. A gap: positions stay unique and correctly ordered, and
137
+ * `Model.resort('position')` closes it. Or two live rows sharing a position,
138
+ * which the deferrable unique constraint refuses when the later of the two
139
+ * transactions reaches COMMIT — as a unique violation, or as a deadlock when
140
+ * both reach it together — so nothing half-done commits and no commit hook
141
+ * runs. A `destroy()` or `undestroy()` whose transaction Dream opened runs the
142
+ * whole operation again on such a refusal, up to three times, before letting
143
+ * the adapter's error escape. Inside a transaction you opened, that error is
144
+ * your COMMIT failing: retry the transaction from the beginning, as for a
145
+ * deadlock. The other writer can lose the same race and fail at its own
146
+ * COMMIT instead, an ordinary `create` or `update` included, and Dream does
147
+ * not run those again: a bounded, occasional failed request in one contended
148
+ * scope is the deliberate price of cascades that hold no locks, where the
149
+ * lock count would otherwise be set by the data. A sort scope with a
150
+ * nullable column has no such backstop unless its constraint is declared
151
+ * `NULLS NOT DISTINCT`, since a plain unique constraint never sees two NULLs
152
+ * as equal.
153
+ *
154
+ * `SortableScopeDidNotStabilize` and `SortableScopeLockWaitTimedOut` are the
155
+ * expected Dream errors an application may choose to retry. Database deadlocks
156
+ * remain native adapter errors (for example PostgreSQL code `40P01` or MySQL
157
+ * errno `1213`) so their driver fields and stacks remain intact. A database
158
+ * deadlock aborts the whole transaction: retry by starting the transaction
159
+ * again from the beginning, never by continuing it or retrying only the failed
160
+ * statement. Recurrent deadlocks call for shorter transactions or a consistent
161
+ * multi-resource acquisition order; they do not by themselves prove Sortable
162
+ * caused the cycle.
163
+ *
164
+ * ```ts
165
+ * import {
166
+ * SortableScopeDidNotStabilize,
167
+ * SortableScopeLockWaitTimedOut,
168
+ * } from '@rvoh/dream/errors'
169
+ *
170
+ * const runTransaction = async () =>
171
+ * await ApplicationModel.transaction(async txn => {
172
+ * // bind every operation in the attempt to txn
173
+ * })
174
+ *
175
+ * try {
176
+ * await runTransaction()
177
+ * } catch (error) {
178
+ * const adapterDeadlock =
179
+ * (error as { code?: string }).code === '40P01' ||
180
+ * (error as { errno?: number }).errno === 1213
181
+ * const retryable =
182
+ * error instanceof SortableScopeDidNotStabilize ||
183
+ * error instanceof SortableScopeLockWaitTimedOut ||
184
+ * adapterDeadlock
185
+ * if (!retryable) throw error
186
+ *
187
+ * await runTransaction() // one whole-transaction retry
75
188
  * }
76
189
  * ```
77
190
  *
191
+ * NOTE: the Sortable decorator may not be used in STI child models (it may be used in the STI base class)
192
+ *
78
193
  * @param opts - Configuration options for the sortable decorator
79
194
  * @param opts.scope - The column, association, or combination thereof which you would like to restrict the incrementing logic to. Can be a single column name, a single belongs-to association name, or an array of column/association names
80
195
  * @returns A Sortable decorator
@@ -1,35 +1,8 @@
1
1
  /**
2
- * Marks an integer column as a sortable position: Dream keeps the positions of
3
- * every record in a sort scope contiguous, starting at 1, as records are
4
- * created, moved, destroyed and undestroyed.
5
- *
6
- * ```ts
7
- * class Post extends ApplicationModel {
8
- * @deco.Sortable({ scope: 'user' })
9
- * public position: number
10
- * }
11
- *
12
- * await post.update({ position: 2 }) // the records at 2 and above shift up
13
- * ```
14
- *
15
- * A position past the end of the scope is clamped to the end, and a position
16
- * below 1 — or none at all — lands the record at the end.
17
- *
18
- * **A save that changes the sort scope ignores a position given alongside it.**
19
- * The record lands at the end of the scope it moves into, whatever position the
20
- * same `update` supplied:
21
- *
22
- * ```ts
23
- * await post.update({ user: otherUser, position: 1 })
24
- * // post is now the last record in otherUser's scope, not the first
25
- * ```
26
- *
27
- * Move it in two saves to place it: `await post.update({ user: otherUser })`,
28
- * then `await post.update({ position: 1 })`.
29
- *
30
- * Sortable requires a query driver that supports advisory transaction locks —
31
- * the `PostgresQueryDriver` does — since every position write serializes the
32
- * writers of its sort scope on one.
2
+ * The implementation behind `@deco.Sortable`. The decorator's semantics —
3
+ * position clamping, what a scope-changing save does with a position given
4
+ * alongside it, and the advisory-lock concurrency protocol — are documented on
5
+ * `Decorators#Sortable`, which is where a consumer reads them.
33
6
  */
34
7
  export default function Sortable(opts?: SortableOpts): any;
35
8
  interface SortableOpts {
@@ -0,0 +1,39 @@
1
+ import Dream from '../../../../Dream.js';
2
+ import { SortableFieldConfig } from '../Sortable.js';
3
+ import { SortableCascadeEdge } from './sortableCascadeEdge.js';
4
+ /**
5
+ * @internal
6
+ *
7
+ * Whether a cascade reaching this record through `edge` reaches, and so
8
+ * destroys, every live row of the sortable field's sort scope. Such a scope is
9
+ * left with no survivor, so
10
+ * the compaction a destroy performs would close a vacancy nothing can observe,
11
+ * and the cascade skips it along with the snapshot read it needs. Every clause
12
+ * fails closed: a shape not positively recognized here compacts as a direct
13
+ * destroy does, unlocked, which costs statements and never a lock.
14
+ *
15
+ * The edge is the `dependent: 'destroy'` association the cascade destroyed
16
+ * through, the only kind the caller passes, and the answer is about that edge
17
+ * alone: another dependent edge of the same owner may reach the rows this one
18
+ * does not, and this fails closed rather than look for it.
19
+ *
20
+ * 1. The edge is a `HasMany`. A `HasOne` reaches one row of a scope that may
21
+ * hold others.
22
+ * 2. The edge is not polymorphic, is not a `through` association, and carries
23
+ * no `and`, `andNot`, `andAny`, `selfAnd` or `selfAndNot`. Each of those
24
+ * reaches a subset of the rows sharing the foreign key, and the rest can
25
+ * stay live: a soft-deleted owner's row remains, so no foreign key forces
26
+ * them out, and they keep their positions in a scope the cascade did not
27
+ * empty. (A hard destroy under a database foreign key would have refused to
28
+ * leave them, and compacting there is the harmless direction to be wrong in.)
29
+ * 3. The target is not an STI child, whose rows share a sort scope with its
30
+ * siblings' rows.
31
+ * 4. The target declares no default scope other than SoftDelete, since any
32
+ * other hides rows from the cascade's own load. A soft-deleted row holds no
33
+ * position and so is in no sort scope to begin with.
34
+ * 5. The edge's foreign key is one of the field's scope columns: every row the
35
+ * cascade destroys carries the owner's key, and so does every row of any sort
36
+ * scope that includes that column. Further scope members only partition that
37
+ * same set.
38
+ */
39
+ export default function cascadeReachesEveryRowInSortScope(dream: Dream, config: SortableFieldConfig, edge: SortableCascadeEdge): boolean;
@@ -1,5 +1,6 @@
1
1
  import Dream from '../../../../Dream.js';
2
2
  import DreamTransaction from '../../../../dream/DreamTransaction.js';
3
+ import { SortableFieldConfig } from '../Sortable.js';
3
4
  /**
4
5
  * @internal
5
6
  *
@@ -10,5 +11,9 @@ import DreamTransaction from '../../../../dream/DreamTransaction.js';
10
11
  * `afterDestroy` hook — never registered as a hook among them — so every
11
12
  * after-destroy hook observes the compacted scope regardless of where it was
12
13
  * declared, exactly as every after-save hook observes the computed position.
14
+ *
15
+ * @param sortableFields - the fields the preparation phase read a snapshot for,
16
+ * which on a cascaded destroy excludes every field whose whole sort scope the
17
+ * cascade is destroying
13
18
  */
14
- export default function performSortableDestroyWork(dream: Dream, txn: DreamTransaction<any>): Promise<void>;
19
+ export default function performSortableDestroyWork(dream: Dream, txn: DreamTransaction<any>, sortableFields: SortableFieldConfig[]): Promise<void>;