@rvoh/dream 2.26.0 → 2.28.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 (331) hide show
  1. package/dist/cjs/src/Dream.js +32 -17
  2. package/dist/cjs/src/db/errors.js +4 -0
  3. package/dist/cjs/src/decorators/field/sortable/Sortable.js +43 -118
  4. package/dist/cjs/src/decorators/field/sortable/helpers/acquireSortableScopeLocks.js +38 -0
  5. package/dist/cjs/src/decorators/field/sortable/helpers/acquireStabilizedSortableBatchLocks.js +183 -0
  6. package/dist/cjs/src/decorators/field/sortable/helpers/acquireStabilizedSortableScopeLocks.js +40 -0
  7. package/dist/cjs/src/decorators/field/sortable/helpers/canonicalScopeValue.js +194 -0
  8. package/dist/cjs/src/decorators/field/sortable/helpers/clearCachedSortableValues.js +18 -0
  9. package/dist/cjs/src/decorators/field/sortable/helpers/decrementScopedRecordsGreaterThanPosition.js +9 -9
  10. package/dist/cjs/src/decorators/field/sortable/helpers/filterQueryToInstanceScope.js +20 -0
  11. package/dist/cjs/src/decorators/field/sortable/helpers/filterQueryToScopeValues.js +32 -0
  12. package/dist/cjs/src/decorators/field/sortable/helpers/heldSortableScopeLockKeys.js +34 -0
  13. package/dist/cjs/src/decorators/field/sortable/helpers/performSortableDestroyWork.js +21 -0
  14. package/dist/cjs/src/decorators/field/sortable/helpers/performSortablePositionWork.js +36 -0
  15. package/dist/cjs/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.js +56 -0
  16. package/dist/cjs/src/decorators/field/sortable/helpers/prepareSortableFieldsForSave.js +84 -0
  17. package/dist/cjs/src/decorators/field/sortable/helpers/resortAllRecords.js +186 -29
  18. package/dist/cjs/src/decorators/field/sortable/helpers/setPosition.js +90 -46
  19. package/dist/cjs/src/decorators/field/sortable/helpers/sortableRowCache.js +73 -0
  20. package/dist/cjs/src/decorators/field/sortable/helpers/sortableScopeColumns.js +42 -0
  21. package/dist/cjs/src/decorators/field/sortable/helpers/sortableScopeLockKey.js +99 -0
  22. package/dist/cjs/src/decorators/field/sortable/helpers/sortableScopeLockKeys.js +72 -0
  23. package/dist/cjs/src/decorators/field/sortable/helpers/sortableSnapshot.js +101 -0
  24. package/dist/cjs/src/decorators/field/sortable/helpers/stabilizeSortableScopeLocks.js +86 -0
  25. package/dist/cjs/src/decorators/field/sortable/helpers/validPosition.js +21 -4
  26. package/dist/cjs/src/decorators/field/sortable/hooks/afterSortableDestroy.js +24 -7
  27. package/dist/cjs/src/decorators/field/sortable/hooks/afterSortableUpdate.js +10 -0
  28. package/dist/cjs/src/dream/DreamClassTransactionBuilder.js +0 -18
  29. package/dist/cjs/src/dream/Query.js +64 -32
  30. package/dist/cjs/src/dream/QueryDriver/Base.js +75 -2
  31. package/dist/cjs/src/dream/QueryDriver/Kysely.js +19 -12
  32. package/dist/cjs/src/dream/QueryDriver/Postgres.js +74 -0
  33. package/dist/cjs/src/dream/internal/destroyDream.js +60 -6
  34. package/dist/cjs/src/dream/internal/saveDream.js +109 -7
  35. package/dist/cjs/src/dream/internal/similarity/SimilarityBuilder.js +19 -4
  36. package/dist/cjs/src/dream/internal/softDeleteDream.js +13 -1
  37. package/dist/cjs/src/dream/internal/undestroyDream.js +56 -8
  38. package/dist/cjs/src/dream-app/index.js +21 -0
  39. package/dist/cjs/src/errors/SortableBatchRequiresTooManyScopeLocks.js +57 -0
  40. package/dist/cjs/src/errors/SortableRequiresAdvisoryTransactionLocks.js +26 -0
  41. package/dist/cjs/src/errors/SortableScopeDidNotStabilize.js +39 -0
  42. package/dist/cjs/src/errors/SortableScopeLockWaitTimedOut.js +50 -0
  43. package/dist/cjs/src/package-exports/errors.js +3 -0
  44. package/dist/esm/src/Dream.js +32 -17
  45. package/dist/esm/src/db/errors.js +4 -0
  46. package/dist/esm/src/decorators/field/sortable/Sortable.js +43 -118
  47. package/dist/esm/src/decorators/field/sortable/helpers/acquireSortableScopeLocks.js +38 -0
  48. package/dist/esm/src/decorators/field/sortable/helpers/acquireStabilizedSortableBatchLocks.js +183 -0
  49. package/dist/esm/src/decorators/field/sortable/helpers/acquireStabilizedSortableScopeLocks.js +40 -0
  50. package/dist/esm/src/decorators/field/sortable/helpers/canonicalScopeValue.js +194 -0
  51. package/dist/esm/src/decorators/field/sortable/helpers/clearCachedSortableValues.js +18 -0
  52. package/dist/esm/src/decorators/field/sortable/helpers/decrementScopedRecordsGreaterThanPosition.js +9 -9
  53. package/dist/esm/src/decorators/field/sortable/helpers/filterQueryToInstanceScope.js +20 -0
  54. package/dist/esm/src/decorators/field/sortable/helpers/filterQueryToScopeValues.js +32 -0
  55. package/dist/esm/src/decorators/field/sortable/helpers/heldSortableScopeLockKeys.js +34 -0
  56. package/dist/esm/src/decorators/field/sortable/helpers/performSortableDestroyWork.js +21 -0
  57. package/dist/esm/src/decorators/field/sortable/helpers/performSortablePositionWork.js +36 -0
  58. package/dist/esm/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.js +56 -0
  59. package/dist/esm/src/decorators/field/sortable/helpers/prepareSortableFieldsForSave.js +84 -0
  60. package/dist/esm/src/decorators/field/sortable/helpers/resortAllRecords.js +186 -29
  61. package/dist/esm/src/decorators/field/sortable/helpers/setPosition.js +90 -46
  62. package/dist/esm/src/decorators/field/sortable/helpers/sortableRowCache.js +73 -0
  63. package/dist/esm/src/decorators/field/sortable/helpers/sortableScopeColumns.js +42 -0
  64. package/dist/esm/src/decorators/field/sortable/helpers/sortableScopeLockKey.js +99 -0
  65. package/dist/esm/src/decorators/field/sortable/helpers/sortableScopeLockKeys.js +72 -0
  66. package/dist/esm/src/decorators/field/sortable/helpers/sortableSnapshot.js +101 -0
  67. package/dist/esm/src/decorators/field/sortable/helpers/stabilizeSortableScopeLocks.js +86 -0
  68. package/dist/esm/src/decorators/field/sortable/helpers/validPosition.js +21 -4
  69. package/dist/esm/src/decorators/field/sortable/hooks/afterSortableDestroy.js +24 -7
  70. package/dist/esm/src/decorators/field/sortable/hooks/afterSortableUpdate.js +10 -0
  71. package/dist/esm/src/dream/DreamClassTransactionBuilder.js +0 -18
  72. package/dist/esm/src/dream/Query.js +64 -32
  73. package/dist/esm/src/dream/QueryDriver/Base.js +75 -2
  74. package/dist/esm/src/dream/QueryDriver/Kysely.js +19 -12
  75. package/dist/esm/src/dream/QueryDriver/Postgres.js +74 -0
  76. package/dist/esm/src/dream/internal/destroyDream.js +60 -6
  77. package/dist/esm/src/dream/internal/saveDream.js +109 -7
  78. package/dist/esm/src/dream/internal/similarity/SimilarityBuilder.js +19 -4
  79. package/dist/esm/src/dream/internal/softDeleteDream.js +13 -1
  80. package/dist/esm/src/dream/internal/undestroyDream.js +56 -8
  81. package/dist/esm/src/dream-app/index.js +21 -0
  82. package/dist/esm/src/errors/SortableBatchRequiresTooManyScopeLocks.js +57 -0
  83. package/dist/esm/src/errors/SortableRequiresAdvisoryTransactionLocks.js +26 -0
  84. package/dist/esm/src/errors/SortableScopeDidNotStabilize.js +39 -0
  85. package/dist/esm/src/errors/SortableScopeLockWaitTimedOut.js +50 -0
  86. package/dist/esm/src/package-exports/errors.js +3 -0
  87. package/dist/types/src/Dream.d.ts +35 -2
  88. package/dist/types/src/db/errors.d.ts +2 -0
  89. package/dist/types/src/decorators/field/sortable/Sortable.d.ts +33 -0
  90. package/dist/types/src/decorators/field/sortable/helpers/acquireSortableScopeLocks.d.ts +25 -0
  91. package/dist/types/src/decorators/field/sortable/helpers/acquireStabilizedSortableBatchLocks.d.ts +91 -0
  92. package/dist/types/src/decorators/field/sortable/helpers/acquireStabilizedSortableScopeLocks.d.ts +31 -0
  93. package/dist/types/src/decorators/field/sortable/helpers/canonicalScopeValue.d.ts +85 -0
  94. package/dist/types/src/decorators/field/sortable/helpers/clearCachedSortableValues.d.ts +13 -0
  95. package/dist/types/src/decorators/field/sortable/helpers/decrementScopedRecordsGreaterThanPosition.d.ts +8 -1
  96. package/dist/types/src/decorators/field/sortable/helpers/filterQueryToInstanceScope.d.ts +24 -0
  97. package/dist/types/src/decorators/field/sortable/helpers/filterQueryToScopeValues.d.ts +22 -0
  98. package/dist/types/src/decorators/field/sortable/helpers/heldSortableScopeLockKeys.d.ts +27 -0
  99. package/dist/types/src/decorators/field/sortable/helpers/performSortableDestroyWork.d.ts +14 -0
  100. package/dist/types/src/decorators/field/sortable/helpers/performSortablePositionWork.d.ts +15 -0
  101. package/dist/types/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.d.ts +41 -0
  102. package/dist/types/src/decorators/field/sortable/helpers/prepareSortableFieldsForSave.d.ts +35 -0
  103. package/dist/types/src/decorators/field/sortable/helpers/resortAllRecords.d.ts +32 -0
  104. package/dist/types/src/decorators/field/sortable/helpers/setPosition.d.ts +27 -11
  105. package/dist/types/src/decorators/field/sortable/helpers/sortableRowCache.d.ts +48 -0
  106. package/dist/types/src/decorators/field/sortable/helpers/sortableScopeColumns.d.ts +30 -0
  107. package/dist/types/src/decorators/field/sortable/helpers/sortableScopeLockKey.d.ts +44 -0
  108. package/dist/types/src/decorators/field/sortable/helpers/sortableScopeLockKeys.d.ts +51 -0
  109. package/dist/types/src/decorators/field/sortable/helpers/sortableSnapshot.d.ts +92 -0
  110. package/dist/types/src/decorators/field/sortable/helpers/stabilizeSortableScopeLocks.d.ts +59 -0
  111. package/dist/types/src/decorators/field/sortable/helpers/validPosition.d.ts +14 -2
  112. package/dist/types/src/decorators/field/sortable/hooks/afterSortableCreate.d.ts +1 -1
  113. package/dist/types/src/decorators/field/sortable/hooks/afterSortableDestroy.d.ts +3 -1
  114. package/dist/types/src/decorators/field/sortable/hooks/afterSortableUpdate.d.ts +1 -1
  115. package/dist/types/src/dream/DreamClassTransactionBuilder.d.ts +3 -2
  116. package/dist/types/src/dream/Query.d.ts +44 -9
  117. package/dist/types/src/dream/QueryDriver/Base.d.ts +61 -1
  118. package/dist/types/src/dream/QueryDriver/Kysely.d.ts +6 -2
  119. package/dist/types/src/dream/QueryDriver/Postgres.d.ts +40 -0
  120. package/dist/types/src/dream/internal/softDeleteDream.d.ts +7 -1
  121. package/dist/types/src/dream-app/index.d.ts +18 -2
  122. package/dist/types/src/errors/SortableBatchRequiresTooManyScopeLocks.d.ts +37 -0
  123. package/dist/types/src/errors/SortableRequiresAdvisoryTransactionLocks.d.ts +13 -0
  124. package/dist/types/src/errors/SortableScopeDidNotStabilize.d.ts +23 -0
  125. package/dist/types/src/errors/SortableScopeLockWaitTimedOut.d.ts +34 -0
  126. package/dist/types/src/package-exports/errors.d.ts +3 -0
  127. package/dist/types/src/types/associations/shared.d.ts +1 -1
  128. package/dist/types/src/types/dream.d.ts +3 -0
  129. package/dist/types/src/types/query.d.ts +1 -1
  130. package/dist/types/src/types/types/associations/shared.ts +8 -6
  131. package/dist/types/src/types/types/dream.ts +6 -0
  132. package/dist/types/src/types/types/query.ts +1 -1
  133. package/docs/assets/hierarchy.js +1 -1
  134. package/docs/assets/navigation.js +1 -1
  135. package/docs/assets/search.js +1 -1
  136. package/docs/classes/db.DreamMigrationHelpers.html +11 -11
  137. package/docs/classes/db.KyselyQueryDriver.html +88 -41
  138. package/docs/classes/db.PostgresQueryDriver.html +82 -42
  139. package/docs/classes/db.QueryDriverBase.html +86 -39
  140. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  141. package/docs/classes/errors.ColumnOverflow.html +3 -3
  142. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  143. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  144. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  145. package/docs/classes/errors.DecryptionError.html +2 -2
  146. package/docs/classes/errors.DecryptionParseError.html +2 -2
  147. package/docs/classes/errors.DecryptionRotationError.html +3 -3
  148. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  149. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  150. package/docs/classes/errors.InvalidClockTime.html +2 -2
  151. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  152. package/docs/classes/errors.InvalidDateTime.html +2 -2
  153. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  154. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  155. package/docs/classes/errors.NotNullViolation.html +3 -3
  156. package/docs/classes/errors.RecordNotFound.html +3 -3
  157. package/docs/classes/errors.SortableBatchRequiresTooManyScopeLocks.html +61 -0
  158. package/docs/classes/errors.SortableScopeDidNotStabilize.html +48 -0
  159. package/docs/classes/errors.SortableScopeLockWaitTimedOut.html +58 -0
  160. package/docs/classes/errors.ValidationError.html +3 -3
  161. package/docs/classes/index.CalendarDate.html +33 -33
  162. package/docs/classes/index.ClockTime.html +32 -32
  163. package/docs/classes/index.ClockTimeTz.html +35 -35
  164. package/docs/classes/index.DateTime.html +86 -86
  165. package/docs/classes/index.Decorators.html +19 -19
  166. package/docs/classes/index.Dream.html +159 -124
  167. package/docs/classes/index.DreamApp.html +11 -10
  168. package/docs/classes/index.DreamTransaction.html +2 -2
  169. package/docs/classes/index.Env.html +2 -2
  170. package/docs/classes/index.Query.html +122 -99
  171. package/docs/classes/system.CliFileWriter.html +4 -4
  172. package/docs/classes/system.DreamBin.html +2 -2
  173. package/docs/classes/system.DreamCLI.html +7 -7
  174. package/docs/classes/system.DreamImporter.html +2 -2
  175. package/docs/classes/system.DreamLogos.html +2 -2
  176. package/docs/classes/system.DreamSerializerBuilder.html +11 -11
  177. package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
  178. package/docs/classes/system.PathHelpers.html +3 -3
  179. package/docs/classes/utils.Encrypt.html +3 -3
  180. package/docs/classes/utils.Range.html +2 -2
  181. package/docs/functions/db.closeAllDbConnections.html +1 -1
  182. package/docs/functions/db.dreamDbConnections.html +1 -1
  183. package/docs/functions/db.untypedDb.html +1 -1
  184. package/docs/functions/db.validateColumn.html +1 -1
  185. package/docs/functions/db.validateTable.html +1 -1
  186. package/docs/functions/errors.pgErrorType.html +1 -1
  187. package/docs/functions/index.DreamSerializer.html +1 -1
  188. package/docs/functions/index.ObjectSerializer.html +1 -1
  189. package/docs/functions/index.ReplicaSafe.html +1 -1
  190. package/docs/functions/index.STI.html +1 -1
  191. package/docs/functions/index.SoftDelete.html +1 -1
  192. package/docs/functions/utils.camelize.html +1 -1
  193. package/docs/functions/utils.capitalize.html +1 -1
  194. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  195. package/docs/functions/utils.compact.html +1 -1
  196. package/docs/functions/utils.groupBy.html +1 -1
  197. package/docs/functions/utils.hyphenize.html +1 -1
  198. package/docs/functions/utils.intersection.html +1 -1
  199. package/docs/functions/utils.isEmpty.html +1 -1
  200. package/docs/functions/utils.normalizeUnicode.html +1 -1
  201. package/docs/functions/utils.pascalize.html +1 -1
  202. package/docs/functions/utils.percent.html +1 -1
  203. package/docs/functions/utils.range.html +1 -1
  204. package/docs/functions/utils.round.html +1 -1
  205. package/docs/functions/utils.sanitizeString.html +1 -1
  206. package/docs/functions/utils.snakeify.html +1 -1
  207. package/docs/functions/utils.sort.html +2 -2
  208. package/docs/functions/utils.sortBy.html +1 -1
  209. package/docs/functions/utils.sortObjectByKey.html +1 -1
  210. package/docs/functions/utils.sortObjectByValue.html +1 -1
  211. package/docs/functions/utils.uncapitalize.html +1 -1
  212. package/docs/functions/utils.uniq.html +1 -1
  213. package/docs/hierarchy.html +1 -1
  214. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  215. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  216. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  217. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  218. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  219. package/docs/interfaces/types.DecoratorContext.html +2 -2
  220. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  221. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  222. package/docs/interfaces/types.DreamDbConfig.html +5 -5
  223. package/docs/interfaces/types.DurationObject.html +2 -2
  224. package/docs/interfaces/types.EncryptOptions.html +2 -2
  225. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  226. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  227. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  228. package/docs/modules/errors.html +1 -1
  229. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  230. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  231. package/docs/types/openapi.OpenapiFormats.html +1 -1
  232. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  233. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  234. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  235. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  236. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  237. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  238. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  239. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  240. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  241. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
  242. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
  243. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
  244. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  245. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  246. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  247. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  248. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  249. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  250. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  251. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  252. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
  253. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
  254. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  255. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  256. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
  257. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
  258. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  259. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  260. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
  261. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
  262. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
  263. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  264. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  265. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  266. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  267. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  268. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  269. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  270. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  271. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  272. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  273. package/docs/types/types.CalendarDateObject.html +1 -1
  274. package/docs/types/types.Camelized.html +1 -1
  275. package/docs/types/types.ClockTimeObject.html +1 -1
  276. package/docs/types/types.DbConnectionType.html +1 -1
  277. package/docs/types/types.DbTypes.html +1 -1
  278. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  279. package/docs/types/types.DreamAttributes.html +1 -1
  280. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  281. package/docs/types/types.DreamClassColumn.html +1 -1
  282. package/docs/types/types.DreamColumn.html +1 -1
  283. package/docs/types/types.DreamColumnNames.html +1 -1
  284. package/docs/types/types.DreamLogLevel.html +1 -1
  285. package/docs/types/types.DreamLogger.html +2 -2
  286. package/docs/types/types.DreamModelSerializerType.html +1 -1
  287. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  288. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  289. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  290. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  291. package/docs/types/types.DreamSerializable.html +1 -1
  292. package/docs/types/types.DreamSerializableArray.html +1 -1
  293. package/docs/types/types.DreamSerializerKey.html +1 -1
  294. package/docs/types/types.DreamSerializers.html +1 -1
  295. package/docs/types/types.DreamVirtualColumns.html +1 -1
  296. package/docs/types/types.DurationUnit.html +1 -1
  297. package/docs/types/types.EncryptAlgorithm.html +1 -1
  298. package/docs/types/types.HasManyStatement.html +1 -1
  299. package/docs/types/types.HasOneStatement.html +1 -1
  300. package/docs/types/types.Hyphenized.html +1 -1
  301. package/docs/types/types.Pascalized.html +1 -1
  302. package/docs/types/types.PrimaryKeyType.html +1 -1
  303. package/docs/types/types.RoundingPrecision.html +1 -1
  304. package/docs/types/types.SerializerCasing.html +1 -1
  305. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  306. package/docs/types/types.Snakeified.html +1 -1
  307. package/docs/types/types.StrictInterface.html +1 -1
  308. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  309. package/docs/types/types.UpdateableProperties.html +1 -1
  310. package/docs/types/types.ValidationType.html +1 -1
  311. package/docs/types/types.ViewModel.html +2 -2
  312. package/docs/types/types.ViewModelClass.html +1 -1
  313. package/docs/types/types.WeekdayName.html +1 -1
  314. package/docs/types/types.WhereStatementForDream.html +1 -1
  315. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  316. package/docs/variables/index.DreamConst.html +1 -1
  317. package/docs/variables/index.ops.html +1 -1
  318. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  319. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  320. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  321. package/docs/variables/system.primaryKeyTypes.html +1 -1
  322. package/package.json +1 -1
  323. package/dist/cjs/src/decorators/field/sortable/helpers/applySortableScopeToQuery.js +0 -13
  324. package/dist/cjs/src/decorators/field/sortable/helpers/sortableQueryExcludingDream.js +0 -7
  325. package/dist/cjs/src/decorators/field/sortable/hooks/beforeSortableSave.js +0 -41
  326. package/dist/esm/src/decorators/field/sortable/helpers/applySortableScopeToQuery.js +0 -13
  327. package/dist/esm/src/decorators/field/sortable/helpers/sortableQueryExcludingDream.js +0 -7
  328. package/dist/esm/src/decorators/field/sortable/hooks/beforeSortableSave.js +0 -41
  329. package/dist/types/src/decorators/field/sortable/helpers/applySortableScopeToQuery.d.ts +0 -12
  330. package/dist/types/src/decorators/field/sortable/helpers/sortableQueryExcludingDream.d.ts +0 -12
  331. package/dist/types/src/decorators/field/sortable/hooks/beforeSortableSave.d.ts +0 -12
