@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
@@ -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
+ }
@@ -0,0 +1,99 @@
1
+ import DreamApp from '../../../../dream-app/index.js';
2
+ import canonicalScopeValue from './canonicalScopeValue.js';
3
+ const FNV_OFFSET_BASIS = 0xcbf29ce484222325n;
4
+ const FNV_PRIME = 0x100000001b3n;
5
+ const UINT64_MASK = 0xffffffffffffffffn;
6
+ const INT64_SIGN_BIT = 0x8000000000000000n;
7
+ const UINT64_MODULUS = 0x10000000000000000n;
8
+ const UTF8 = new TextEncoder();
9
+ /**
10
+ * The hash of the part of the key that cannot vary for one sortable field —
11
+ * database, table, position field — so that a path deriving a key per row, per
12
+ * field, per stabilization round only ever serializes and hashes the scope
13
+ * values. Keyed by the serialization of those three, so a `DreamApp` whose
14
+ * database name changes derives a new key rather than reusing a stale one.
15
+ */
16
+ const invariantPrefixHashes = new Map();
17
+ /**
18
+ * @internal
19
+ *
20
+ * Derives the advisory lock key that makes one sort scope's position
21
+ * computation exclusive. Every position-mutating path derives it the same way,
22
+ * since two paths that disagree about the key do not exclude each other.
23
+ *
24
+ * The key covers:
25
+ *
26
+ * - the **physical database name**, because Postgres advisory locks are scoped
27
+ * to the cluster rather than to a database. Without it, two unrelated
28
+ * databases in one cluster — including this repo's own parallel test
29
+ * databases — would contend on the same key, and that contention shows up
30
+ * only as writers of unrelated scopes queueing behind each other.
31
+ * - the table and the position field, since a model can declare several
32
+ * sortable fields, each with its own position space.
33
+ * - the scope column values, which are what a "sort scope" actually is.
34
+ *
35
+ * Values are encoded structurally rather than joined into a string, so a null
36
+ * member is distinct from an empty string and no combination of delimiters in
37
+ * one member can be mistaken for a different tuple. They are also canonicalized
38
+ * against the column's database type (`canonicalScopeValue`), so a writer
39
+ * holding a pending JavaScript value and a writer holding the value Postgres
40
+ * hydrated derive one key rather than two.
41
+ *
42
+ * **Scope values are assumed non-adversarial.** The hash is FNV-1a, which is
43
+ * unkeyed: anyone who can choose a scope column's value — where that column
44
+ * holds user-supplied text rather than a foreign key — can compute a value
45
+ * whose key equals another scope's, and share that scope's lock with it. A
46
+ * collision costs contention and never correctness, since a shared lock
47
+ * over-serializes rather than under-locking, and the bounded wait
48
+ * (`sortableScopeLockTimeout`) caps what a deliberate one can achieve at
49
+ * stalls rather than at hangs. A keyed digest would make deliberate collisions
50
+ * expensive, and is not used: changing the derivation would give old and new
51
+ * processes different keys for one scope during a rolling deploy, which is the
52
+ * race this lock exists to eliminate.
53
+ *
54
+ * @param dream - the record whose scope is being locked
55
+ * @param positionField - the sortable position column
56
+ * @param scopeValues - `[column, value]` pairs for the scope being locked
57
+ * @returns a 64-bit signed key
58
+ */
59
+ export default function sortableScopeLockKey(dream, positionField, scopeValues) {
60
+ const databaseName = DreamApp.getOrFail().dbName(dream.connectionName || 'default', 'primary');
61
+ const normalizedScopeValues = [...scopeValues]
62
+ .sort(([columnA], [columnB]) => (columnA < columnB ? -1 : columnA > columnB ? 1 : 0))
63
+ .map(([column, value]) => [column, canonicalScopeValue(dream, column, value)]);
64
+ // the hashed text is exactly
65
+ // `JSON.stringify([databaseName, table, positionField, normalizedScopeValues])`,
66
+ // hashed in two halves so the invariant half is hashed once per sortable
67
+ // field rather than once per row. FNV-1a is a streaming hash, so resuming
68
+ // from the cached state gives the same key byte for byte
69
+ return toSignedKey(fnv1a64(JSON.stringify(normalizedScopeValues) + ']', invariantPrefixHash(databaseName, dream.table, positionField)));
70
+ }
71
+ function invariantPrefixHash(databaseName, table, positionField) {
72
+ // the serialization of the first three members, with the array's closing
73
+ // bracket replaced by the separator the scope values follow. It is also the
74
+ // memo key: a JSON array of the three members is unambiguous, where any
75
+ // joined form would need a delimiter no member could contain
76
+ const prefix = JSON.stringify([databaseName, table, positionField]).slice(0, -1) + ',';
77
+ const cached = invariantPrefixHashes.get(prefix);
78
+ if (cached !== undefined)
79
+ return cached;
80
+ const hash = fnv1a64(prefix, FNV_OFFSET_BASIS);
81
+ invariantPrefixHashes.set(prefix, hash);
82
+ return hash;
83
+ }
84
+ /**
85
+ * 64-bit FNV-1a, resumed from a previously hashed prefix.
86
+ */
87
+ function fnv1a64(value, hash) {
88
+ for (const byte of UTF8.encode(value)) {
89
+ hash = ((hash ^ BigInt(byte)) * FNV_PRIME) & UINT64_MASK;
90
+ }
91
+ return hash;
92
+ }
93
+ /**
94
+ * Folds the unsigned hash into the signed range Postgres's
95
+ * `pg_advisory_xact_lock(bigint)` accepts.
96
+ */
97
+ function toSignedKey(hash) {
98
+ return hash >= INT64_SIGN_BIT ? hash - UINT64_MODULUS : hash;
99
+ }