@rvoh/dream 2.35.0 → 2.36.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 (302) hide show
  1. package/README.md +2 -2
  2. package/dist/cjs/src/Dream.js +258 -55
  3. package/dist/cjs/src/bin/index.js +13 -0
  4. package/dist/cjs/src/cli/index.js +28 -17
  5. package/dist/cjs/src/db/migration-helpers/DreamMigrationHelpers.js +23 -21
  6. package/dist/cjs/src/decorators/Decorators.js +26 -4
  7. package/dist/cjs/src/decorators/field/Encrypted.js +30 -6
  8. package/dist/cjs/src/dream/DreamClassTransactionBuilder.js +6 -6
  9. package/dist/cjs/src/dream/DreamInstanceTransactionBuilder.js +63 -27
  10. package/dist/cjs/src/dream/LeftJoinLoadBuilder.js +3 -3
  11. package/dist/cjs/src/dream/LoadBuilder.js +1 -1
  12. package/dist/cjs/src/dream/Query.js +542 -167
  13. package/dist/cjs/src/dream/QueryDriver/Base.js +72 -0
  14. package/dist/cjs/src/dream/QueryDriver/Kysely.js +216 -50
  15. package/dist/cjs/src/dream/internal/associations/assertRecognizedAssociationConditionKeys.js +48 -0
  16. package/dist/cjs/src/dream/internal/associations/associationQuery.js +4 -0
  17. package/dist/cjs/src/dream/internal/reload.js +6 -1
  18. package/dist/cjs/src/dream/internal/saveDream.js +8 -2
  19. package/dist/cjs/src/dream/internal/sqlResultToDreamInstance.js +2 -0
  20. package/dist/cjs/src/dream/internal/undestroyDream.js +1 -1
  21. package/dist/cjs/src/dream-app/index.js +38 -0
  22. package/dist/cjs/src/encrypt/InternalEncrypt.js +49 -4
  23. package/dist/cjs/src/encrypt/defaultEncryptedColumnName.js +10 -0
  24. package/dist/cjs/src/encrypt/index.js +76 -9
  25. package/dist/cjs/src/errors/DoNotSetEncryptedFieldsDirectly.js +4 -0
  26. package/dist/cjs/src/errors/associations/UnrecognizedAssociationConditionKeys.js +25 -0
  27. package/dist/cjs/src/helpers/Env.js +28 -0
  28. package/dist/cjs/src/helpers/cli/generateDreamContent.js +2 -1
  29. package/dist/cjs/src/helpers/cli/generateFactoryContent.js +1 -1
  30. package/dist/cjs/src/helpers/cli/generateMigrationContent.js +9 -9
  31. package/dist/cjs/src/helpers/cli/generateSerializerContent.js +21 -8
  32. package/dist/cjs/src/serializer/SerializerRenderer.js +65 -13
  33. package/dist/cjs/src/serializer/builders/DreamSerializerBuilder.js +54 -38
  34. package/dist/cjs/src/serializer/builders/ObjectSerializerBuilder.js +34 -14
  35. package/dist/cjs/src/serializer/helpers/serializersForAssociatedClass.js +43 -0
  36. package/dist/esm/src/Dream.js +258 -55
  37. package/dist/esm/src/bin/index.js +13 -0
  38. package/dist/esm/src/cli/index.js +28 -17
  39. package/dist/esm/src/db/migration-helpers/DreamMigrationHelpers.js +23 -21
  40. package/dist/esm/src/decorators/Decorators.js +26 -4
  41. package/dist/esm/src/decorators/field/Encrypted.js +30 -6
  42. package/dist/esm/src/dream/DreamClassTransactionBuilder.js +6 -6
  43. package/dist/esm/src/dream/DreamInstanceTransactionBuilder.js +63 -27
  44. package/dist/esm/src/dream/LeftJoinLoadBuilder.js +3 -3
  45. package/dist/esm/src/dream/LoadBuilder.js +1 -1
  46. package/dist/esm/src/dream/Query.js +542 -167
  47. package/dist/esm/src/dream/QueryDriver/Base.js +72 -0
  48. package/dist/esm/src/dream/QueryDriver/Kysely.js +216 -50
  49. package/dist/esm/src/dream/internal/associations/assertRecognizedAssociationConditionKeys.js +48 -0
  50. package/dist/esm/src/dream/internal/associations/associationQuery.js +4 -0
  51. package/dist/esm/src/dream/internal/reload.js +6 -1
  52. package/dist/esm/src/dream/internal/saveDream.js +8 -2
  53. package/dist/esm/src/dream/internal/sqlResultToDreamInstance.js +2 -0
  54. package/dist/esm/src/dream/internal/undestroyDream.js +1 -1
  55. package/dist/esm/src/dream-app/index.js +38 -0
  56. package/dist/esm/src/encrypt/InternalEncrypt.js +49 -4
  57. package/dist/esm/src/encrypt/defaultEncryptedColumnName.js +10 -0
  58. package/dist/esm/src/encrypt/index.js +76 -9
  59. package/dist/esm/src/errors/DoNotSetEncryptedFieldsDirectly.js +4 -0
  60. package/dist/esm/src/errors/associations/UnrecognizedAssociationConditionKeys.js +25 -0
  61. package/dist/esm/src/helpers/Env.js +28 -0
  62. package/dist/esm/src/helpers/cli/generateDreamContent.js +2 -1
  63. package/dist/esm/src/helpers/cli/generateFactoryContent.js +1 -1
  64. package/dist/esm/src/helpers/cli/generateMigrationContent.js +9 -9
  65. package/dist/esm/src/helpers/cli/generateSerializerContent.js +21 -8
  66. package/dist/esm/src/serializer/SerializerRenderer.js +65 -13
  67. package/dist/esm/src/serializer/builders/DreamSerializerBuilder.js +54 -38
  68. package/dist/esm/src/serializer/builders/ObjectSerializerBuilder.js +34 -14
  69. package/dist/esm/src/serializer/helpers/serializersForAssociatedClass.js +43 -0
  70. package/dist/types/src/Dream.d.ts +206 -42
  71. package/dist/types/src/bin/index.d.ts +7 -1
  72. package/dist/types/src/cli/index.d.ts +13 -0
  73. package/dist/types/src/db/migration-helpers/DreamMigrationHelpers.d.ts +2 -2
  74. package/dist/types/src/decorators/Decorators.d.ts +26 -3
  75. package/dist/types/src/decorators/field/Encrypted.d.ts +6 -1
  76. package/dist/types/src/dream/DreamClassTransactionBuilder.d.ts +9 -9
  77. package/dist/types/src/dream/DreamInstanceTransactionBuilder.d.ts +52 -25
  78. package/dist/types/src/dream/LeftJoinLoadBuilder.d.ts +4 -4
  79. package/dist/types/src/dream/LoadBuilder.d.ts +1 -1
  80. package/dist/types/src/dream/Query.d.ts +272 -33
  81. package/dist/types/src/dream/QueryDriver/Base.d.ts +63 -1
  82. package/dist/types/src/dream/QueryDriver/Kysely.d.ts +85 -1
  83. package/dist/types/src/dream/internal/associations/assertRecognizedAssociationConditionKeys.d.ts +29 -0
  84. package/dist/types/src/dream-app/index.d.ts +56 -2
  85. package/dist/types/src/encrypt/InternalEncrypt.d.ts +20 -1
  86. package/dist/types/src/encrypt/defaultEncryptedColumnName.d.ts +7 -0
  87. package/dist/types/src/encrypt/index.d.ts +75 -6
  88. package/dist/types/src/errors/associations/UnrecognizedAssociationConditionKeys.d.ts +8 -0
  89. package/dist/types/src/helpers/Env.d.ts +28 -0
  90. package/dist/types/src/package-exports/openapi.d.ts +1 -1
  91. package/dist/types/src/package-exports/types.d.ts +2 -2
  92. package/dist/types/src/serializer/SerializerRenderer.d.ts +13 -0
  93. package/dist/types/src/serializer/builders/DreamSerializerBuilder.d.ts +59 -55
  94. package/dist/types/src/serializer/builders/ObjectSerializerBuilder.d.ts +40 -19
  95. package/dist/types/src/serializer/helpers/serializersForAssociatedClass.d.ts +17 -0
  96. package/dist/types/src/types/lifecycle.d.ts +6 -0
  97. package/dist/types/src/types/openapi.d.ts +52 -29
  98. package/dist/types/src/types/serializer.d.ts +43 -2
  99. package/dist/types/src/types/types/lifecycle.ts +6 -0
  100. package/dist/types/src/types/types/openapi.ts +51 -37
  101. package/dist/types/src/types/types/serializer.ts +62 -2
  102. package/dist/types/src/types/types/variadic.ts +99 -43
  103. package/dist/types/src/types/variadic.d.ts +29 -3
  104. package/docs/assets/hierarchy.js +1 -1
  105. package/docs/assets/highlight.css +4 -11
  106. package/docs/assets/navigation.js +1 -1
  107. package/docs/assets/search.js +1 -1
  108. package/docs/classes/db.DreamMigrationHelpers.html +13 -13
  109. package/docs/classes/db.KyselyQueryDriver.html +85 -46
  110. package/docs/classes/db.PostgresQueryDriver.html +86 -47
  111. package/docs/classes/db.QueryDriverBase.html +84 -45
  112. package/docs/classes/errors.CannotSaveMissingDream.html +2 -2
  113. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  114. package/docs/classes/errors.ColumnOverflow.html +3 -3
  115. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  116. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  117. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  118. package/docs/classes/errors.DecryptionError.html +2 -2
  119. package/docs/classes/errors.DecryptionParseError.html +2 -2
  120. package/docs/classes/errors.DecryptionRotationError.html +3 -3
  121. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  122. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  123. package/docs/classes/errors.InvalidClockTime.html +2 -2
  124. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  125. package/docs/classes/errors.InvalidDateTime.html +2 -2
  126. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  127. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  128. package/docs/classes/errors.NotNullViolation.html +3 -3
  129. package/docs/classes/errors.RecordNotFound.html +3 -3
  130. package/docs/classes/errors.SortableScopeDidNotStabilize.html +3 -3
  131. package/docs/classes/errors.SortableScopeLockWaitTimedOut.html +3 -3
  132. package/docs/classes/errors.ValidationError.html +3 -3
  133. package/docs/classes/index.CalendarDate.html +33 -33
  134. package/docs/classes/index.ClockTime.html +32 -32
  135. package/docs/classes/index.ClockTimeTz.html +35 -35
  136. package/docs/classes/index.DateTime.html +86 -86
  137. package/docs/classes/index.Decorators.html +37 -23
  138. package/docs/classes/index.Dream.html +270 -142
  139. package/docs/classes/index.DreamApp.html +40 -10
  140. package/docs/classes/index.DreamTransaction.html +3 -3
  141. package/docs/classes/index.Env.html +16 -2
  142. package/docs/classes/index.Query.html +331 -129
  143. package/docs/classes/system.CliFileWriter.html +4 -4
  144. package/docs/classes/system.DreamBin.html +6 -2
  145. package/docs/classes/system.DreamCLI.html +7 -7
  146. package/docs/classes/system.DreamImporter.html +2 -2
  147. package/docs/classes/system.DreamLogos.html +2 -2
  148. package/docs/classes/system.DreamSerializerBuilder.html +52 -34
  149. package/docs/classes/system.ObjectSerializerBuilder.html +41 -22
  150. package/docs/classes/system.PathHelpers.html +4 -4
  151. package/docs/classes/utils.Encrypt.html +54 -10
  152. package/docs/classes/utils.Range.html +2 -2
  153. package/docs/functions/db.closeAllDbConnections.html +1 -1
  154. package/docs/functions/db.dreamDbConnections.html +1 -1
  155. package/docs/functions/db.untypedDb.html +1 -1
  156. package/docs/functions/db.validateColumn.html +1 -1
  157. package/docs/functions/db.validateTable.html +1 -1
  158. package/docs/functions/errors.pgErrorType.html +1 -1
  159. package/docs/functions/index.DreamSerializer.html +1 -1
  160. package/docs/functions/index.ObjectSerializer.html +1 -1
  161. package/docs/functions/index.ReplicaSafe.html +1 -1
  162. package/docs/functions/index.STI.html +1 -1
  163. package/docs/functions/index.SoftDelete.html +1 -1
  164. package/docs/functions/utils.camelize.html +1 -1
  165. package/docs/functions/utils.capitalize.html +1 -1
  166. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  167. package/docs/functions/utils.compact.html +1 -1
  168. package/docs/functions/utils.groupBy.html +1 -1
  169. package/docs/functions/utils.hyphenize.html +1 -1
  170. package/docs/functions/utils.intersection.html +1 -1
  171. package/docs/functions/utils.isEmpty.html +1 -1
  172. package/docs/functions/utils.normalizeUnicode.html +1 -1
  173. package/docs/functions/utils.pascalize.html +1 -1
  174. package/docs/functions/utils.percent.html +1 -1
  175. package/docs/functions/utils.range.html +1 -1
  176. package/docs/functions/utils.round.html +1 -1
  177. package/docs/functions/utils.sanitizeString.html +1 -1
  178. package/docs/functions/utils.snakeify.html +1 -1
  179. package/docs/functions/utils.sort.html +1 -1
  180. package/docs/functions/utils.sortBy.html +1 -1
  181. package/docs/functions/utils.sortObjectByKey.html +1 -1
  182. package/docs/functions/utils.sortObjectByValue.html +1 -1
  183. package/docs/functions/utils.uncapitalize.html +1 -1
  184. package/docs/functions/utils.uniq.html +1 -1
  185. package/docs/hierarchy.html +1 -1
  186. package/docs/index.html +1 -1
  187. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  188. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  189. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  190. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  191. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  192. package/docs/interfaces/types.DecoratorContext.html +2 -2
  193. package/docs/interfaces/types.DecryptCallbacks.html +6 -0
  194. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  195. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  196. package/docs/interfaces/types.DreamDbConfig.html +6 -6
  197. package/docs/interfaces/types.DurationObject.html +2 -2
  198. package/docs/interfaces/types.EncryptOptions.html +2 -2
  199. package/docs/interfaces/types.EncryptionLegacyKeyUsedEvent.html +10 -0
  200. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  201. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  202. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  203. package/docs/modules/openapi.html +1 -1
  204. package/docs/modules/types.html +1 -1
  205. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  206. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  207. package/docs/types/openapi.OpenapiFormats.html +1 -1
  208. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  209. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  210. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  211. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  212. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  213. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  214. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  215. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  216. package/docs/types/openapi.OpenapiSchemaCombinatorFields.html +4 -0
  217. package/docs/types/openapi.OpenapiSchemaCommonFields.html +5 -1
  218. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +1 -2
  219. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +1 -2
  220. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +1 -2
  221. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  222. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  223. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  224. package/docs/types/openapi.OpenapiSchemaNull.html +4 -2
  225. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  226. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  227. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +5 -1
  228. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +2 -1
  229. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  230. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  231. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  232. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  233. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +1 -2
  234. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +1 -2
  235. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +1 -2
  236. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  237. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  238. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  239. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  240. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  241. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  242. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  243. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  244. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  245. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  246. package/docs/types/types.CalendarDateObject.html +1 -1
  247. package/docs/types/types.Camelized.html +1 -1
  248. package/docs/types/types.ClockTimeObject.html +1 -1
  249. package/docs/types/types.DbConnectionType.html +1 -1
  250. package/docs/types/types.DbTypes.html +1 -1
  251. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  252. package/docs/types/types.DreamAttributes.html +1 -1
  253. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  254. package/docs/types/types.DreamClassColumn.html +1 -1
  255. package/docs/types/types.DreamColumn.html +1 -1
  256. package/docs/types/types.DreamColumnNames.html +1 -1
  257. package/docs/types/types.DreamLogLevel.html +1 -1
  258. package/docs/types/types.DreamLogger.html +2 -2
  259. package/docs/types/types.DreamModelSerializerType.html +1 -1
  260. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  261. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  262. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  263. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  264. package/docs/types/types.DreamSerializable.html +1 -1
  265. package/docs/types/types.DreamSerializableArray.html +1 -1
  266. package/docs/types/types.DreamSerializerKey.html +1 -1
  267. package/docs/types/types.DreamSerializers.html +1 -1
  268. package/docs/types/types.DreamVirtualColumns.html +1 -1
  269. package/docs/types/types.DurationUnit.html +1 -1
  270. package/docs/types/types.EncryptAlgorithm.html +1 -1
  271. package/docs/types/types.HasManyStatement.html +1 -1
  272. package/docs/types/types.HasOneStatement.html +1 -1
  273. package/docs/types/types.Hyphenized.html +1 -1
  274. package/docs/types/types.Pascalized.html +1 -1
  275. package/docs/types/types.PrimaryKeyType.html +1 -1
  276. package/docs/types/types.RoundingPrecision.html +1 -1
  277. package/docs/types/types.SerializerCasing.html +1 -1
  278. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  279. package/docs/types/types.Snakeified.html +1 -1
  280. package/docs/types/types.StrictInterface.html +1 -1
  281. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  282. package/docs/types/types.UpdateableProperties.html +1 -1
  283. package/docs/types/types.ValidationType.html +1 -1
  284. package/docs/types/types.ViewModel.html +2 -2
  285. package/docs/types/types.ViewModelClass.html +1 -1
  286. package/docs/types/types.WeekdayName.html +1 -1
  287. package/docs/types/types.WhereStatementForDream.html +1 -1
  288. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  289. package/docs/variables/index.DreamConst.html +1 -1
  290. package/docs/variables/index.ops.html +1 -1
  291. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  292. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  293. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  294. package/docs/variables/system.primaryKeyTypes.html +1 -1
  295. package/package.json +1 -1
  296. package/dist/cjs/src/serializer/helpers/serializerForAssociatedClass.js +0 -37
  297. package/dist/esm/src/serializer/helpers/serializerForAssociatedClass.js +0 -37
  298. package/dist/types/src/serializer/helpers/serializerForAssociatedClass.d.ts +0 -9
  299. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +0 -1
  300. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +0 -1
  301. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +0 -1
  302. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +0 -1