@@ -0,0 +1,56 @@
1
+ import acquireStabilizedSortableScopeLocks from './acquireStabilizedSortableScopeLocks.js';
2
+ import clearCachedSortableValues from './clearCachedSortableValues.js';
3
+ import { cacheSortableSnapshots, clearSortableSnapshot } from './sortableSnapshot.js';
4
+ /**
5
+ * @internal
6
+ *
7
+ * The sortable preparation phase of a destroy. Called from `destroyDream` after
8
+ * every user `beforeDestroy` hook has run and before the delete — a phase, not
9
+ * a hook among them — so the operation's whole advisory key set, across every
10
+ * sortable field, is taken in one sorted stabilized acquisition, with one
11
+ * snapshot SELECT covering all fields. Per-field acquisition would take
12
+ * multi-field keys in declaration order, which deadlocks against the paths
13
+ * that acquire sorted.
14
+ *
15
+ * Pre-delete is the only seat available for the snapshot read: on a hard
16
+ * destroy the row is gone afterwards, and a soft destroy nulls the position
17
+ * columns in the same UPDATE as `deletedAt`. The lock is taken around the read
18
+ * so the snapshot cannot be invalidated between the read and the compaction;
19
+ * it is transaction-scoped, and `destroyDream` opens a transaction that spans
20
+ * the delete and the after-destroy hooks, so one acquisition covers both.
21
+ *
22
+ * The per-field compactions (`performSortableDestroyWork`) consume the cached
23
+ * snapshots.
24
+ *
25
+ * @returns false when the snapshot read found no row — this destroy has nothing
26
+ * to vacate, and compacting from the instance's remembered position would
27
+ * shift a scope some other writer has already closed. The delete's own
28
+ * affected-row count is the stronger signal (a writer outside the sortable
29
+ * path can remove the row after this read), so the caller checks both.
30
+ */
31
+ export default async function prepareSortableFieldsForDestroy(dream, txn) {
32
+ const sortableFields = dream.constructor['sortableFields'];
33
+ if (!sortableFields.length)
34
+ return true;
35
+ const { rowExists, snapshots } = await acquireStabilizedSortableScopeLocks(dream, txn, sortableFields);
36
+ cacheSortableSnapshots(dream, snapshots);
37
+ return rowExists;
38
+ }
39
+ /**
40
+ * @internal
41
+ *
42
+ * Discards what the preparation phase stashed on the instance. The per-field
43
+ * compaction clears its own snapshot as it consumes it, so this
44
+ * exists for the path where that compaction never runs: a destroy that removed
45
+ * nothing leaves the snapshot behind, and a later save or destroy of the same
46
+ * instance would compute its shift from a reading of a row this operation
47
+ * already found gone.
48
+ */
49
+ export function clearSortableFieldsForDestroy(dream) {
50
+ const sortableFields = (dream.constructor['sortableFields'] ??
51
+ []);
52
+ for (const { positionField } of sortableFields) {
53
+ clearCachedSortableValues(dream, positionField);
54
+ clearSortableSnapshot(dream, positionField);
55
+ }
56
+ }
@@ -0,0 +1,84 @@
1
+ import positionIsInvalid from './positionIsInvalid.js';
2
+ import sortableCacheValuesName from './sortableCacheValuesName.js';
3
+ /**
4
+ * @internal
5
+ *
6
+ * The sortable preparation phase of a save. Called from `saveDream` after every
7
+ * consumer before-hook has run — a phase, not a hook among them — so the
8
+ * decision is made from the final state of the instance: a position or scope
9
+ * change introduced by a `beforeSave` hook registered after Sortable's gets the
10
+ * same handling as one made before `save` was called, and a cache is never
11
+ * carried over from a state an intervening hook has made stale.
12
+ *
13
+ * For each sortable field this caches the values the position work needs and
14
+ * applies the position sentinel, then returns the fields whose position work
15
+ * this save will perform — the fields that make the save need a transaction of
16
+ * its own, so that the write and the position write that follows it commit
17
+ * together under one scope lock.
18
+ *
19
+ * - every create on a model with sortable fields performs position work;
20
+ * - an update performs it only when the save changes the position or one of the
21
+ * sortable scope columns.
22
+ *
23
+ * A save that skips hooks performs no position work at all, so it needs no
24
+ * transaction.
25
+ */
26
+ export default function prepareSortableFieldsForSave(dream, { alreadyPersisted, skipHooks }) {
27
+ if (skipHooks)
28
+ return [];
29
+ const sortableFields = dream.constructor['sortableFields'];
30
+ if (!sortableFields.length)
31
+ return [];
32
+ sortableFields.forEach(config => prepareSortableFieldForSave(dream, config));
33
+ if (!alreadyPersisted)
34
+ return [...sortableFields];
35
+ return sortableFields.filter(config => dream[sortableCacheValuesName(config.positionField)] !== undefined);
36
+ }
37
+ /**
38
+ * Caches the values one sortable field's position work needs and applies the
39
+ * position sentinel to the instance, so that the driver write never commits a
40
+ * caller-supplied position the scope has not made room for.
41
+ */
42
+ function prepareSortableFieldForSave(dream, { positionField, scope }) {
43
+ const cachedValuesName = sortableCacheValuesName(positionField);
44
+ const dreamAsAny = dream;
45
+ const changingScope = dream.isPersisted &&
46
+ scope.filter(scopeField => {
47
+ const association = dream['getAssociationMetadata'](scopeField);
48
+ return association
49
+ ? dream.willSaveChangeToAttribute(association.foreignKey())
50
+ : dream.willSaveChangeToAttribute(scopeField);
51
+ }).length > 0;
52
+ if (dream.willSaveChangeToAttribute(positionField) || changingScope) {
53
+ const position = dreamAsAny[positionField];
54
+ // store values to be used by the position work later in the save
55
+ const values = {
56
+ wasNewRecord: dream.isNewRecord,
57
+ changingScope,
58
+ position: positionIsInvalid(position) ? undefined : position,
59
+ previousPosition: dream.isPersisted && dream.willSaveChangeToAttribute(positionField)
60
+ ? dream.changes()[positionField]?.was
61
+ : changingScope
62
+ ? position
63
+ : undefined,
64
+ };
65
+ dreamAsAny[cachedValuesName] = values;
66
+ }
67
+ else {
68
+ // computed from the final state of the instance, so a cache from an earlier,
69
+ // now-stale state must not survive this decision
70
+ dreamAsAny[cachedValuesName] = undefined;
71
+ }
72
+ if (dream.isNewRecord || changingScope) {
73
+ // if the dream is not saved, or is being moved between scopes, set position to 0
74
+ // to prevent collisions with existing position values.
75
+ // the real position is computed and written under the scope lock later in the save.
76
+ dreamAsAny[positionField] = 0;
77
+ }
78
+ else if (dream.willSaveChangeToAttribute(positionField)) {
79
+ // if the dream is saved and the position is being changed, set the position field to
80
+ // undefined, which will cause the update cycle to ignore the position field. The
81
+ // position work later in the save writes the real value.
82
+ dreamAsAny[positionField] = undefined;
83
+ }
84
+ }
@@ -1,35 +1,192 @@
1
- import getColumnForSortableScope from './getColumnForSortableScope.js';
2
- import isSortedCorrectly from './isSortedCorrectly.js';
3
- import scopeArray from './scopeArray.js';
1
+ import { sql } from 'kysely';
2
+ import Query from '../../../../dream/Query.js';
3
+ import SortableRequiresAdvisoryTransactionLocks from '../../../../errors/SortableRequiresAdvisoryTransactionLocks.js';
4
+ import { STI_SCOPE_NAME } from '../../../class/STI.js';
5
+ import acquireSortableScopeLocks from './acquireSortableScopeLocks.js';
6
+ import canonicalScopeValue from './canonicalScopeValue.js';
7
+ import filterQueryToScopeValues from './filterQueryToScopeValues.js';
8
+ import sortableScopeColumns from './sortableScopeColumns.js';
9
+ import sortableScopeLockKey from './sortableScopeLockKey.js';
10
+ /**
11
+ * The alias and column names the renumbering UPDATE joins its `row_number()`
12
+ * subquery on. They are namespaced so that no real column of the table being
13
+ * resorted can collide with them.
14
+ */
15
+ const RENUMBER_ALIAS = 'dream_resort_renumber';
16
+ const RENUMBER_PRIMARY_KEY = 'dream_resort_primary_key';
17
+ const RENUMBER_POSITION = 'dream_resort_position';
18
+ /**
19
+ * The aliases the one-row summary of a sort scope's positions comes back under.
20
+ * They are namespaced for the same reason the renumbering aliases are, and are
21
+ * lowercase without underscores so that Kysely's CamelCasePlugin passes them
22
+ * through untouched and each value can be read straight off the result row (the
23
+ * same alias strategy `pluck` and the grouped aggregates use).
24
+ */
25
+ const SUMMARY_ROW_COUNT = 'dreamresortrowcount';
26
+ const SUMMARY_DISTINCT_POSITIONS = 'dreamresortdistinctpositions';
27
+ const SUMMARY_MIN_POSITION = 'dreamresortminposition';
28
+ const SUMMARY_MAX_POSITION = 'dreamresortmaxposition';
29
+ /**
30
+ * @internal
31
+ *
32
+ * Renumbers every sort scope of a model so that each one runs 1..n with no gaps.
33
+ *
34
+ * Four things make this safe to run against a live table:
35
+ *
36
+ * - **A driver that cannot take the scope lock fails before the table is
37
+ * read**, rather than succeeding for whichever scopes happen to be contiguous
38
+ * and throwing on the first one that is not.
39
+ * - **The STI scope is stripped**, exactly as every sortable hook strips it. An
40
+ * STI hierarchy shares one position space, so renumbering one child's rows in
41
+ * isolation would hand them positions its siblings already hold.
42
+ * - **Groups are identified structurally**, by the scope columns' `[column,
43
+ * value]` tuple rather than by joining the values into one string. Joining
44
+ * merges scopes whose values differ only in where a delimiter falls, and
45
+ * merges a null member with an empty-string one; a merged group is two
46
+ * physical sort scopes renumbered as though they were one, and no single lock
47
+ * key covers it.
48
+ * - **Every discovered scope is renumbered under its own scope lock, from a
49
+ * read taken inside that lock.** The unlocked pass below only discovers
50
+ * *which* sort scopes exist; it decides nothing about their contents, because
51
+ * a concurrent create, destroy or raw update between the two reads is
52
+ * precisely the case this exists to repair. Whether a scope needs renumbering
53
+ * at all is decided under the lock.
54
+ *
55
+ * Each scope's transaction is this function's own: `resort` cannot join a
56
+ * transaction the caller owns, and must not be called from inside one. It would
57
+ * wait on a connection of its own for a scope lock the caller's transaction may
58
+ * already hold — a wait no deadlock detector can see, which ends in
59
+ * `SortableScopeLockWaitTimedOut` rather than in progress.
60
+ */
4
61
  export default async function resortAllRecords(dreamClass, positionField, scope) {
5
- const dreams = await dreamClass.order({ [positionField]: 'asc' }).all();
6
- const hash = {};
7
- for (const dream of dreams) {
8
- const foreignKeys = foreignKeysForScope(dream, scope);
9
- hash[foreignKeys.join(':')] ||= [];
10
- hash[foreignKeys.join(':')].push(dream);
11
- }
12
- for (const dreamArr of Object.values(hash)) {
13
- const dreams = dreamArr;
14
- if (isSortedCorrectly(dreams, positionField))
15
- continue;
62
+ const queryDriverClass = Query.dbDriverClass(dreamClass.prototype.connectionName || 'default');
63
+ if (!queryDriverClass.supportsAdvisoryTransactionLocks)
64
+ throw new SortableRequiresAdvisoryTransactionLocks(queryDriverClass.name);
65
+ // every scope value, lock key and group identity below is derived from class
66
+ // level metadata — the table, the connection, the scope members' columns and
67
+ // their database types — so one bare instance stands in for every row
68
+ const derivationRecord = dreamClass.new();
69
+ for (const scopeValues of await discoverSortScopes(dreamClass, derivationRecord, scope)) {
16
70
  await dreamClass.transaction(async (txn) => {
17
- let counter = 1;
18
- for (const dream of dreams) {
19
- await dreamClass
20
- .txn(txn)
21
- .where({ [dream['_primaryKey']]: dream.primaryKeyValue() })
22
- .toKysely('update')
23
- .set({
24
- [positionField]: counter++,
25
- })
26
- .execute();
27
- }
71
+ await acquireSortableScopeLocks(derivationRecord, txn, [
72
+ sortableScopeLockKey(derivationRecord, positionField, scopeValues),
73
+ ]);
74
+ if (await scopeIsAlreadyOrdered({ dreamClass, derivationRecord, positionField, scope, scopeValues, txn }))
75
+ return;
76
+ await renumberScope({ dreamClass, derivationRecord, positionField, scope, scopeValues, txn });
28
77
  });
29
78
  }
30
79
  }
31
- function foreignKeysForScope(dream, scope) {
32
- return scopeArray(scope)
33
- .map(singleScope => getColumnForSortableScope(dream, singleScope))
34
- .map(fk => dream[fk]);
80
+ /**
81
+ * Every physical sort scope the table currently holds, as the `[column, value]`
82
+ * tuples a lock key is derived from.
83
+ *
84
+ * One distinct read over the scope columns alone, which both bounds the cost —
85
+ * a single round trip returning one row per sort scope, however many rows the
86
+ * table holds — and makes the answer stable: a walk that windows the table can
87
+ * be shifted past rows by a concurrent delete, and a sort scope whose only rows
88
+ * fall in that gap would never be renumbered at all. Each discovered scope's
89
+ * rows are read again under its own lock before anything is written.
90
+ *
91
+ * A sortable field with no scope has exactly one sort scope, the whole table,
92
+ * so its answer needs no read.
93
+ */
94
+ async function discoverSortScopes(dreamClass, derivationRecord, scope) {
95
+ const columns = sortableScopeColumns(derivationRecord, scope);
96
+ if (columns.length === 0)
97
+ return [[]];
98
+ const rows = (await stiUnscopedQuery(dreamClass)
99
+ .toKysely('select')
100
+ .clearSelect()
101
+ .distinct()
102
+ .select(columns)
103
+ .execute());
104
+ // the database's `distinct` is by raw value; two raw values the column's type
105
+ // treats as one (a citext scope column matching case-insensitively) are one
106
+ // sort scope under one lock key, and canonicalization is what says so
107
+ const sortScopes = new Map();
108
+ for (const row of rows) {
109
+ const scopeValues = columns.map((column) => [column, row[column]]);
110
+ const identity = scopeIdentity(derivationRecord, scopeValues);
111
+ if (!sortScopes.has(identity))
112
+ sortScopes.set(identity, scopeValues);
113
+ }
114
+ return [...sortScopes.values()];
115
+ }
116
+ /**
117
+ * Whether the sort scope already runs 1..n, decided inside the scope's lock by
118
+ * one aggregate row rather than by shipping every position in the scope to the
119
+ * client: a sort scope can hold as many rows as the table does, and this runs
120
+ * with the scope's lock held.
121
+ *
122
+ * A scope of `n` rows is already correct exactly when its positions are `n`
123
+ * distinct non-null values running from 1 to n. `count(distinct position)`
124
+ * drops both duplicates and nulls, so it agrees with `count(*)` only when every
125
+ * row holds a position of its own, and `min`/`max` pin that set to 1..n. An
126
+ * empty scope — one whose last row was destroyed between its discovery and this
127
+ * lock — has nothing to renumber.
128
+ */
129
+ async function scopeIsAlreadyOrdered({ dreamClass, derivationRecord, positionField, scope, scopeValues, txn, }) {
130
+ const summary = (await scopedSelect({ dreamClass, derivationRecord, scope, scopeValues, txn })
131
+ .clearSelect()
132
+ .select([
133
+ sql `count(*)`.as(SUMMARY_ROW_COUNT),
134
+ sql `count(distinct ${sql.ref(positionField)})`.as(SUMMARY_DISTINCT_POSITIONS),
135
+ sql `min(${sql.ref(positionField)})`.as(SUMMARY_MIN_POSITION),
136
+ sql `max(${sql.ref(positionField)})`.as(SUMMARY_MAX_POSITION),
137
+ ])
138
+ .executeTakeFirstOrThrow());
139
+ // counts come back from some drivers as strings, since a count is a bigint
140
+ const rowCount = Number(summary[SUMMARY_ROW_COUNT]);
141
+ if (rowCount === 0)
142
+ return true;
143
+ return (Number(summary[SUMMARY_DISTINCT_POSITIONS]) === rowCount &&
144
+ Number(summary[SUMMARY_MIN_POSITION]) === 1 &&
145
+ Number(summary[SUMMARY_MAX_POSITION]) === rowCount);
146
+ }
147
+ /**
148
+ * Renumbers the whole sort scope in one statement, from a `row_number()` taken
149
+ * over the scope's rows ordered by position, nulls first, ties broken by
150
+ * primary key. A row already holding the position it is owed is left alone, so
151
+ * a scope with a single gap in it writes a single row rather than all of them.
152
+ */
153
+ async function renumberScope({ dreamClass, derivationRecord, positionField, scope, scopeValues, txn, }) {
154
+ const table = derivationRecord.table;
155
+ const primaryKey = derivationRecord['_primaryKey'];
156
+ const renumbered = scopedSelect({ dreamClass, derivationRecord, scope, scopeValues, txn })
157
+ .clearSelect()
158
+ .select([
159
+ sql.ref(primaryKey).as(RENUMBER_PRIMARY_KEY),
160
+ sql `row_number() over (order by ${sql.ref(positionField)} asc nulls first, ${sql.ref(primaryKey)} asc)`.as(RENUMBER_POSITION),
161
+ ]);
162
+ await stiUnscopedQuery(dreamClass)
163
+ .txn(txn)
164
+ .toKysely('update')
165
+ .from(renumbered.as(RENUMBER_ALIAS))
166
+ .set({ [positionField]: sql.ref(`${RENUMBER_ALIAS}.${RENUMBER_POSITION}`) })
167
+ .whereRef(`${table}.${primaryKey}`, '=', `${RENUMBER_ALIAS}.${RENUMBER_PRIMARY_KEY}`)
168
+ .where(sql.ref(`${table}.${positionField}`), 'is distinct from', sql.ref(`${RENUMBER_ALIAS}.${RENUMBER_POSITION}`))
169
+ .execute();
170
+ }
171
+ /**
172
+ * A kysely select over the rows of one physical sort scope, carrying the
173
+ * model's default scopes (a soft-deleted row is not part of any sort scope) but
174
+ * not the STI scope, and matching a null scope member with `is null`.
175
+ */
176
+ function scopedSelect({ dreamClass, derivationRecord, scope, scopeValues, txn, }) {
177
+ const values = new Map(scopeValues);
178
+ return filterQueryToScopeValues(derivationRecord, stiUnscopedQuery(dreamClass).txn(txn).toKysely('select'), column => values.get(column) ?? null, scope);
179
+ }
180
+ function stiUnscopedQuery(dreamClass) {
181
+ return dreamClass.query().removeDefaultScope(STI_SCOPE_NAME);
182
+ }
183
+ /**
184
+ * A collision-free identity for a physical sort scope. Values are canonicalized
185
+ * against the column's database type by the same function the advisory lock key
186
+ * uses (null distinct from every string, a pending value agreeing with its
187
+ * hydrated counterpart) and encoded structurally, so two distinct tuples can
188
+ * never share an identity and a scope can never disagree with its own lock.
189
+ */
190
+ function scopeIdentity(derivationRecord, scopeValues) {
191
+ return JSON.stringify(scopeValues.map(([column, value]) => [column, canonicalScopeValue(derivationRecord, column, value)]));
35
192
  }
@@ -1,62 +1,116 @@
1
- import DreamApp from '../../../../dream-app/index.js';
2
- import PostgresQueryDriver from '../../../../dream/QueryDriver/Postgres.js';
3
1
  import range from '../../../../helpers/range.js';
4
- import getColumnForSortableScope from './getColumnForSortableScope.js';
5
- import scopeArray from './scopeArray.js';
2
+ import ops from '../../../../ops/index.js';
3
+ import acquireSortableScopeLocks from './acquireSortableScopeLocks.js';
4
+ import canonicalScopeValue from './canonicalScopeValue.js';
5
+ import filterQueryToScopeValues from './filterQueryToScopeValues.js';
6
+ import sortableScopeColumns from './sortableScopeColumns.js';
7
+ import { sortableScopeLockKeyForCurrentScope } from './sortableScopeLockKeys.js';
8
+ import { invalidateSortableRowCache } from './sortableRowCache.js';
9
+ import { snapshotScopeValue } from './sortableSnapshot.js';
6
10
  import validPosition from './validPosition.js';
7
11
  export default async function setPosition(obj) {
8
12
  const { dream, txn } = obj;
9
- if (txn) {
10
- await applyUpdates({ ...obj, txn });
11
- await dream.txn(txn).reload();
12
- }
13
- else {
14
- await dream.constructor.transaction(async (txn) => {
15
- await applyUpdates({ ...obj, txn });
16
- await dream.txn(txn).reload();
17
- });
18
- }
13
+ await applyUpdates(obj);
14
+ await dream.txn(txn).reload();
19
15
  }
20
16
  async function applyUpdates(obj) {
21
- const position = await validPosition(obj);
22
- await updateConflictingRecords({ ...obj, position });
23
- await updatePositionForRecord({ ...obj, position });
17
+ const { dream, positionField, scope, txn } = obj;
18
+ // Serialize every writer of this sort scope for the rest of the transaction,
19
+ // before the first position read. The operation-level phase has already
20
+ // acquired this key in its one sorted pass over every sortable field's keys
21
+ // (performSortablePositionWork on create; saveDream, before the driver
22
+ // write, on update), so this asks for a key the transaction holds and issues
23
+ // no statement at all.
24
+ await acquireSortableScopeLocks(dream, txn, [
25
+ sortableScopeLockKeyForCurrentScope(dream, positionField, scope),
26
+ ]);
27
+ // A pending scope change that lands on the scope the row already physically
28
+ // occupies moves the record between no scopes at all, and the rest of the
29
+ // write has to see it that way: the scope being vacated *is* the destination,
30
+ // so one source computes a new position from a max the other is about to
31
+ // shift out from under it. The record still counts toward the destination's
32
+ // extent — its row is in the scope, holding the position sentinel in place of
33
+ // the position the snapshot read — so the position write carries that
34
+ // position through to the clamp, and a record that was already last in the
35
+ // scope stays last rather than swapping with the record below it.
36
+ const write = alreadyInDestinationScope(obj)
37
+ ? { ...obj, changingScope: false, positionUnderSentinel: obj.previousPosition }
38
+ : obj;
39
+ const position = await validPosition(write);
40
+ await updateConflictingRecords({ ...write, position });
41
+ await updatePositionForRecord({ ...write, position });
42
+ // the shift above moved rows this transaction may be holding preflight reads
43
+ // of — every position a locked batch's later records were read with is now a
44
+ // position they no longer have
45
+ invalidateSortableRowCache(txn);
46
+ }
47
+ /**
48
+ * Whether a save that means to change sort scope is really a move within one
49
+ * scope: the snapshot read under the lock says the row already sits in the
50
+ * scope the instance is being written into, so nothing joins the destination
51
+ * and nothing is left behind.
52
+ *
53
+ * Values are compared through `canonicalScopeValue`, the same normalization the
54
+ * advisory lock key is derived from, so two shapes of one physical value — the
55
+ * pending `'1.00'` and the hydrated `1` — are one scope here exactly as they
56
+ * are when the key is taken.
57
+ *
58
+ * A row whose snapshot position is null holds no position in that scope, so it
59
+ * is joining the destination's position space and remains a scope change.
60
+ */
61
+ function alreadyInDestinationScope({ changingScope, dream, previousPosition, scope, snapshot, }) {
62
+ if (!changingScope || snapshot === undefined)
63
+ return false;
64
+ if (previousPosition === undefined || previousPosition === null)
65
+ return false;
66
+ const columns = sortableScopeColumns(dream, scope);
67
+ if (!columns.length)
68
+ return false;
69
+ return columns.every(column => snapshot.scopeValues.has(column) &&
70
+ canonicalScopeValue(dream, column, snapshot.scopeValues.get(column)) ===
71
+ canonicalScopeValue(dream, column, dream[column]));
24
72
  }
25
73
  async function updateConflictingRecords(obj) {
26
- const { wasNewRecord, position, previousPosition, dream, positionField, query, scope, txn, changingScope } = obj;
74
+ const { wasNewRecord, position, previousPosition, dream, positionField, query, scope, txn, changingScope, snapshot, } = obj;
75
+ // A scope move spans two unrelated position spaces: `previousPosition` is a
76
+ // position in the scope being left, `position` one in the scope being
77
+ // entered. Only the scope being left is compacted here, and it compacts all
78
+ // the way up — bounding it by a position from the other space closes nothing
79
+ // at all whenever the destination is the smaller of the two, leaving a
80
+ // permanent hole. The destination needs no room made: a scope-changing save
81
+ // always lands at the end of it, because `afterSortableUpdate` passes no
82
+ // position and `validPosition` then returns the destination's max + 1.
83
+ //
84
+ // A record with no position to vacate — a soft-deleted row, whose position
85
+ // column is NULL — leaves the scope it is moving out of untouched.
86
+ if (changingScope && (previousPosition === undefined || previousPosition === null))
87
+ return;
27
88
  const increasing = changingScope ||
28
89
  position === undefined ||
29
90
  (previousPosition !== undefined && previousPosition !== null && previousPosition < position);
91
+ const conflictingPositions = changingScope
92
+ ? ops.greaterThanOrEqualTo(previousPosition)
93
+ : increasing
94
+ ? range(previousPosition, position)
95
+ : range(position, previousPosition);
30
96
  let kyselyQuery = query
31
97
  .txn(txn)
32
98
  .whereNot({ [dream['_primaryKey']]: dream.primaryKeyValue() })
33
99
  .where({
34
- [positionField]: increasing ? range(previousPosition, position) : range(position, previousPosition),
100
+ [positionField]: conflictingPositions,
35
101
  })
36
102
  .toKysely('update')
37
103
  .set((eb) => ({
38
104
  [positionField]: eb(positionField, increasing ? '-' : '+', 1),
39
105
  }));
40
- kyselyQuery = applySortableScopesToQuery(dream, kyselyQuery, column => !wasNewRecord && dream.savedChangeToAttribute(column)
106
+ // the range being compacted belongs to the scope the record is *leaving*, so
107
+ // the filter has to name that scope's real column values — the snapshot read
108
+ // under the lock before the write, not what the instance remembers
109
+ kyselyQuery = filterQueryToScopeValues(dream, kyselyQuery, column => snapshotScopeValue(snapshot, column, () => !wasNewRecord && dream.savedChangeToAttribute(column)
41
110
  ? (dream.changes()[column]?.was ?? dream[column])
42
- : dream[column], scope);
111
+ : dream[column]), scope);
43
112
  await kyselyQuery.execute();
44
113
  }
45
- export function applySortableScopesToQuery(dream, kyselyQuery, whereValueCB, scope) {
46
- for (const singleScope of scopeArray(scope)) {
47
- const column = getColumnForSortableScope(dream, singleScope);
48
- if (column) {
49
- const columnValue = whereValueCB(column);
50
- if (columnValue === null) {
51
- kyselyQuery = kyselyQuery.where(column, 'is', null);
52
- }
53
- else {
54
- kyselyQuery = kyselyQuery.where(column, '=', columnValue);
55
- }
56
- }
57
- }
58
- return kyselyQuery;
59
- }
60
114
  async function updatePositionForRecord(obj) {
61
115
  const { txn, dream, positionField, position } = obj;
62
116
  await txn.kyselyTransaction
@@ -67,13 +121,3 @@ async function updatePositionForRecord(obj) {
67
121
  })
68
122
  .execute();
69
123
  }
70
- export function getPostgresQueryDriver(connectionName) {
71
- const queryDriverClass = DreamApp.getOrFail().dbConnectionQueryDriverClass(connectionName);
72
- if (!(queryDriverClass === PostgresQueryDriver)) {
73
- throw new Error(`
74
- ${queryDriverClass.name} is not an instance of PostgresQueryDriver.
75
- You must be using the PostgresQueryDriver when leveraging the Sortable decorator
76
- `);
77
- }
78
- return queryDriverClass;
79
- }
@@ -0,0 +1,73 @@
1
+ // The cached rows of every live transaction, keyed by the `DreamTransaction`
2
+ // object itself. Module-level state is safe here because the key is an object
3
+ // identity: every transaction — including a nested one — is its own
4
+ // `DreamTransaction` instance, so no lookup can ever see rows stashed by
5
+ // another transaction, whatever request or async flow it belongs to. The keys
6
+ // are held weakly, so an entry becomes collectible with its transaction and
7
+ // nothing accumulates for the life of the process. (The lock ledger in
8
+ // `heldSortableScopeLockKeys.ts` relies on the same properties.)
9
+ const rowsByTransaction = new WeakMap();
10
+ // a JSON tuple rather than a joined string, so that no table name and no
11
+ // primary key value can combine into another pair's key
12
+ function cacheKey(table, primaryKeyValue) {
13
+ return JSON.stringify([table, String(primaryKeyValue)]);
14
+ }
15
+ /**
16
+ * @internal
17
+ *
18
+ * Stashes rows a locked batch's preflight read under the batch's scope locks,
19
+ * so that the per-record position work that follows does not read them a second
20
+ * time. The preflight reads every candidate's position and sort scope columns
21
+ * to derive the key set; each record's own snapshot read then asks the same
22
+ * question of the same row, in the same transaction, under the same locks.
23
+ *
24
+ * Called once per preflight, with the rows of the pass that converged: an
25
+ * earlier pass read before some of the locks were held, and a preflight that
26
+ * raised holds no lock the rest of the transaction can rely on. The rows are
27
+ * consumed one at a time and the whole entry is dropped by the first position
28
+ * write of the batch, since a compaction moves rows this holds stale positions
29
+ * for — so what it saves is the snapshot reads of the records walked before
30
+ * that first write.
31
+ *
32
+ * @param txn - the batch's transaction
33
+ * @param table - the table the rows came from
34
+ * @param primaryKey - the column the rows are keyed by
35
+ * @param rows - the rows read, carrying at least the position and scope columns
36
+ */
37
+ export function cacheSortableRowsReadUnderLock(txn, table, primaryKey, rows) {
38
+ rowsByTransaction.set(txn, new Map(rows.map(row => [cacheKey(table, row[primaryKey]), row])));
39
+ }
40
+ /**
41
+ * @internal
42
+ *
43
+ * The cached row for one record, removed from the cache as it is handed over.
44
+ *
45
+ * Consuming is destructive because the consumer is a position-mutating
46
+ * operation: it is about to compact or renumber the scope the row sits in, and
47
+ * a second read of the same cached row would see the position the row had
48
+ * before that. {@link invalidateSortableRowCache} covers the same hazard for
49
+ * the *other* records a compaction moves.
50
+ */
51
+ export function takeCachedSortableRow(txn, table, primaryKeyValue) {
52
+ const rows = rowsByTransaction.get(txn);
53
+ if (!rows)
54
+ return undefined;
55
+ const key = cacheKey(table, primaryKeyValue);
56
+ const row = rows.get(key);
57
+ rows.delete(key);
58
+ return row;
59
+ }
60
+ /**
61
+ * @internal
62
+ *
63
+ * Discards everything cached for this transaction. Called for two reasons: a
64
+ * position write just moved rows the cache cannot identify — a compaction
65
+ * shifts every row in one sort scope above a position, and the cache holds no
66
+ * index by scope — and the batch the rows were read for has finished, after
67
+ * which they describe nothing the rest of a caller-owned transaction should
68
+ * trust. The records still to be walked read their own rows again, which is
69
+ * what they did before the cache existed.
70
+ */
71
+ export function invalidateSortableRowCache(txn) {
72
+ rowsByTransaction.delete(txn);
73
+ }
@@ -0,0 +1,42 @@
1
+ import getColumnForSortableScope from './getColumnForSortableScope.js';
2
+ import scopeArray from './scopeArray.js';
3
+ /**
4
+ * @internal
5
+ *
6
+ * The scope members of a sortable field paired with the database columns they
7
+ * resolve to. A member names either the column itself or the BelongsTo
8
+ * association whose foreign key backs it, and a caller matching incoming
9
+ * attributes needs both spellings; a member that resolves to no column is
10
+ * dropped.
11
+ *
12
+ * Every sortable path resolves its scope columns through this one function, so
13
+ * that a lock key, a snapshot read, a group identity and a scope filter can
14
+ * never disagree about which columns a sort scope is made of.
15
+ */
16
+ export function sortableScopeColumnPairs(dream, scope) {
17
+ return scopeArray(scope).reduce((acc, singleScope) => {
18
+ const column = getColumnForSortableScope(dream, singleScope);
19
+ if (column)
20
+ acc.push([singleScope, column]);
21
+ return acc;
22
+ }, []);
23
+ }
24
+ /**
25
+ * @internal
26
+ *
27
+ * The database columns a sortable field's scope resolves to.
28
+ */
29
+ export default function sortableScopeColumns(dream, scope) {
30
+ return sortableScopeColumnPairs(dream, scope).map(([, column]) => column);
31
+ }
32
+ /**
33
+ * @internal
34
+ *
35
+ * The sort scope as `[column, value]` pairs — the shape a lock key is derived
36
+ * from and a group is identified by. The value for each column comes from the
37
+ * caller, which decides whether it is reading the in-memory instance or a
38
+ * snapshot of the row.
39
+ */
40
+ export function sortableScopeValues(dream, scope, valueForColumn) {
41
+ return sortableScopeColumns(dream, scope).map(column => [column, valueForColumn(column)]);
42
+ }