@rvoh/dream 2.27.0 → 2.28.1

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 (330) hide show
  1. package/dist/cjs/src/Dream.js +63 -1
  2. package/dist/cjs/src/db/errors.js +4 -0
  3. package/dist/cjs/src/decorators/field/Encrypted.js +12 -0
  4. package/dist/cjs/src/decorators/field/sortable/Sortable.js +43 -118
  5. package/dist/cjs/src/decorators/field/sortable/helpers/acquireSortableScopeLocks.js +38 -0
  6. package/dist/cjs/src/decorators/field/sortable/helpers/acquireStabilizedSortableBatchLocks.js +183 -0
  7. package/dist/cjs/src/decorators/field/sortable/helpers/acquireStabilizedSortableScopeLocks.js +40 -0
  8. package/dist/cjs/src/decorators/field/sortable/helpers/canonicalScopeValue.js +194 -0
  9. package/dist/cjs/src/decorators/field/sortable/helpers/clearCachedSortableValues.js +18 -0
  10. package/dist/cjs/src/decorators/field/sortable/helpers/decrementScopedRecordsGreaterThanPosition.js +9 -9
  11. package/dist/cjs/src/decorators/field/sortable/helpers/filterQueryToInstanceScope.js +20 -0
  12. package/dist/cjs/src/decorators/field/sortable/helpers/filterQueryToScopeValues.js +32 -0
  13. package/dist/cjs/src/decorators/field/sortable/helpers/heldSortableScopeLockKeys.js +34 -0
  14. package/dist/cjs/src/decorators/field/sortable/helpers/performSortableDestroyWork.js +21 -0
  15. package/dist/cjs/src/decorators/field/sortable/helpers/performSortablePositionWork.js +36 -0
  16. package/dist/cjs/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.js +56 -0
  17. package/dist/cjs/src/decorators/field/sortable/helpers/prepareSortableFieldsForSave.js +84 -0
  18. package/dist/cjs/src/decorators/field/sortable/helpers/resortAllRecords.js +186 -29
  19. package/dist/cjs/src/decorators/field/sortable/helpers/setPosition.js +90 -46
  20. package/dist/cjs/src/decorators/field/sortable/helpers/sortableRowCache.js +73 -0
  21. package/dist/cjs/src/decorators/field/sortable/helpers/sortableScopeColumns.js +42 -0
  22. package/dist/cjs/src/decorators/field/sortable/helpers/sortableScopeLockKey.js +99 -0
  23. package/dist/cjs/src/decorators/field/sortable/helpers/sortableScopeLockKeys.js +72 -0
  24. package/dist/cjs/src/decorators/field/sortable/helpers/sortableSnapshot.js +101 -0
  25. package/dist/cjs/src/decorators/field/sortable/helpers/stabilizeSortableScopeLocks.js +86 -0
  26. package/dist/cjs/src/decorators/field/sortable/helpers/validPosition.js +21 -4
  27. package/dist/cjs/src/decorators/field/sortable/hooks/afterSortableDestroy.js +24 -7
  28. package/dist/cjs/src/decorators/field/sortable/hooks/afterSortableUpdate.js +10 -0
  29. package/dist/cjs/src/dream/Query.js +73 -11
  30. package/dist/cjs/src/dream/QueryDriver/Base.js +75 -2
  31. package/dist/cjs/src/dream/QueryDriver/Kysely.js +11 -4
  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/encrypt/assertNoEncryptedColumnWrites.js +62 -0
  40. package/dist/cjs/src/errors/SortableBatchRequiresTooManyScopeLocks.js +57 -0
  41. package/dist/cjs/src/errors/SortableRequiresAdvisoryTransactionLocks.js +26 -0
  42. package/dist/cjs/src/errors/SortableScopeDidNotStabilize.js +39 -0
  43. package/dist/cjs/src/errors/SortableScopeLockWaitTimedOut.js +50 -0
  44. package/dist/cjs/src/errors/encrypt/CannotSetEncryptedColumnInQueryUpdate.js +31 -0
  45. package/dist/cjs/src/package-exports/errors.js +3 -0
  46. package/dist/esm/src/Dream.js +63 -1
  47. package/dist/esm/src/db/errors.js +4 -0
  48. package/dist/esm/src/decorators/field/Encrypted.js +12 -0
  49. package/dist/esm/src/decorators/field/sortable/Sortable.js +43 -118
  50. package/dist/esm/src/decorators/field/sortable/helpers/acquireSortableScopeLocks.js +38 -0
  51. package/dist/esm/src/decorators/field/sortable/helpers/acquireStabilizedSortableBatchLocks.js +183 -0
  52. package/dist/esm/src/decorators/field/sortable/helpers/acquireStabilizedSortableScopeLocks.js +40 -0
  53. package/dist/esm/src/decorators/field/sortable/helpers/canonicalScopeValue.js +194 -0
  54. package/dist/esm/src/decorators/field/sortable/helpers/clearCachedSortableValues.js +18 -0
  55. package/dist/esm/src/decorators/field/sortable/helpers/decrementScopedRecordsGreaterThanPosition.js +9 -9
  56. package/dist/esm/src/decorators/field/sortable/helpers/filterQueryToInstanceScope.js +20 -0
  57. package/dist/esm/src/decorators/field/sortable/helpers/filterQueryToScopeValues.js +32 -0
  58. package/dist/esm/src/decorators/field/sortable/helpers/heldSortableScopeLockKeys.js +34 -0
  59. package/dist/esm/src/decorators/field/sortable/helpers/performSortableDestroyWork.js +21 -0
  60. package/dist/esm/src/decorators/field/sortable/helpers/performSortablePositionWork.js +36 -0
  61. package/dist/esm/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.js +56 -0
  62. package/dist/esm/src/decorators/field/sortable/helpers/prepareSortableFieldsForSave.js +84 -0
  63. package/dist/esm/src/decorators/field/sortable/helpers/resortAllRecords.js +186 -29
  64. package/dist/esm/src/decorators/field/sortable/helpers/setPosition.js +90 -46
  65. package/dist/esm/src/decorators/field/sortable/helpers/sortableRowCache.js +73 -0
  66. package/dist/esm/src/decorators/field/sortable/helpers/sortableScopeColumns.js +42 -0
  67. package/dist/esm/src/decorators/field/sortable/helpers/sortableScopeLockKey.js +99 -0
  68. package/dist/esm/src/decorators/field/sortable/helpers/sortableScopeLockKeys.js +72 -0
  69. package/dist/esm/src/decorators/field/sortable/helpers/sortableSnapshot.js +101 -0
  70. package/dist/esm/src/decorators/field/sortable/helpers/stabilizeSortableScopeLocks.js +86 -0
  71. package/dist/esm/src/decorators/field/sortable/helpers/validPosition.js +21 -4
  72. package/dist/esm/src/decorators/field/sortable/hooks/afterSortableDestroy.js +24 -7
  73. package/dist/esm/src/decorators/field/sortable/hooks/afterSortableUpdate.js +10 -0
  74. package/dist/esm/src/dream/Query.js +73 -11
  75. package/dist/esm/src/dream/QueryDriver/Base.js +75 -2
  76. package/dist/esm/src/dream/QueryDriver/Kysely.js +11 -4
  77. package/dist/esm/src/dream/QueryDriver/Postgres.js +74 -0
  78. package/dist/esm/src/dream/internal/destroyDream.js +60 -6
  79. package/dist/esm/src/dream/internal/saveDream.js +109 -7
  80. package/dist/esm/src/dream/internal/similarity/SimilarityBuilder.js +19 -4
  81. package/dist/esm/src/dream/internal/softDeleteDream.js +13 -1
  82. package/dist/esm/src/dream/internal/undestroyDream.js +56 -8
  83. package/dist/esm/src/dream-app/index.js +21 -0
  84. package/dist/esm/src/encrypt/assertNoEncryptedColumnWrites.js +62 -0
  85. package/dist/esm/src/errors/SortableBatchRequiresTooManyScopeLocks.js +57 -0
  86. package/dist/esm/src/errors/SortableRequiresAdvisoryTransactionLocks.js +26 -0
  87. package/dist/esm/src/errors/SortableScopeDidNotStabilize.js +39 -0
  88. package/dist/esm/src/errors/SortableScopeLockWaitTimedOut.js +50 -0
  89. package/dist/esm/src/errors/encrypt/CannotSetEncryptedColumnInQueryUpdate.js +31 -0
  90. package/dist/esm/src/package-exports/errors.js +3 -0
  91. package/dist/types/src/Dream.d.ts +56 -0
  92. package/dist/types/src/db/errors.d.ts +2 -0
  93. package/dist/types/src/decorators/field/sortable/Sortable.d.ts +33 -0
  94. package/dist/types/src/decorators/field/sortable/helpers/acquireSortableScopeLocks.d.ts +25 -0
  95. package/dist/types/src/decorators/field/sortable/helpers/acquireStabilizedSortableBatchLocks.d.ts +91 -0
  96. package/dist/types/src/decorators/field/sortable/helpers/acquireStabilizedSortableScopeLocks.d.ts +31 -0
  97. package/dist/types/src/decorators/field/sortable/helpers/canonicalScopeValue.d.ts +85 -0
  98. package/dist/types/src/decorators/field/sortable/helpers/clearCachedSortableValues.d.ts +13 -0
  99. package/dist/types/src/decorators/field/sortable/helpers/decrementScopedRecordsGreaterThanPosition.d.ts +8 -1
  100. package/dist/types/src/decorators/field/sortable/helpers/filterQueryToInstanceScope.d.ts +24 -0
  101. package/dist/types/src/decorators/field/sortable/helpers/filterQueryToScopeValues.d.ts +22 -0
  102. package/dist/types/src/decorators/field/sortable/helpers/heldSortableScopeLockKeys.d.ts +27 -0
  103. package/dist/types/src/decorators/field/sortable/helpers/performSortableDestroyWork.d.ts +14 -0
  104. package/dist/types/src/decorators/field/sortable/helpers/performSortablePositionWork.d.ts +15 -0
  105. package/dist/types/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.d.ts +41 -0
  106. package/dist/types/src/decorators/field/sortable/helpers/prepareSortableFieldsForSave.d.ts +35 -0
  107. package/dist/types/src/decorators/field/sortable/helpers/resortAllRecords.d.ts +32 -0
  108. package/dist/types/src/decorators/field/sortable/helpers/setPosition.d.ts +27 -11
  109. package/dist/types/src/decorators/field/sortable/helpers/sortableRowCache.d.ts +48 -0
  110. package/dist/types/src/decorators/field/sortable/helpers/sortableScopeColumns.d.ts +30 -0
  111. package/dist/types/src/decorators/field/sortable/helpers/sortableScopeLockKey.d.ts +44 -0
  112. package/dist/types/src/decorators/field/sortable/helpers/sortableScopeLockKeys.d.ts +51 -0
  113. package/dist/types/src/decorators/field/sortable/helpers/sortableSnapshot.d.ts +92 -0
  114. package/dist/types/src/decorators/field/sortable/helpers/stabilizeSortableScopeLocks.d.ts +59 -0
  115. package/dist/types/src/decorators/field/sortable/helpers/validPosition.d.ts +14 -2
  116. package/dist/types/src/decorators/field/sortable/hooks/afterSortableCreate.d.ts +1 -1
  117. package/dist/types/src/decorators/field/sortable/hooks/afterSortableDestroy.d.ts +3 -1
  118. package/dist/types/src/decorators/field/sortable/hooks/afterSortableUpdate.d.ts +1 -1
  119. package/dist/types/src/dream/Query.d.ts +47 -9
  120. package/dist/types/src/dream/QueryDriver/Base.d.ts +61 -1
  121. package/dist/types/src/dream/QueryDriver/Kysely.d.ts +6 -2
  122. package/dist/types/src/dream/QueryDriver/Postgres.d.ts +40 -0
  123. package/dist/types/src/dream/internal/softDeleteDream.d.ts +7 -1
  124. package/dist/types/src/dream-app/index.d.ts +18 -2
  125. package/dist/types/src/encrypt/assertNoEncryptedColumnWrites.d.ts +27 -0
  126. package/dist/types/src/errors/SortableBatchRequiresTooManyScopeLocks.d.ts +37 -0
  127. package/dist/types/src/errors/SortableRequiresAdvisoryTransactionLocks.d.ts +13 -0
  128. package/dist/types/src/errors/SortableScopeDidNotStabilize.d.ts +23 -0
  129. package/dist/types/src/errors/SortableScopeLockWaitTimedOut.d.ts +34 -0
  130. package/dist/types/src/errors/encrypt/CannotSetEncryptedColumnInQueryUpdate.d.ts +8 -0
  131. package/dist/types/src/package-exports/errors.d.ts +3 -0
  132. package/docs/assets/hierarchy.js +1 -1
  133. package/docs/assets/navigation.js +1 -1
  134. package/docs/assets/search.js +1 -1
  135. package/docs/classes/db.DreamMigrationHelpers.html +11 -11
  136. package/docs/classes/db.KyselyQueryDriver.html +88 -41
  137. package/docs/classes/db.PostgresQueryDriver.html +82 -42
  138. package/docs/classes/db.QueryDriverBase.html +86 -39
  139. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  140. package/docs/classes/errors.ColumnOverflow.html +3 -3
  141. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  142. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  143. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  144. package/docs/classes/errors.DecryptionError.html +2 -2
  145. package/docs/classes/errors.DecryptionParseError.html +2 -2
  146. package/docs/classes/errors.DecryptionRotationError.html +3 -3
  147. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  148. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  149. package/docs/classes/errors.InvalidClockTime.html +2 -2
  150. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  151. package/docs/classes/errors.InvalidDateTime.html +2 -2
  152. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  153. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  154. package/docs/classes/errors.NotNullViolation.html +3 -3
  155. package/docs/classes/errors.RecordNotFound.html +3 -3
  156. package/docs/classes/errors.SortableBatchRequiresTooManyScopeLocks.html +61 -0
  157. package/docs/classes/errors.SortableScopeDidNotStabilize.html +48 -0
  158. package/docs/classes/errors.SortableScopeLockWaitTimedOut.html +58 -0
  159. package/docs/classes/errors.ValidationError.html +3 -3
  160. package/docs/classes/index.CalendarDate.html +33 -33
  161. package/docs/classes/index.ClockTime.html +32 -32
  162. package/docs/classes/index.ClockTimeTz.html +35 -35
  163. package/docs/classes/index.DateTime.html +86 -86
  164. package/docs/classes/index.Decorators.html +19 -19
  165. package/docs/classes/index.Dream.html +151 -124
  166. package/docs/classes/index.DreamApp.html +11 -10
  167. package/docs/classes/index.DreamTransaction.html +2 -2
  168. package/docs/classes/index.Env.html +2 -2
  169. package/docs/classes/index.Query.html +124 -98
  170. package/docs/classes/system.CliFileWriter.html +4 -4
  171. package/docs/classes/system.DreamBin.html +2 -2
  172. package/docs/classes/system.DreamCLI.html +7 -7
  173. package/docs/classes/system.DreamImporter.html +2 -2
  174. package/docs/classes/system.DreamLogos.html +2 -2
  175. package/docs/classes/system.DreamSerializerBuilder.html +11 -11
  176. package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
  177. package/docs/classes/system.PathHelpers.html +3 -3
  178. package/docs/classes/utils.Encrypt.html +3 -3
  179. package/docs/classes/utils.Range.html +2 -2
  180. package/docs/functions/db.closeAllDbConnections.html +1 -1
  181. package/docs/functions/db.dreamDbConnections.html +1 -1
  182. package/docs/functions/db.untypedDb.html +1 -1
  183. package/docs/functions/db.validateColumn.html +1 -1
  184. package/docs/functions/db.validateTable.html +1 -1
  185. package/docs/functions/errors.pgErrorType.html +1 -1
  186. package/docs/functions/index.DreamSerializer.html +1 -1
  187. package/docs/functions/index.ObjectSerializer.html +1 -1
  188. package/docs/functions/index.ReplicaSafe.html +1 -1
  189. package/docs/functions/index.STI.html +1 -1
  190. package/docs/functions/index.SoftDelete.html +1 -1
  191. package/docs/functions/utils.camelize.html +1 -1
  192. package/docs/functions/utils.capitalize.html +1 -1
  193. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  194. package/docs/functions/utils.compact.html +1 -1
  195. package/docs/functions/utils.groupBy.html +1 -1
  196. package/docs/functions/utils.hyphenize.html +1 -1
  197. package/docs/functions/utils.intersection.html +1 -1
  198. package/docs/functions/utils.isEmpty.html +1 -1
  199. package/docs/functions/utils.normalizeUnicode.html +1 -1
  200. package/docs/functions/utils.pascalize.html +1 -1
  201. package/docs/functions/utils.percent.html +1 -1
  202. package/docs/functions/utils.range.html +1 -1
  203. package/docs/functions/utils.round.html +1 -1
  204. package/docs/functions/utils.sanitizeString.html +1 -1
  205. package/docs/functions/utils.snakeify.html +1 -1
  206. package/docs/functions/utils.sort.html +2 -2
  207. package/docs/functions/utils.sortBy.html +1 -1
  208. package/docs/functions/utils.sortObjectByKey.html +1 -1
  209. package/docs/functions/utils.sortObjectByValue.html +1 -1
  210. package/docs/functions/utils.uncapitalize.html +1 -1
  211. package/docs/functions/utils.uniq.html +1 -1
  212. package/docs/hierarchy.html +1 -1
  213. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  214. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  215. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  216. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  217. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  218. package/docs/interfaces/types.DecoratorContext.html +2 -2
  219. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  220. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  221. package/docs/interfaces/types.DreamDbConfig.html +5 -5
  222. package/docs/interfaces/types.DurationObject.html +2 -2
  223. package/docs/interfaces/types.EncryptOptions.html +2 -2
  224. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  225. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  226. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  227. package/docs/modules/errors.html +1 -1
  228. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  229. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  230. package/docs/types/openapi.OpenapiFormats.html +1 -1
  231. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  232. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  233. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  234. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  235. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  236. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  237. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  238. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  239. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  240. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
  241. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
  242. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
  243. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  244. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  245. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  246. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  247. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  248. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  249. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  250. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  251. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
  252. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
  253. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  254. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  255. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
  256. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
  257. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  258. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  259. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
  260. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
  261. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
  262. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  263. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  264. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  265. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  266. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  267. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  268. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  269. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  270. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  271. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  272. package/docs/types/types.CalendarDateObject.html +1 -1
  273. package/docs/types/types.Camelized.html +1 -1
  274. package/docs/types/types.ClockTimeObject.html +1 -1
  275. package/docs/types/types.DbConnectionType.html +1 -1
  276. package/docs/types/types.DbTypes.html +1 -1
  277. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  278. package/docs/types/types.DreamAttributes.html +1 -1
  279. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  280. package/docs/types/types.DreamClassColumn.html +1 -1
  281. package/docs/types/types.DreamColumn.html +1 -1
  282. package/docs/types/types.DreamColumnNames.html +1 -1
  283. package/docs/types/types.DreamLogLevel.html +1 -1
  284. package/docs/types/types.DreamLogger.html +2 -2
  285. package/docs/types/types.DreamModelSerializerType.html +1 -1
  286. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  287. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  288. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  289. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  290. package/docs/types/types.DreamSerializable.html +1 -1
  291. package/docs/types/types.DreamSerializableArray.html +1 -1
  292. package/docs/types/types.DreamSerializerKey.html +1 -1
  293. package/docs/types/types.DreamSerializers.html +1 -1
  294. package/docs/types/types.DreamVirtualColumns.html +1 -1
  295. package/docs/types/types.DurationUnit.html +1 -1
  296. package/docs/types/types.EncryptAlgorithm.html +1 -1
  297. package/docs/types/types.HasManyStatement.html +1 -1
  298. package/docs/types/types.HasOneStatement.html +1 -1
  299. package/docs/types/types.Hyphenized.html +1 -1
  300. package/docs/types/types.Pascalized.html +1 -1
  301. package/docs/types/types.PrimaryKeyType.html +1 -1
  302. package/docs/types/types.RoundingPrecision.html +1 -1
  303. package/docs/types/types.SerializerCasing.html +1 -1
  304. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  305. package/docs/types/types.Snakeified.html +1 -1
  306. package/docs/types/types.StrictInterface.html +1 -1
  307. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  308. package/docs/types/types.UpdateableProperties.html +1 -1
  309. package/docs/types/types.ValidationType.html +1 -1
  310. package/docs/types/types.ViewModel.html +2 -2
  311. package/docs/types/types.ViewModelClass.html +1 -1
  312. package/docs/types/types.WeekdayName.html +1 -1
  313. package/docs/types/types.WhereStatementForDream.html +1 -1
  314. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  315. package/docs/variables/index.DreamConst.html +1 -1
  316. package/docs/variables/index.ops.html +1 -1
  317. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  318. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  319. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  320. package/docs/variables/system.primaryKeyTypes.html +1 -1
  321. package/package.json +1 -1
  322. package/dist/cjs/src/decorators/field/sortable/helpers/applySortableScopeToQuery.js +0 -13
  323. package/dist/cjs/src/decorators/field/sortable/helpers/sortableQueryExcludingDream.js +0 -7
  324. package/dist/cjs/src/decorators/field/sortable/hooks/beforeSortableSave.js +0 -41
  325. package/dist/esm/src/decorators/field/sortable/helpers/applySortableScopeToQuery.js +0 -13
  326. package/dist/esm/src/decorators/field/sortable/helpers/sortableQueryExcludingDream.js +0 -7
  327. package/dist/esm/src/decorators/field/sortable/hooks/beforeSortableSave.js +0 -41
  328. package/dist/types/src/decorators/field/sortable/helpers/applySortableScopeToQuery.d.ts +0 -12
  329. package/dist/types/src/decorators/field/sortable/helpers/sortableQueryExcludingDream.d.ts +0 -12
  330. package/dist/types/src/decorators/field/sortable/hooks/beforeSortableSave.d.ts +0 -12