@@ -554,6 +554,78 @@ export default class QueryDriverBase {
554
554
  async pluck(...fields) {
555
555
  throw new Error('implement pluck in child class');
556
556
  }
557
+ /**
558
+ * @internal
559
+ *
560
+ * The order statements that the declared `order` of the associations the
561
+ * Query is built on or joins contributes to its ORDER BY, in the order they
562
+ * sort, each column namespaced by the alias it is read from. Cursor
563
+ * pagination folds these into its cursor.
564
+ *
565
+ * @returns An array of namespaced order statements
566
+ */
567
+ associationOrderStatements() {
568
+ throw new Error('implement associationOrderStatements in child class');
569
+ }
570
+ /**
571
+ * @internal
572
+ *
573
+ * The primary key of each table the Query reaches through a HasOne or
574
+ * HasMany association (one it joins, one an association it joins goes
575
+ * through, or one its association query is built on), namespaced by the
576
+ * alias it is read from, in join order. Such a join can return a row of the
577
+ * Query's table once for each row it matches, and these keys, with the
578
+ * Query's own primary key, tell those rows apart. Cursor pagination pages by
579
+ * them after the Query's own primary key.
580
+ *
581
+ * @returns An array of namespaced primary key columns
582
+ */
583
+ joinedRowKeyColumns() {
584
+ throw new Error('implement joinedRowKeyColumns in child class');
585
+ }
586
+ /**
587
+ * @internal
588
+ *
589
+ * A page of a select that keeps one row of each group (DISTINCT ON, as a
590
+ * Query that calls `distinct`, or an association that declares `distinct`,
591
+ * produces), or null for any other select, whose pages cursor pagination
592
+ * reads as it reads any select's.
593
+ *
594
+ * The page is the values of the Query's order statement columns for each
595
+ * row the select returns that matches any of `rowsAfter` (every row when it
596
+ * is null), sorted by the Query's order statements and limited by its
597
+ * limit. The select chooses each group's row before `rowsAfter` narrows the
598
+ * rows, so that the page holds only rows the select returns.
599
+ *
600
+ * A driver whose selects never keep one row of each group returns null.
601
+ *
602
+ * @param rowsAfter - where statements, any of which a row of the page matches
603
+ * @returns The page's rows, each an array of its order statement columns' values, or null
604
+ */
605
+ // eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars
606
+ async pluckDistinctOnPage(rowsAfter) {
607
+ return null;
608
+ }
609
+ /**
610
+ * @internal
611
+ *
612
+ * For a select that keeps one row of each group (DISTINCT ON, as a Query
613
+ * that calls `distinct`, or an association that declares `distinct`,
614
+ * produces), the values of `rowKeyColumns` for every row it returns, sorted
615
+ * by those columns, ascending; null for any other select. `findEach` and
616
+ * `pluckEach` read a select that keeps one row of each group from these
617
+ * keys rather than by a cursor, which would narrow the select before it
618
+ * chooses each group's row.
619
+ *
620
+ * A driver whose selects never keep one row of each group returns null.
621
+ *
622
+ * @param rowKeyColumns - the namespaced columns that tell the select's rows apart
623
+ * @returns The rows' values of `rowKeyColumns`, or null
624
+ */
625
+ // eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars
626
+ async pluckDistinctOnRowKeys(rowKeyColumns) {
627
+ return null;
628
+ }
557
629
  /**
558
630
  * Returns a new Kysely SelectQueryBuilder instance to be used
559
631
  * in a sub Query
@@ -76,6 +76,21 @@ import checkForNeedToBeRunMigrations from './helpers/kysely/checkForNeedToBeRunM
76
76
  import foreignKeyTypeFromPrimaryKey from './helpers/kysely/foreignKeyTypeFromPrimaryKey.js';
77
77
  import runMigration from './helpers/kysely/runMigration.js';
78
78
  export default class KyselyQueryDriver extends QueryDriverBase {
79
+ /**
80
+ * @internal
81
+ *
82
+ * While `associationOrderStatements` builds a select, the order statements
83
+ * that association joins add to it, in the order they are added.
84
+ */
85
+ associationOrderCollector = null;
86
+ /**
87
+ * @internal
88
+ *
89
+ * While `joinedRowKeyColumns` builds a select, the namespaced primary keys of
90
+ * the tables that HasOne and HasMany joins add to it, in the order they are
91
+ * added.
92
+ */
93
+ joinedRowKeyCollector = null;
79
94
  // ATTENTION FRED
80
95
  // stop trying to make this async. You never learn...
81
96
  //
@@ -639,7 +654,7 @@ export default class KyselyQueryDriver extends QueryDriverBase {
639
654
  bypassOrder: true,
640
655
  });
641
656
  const countClause = distinctColumn
642
- ? count(sql `DISTINCT ${distinctColumn}`)
657
+ ? count(sql `DISTINCT ${sql.ref(distinctColumn)}`)
643
658
  : count(query['namespaceColumn'](query.dreamInstance['_primaryKey']));
644
659
  kyselyQuery = kyselyQuery.select(countClause.as('tablecount'));
645
660
  const data = await executeDatabaseQuery(kyselyQuery, 'executeTakeFirstOrThrow');
@@ -884,13 +899,58 @@ export default class KyselyQueryDriver extends QueryDriverBase {
884
899
  return new this.constructor(results.clone).buildCommon(results.kyselyQuery);
885
900
  }
886
901
  buildSelect({ bypassSelectAll = false, bypassOrder = false, columns, } = {}) {
902
+ const selectCore = this.buildSelectCore({ bypassOrder });
903
+ let kyselyQuery = selectCore.kyselyQuery;
904
+ if (this.query['bypassImplicitOrder']) {
905
+ // a keyset cursor compares only the Query's order statements, so the
906
+ // orderings emitted above (a similarity rank, an association's declared
907
+ // `order`, including those of an association query's base select) are
908
+ // cleared rather than left to sort ahead of them
909
+ kyselyQuery = kyselyQuery.clearOrderBy();
910
+ }
911
+ if (this.query['orderStatements'].length && !bypassOrder) {
912
+ this.query['orderStatements'].forEach(orderStatement => {
913
+ this.validatePlainColumn(this.dreamClass, orderStatement.column);
914
+ kyselyQuery = kyselyQuery.orderBy(this.namespaceColumn(orderStatement.column), this.orderByDirection(orderStatement.direction));
915
+ });
916
+ }
917
+ if (this.query['limitStatement'])
918
+ kyselyQuery = kyselyQuery.limit(this.query['limitStatement']);
919
+ if (this.query['offsetStatement'])
920
+ kyselyQuery = kyselyQuery.offset(this.query['offsetStatement']);
921
+ if (columns) {
922
+ kyselyQuery = kyselyQuery.select(this.columnsWithRequiredLoadColumns(columns).map(column => this.namespaceColumn(column)));
923
+ }
924
+ else if (!bypassSelectAll) {
925
+ kyselyQuery = kyselyQuery.selectAll(this.query['baseSqlAlias']);
926
+ }
927
+ // even though we manually bypass explicit order statements above,
928
+ // associations can contain their own ordering systems. If we do not
929
+ // escape all orders, we can mistakenly allow an order clause to sneak in.
930
+ if (bypassOrder)
931
+ kyselyQuery = kyselyQuery.clearOrderBy();
932
+ return kyselyQuery;
933
+ }
934
+ /**
935
+ * @internal
936
+ *
937
+ * The select's source, joins, conditions, DISTINCT ON and similarity
938
+ * ranking, before the Query's own order statements, limit, offset and
939
+ * selection are applied, together with the connection the select runs on.
940
+ */
941
+ buildSelectCore({ bypassOrder = false } = {}) {
887
942
  let kyselyQuery;
943
+ let selectDb;
888
944
  if (this.query['baseSelectQuery']) {
889
945
  const connectionOverride = this.query['connectionOverride'];
890
946
  const query = connectionOverride
891
947
  ? this.query['baseSelectQuery'].connection(connectionOverride)
892
948
  : this.query['baseSelectQuery'];
893
- kyselyQuery = new this.constructor(query).buildSelect({
949
+ const baseSelectDriver = new this.constructor(query);
950
+ baseSelectDriver.associationOrderCollector = this.associationOrderCollector;
951
+ baseSelectDriver.joinedRowKeyCollector = this.joinedRowKeyCollector;
952
+ selectDb = baseSelectDriver.dbFor('select');
953
+ kyselyQuery = baseSelectDriver.buildSelect({
894
954
  bypassSelectAll: true,
895
955
  });
896
956
  }
@@ -898,7 +958,8 @@ export default class KyselyQueryDriver extends QueryDriverBase {
898
958
  const from = this.query['baseSqlAlias'] === this.query['tableName']
899
959
  ? this.query['tableName']
900
960
  : `${this.query['tableName']} as ${this.query['baseSqlAlias']}`;
901
- kyselyQuery = this.dbFor('select').selectFrom(from);
961
+ selectDb = this.dbFor('select');
962
+ kyselyQuery = selectDb.selectFrom(from);
902
963
  }
903
964
  if (this.query['distinctColumn']) {
904
965
  kyselyQuery = kyselyQuery.distinctOn(this.query['distinctColumn']);
@@ -907,28 +968,135 @@ export default class KyselyQueryDriver extends QueryDriverBase {
907
968
  kyselyQuery = this.conditionallyAttachSimilarityColumnsToSelect(kyselyQuery, {
908
969
  bypassOrder: bypassOrder || !!this.query['distinctColumn'],
909
970
  });
910
- if (this.query['orderStatements'].length && !bypassOrder) {
911
- this.query['orderStatements'].forEach(orderStatement => {
912
- this.validatePlainColumn(this.dreamClass, orderStatement.column);
913
- kyselyQuery = kyselyQuery.orderBy(this.namespaceColumn(orderStatement.column), this.orderByDirection(orderStatement.direction));
914
- });
971
+ return { kyselyQuery, selectDb };
972
+ }
973
+ /**
974
+ * @internal
975
+ *
976
+ * The order statements that the declared `order` of the associations the
977
+ * Query is built on or joins contributes to its ORDER BY, in the order they
978
+ * sort, each column namespaced by the alias it is read from. They are read
979
+ * off the same join code that emits them, by building the select.
980
+ *
981
+ * @returns An array of namespaced order statements
982
+ */
983
+ associationOrderStatements() {
984
+ const collected = [];
985
+ this.associationOrderCollector = collected;
986
+ try {
987
+ this.buildSelect();
915
988
  }
916
- if (this.query['limitStatement'])
917
- kyselyQuery = kyselyQuery.limit(this.query['limitStatement']);
918
- if (this.query['offsetStatement'])
919
- kyselyQuery = kyselyQuery.offset(this.query['offsetStatement']);
920
- if (columns) {
921
- kyselyQuery = kyselyQuery.select(this.columnsWithRequiredLoadColumns(columns).map(column => this.namespaceColumn(column)));
989
+ finally {
990
+ this.associationOrderCollector = null;
922
991
  }
923
- else if (!bypassSelectAll) {
924
- kyselyQuery = kyselyQuery.selectAll(this.query['baseSqlAlias']);
992
+ return collected;
993
+ }
994
+ /**
995
+ * @internal
996
+ *
997
+ * The primary key of each table the Query reaches through a HasOne or
998
+ * HasMany association (one it joins, one an association it joins goes
999
+ * through, or one its association query is built on), namespaced by the
1000
+ * alias it is read from, in join order. They are read off the same join code
1001
+ * that emits the joins, by building the select.
1002
+ *
1003
+ * @returns An array of namespaced primary key columns
1004
+ */
1005
+ joinedRowKeyColumns() {
1006
+ const collected = [];
1007
+ this.joinedRowKeyCollector = collected;
1008
+ try {
1009
+ this.buildSelect();
925
1010
  }
926
- // even though we manually bypass explicit order statements above,
927
- // associations can contain their own ordering systems. If we do not
928
- // escape all orders, we can mistakenly allow an order clause to sneak in.
929
- if (bypassOrder)
930
- kyselyQuery = kyselyQuery.clearOrderBy();
931
- return kyselyQuery;
1011
+ finally {
1012
+ this.joinedRowKeyCollector = null;
1013
+ }
1014
+ return collected;
1015
+ }
1016
+ /**
1017
+ * @internal
1018
+ *
1019
+ * A page of a select that carries DISTINCT ON (as a Query that calls
1020
+ * `distinct`, or an association that declares `distinct`, produces), or null
1021
+ * for any other select.
1022
+ *
1023
+ * Such a select keeps the row of each group that sorts first in its ORDER
1024
+ * BY, so narrowing it to the rows after a cursor would let it keep a row it
1025
+ * does not return (a group's later row, once the cursor has passed the
1026
+ * group's first). It is read as a derived table instead, and narrowed to
1027
+ * `rowsAfter`, sorted and limited outside it. The Query's order statements
1028
+ * follow the select's own ORDER BY, if it has one, so that the select keeps
1029
+ * the same row of each group every time it runs, and each of their columns
1030
+ * is read out of it under a short alias (as `pluck` reads them), since a
1031
+ * joined table's column cannot be named outside it.
1032
+ *
1033
+ * @param rowsAfter - where statements, any of which a row of the page matches
1034
+ * @returns The page's rows, each an array of its order statement columns' values, or null
1035
+ */
1036
+ async pluckDistinctOnPage(rowsAfter) {
1037
+ const { kyselyQuery: selectCore, selectDb } = this.buildSelectCore();
1038
+ const selectNode = selectCore.toOperationNode();
1039
+ if (!selectNode.distinctOn?.length)
1040
+ return null;
1041
+ const baseSqlAlias = this.query['baseSqlAlias'];
1042
+ const orderStatements = this.query['orderStatements'];
1043
+ const pageColumns = orderStatements.map((orderStatement, index) => {
1044
+ this.validatePlainColumn(this.dreamClass, orderStatement.column);
1045
+ return { column: this.namespaceColumn(orderStatement.column), alias: `pluck${index}` };
1046
+ });
1047
+ let distinctSelect = selectCore;
1048
+ orderStatements.forEach(orderStatement => {
1049
+ distinctSelect = distinctSelect.orderBy(this.namespaceColumn(orderStatement.column), this.orderByDirection(orderStatement.direction));
1050
+ });
1051
+ distinctSelect = distinctSelect.select(pageColumns.map(({ column, alias }) => `${column} as ${alias}`));
1052
+ let pageSelect = selectDb.selectFrom(distinctSelect.as(baseSqlAlias));
1053
+ pageSelect = pageSelect.select(pageColumns.map(({ alias }) => `${baseSqlAlias}.${alias}`));
1054
+ if (rowsAfter) {
1055
+ const derivedColumns = new Map(pageColumns.map(({ column, alias }) => [column, `${baseSqlAlias}.${alias}`]));
1056
+ pageSelect = pageSelect.where((eb) => eb.or(rowsAfter.map(whereStatement => this.whereStatementToExpressionWrapper(this.dreamClass, eb, Object.fromEntries(Object.entries(whereStatement).map(([column, value]) => [
1057
+ derivedColumns.get(this.namespaceColumn(column)),
1058
+ value,
1059
+ ]))))));
1060
+ }
1061
+ pageColumns.forEach(({ alias }, index) => {
1062
+ pageSelect = pageSelect.orderBy(`${baseSqlAlias}.${alias}`, this.orderByDirection(orderStatements[index].direction));
1063
+ });
1064
+ if (this.query['limitStatement'])
1065
+ pageSelect = pageSelect.limit(this.query['limitStatement']);
1066
+ return (await executeDatabaseQuery(pageSelect, 'execute')).map(row => pageColumns.map(({ alias }) => row[alias]));
1067
+ }
1068
+ /**
1069
+ * @internal
1070
+ *
1071
+ * For a select that carries DISTINCT ON (as a Query that calls `distinct`,
1072
+ * or an association that declares `distinct`, produces), the values of
1073
+ * `rowKeyColumns` for every row it returns, sorted by those columns,
1074
+ * ascending; null for any other select.
1075
+ *
1076
+ * The select runs as `all()` runs it, with its own ORDER BY choosing each
1077
+ * group's row, and is read as a derived table, sorted outside it. Each of
1078
+ * `rowKeyColumns` is read out of it under a short alias (as `pluck` reads
1079
+ * them), since a joined table's column cannot be named outside it.
1080
+ *
1081
+ * @param rowKeyColumns - the namespaced columns that tell the select's rows apart
1082
+ * @returns The rows' values of `rowKeyColumns`, or null
1083
+ */
1084
+ async pluckDistinctOnRowKeys(rowKeyColumns) {
1085
+ const { kyselyQuery: selectCore, selectDb } = this.buildSelectCore();
1086
+ if (!selectCore.toOperationNode().distinctOn?.length)
1087
+ return null;
1088
+ const baseSqlAlias = this.query['baseSqlAlias'];
1089
+ const keyColumns = rowKeyColumns.map((column, index) => {
1090
+ this.validatePlainColumn(this.dreamClass, column);
1091
+ return { column: this.namespaceColumn(column), alias: `key${index}` };
1092
+ });
1093
+ const distinctSelect = this.buildSelect({ bypassSelectAll: true }).select(keyColumns.map(({ column, alias }) => `${column} as ${alias}`));
1094
+ let keySelect = selectDb.selectFrom(distinctSelect.as(baseSqlAlias));
1095
+ keySelect = keySelect.select(keyColumns.map(({ alias }) => `${baseSqlAlias}.${alias}`));
1096
+ keyColumns.forEach(({ alias }) => {
1097
+ keySelect = keySelect.orderBy(`${baseSqlAlias}.${alias}`, this.orderByDirection('asc'));
1098
+ });
1099
+ return (await executeDatabaseQuery(keySelect, 'execute')).map(row => keyColumns.map(({ alias }) => row[alias]));
932
1100
  }
933
1101
  orderByDirection(direction) {
934
1102
  return orderByDirection(direction);
@@ -1500,26 +1668,22 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1500
1668
  c = val.value;
1501
1669
  }
1502
1670
  else if (val instanceof Range) {
1503
- const rangeStart = val.begin;
1504
- const rangeEnd = val.end;
1505
- const excludeEnd = val.excludeEnd;
1506
- if (rangeStart && rangeEnd) {
1507
- a = attr;
1508
- b = '>=';
1509
- c = rangeStart;
1671
+ // a bound is present unless it is null or undefined, matching the Range
1672
+ // constructor, so falsy bounds such as 0, 0n and '' still constrain the query
1673
+ const boundConditions = [];
1674
+ if (val.begin != null)
1675
+ boundConditions.push({ operator: '>=', value: val.begin });
1676
+ if (val.end != null)
1677
+ boundConditions.push({ operator: val.excludeEnd ? '<' : '<=', value: val.end });
1678
+ // the Range constructor rejects a range with neither bound, so firstBound is always set
1679
+ const [firstBound, secondBound] = boundConditions;
1680
+ a = attr;
1681
+ b = firstBound.operator;
1682
+ c = firstBound.value;
1683
+ if (secondBound) {
1510
1684
  a2 = attr;
1511
- b2 = excludeEnd ? '<' : '<=';
1512
- c2 = rangeEnd;
1513
- }
1514
- else if (rangeStart) {
1515
- a = attr;
1516
- b = '>=';
1517
- c = rangeStart;
1518
- }
1519
- else {
1520
- a = attr;
1521
- b = excludeEnd ? '<' : '<=';
1522
- c = rangeEnd;
1685
+ b2 = secondBound.operator;
1686
+ c2 = secondBound.value;
1523
1687
  }
1524
1688
  }
1525
1689
  else {
@@ -2253,6 +2417,7 @@ export default class KyselyQueryDriver extends QueryDriverBase {
2253
2417
  join = this.applyJoinAndStatement(modelClassUsedForJoinConditions, join, joinAndStatement, currentTableAlias);
2254
2418
  return join;
2255
2419
  });
2420
+ this.joinedRowKeyCollector?.push(this.namespaceColumn(association.modelCB().primaryKey, currentTableAlias));
2256
2421
  if (association.type === 'HasMany') {
2257
2422
  if (association.order) {
2258
2423
  query = this.applyOrderStatementForAssociation({
@@ -2280,15 +2445,16 @@ export default class KyselyQueryDriver extends QueryDriverBase {
2280
2445
  return query;
2281
2446
  let selectQuery = query;
2282
2447
  const orderStatement = association.order;
2283
- if (typeof orderStatement === 'string') {
2284
- selectQuery = selectQuery.orderBy(this.namespaceColumn(orderStatement, tableNameOrAlias), 'asc');
2285
- }
2286
- else {
2287
- Object.keys(orderStatement).forEach(column => {
2288
- const direction = orderStatement[column];
2289
- selectQuery = selectQuery.orderBy(this.namespaceColumn(column, tableNameOrAlias), direction);
2290
- });
2291
- }
2448
+ const orderQueryStatements = typeof orderStatement === 'string'
2449
+ ? [{ column: this.namespaceColumn(orderStatement, tableNameOrAlias), direction: 'asc' }]
2450
+ : Object.keys(orderStatement).map(column => ({
2451
+ column: this.namespaceColumn(column, tableNameOrAlias),
2452
+ direction: orderStatement[column],
2453
+ }));
2454
+ orderQueryStatements.forEach(({ column, direction }) => {
2455
+ selectQuery = selectQuery.orderBy(column, direction);
2456
+ });
2457
+ this.associationOrderCollector?.push(...orderQueryStatements);
2292
2458
  return selectQuery;
2293
2459
  }
2294
2460
  distinctColumnNameForAssociation({ association, tableNameOrAlias, foreignKey, }) {
@@ -0,0 +1,48 @@
1
+ import UnrecognizedAssociationConditionKeys from '../../../errors/associations/UnrecognizedAssociationConditionKeys.js';
2
+ import isObject from '../../../helpers/isObject.js';
3
+ /**
4
+ * The keys a condition object on an association accepts, whether it is
5
+ * passed in a chain (`preload('posts', { and: { ... } })`), returned from a
6
+ * `preloadFor`/`loadFor` modifier, or passed to `associationQuery`.
7
+ */
8
+ export const ASSOCIATION_CONDITION_KEYS = ['and', 'andNot', 'andAny'];
9
+ /**
10
+ * The keys accepted by `destroyAssociation`, `reallyDestroyAssociation`
11
+ * and `undestroyAssociation`.
12
+ */
13
+ export const DESTROY_ASSOCIATION_OPTION_KEYS = [
14
+ ...ASSOCIATION_CONDITION_KEYS,
15
+ 'bypassAllDefaultScopes',
16
+ 'defaultScopesToBypass',
17
+ 'cascade',
18
+ 'skipHooks',
19
+ ];
20
+ /**
21
+ * The keys accepted by `updateAssociation`.
22
+ */
23
+ export const UPDATE_ASSOCIATION_OPTION_KEYS = [
24
+ ...ASSOCIATION_CONDITION_KEYS,
25
+ 'bypassAllDefaultScopes',
26
+ 'defaultScopesToBypass',
27
+ 'skipHooks',
28
+ ];
29
+ /**
30
+ * Throws UnrecognizedAssociationConditionKeys when `value` is an object
31
+ * carrying any own key outside `acceptedKeys`, including a key whose value
32
+ * is `undefined`. Dream reads only the accepted keys, so an unrecognized key
33
+ * would otherwise be silently ignored (and, for the destroy family, act on
34
+ * every associated row instead of the ones the caller meant to select).
35
+ * Non-object values (`undefined`, `null`) pass through untouched.
36
+ *
37
+ * @param value - The condition object or options object to check
38
+ * @param acceptedKeys - The keys `value` may carry
39
+ * @param associationName - The association the object applies to, for the error message
40
+ * @param methodName - The public method the options were passed to, or null for a chain condition
41
+ */
42
+ export default function assertRecognizedAssociationConditionKeys(value, acceptedKeys, associationName, methodName = null) {
43
+ if (!isObject(value))
44
+ return;
45
+ const unrecognizedKeys = Object.keys(value).filter(key => !acceptedKeys.includes(key));
46
+ if (unrecognizedKeys.length)
47
+ throw new UnrecognizedAssociationConditionKeys(String(associationName), unrecognizedKeys, acceptedKeys, methodName);
48
+ }
@@ -1,5 +1,9 @@
1
1
  import applyScopeBypassingSettingsToQuery from '../applyScopeBypassingSettingsToQuery.js';
2
+ import assertRecognizedAssociationConditionKeys, { ASSOCIATION_CONDITION_KEYS, } from './assertRecognizedAssociationConditionKeys.js';
2
3
  export default function associationQuery(dream, txn = null, associationName, { joinAndStatements, bypassAllDefaultScopes, defaultScopesToBypass, }) {
4
+ // a condition carrying none of and/andNot/andAny skips the join below, so the
5
+ // chain check in Query never sees it
6
+ assertRecognizedAssociationConditionKeys(joinAndStatements, ASSOCIATION_CONDITION_KEYS, associationName, 'associationQuery');
3
7
  const association = dream['associationMetadataMap']()[associationName];
4
8
  const associationClass = association.modelCB();
5
9
  const dreamClass = dream.constructor;
@@ -14,7 +14,12 @@ export default async function reload(dream, txn = null) {
14
14
  [dream['_primaryKey']]: dream.primaryKeyValue(),
15
15
  });
16
16
  const reloadedRecord = await query.firstOrFail();
17
- dream.setAttributes(reloadedRecord.getAttributes());
17
+ // a row read from the database, so its @Encrypted backing columns hold the
18
+ // ciphertext Dream stored and are written without setAttribute's decryption check
19
+ dream['_setAttributes'](reloadedRecord.getAttributes(), {
20
+ bypassUserDefinedSetters: true,
21
+ fromDatabase: true,
22
+ });
18
23
  dream['freezeAttributes']();
19
24
  dream['originalAttributes'] = dream.getAttributes();
20
25
  return dream;
@@ -132,8 +132,14 @@ async function writeDream(dream, txn, { alreadyPersisted, sortableFieldConfigs,
132
132
  // the row returned by the driver comes from `RETURNING *` (or a
133
133
  // `select *` reload on drivers without RETURNING support), so under
134
134
  // schema/image skew it can contain columns this build's compiled
135
- // schema doesn't know about; those must never reach setAttributes
136
- dream.setAttributes(filterRowToKnownColumns(data, dream.columns()));
135
+ // schema doesn't know about; those are filtered out before the row is
136
+ // written back. It is a row read from the database, so its @Encrypted
137
+ // backing columns hold the ciphertext Dream stored and are written
138
+ // without setAttribute's decryption check
139
+ dream['_setAttributes'](filterRowToKnownColumns(data, dream.columns()), {
140
+ bypassUserDefinedSetters: true,
141
+ fromDatabase: true,
142
+ });
137
143
  }
138
144
  // set frozen attributes to what has already been saved
139
145
  dream['freezeAttributes']();
@@ -16,6 +16,7 @@ export default function sqlResultToDreamInstance(dreamClass, sqlResult) {
16
16
  throw new STIChildMissing(dreamClass, sqlResult.type, sqlResult[dreamClass.primaryKey]);
17
17
  const dreamModel = new extendingDreamClass(filterRowToKnownColumns(sqlResult, extendingDreamClass.columns()), {
18
18
  bypassUserDefinedSetters: true,
19
+ fromDatabase: true,
19
20
  isPersisted: true,
20
21
  _internalUseOnly: true,
21
22
  });
@@ -25,6 +26,7 @@ export default function sqlResultToDreamInstance(dreamClass, sqlResult) {
25
26
  else {
26
27
  const dreamModel = new dreamClass(filterRowToKnownColumns(sqlResult, dreamClass.columns()), {
27
28
  bypassUserDefinedSetters: true,
29
+ fromDatabase: true,
28
30
  isPersisted: true,
29
31
  _internalUseOnly: true,
30
32
  });
@@ -148,7 +148,7 @@ async function undestroyAssociatedRecords(dream, txn, options) {
148
148
  bypassAllDefaultScopes: options.bypassAllDefaultScopes ?? false,
149
149
  defaultScopesToBypass: (options.defaultScopesToBypass ?? []),
150
150
  });
151
- await query.removeDefaultScope(SOFT_DELETE_SCOPE_NAME).findEach(async (record) => {
151
+ await query.removeDefaultScope(SOFT_DELETE_SCOPE_NAME)['findEachRecordOnce'](async (record) => {
152
152
  markSortableCascadeEdge(record, association);
153
153
  await record.txn(txn).undestroy(options);
154
154
  });
@@ -179,6 +179,7 @@ export default class DreamApp {
179
179
  _specialHooks = {
180
180
  dbLog: [],
181
181
  replStart: [],
182
+ encryptionLegacyKeyUsed: [],
182
183
  };
183
184
  get specialHooks() {
184
185
  return this._specialHooks;
@@ -520,6 +521,40 @@ export default class DreamApp {
520
521
  }
521
522
  }
522
523
  }
524
+ /**
525
+ * Registers a callback for an app-level event. Callbacks for the same
526
+ * event run in the order they were registered.
527
+ *
528
+ * - `'db:log'`: called with a {@link KyselyLogEvent} each time a query on a
529
+ * Dream database connection completes or fails.
530
+ * - `'repl:start'`: called, and awaited, with the console's REPL context
531
+ * when the Dream console starts.
532
+ * - `'encryption:legacy-key-used'`: called with an
533
+ * {@link EncryptionLegacyKeyUsedEvent} when reading an `@deco.Encrypted`
534
+ * property needed the `legacy` column encryption key, not `current`, to
535
+ * open the stored value. Use it during a key rotation to find rows still
536
+ * encrypted with the old key. The property decrypts on every read, so a
537
+ * record whose value is still under the old key fires the event each time
538
+ * the property is read. A row nobody reads never fires it, so a quiet
539
+ * event does not prove that `legacy` is safe to drop;
540
+ * `Model.reencryptEncryptedColumns()` rewrites every row instead. Neither
541
+ * it nor the migration helpers fire the event. The callback runs
542
+ * synchronously inside the property read, and an error it throws
543
+ * propagates from that read. Reading the same property of the same
544
+ * record inside the callback returns its value without firing the event
545
+ * again, so a callback can read the property and assign the value back,
546
+ * which re-encrypts it under `current` for the record's next save. That
547
+ * holds only while the callback runs synchronously: a callback that
548
+ * reads the property after an `await` fires the event again from that
549
+ * read, so an `async` callback should read the property before its
550
+ * first `await`.
551
+ *
552
+ * ```ts
553
+ * dreamApp.on('encryption:legacy-key-used', ({ dream, property }) => {
554
+ * console.warn(`${dream.constructor.name} ${dream.primaryKeyValue()} ${property} still uses the legacy key`)
555
+ * })
556
+ * ```
557
+ */
523
558
  on(hookEventType, cb) {
524
559
  switch (hookEventType) {
525
560
  case 'db:log':
@@ -528,6 +563,9 @@ export default class DreamApp {
528
563
  case 'repl:start':
529
564
  this._specialHooks.replStart.push(cb);
530
565
  break;
566
+ case 'encryption:legacy-key-used':
567
+ this._specialHooks.encryptionLegacyKeyUsed.push(cb);
568
+ break;
531
569
  }
532
570
  }
533
571
  }
@@ -1,4 +1,7 @@
1
1
  import DreamApp from '../dream-app/index.js';
2
+ import DecryptionError from '../errors/encrypt/DecryptionError.js';
3
+ import DecryptionParseError from '../errors/encrypt/DecryptionParseError.js';
4
+ import DecryptionRotationError from '../errors/encrypt/DecryptionRotationError.js';
2
5
  import MissingColumnEncryptionOpts from '../errors/encrypt/MissingColumnEncryptionOpts.js';
3
6
  import Encrypt from './index.js';
4
7
  export default class InternalEncrypt {
@@ -11,19 +14,61 @@ export default class InternalEncrypt {
11
14
  return null;
12
15
  return this.doEncryption(data, encryptOpts.current);
13
16
  }
14
- static decryptColumn(data) {
17
+ /**
18
+ * Decrypts `data` with the column keys, current then legacy.
19
+ * `onLegacyKeyUsed` is passed to `Encrypt.decrypt`, so it is called only
20
+ * when the legacy key opened the value.
21
+ */
22
+ static decryptColumn(data, callbacks = {}) {
15
23
  const dreamApp = DreamApp.getOrFail();
16
24
  const encryptOpts = dreamApp.encryption?.columns;
17
25
  if (!encryptOpts)
18
26
  throw new MissingColumnEncryptionOpts();
19
27
  if (data === null || data === undefined)
20
28
  return null;
21
- return this.doDecryption(data, encryptOpts.current, encryptOpts.legacy);
29
+ return this.doDecryption(data, encryptOpts.current, encryptOpts.legacy, callbacks);
30
+ }
31
+ /**
32
+ * Encrypts `data`, a column's ciphertext, again with the current column key
33
+ * (see `Encrypt.reencrypt`), opening it with the current key or, failing
34
+ * that, the legacy one. It does not call `onLegacyKeyUsed`.
35
+ */
36
+ static reencryptColumn(data) {
37
+ const dreamApp = DreamApp.getOrFail();
38
+ const encryptOpts = dreamApp.encryption?.columns;
39
+ if (!encryptOpts)
40
+ throw new MissingColumnEncryptionOpts();
41
+ if (data === null || data === undefined)
42
+ return null;
43
+ return Encrypt.reencrypt(data, encryptOpts.current, encryptOpts.legacy);
44
+ }
45
+ /**
46
+ * Whether `data` is ciphertext that the column keys (current, then legacy)
47
+ * open to a value `decryptColumn` returns. A missing column encryption
48
+ * config or key still throws, since that is a configuration error rather
49
+ * than a property of `data`.
50
+ */
51
+ static isDecryptableColumnCiphertext(data) {
52
+ const dreamApp = DreamApp.getOrFail();
53
+ const encryptOpts = dreamApp.encryption?.columns;
54
+ if (!encryptOpts)
55
+ throw new MissingColumnEncryptionOpts();
56
+ try {
57
+ this.doDecryption(data, encryptOpts.current, encryptOpts.legacy);
58
+ return true;
59
+ }
60
+ catch (err) {
61
+ if (err instanceof DecryptionError ||
62
+ err instanceof DecryptionParseError ||
63
+ err instanceof DecryptionRotationError)
64
+ return false;
65
+ throw err;
66
+ }
22
67
  }
23
68
  static doEncryption(data, encryptionOpts) {
24
69
  return Encrypt.encrypt(data, encryptionOpts);
25
70
  }
26
- static doDecryption(data, encryptionOpts, legacyEncryptionOpts) {
27
- return Encrypt.decrypt(data, encryptionOpts, legacyEncryptionOpts);
71
+ static doDecryption(data, encryptionOpts, legacyEncryptionOpts, callbacks = {}) {
72
+ return Encrypt.decrypt(data, encryptionOpts, legacyEncryptionOpts, callbacks);
28
73
  }
29
74
  }