@@ -10,7 +10,9 @@ import pg from 'pg';
10
10
  import { sql } from 'kysely';
11
11
  import DreamCLI from '../../cli/index.js';
12
12
  import { isPrimitiveDataType } from '../../db/dataTypes.js';
13
+ import { LOCK_NOT_AVAILABLE, pgErrorType } from '../../db/errors.js';
13
14
  import DreamApp from '../../dream-app/index.js';
15
+ import SortableScopeLockWaitTimedOut from '../../errors/SortableScopeLockWaitTimedOut.js';
14
16
  import camelize from '../../helpers/camelize.js';
15
17
  import { findCitextArrayOid, findCorrespondingArrayOid, findEnumArrayOids, parsePostgresBigint, parsePostgresDate, parsePostgresDatetime, parsePostgresDatetimeTz, parsePostgresDecimal, parsePostgresTime, parsePostgresTimeTz, } from '../../helpers/customPgParsers.js';
16
18
  import { testDatabasePoolSize } from '../../db/testDatabasePool.js';
@@ -219,6 +221,78 @@ export default class PostgresQueryDriver extends KyselyQueryDriver {
219
221
  applyRowLock(kyselyQuery, tableAlias) {
220
222
  return kyselyQuery.forUpdate(tableAlias);
221
223
  }
224
+ static supportsAdvisoryTransactionLocks = true;
225
+ /**
226
+ * @internal
227
+ *
228
+ * Postgres implementation of the transaction-scoped advisory lock seam.
229
+ *
230
+ * Uses the single-`bigint` form `pg_advisory_xact_lock(bigint)`, whose lock
231
+ * space is distinct from the two-`int4` form the test-database pool claims
232
+ * with (see {@link openTestDatabaseLockSession}), so an application lock can
233
+ * never collide with a pool claim.
234
+ *
235
+ * Every key is taken in one statement, over `unnest` of the key array: a
236
+ * function scan yields its rows in array order, so the caller's sorted
237
+ * acquisition order — the thing that keeps two operations over the same
238
+ * scopes from deadlocking — is preserved, while a batch that needs one key
239
+ * per candidate record costs one round trip rather than N.
240
+ *
241
+ * The locks are held until the enclosing transaction ends, and are waiting
242
+ * (no `NOWAIT`): a second transaction asking for the same key blocks at that
243
+ * key's row until the holder commits or rolls back. Because they are
244
+ * transaction-scoped there is nothing to release, and re-acquiring a key
245
+ * within one transaction is a no-op rather than an error.
246
+ *
247
+ * The wait is bounded by `lock_timeout`, taken from
248
+ * {@link DreamApp.sortableScopeLockTimeout} and applied to the acquisition
249
+ * statement alone: the prior value is read in the same statement that sets
250
+ * the bound and restored in the next one, so the transaction's row-lock waits
251
+ * keep whatever the application configured. Postgres cancels the statement
252
+ * with `55P03` when the bound elapses, and that is translated here into
253
+ * {@link SortableScopeLockWaitTimedOut} so no Postgres error code escapes the
254
+ * driver.
255
+ *
256
+ * A timeout of `0` means "no bound", which is what `lock_timeout` itself
257
+ * means by it, and is sent as the bound rather than skipped: an application
258
+ * whose connection carries a `lock_timeout` of its own would otherwise have
259
+ * its acquisition cancelled by that bound, which is neither the unbounded
260
+ * wait `0` asks for nor a cancellation this driver could translate.
261
+ */
262
+ static async acquireAdvisoryTransactionLocks(txn, keys) {
263
+ if (!keys.length)
264
+ return;
265
+ // the keys are passed as text and cast, since a JS bigint is not a value
266
+ // the pg driver can bind directly
267
+ const lockKeys = keys.map(key => key.toString());
268
+ const timeoutMs = DreamApp.getOrFail().sortableScopeLockTimeout;
269
+ let priorLockTimeout;
270
+ try {
271
+ // one statement, three things in a forced order: `bounded` selects from
272
+ // `prior_setting`, so the prior value is read before the bound replaces
273
+ // it, and both are `from` items, so both are scanned before the target
274
+ // list takes a single lock. Merging them costs nothing where separate
275
+ // statements would cost round trips inside the lock window.
276
+ //
277
+ // The returned column is aliased to a single word deliberately: result
278
+ // keys come back camelized, and a one-word alias is the same either way
279
+ const result = await sql `
280
+ with prior_setting as (select current_setting('lock_timeout') as value),
281
+ bounded as (select set_config('lock_timeout', ${timeoutMs.toString()}, true) as value from prior_setting)
282
+ select prior_setting.value as prior, pg_advisory_xact_lock(sortable_lock_keys.lock_key)
283
+ from prior_setting, bounded, unnest(${lockKeys}::bigint[]) as sortable_lock_keys(lock_key)
284
+ `.execute(txn.kyselyTransaction);
285
+ priorLockTimeout = result.rows[0].prior;
286
+ }
287
+ catch (error) {
288
+ if (pgErrorType(error) === LOCK_NOT_AVAILABLE)
289
+ throw new SortableScopeLockWaitTimedOut(timeoutMs);
290
+ throw error;
291
+ }
292
+ // `current_setting` returns the value with its unit ('0', '5s', '250ms'),
293
+ // which is a form `set_config` accepts back unchanged
294
+ await sql `select set_config('lock_timeout', ${priorLockTimeout}, true)`.execute(txn.kyselyTransaction);
295
+ }
222
296
  static supportsParallelTestDatabases = true;
223
297
  /**
224
298
  * @internal
@@ -1,3 +1,5 @@
1
+ import performSortableDestroyWork from '../../decorators/field/sortable/helpers/performSortableDestroyWork.js';
2
+ import prepareSortableFieldsForDestroy, { clearSortableFieldsForDestroy, } from '../../decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.js';
1
3
  import DreamApp from '../../dream-app/index.js';
2
4
  import destroyAssociatedRecords from './destroyAssociatedRecords.js';
3
5
  import runHooksFor from './runHooksFor.js';
@@ -29,6 +31,17 @@ export default async function destroyDream(dream, txn = null, options) {
29
31
  */
30
32
  async function destroyDreamWithTransaction(dream, txn, options) {
31
33
  const { cascade, reallyDestroy, skipHooks } = options;
34
+ if (!skipHooks)
35
+ await runHooksFor('beforeDestroy', dream, true, null, txn);
36
+ // `preventDeletion` is fail-closed: a veto returns before the
37
+ // dependent-destroy cascade, so neither this record nor any of its
38
+ // `dependent: 'destroy'` associations are touched. The check sits outside the
39
+ // hook guard because the flag can also be set by the caller ahead of a
40
+ // `skipHooks` destroy, which runs no hooks but must not cascade either.
41
+ // Recursing through the cascade means a vetoing descendant stops its own
42
+ // subtree while the destroy that reached it carries on.
43
+ if (dream['_preventDeletion'])
44
+ return dream;
32
45
  if (cascade) {
33
46
  // NOTE: the dependent-destroy tree is loaded lazily, inside
34
47
  // destroyAssociatedRecords, so that a cascaded descendant whose
@@ -37,15 +50,56 @@ async function destroyDreamWithTransaction(dream, txn, options) {
37
50
  // clone: the hooks and the delete below operate on the original.
38
51
  await destroyAssociatedRecords(dream, txn, options);
39
52
  }
40
- if (!skipHooks) {
41
- await runHooksFor('beforeDestroy', dream, true, null, txn);
53
+ // Sortable preparation runs as a phase after every user beforeDestroy hook,
54
+ // never as a hook among them: the destroy's whole advisory key set — every
55
+ // sortable field — is acquired in one sorted pass, and the row's real
56
+ // position and scope values are read in one snapshot SELECT while the row
57
+ // still exists. It sits after the cascade so this destroy does not hold the
58
+ // scope lock across every descendant's destroy — true of the destroy itself,
59
+ // not of the transaction it runs in: under `Query#destroy({ lock: true })`
60
+ // the batch's whole key set is already held, taken before any row was
61
+ // claimed. A destroy that skips hooks performs no compaction, and one a
62
+ // cascaded descendant's hook has vetoed will not delete, so neither needs
63
+ // locks or a snapshot.
64
+ let rowFoundBeforeDelete = true;
65
+ if (!skipHooks && !dream['_preventDeletion']) {
66
+ rowFoundBeforeDelete = await prepareSortableFieldsForDestroy(dream, txn);
42
67
  }
43
- await maybeDestroyDream(dream, txn, reallyDestroy);
44
- if (!skipHooks) {
68
+ const rowsRemoved = await maybeDestroyDream(dream, txn, reallyDestroy);
69
+ // `preventDeletion` can still arrive after the veto check above has passed:
70
+ // a cascaded descendant's hook can reach back to this record and call it. The
71
+ // record is then still present, so neither the after-destroy hooks nor the
72
+ // reload of a soft deleted row have anything to run against, and the driver
73
+ // leaves the row alone. A veto that late cannot be made fail-closed against
74
+ // the cascade — the descendants are already destroyed by the time it lands.
75
+ const deletionPrevented = dream['_preventDeletion'];
76
+ // A destroy of a row that is already gone — a second `destroy()` on the same
77
+ // instance, a job retry, two workers racing — removes nothing, so there is
78
+ // nothing for the after-destroy hooks to react to and, for a sortable model,
79
+ // no vacancy to close. Compacting anyway would shift the scope a second time
80
+ // from the instance's remembered position, moving survivors onto occupied
81
+ // positions. A veto also removes nothing, and keeps its own path: it is
82
+ // recognized by the flag above, never by the count.
83
+ //
84
+ // Both signals are read because they catch opposite races. The count is the
85
+ // stronger of the two — a writer outside the sortable path can remove the row
86
+ // after the snapshot read, and only the count sees that. The snapshot's
87
+ // existence signal catches the mirror shape: the row was already gone when
88
+ // the snapshot read ran, so this destroy holds no position to vacate, and a
89
+ // row that reappeared under the same primary key is what the delete removed.
90
+ const removedNothing = !deletionPrevented && (rowsRemoved === 0 || !rowFoundBeforeDelete);
91
+ if (removedNothing)
92
+ clearSortableFieldsForDestroy(dream);
93
+ if (!skipHooks && !deletionPrevented && !removedNothing) {
94
+ // The compaction is a phase before the after-destroy hooks, never a hook
95
+ // among them, so every user afterDestroy hook observes the compacted scope
96
+ // regardless of where it was declared — the destroy-side counterpart of
97
+ // performSortablePositionWork running before the after-save hooks.
98
+ await performSortableDestroyWork(dream, txn);
45
99
  await runHooksFor('afterDestroy', dream, true, null, txn);
46
100
  await runHooksFor('afterDestroyCommit', dream, true, null, txn);
47
101
  }
48
- if (shouldSoftDelete(dream, reallyDestroy)) {
102
+ if (shouldSoftDelete(dream, reallyDestroy) && !deletionPrevented) {
49
103
  await dream.txn(txn).reload();
50
104
  }
51
105
  return dream;
@@ -62,5 +116,5 @@ function shouldSoftDelete(dream, reallyDestroy) {
62
116
  */
63
117
  async function maybeDestroyDream(dream, txn, reallyDestroy) {
64
118
  const dbDriverClass = DreamApp.getOrFail().dbConnectionQueryDriverClass(dream.connectionName);
65
- await dbDriverClass.destroyDream(dream, txn, reallyDestroy);
119
+ return await dbDriverClass.destroyDream(dream, txn, reallyDestroy);
66
120
  }
@@ -1,3 +1,8 @@
1
+ import acquireStabilizedSortableScopeLocks from '../../decorators/field/sortable/helpers/acquireStabilizedSortableScopeLocks.js';
2
+ import { clearSortableSaveState } from '../../decorators/field/sortable/helpers/clearCachedSortableValues.js';
3
+ import performSortablePositionWork from '../../decorators/field/sortable/helpers/performSortablePositionWork.js';
4
+ import prepareSortableFieldsForSave from '../../decorators/field/sortable/helpers/prepareSortableFieldsForSave.js';
5
+ import { cacheSortableSnapshots } from '../../decorators/field/sortable/helpers/sortableSnapshot.js';
1
6
  import ValidationError from '../../errors/ValidationError.js';
2
7
  import sqlAttributes from '../../helpers/sqlAttributes.js';
3
8
  import { DateTime } from '../../utils/datetime/DateTime.js';
@@ -13,6 +18,27 @@ export default async function saveDream(dream, txn = null, { skipHooks = false }
13
18
  await runHooksFor('beforeCreate', dream, alreadyPersisted, null, txn);
14
19
  await runHooksFor('beforeSave', dream, alreadyPersisted, null, txn);
15
20
  }
21
+ // Where this save opens a transaction of its own (below), a failure inside it
22
+ // rolls the driver write back, so the instance has to be able to go back to
23
+ // what it was when `save` was called: an instance left holding the primary
24
+ // key of a rolled-back INSERT — and left clean, so that the retry the failure
25
+ // invites returns at the `isDirty` check below without writing anything — is
26
+ // a record whose row does not exist. Captured before the sortable phase
27
+ // applies its position sentinel, so the retry sees the caller's own position.
28
+ // Only a model declaring sortable fields can reach that transaction, so a save
29
+ // of any other model copies nothing.
30
+ const preSaveState = txn || !dream.constructor['sortableFields'].length
31
+ ? null
32
+ : capturePersistenceState(dream);
33
+ // Sortable computes each record's position under an advisory lock on its sort
34
+ // scope, and that lock is transaction-scoped, so the write and the position
35
+ // write that follows it have to commit together. Preparation runs here as a
36
+ // called phase — after every consumer before-hook, never as a hook among
37
+ // them — so that a position or scope change a user's own `beforeSave` hook
38
+ // introduced is inside the boundary regardless of where the hook was
39
+ // registered. Seated before the change capture, so the position sentinel
40
+ // lands in `beforeSaveChanges` exactly as it always has.
41
+ const sortableFieldConfigs = prepareSortableFieldsForSave(dream, { alreadyPersisted, skipHooks });
16
42
  const beforeSaveChanges = dream.changes();
17
43
  // need to check validations after running before hooks, or else
18
44
  // model hooks that might make a model valid cannot run
@@ -20,6 +46,69 @@ export default async function saveDream(dream, txn = null, { skipHooks = false }
20
46
  throw new ValidationError(dream['sanitizedConstructorName'], dream.errors);
21
47
  if (alreadyPersisted && !dream.isDirty)
22
48
  return dream;
49
+ if (sortableFieldConfigs.length && !txn) {
50
+ // The self-opened transaction holds only Dream's own work: the scope
51
+ // locks, the driver write, and the position write, which is its last
52
+ // statement. COMMIT releases the scope locks before any user after-hook
53
+ // runs; the after-hook and `*Commit` families then run with no
54
+ // transaction, exactly as on a save of a non-sortable model, so a hook
55
+ // that throws cannot roll back the committed row and a slow hook blocks
56
+ // no other writer of the scope.
57
+ try {
58
+ await dream.constructor.transaction(async (sortableTxn) => await writeDream(dream, sortableTxn, { alreadyPersisted, sortableFieldConfigs }));
59
+ }
60
+ catch (error) {
61
+ restorePersistenceState(dream, preSaveState);
62
+ clearSortableSaveState(dream, sortableFieldConfigs);
63
+ throw error;
64
+ }
65
+ await runAfterSaveHooks(dream, null, { skipHooks, alreadyPersisted, beforeSaveChanges });
66
+ return dream;
67
+ }
68
+ await writeDream(dream, txn, { alreadyPersisted, sortableFieldConfigs });
69
+ await runAfterSaveHooks(dream, txn, { skipHooks, alreadyPersisted, beforeSaveChanges });
70
+ return dream;
71
+ }
72
+ /**
73
+ * Everything a write changes about how the instance answers "am I saved, and
74
+ * what has changed since?" — the persisted flag, the attributes the driver's
75
+ * `RETURNING` wrote (the primary key among them), and the three attribute
76
+ * snapshots the dirty-checking reads.
77
+ */
78
+ function capturePersistenceState(dream) {
79
+ return {
80
+ isPersisted: dream.isPersisted,
81
+ frozenAttributes: { ...dream['frozenAttributes'] },
82
+ originalAttributes: { ...dream['originalAttributes'] },
83
+ attributesFromBeforeLastSave: { ...dream['attributesFromBeforeLastSave'] },
84
+ currentAttributes: { ...dream['currentAttributes'] },
85
+ };
86
+ }
87
+ function restorePersistenceState(dream, state) {
88
+ dream['isPersisted'] = state.isPersisted;
89
+ dream['frozenAttributes'] = state.frozenAttributes;
90
+ dream['originalAttributes'] = state.originalAttributes;
91
+ dream['attributesFromBeforeLastSave'] = state.attributesFromBeforeLastSave;
92
+ dream['currentAttributes'] = state.currentAttributes;
93
+ }
94
+ async function writeDream(dream, txn, { alreadyPersisted, sortableFieldConfigs, }) {
95
+ let rowFoundBeforeWrite = true;
96
+ // A position- or scope-changing update takes its scope locks here, before the
97
+ // write, because the write overwrites the scope column: after it, neither the
98
+ // scope the record is moving out of nor its real position is readable from the
99
+ // row. The locks are therefore held across the update and the position write
100
+ // that follows it — one transaction, one acquisition — and the snapshot the
101
+ // position work is computed from is read here, under them.
102
+ //
103
+ // A create locks later, around its own position computation: it has no scope
104
+ // to move out of and no prior row to snapshot, and the deferrable unique
105
+ // constraint sortable tables carry tolerates two uncommitted rows at the
106
+ // pre-write position sentinel.
107
+ if (txn && alreadyPersisted && sortableFieldConfigs.length) {
108
+ const { rowExists, snapshots } = await acquireStabilizedSortableScopeLocks(dream, txn, sortableFieldConfigs);
109
+ rowFoundBeforeWrite = rowExists;
110
+ cacheSortableSnapshots(dream, snapshots);
111
+ }
23
112
  const now = DateTime.now();
24
113
  if (!alreadyPersisted && !dream.createdAt && dream.columns().has('createdAt'))
25
114
  dream.createdAt = now;
@@ -41,14 +130,27 @@ export default async function saveDream(dream, txn = null, { skipHooks = false }
41
130
  dream['freezeAttributes']();
42
131
  dream['attributesFromBeforeLastSave'] = dream['originalAttributes'];
43
132
  dream['originalAttributes'] = dream.getAttributes();
44
- if (!skipHooks) {
45
- await runHooksFor('afterSave', dream, alreadyPersisted, beforeSaveChanges, txn);
46
- if (alreadyPersisted)
47
- await runHooksFor('afterUpdate', dream, alreadyPersisted, beforeSaveChanges, txn);
133
+ // An update whose row the snapshot read positively found gone holds no
134
+ // position to move: the vacancy it remembers was closed by whoever removed
135
+ // the row, and shifting the scope from the instance's remembered position
136
+ // would move survivors onto occupied ones. The cached values and the empty
137
+ // snapshot go with it, so a later save of this instance reads the row itself.
138
+ if (txn && sortableFieldConfigs.length) {
139
+ if (rowFoundBeforeWrite)
140
+ await performSortablePositionWork(dream, txn, sortableFieldConfigs, { alreadyPersisted });
48
141
  else
49
- await runHooksFor('afterCreate', dream, alreadyPersisted, beforeSaveChanges, txn);
50
- await runHooksFor(alreadyPersisted ? 'afterUpdateCommit' : 'afterCreateCommit', dream, alreadyPersisted, beforeSaveChanges, txn);
51
- await runHooksFor('afterSaveCommit', dream, alreadyPersisted, beforeSaveChanges, txn);
142
+ clearSortableSaveState(dream, sortableFieldConfigs);
52
143
  }
53
144
  return dream;
54
145
  }
146
+ async function runAfterSaveHooks(dream, txn, { skipHooks, alreadyPersisted, beforeSaveChanges, }) {
147
+ if (skipHooks)
148
+ return;
149
+ await runHooksFor('afterSave', dream, alreadyPersisted, beforeSaveChanges, txn);
150
+ if (alreadyPersisted)
151
+ await runHooksFor('afterUpdate', dream, alreadyPersisted, beforeSaveChanges, txn);
152
+ else
153
+ await runHooksFor('afterCreate', dream, alreadyPersisted, beforeSaveChanges, txn);
154
+ await runHooksFor(alreadyPersisted ? 'afterUpdateCommit' : 'afterCreateCommit', dream, alreadyPersisted, beforeSaveChanges, txn);
155
+ await runHooksFor('afterSaveCommit', dream, alreadyPersisted, beforeSaveChanges, txn);
156
+ }
@@ -8,7 +8,8 @@ import namespaceColumn from '../../../helpers/namespaceColumn.js';
8
8
  import { TRIGRAM_OPERATORS } from '../../constants.js';
9
9
  import similaritySelectSql from './similaritySelectSql.js';
10
10
  import similarityWhereSql from './similarityWhereSql.js';
11
- import { getPostgresQueryDriver } from '../../../decorators/field/sortable/helpers/setPosition.js';
11
+ import DreamApp from '../../../dream-app/index.js';
12
+ import PostgresQueryDriver from '../../QueryDriver/Postgres.js';
12
13
  export default class SimilarityBuilder extends ConnectedToDB {
13
14
  whereStatement;
14
15
  whereNotStatement;
@@ -215,7 +216,7 @@ export default class SimilarityBuilder extends ConnectedToDB {
215
216
  const schema = this.dreamClass.prototype.schema;
216
217
  const primaryKeyName = this.dreamClass.primaryKey;
217
218
  const { tableName, tableAlias, columnName } = similarityStatement;
218
- const queryDriverClass = getPostgresQueryDriver(this.dreamInstance.connectionName);
219
+ const queryDriverClass = postgresQueryDriverForSimilaritySearch(this.dreamInstance.connectionName);
219
220
  // eslint-disable-next-line @typescript-eslint/unbound-method
220
221
  const { ref } = queryDriverClass.dbFor(this.dreamInstance.connectionName, this.dbConnectionType('select')).dynamic;
221
222
  const validatedTableAlias = validateTableAlias(tableAlias);
@@ -247,7 +248,7 @@ export default class SimilarityBuilder extends ConnectedToDB {
247
248
  statementType,
248
249
  });
249
250
  const trigramSearchAlias = this.similaritySearchId(tableAlias, columnName);
250
- const queryDriverClass = getPostgresQueryDriver(this.dreamInstance.connectionName);
251
+ const queryDriverClass = postgresQueryDriverForSimilaritySearch(this.dreamInstance.connectionName);
251
252
  const selectQuery = queryDriverClass
252
253
  .dbFor(this.dreamInstance.connectionName, this.dbConnectionType('select'))
253
254
  .selectFrom(validatedTableAlias)
@@ -261,7 +262,7 @@ export default class SimilarityBuilder extends ConnectedToDB {
261
262
  const { columnName, opsStatement, tableName } = similarityStatement;
262
263
  const validatedTable = validateTable(schema, tableName);
263
264
  const validatedPrimaryKey = validateColumn(schema, tableName, primaryKeyName);
264
- const queryDriverClass = getPostgresQueryDriver(this.dreamInstance.connectionName);
265
+ const queryDriverClass = postgresQueryDriverForSimilaritySearch(this.dreamInstance.connectionName);
265
266
  let nestedQuery = queryDriverClass
266
267
  .dbFor(this.dreamInstance.connectionName, this.dbConnectionType('select'))
267
268
  .selectFrom(tableName)
@@ -314,6 +315,20 @@ export default class SimilarityBuilder extends ConnectedToDB {
314
315
  return `${statementType}_rank_${statementIndex + 1}`;
315
316
  }
316
317
  }
318
+ /**
319
+ * Trigram similarity search is built on Postgres's `pg_trgm` operators and on
320
+ * kysely builders taken straight off the Postgres driver, so it resolves the
321
+ * connection's query driver to that driver — accepting a consumer subclass of
322
+ * it, which inherits the same operators — and otherwise fails loud rather than
323
+ * emitting SQL another engine cannot run.
324
+ */
325
+ function postgresQueryDriverForSimilaritySearch(connectionName) {
326
+ const queryDriverClass = DreamApp.getOrFail().dbConnectionQueryDriverClass(connectionName);
327
+ if (queryDriverClass !== PostgresQueryDriver &&
328
+ !(queryDriverClass.prototype instanceof PostgresQueryDriver))
329
+ throw new Error(`${queryDriverClass.name} is not the PostgresQueryDriver, nor a subclass of it. Similarity search (similarity, wordSimilarity, strictWordSimilarity) requires the PostgresQueryDriver.`);
330
+ return queryDriverClass;
331
+ }
317
332
  export const SIMILARITY_TYPES = ['where', 'join_and'];
318
333
  function removeJoinAndFromObjectHierarchy(obj) {
319
334
  if (obj?.isOpsStatement)
@@ -1,19 +1,31 @@
1
1
  import MissingDeletedAtFieldForSoftDelete from '../../errors/MissingDeletedAtFieldForSoftDelete.js';
2
2
  import isDatetimeOrDatetimeArrayColumn from '../../helpers/db/types/isDatetimeOrDatetimeArrayColumn.js';
3
3
  import { DateTime } from '../../utils/datetime/DateTime.js';
4
+ /**
5
+ * @internal
6
+ *
7
+ * @returns the number of rows soft deleted — zero when the row is already gone,
8
+ * and zero when it is already soft deleted
9
+ */
4
10
  export default async function softDeleteDream(dream, txn) {
5
11
  const deletedAtField = dream['_deletedAtField'];
6
12
  const dreamClass = dream.constructor;
7
13
  if (!isDatetimeOrDatetimeArrayColumn(dreamClass, deletedAtField)) {
8
14
  throw new MissingDeletedAtFieldForSoftDelete(dream.constructor);
9
15
  }
16
+ // An already soft deleted row is logically gone, and this UPDATE is what
17
+ // reports the deletion to the caller: matching such a row again would rewrite
18
+ // its `deletedAt` and count a deletion this destroy did not perform, running
19
+ // the after-destroy hooks a second time on a record nothing happened to.
10
20
  let query = txn.kyselyTransaction
11
21
  .updateTable(dream.table)
12
22
  .where(dream['_primaryKey'], '=', dream.primaryKeyValue())
23
+ .where(deletedAtField, 'is', null)
13
24
  .set(dream['_deletedAtField'], DateTime.now());
14
25
  dreamClass['sortableFields']?.forEach(sortableFieldMetadata => {
15
26
  const positionColumn = sortableFieldMetadata.positionField;
16
27
  query = query.set(positionColumn, null);
17
28
  });
18
- await query.execute();
29
+ const results = await query.execute();
30
+ return Number(results[0]?.numUpdatedRows ?? 0);
19
31
  }
@@ -1,4 +1,6 @@
1
- import { applySortableScopesToQuery } from '../../decorators/field/sortable/helpers/setPosition.js';
1
+ import acquireStabilizedSortableScopeLocks from '../../decorators/field/sortable/helpers/acquireStabilizedSortableScopeLocks.js';
2
+ import filterQueryToScopeValues from '../../decorators/field/sortable/helpers/filterQueryToScopeValues.js';
3
+ import { snapshotScopeValue } from '../../decorators/field/sortable/helpers/sortableSnapshot.js';
2
4
  import runHooksFor from './runHooksFor.js';
3
5
  /**
4
6
  * @internal
@@ -31,11 +33,23 @@ async function undestroyDreamWithTransaction(dream, txn, options) {
31
33
  if (!skipHooks) {
32
34
  await runHooksFor('beforeUpdate', dream, true, null, txn);
33
35
  }
34
- await doUndestroyDream(dream, txn);
36
+ // The cascade runs ahead of the restore, matching `destroyDream`: this
37
+ // record's own scope lock is taken inside `doUndestroyDream`, so a cascade
38
+ // seated after it would hold that lock across every descendant's undestroy,
39
+ // and a parent and its children would take their advisory keys in the
40
+ // opposite order to the one the destroy side takes them in. The nested select
41
+ // the cascade builds bypasses the soft-delete scope on this record's own
42
+ // class (`undestroyOptions`), so it reaches the children while this record is
43
+ // still deleted.
35
44
  if (cascade) {
36
45
  await undestroyAssociatedRecords(dream, txn, options);
37
46
  }
38
- if (!skipHooks) {
47
+ const restoredRowCount = await doUndestroyDream(dream, txn);
48
+ // A restore of a row that is not deleted — a second `undestroy()`, a job
49
+ // retry, or a record that was never destroyed, which `Query#undestroy` reaches
50
+ // routinely since it lifts the soft-delete scope and walks every match —
51
+ // matches no row, so there is nothing for the after-update hooks to react to.
52
+ if (!skipHooks && restoredRowCount > 0) {
39
53
  await runHooksFor('afterUpdate', dream, true, null, txn);
40
54
  await runHooksFor('afterUpdateCommit', dream, true, null, txn);
41
55
  }
@@ -45,22 +59,56 @@ async function undestroyDreamWithTransaction(dream, txn, options) {
45
59
  /**
46
60
  * @internal
47
61
  *
48
- * Destroys the dream iff it was not blocked from
49
- * deleting by one of the beforeDestroy model hooks
62
+ * Clears the record's `deletedAt` and, for a sortable model, gives it a
63
+ * position at the end of the sort scope its row physically occupies.
64
+ *
65
+ * Restoring is conditional on the row still being deleted, and the count of
66
+ * rows the statement matched is what the caller reads that off. A row that is
67
+ * already back would otherwise be inside the `max(position)` its new position
68
+ * is computed from, and would move one past itself on every repeat — a
69
+ * permanent gap in the scope.
70
+ *
71
+ * @returns the number of rows restored: 1, or 0 when the row was not deleted
50
72
  */
51
73
  async function doUndestroyDream(dream, txn) {
52
74
  const updateStatement = txn.kyselyTransaction.updateTable(dream.table);
53
75
  let query = updateStatement
54
76
  .where(dream['_primaryKey'], '=', dream.primaryKeyValue())
77
+ .where(dream['_deletedAtField'], 'is not', null)
55
78
  .set({ [dream['_deletedAtField']]: null });
56
79
  const dreamClass = dream.constructor;
57
- dreamClass['sortableFields']?.forEach(sortableFieldMetadata => {
80
+ const sortableFields = (dreamClass['sortableFields'] ?? []);
81
+ // Undestroy has no sortable hook of its own — the position is recomputed
82
+ // inline, below, before `afterUpdate` ever runs — so the scope lock is taken
83
+ // here, around that recomputation. Without it the `max(position) + 1`
84
+ // subquery races every other writer of the scope the record is being restored
85
+ // into.
86
+ //
87
+ // The scope this record is being restored into is the one its row physically
88
+ // carries, not the one the instance was loaded with: another writer may have
89
+ // moved it between scopes while it was soft deleted, which is what the
90
+ // stabilized acquisition converges on.
91
+ const { snapshots } = await acquireStabilizedSortableScopeLocks(dream, txn, sortableFields);
92
+ sortableFields.forEach(sortableFieldMetadata => {
58
93
  const positionColumn = sortableFieldMetadata.positionField;
94
+ const snapshot = snapshots.get(positionColumn);
59
95
  query = query.set(eb => ({
60
- [positionColumn]: eb(applySortableScopesToQuery(dream, txn.kyselyTransaction.selectFrom(dream.table), column => dream[column], sortableFieldMetadata.scope).select(eb => eb.fn.max(positionColumn).as(positionColumn + '_max')), '+', 1),
96
+ [positionColumn]: eb(filterQueryToScopeValues(dream, txn.kyselyTransaction.selectFrom(dream.table), column => snapshotScopeValue(snapshot, column, () => dream[column]), sortableFieldMetadata.scope).select(eb =>
97
+ // `max` over a scope with no positioned rows left — an empty
98
+ // scope, or one whose every remaining row is soft deleted and so
99
+ // carries a NULL position — is NULL, and NULL + 1 is NULL, which
100
+ // would restore the record with no position at all. Coalescing
101
+ // before the increment restores it at position 1.
102
+ eb.fn.coalesce(eb.fn.max(positionColumn), eb.lit(0)).as(positionColumn + '_max')), '+', 1),
61
103
  }));
62
104
  });
63
- return await query.execute();
105
+ const [result] = await query.execute();
106
+ // No sortable row cache invalidation here: the restored record took a position
107
+ // at the end of the scope it was restored into and nothing else moved, so no
108
+ // other record's cached preflight row went stale, and this record's own cached
109
+ // row was consumed — destructively — by the snapshot read every sortable field
110
+ // above goes through.
111
+ return Number(result?.numUpdatedRows ?? 0);
64
112
  }
65
113
  /**
66
114
  * @internal
@@ -212,6 +212,24 @@ export default class DreamApp {
212
212
  get paginationMaxPageSize() {
213
213
  return this._paginationMaxPageSize;
214
214
  }
215
+ /**
216
+ * How long, in milliseconds, a position-mutating operation on a
217
+ * `@deco.Sortable` model will wait for another transaction to release the
218
+ * sort scope it needs before giving up with
219
+ * `SortableScopeLockWaitTimedOut`. Applied to the lock acquisition statement
220
+ * alone — the transaction's other lock waits keep whatever `lock_timeout`
221
+ * the application has configured.
222
+ *
223
+ * `0` waits without bound, which is what Postgres's `lock_timeout` means by
224
+ * it, and holds even on a connection configured with a `lock_timeout` of its
225
+ * own: the acquisition disables the bound for the duration of the statement.
226
+ *
227
+ * Set it with `dreamApp.set('sortableScopeLockTimeout', milliseconds)`.
228
+ */
229
+ _sortableScopeLockTimeout = 5000;
230
+ get sortableScopeLockTimeout() {
231
+ return this._sortableScopeLockTimeout;
232
+ }
215
233
  _primaryKeyType = 'bigint';
216
234
  get primaryKeyType() {
217
235
  return this._primaryKeyType;
@@ -492,6 +510,9 @@ export default class DreamApp {
492
510
  case 'paginationMaxPageSize':
493
511
  this._paginationMaxPageSize = options;
494
512
  break;
513
+ case 'sortableScopeLockTimeout':
514
+ this._sortableScopeLockTimeout = options;
515
+ break;
495
516
  default: {
496
517
  // protection so that if a new ApplyOpt is ever added, this will throw a type error at build time
497
518
  const _never = applyOption;
@@ -0,0 +1,62 @@
1
+ import CannotSetEncryptedColumnInQueryUpdate from '../errors/encrypt/CannotSetEncryptedColumnInQueryUpdate.js';
2
+ import { snakeifyString } from '../helpers/snakeify.js';
3
+ /**
4
+ * @internal
5
+ *
6
+ * Throws when `attributes` names an @Encrypted backing column with anything other
7
+ * than null. Guards the write paths that never instantiate a model, and so never
8
+ * reach the throwing setter the Encrypted decorator installs for that column.
9
+ *
10
+ * `encryptedAttributes` is class-scoped, but a query rooted at an STI base reaches
11
+ * every one of its children's rows on the shared table, so `dreamClass` and every STI
12
+ * class below it are searched for backing columns.
13
+ *
14
+ * The search deliberately stops there rather than covering the whole hierarchy. A query
15
+ * rooted at an STI child matches only that child's rows, and a backing column only a
16
+ * sibling declares holds nothing meaningful for them: writing plaintext into it corrupts
17
+ * no ciphertext, because there is none to corrupt. Converting such a row to the sibling
18
+ * type is what has to clear that column, and it has to clear it whatever this guard did.
19
+ * Widening the walk to the siblings therefore buys no protection, and this guard runs on
20
+ * every single-statement update, so keep it narrow.
21
+ *
22
+ * Keys are matched in their snakeified form, since Kysely's CamelCasePlugin
23
+ * resolves both the camelCase and the snake_case spelling of a key to the same
24
+ * physical column.
25
+ *
26
+ * null is permitted: it clears the column, and there is no plaintext to mangle.
27
+ */
28
+ export default function assertNoEncryptedColumnWrites(dreamClass, attributes) {
29
+ const encryptedAttributes = encryptedAttributesInStiDescendants(dreamClass);
30
+ if (encryptedAttributes.size === 0)
31
+ return;
32
+ Object.keys(attributes).forEach(key => {
33
+ const encryptedAttribute = encryptedAttributes.get(snakeifyString(key));
34
+ if (encryptedAttribute === undefined)
35
+ return;
36
+ if (attributes[key] === null)
37
+ return;
38
+ throw new CannotSetEncryptedColumnInQueryUpdate(encryptedAttribute.declaringClass, encryptedAttribute.encryptedColumnName, encryptedAttribute.property);
39
+ });
40
+ }
41
+ /**
42
+ * @internal
43
+ *
44
+ * Every @Encrypted backing column declared on `dreamClass` or on an STI class below it,
45
+ * keyed by the snakeified backing column name.
46
+ */
47
+ function encryptedAttributesInStiDescendants(dreamClass) {
48
+ const encryptedAttributes = new Map();
49
+ dreamClass['selfAndStiDescendants']().forEach(stiClass => {
50
+ stiClass['encryptedAttributes'].forEach(({ property, encryptedColumnName }) => {
51
+ const snakeifiedColumnName = snakeifyString(encryptedColumnName);
52
+ if (encryptedAttributes.has(snakeifiedColumnName))
53
+ return;
54
+ encryptedAttributes.set(snakeifiedColumnName, {
55
+ property,
56
+ encryptedColumnName,
57
+ declaringClass: stiClass,
58
+ });
59
+ });
60
+ });
61
+ return encryptedAttributes;
62
+ }