@rvoh/dream 2.35.0 → 2.36.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 (302) hide show
  1. package/README.md +2 -2
  2. package/dist/cjs/src/Dream.js +260 -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 +35 -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 +77 -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 +260 -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 +35 -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 +77 -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 +208 -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 +29 -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
@@ -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,89 @@ export default class InternalEncrypt {
11
14
  return null;
12
15
  return this.doEncryption(data, encryptOpts.current);
13
16
  }
14
- static decryptColumn(data) {
17
+ /**
18
+ * Returns `existingCiphertext` when the current column key opens it to a
19
+ * value whose JSON serialization equals `data`'s, the plaintext
20
+ * `encryptColumn(data)` would encrypt, and otherwise `encryptColumn(data)`.
21
+ * A value only the legacy key opens, ciphertext no key opens, and a
22
+ * missing `existingCiphertext` all get fresh ciphertext. It never calls
23
+ * `onLegacyKeyUsed`.
24
+ */
25
+ static reuseOrEncryptColumn(data, existingCiphertext) {
26
+ if (data === null || data === undefined || typeof existingCiphertext !== 'string')
27
+ return this.encryptColumn(data);
28
+ const dreamApp = DreamApp.getOrFail();
29
+ const encryptOpts = dreamApp.encryption?.columns;
30
+ if (!encryptOpts)
31
+ throw new MissingColumnEncryptionOpts();
32
+ let existingPlaintext;
33
+ try {
34
+ existingPlaintext = Encrypt.decrypt(existingCiphertext, encryptOpts.current);
35
+ }
36
+ catch (err) {
37
+ if (err instanceof DecryptionError || err instanceof DecryptionParseError)
38
+ return this.encryptColumn(data);
39
+ throw err;
40
+ }
41
+ return JSON.stringify(existingPlaintext) === JSON.stringify(data)
42
+ ? existingCiphertext
43
+ : this.encryptColumn(data);
44
+ }
45
+ /**
46
+ * Decrypts `data` with the column keys, current then legacy.
47
+ * `onLegacyKeyUsed` is passed to `Encrypt.decrypt`, so it is called only
48
+ * when the legacy key opened the value.
49
+ */
50
+ static decryptColumn(data, callbacks = {}) {
15
51
  const dreamApp = DreamApp.getOrFail();
16
52
  const encryptOpts = dreamApp.encryption?.columns;
17
53
  if (!encryptOpts)
18
54
  throw new MissingColumnEncryptionOpts();
19
55
  if (data === null || data === undefined)
20
56
  return null;
21
- return this.doDecryption(data, encryptOpts.current, encryptOpts.legacy);
57
+ return this.doDecryption(data, encryptOpts.current, encryptOpts.legacy, callbacks);
58
+ }
59
+ /**
60
+ * Encrypts `data`, a column's ciphertext, again with the current column key
61
+ * (see `Encrypt.reencrypt`), opening it with the current key or, failing
62
+ * that, the legacy one. It does not call `onLegacyKeyUsed`.
63
+ */
64
+ static reencryptColumn(data) {
65
+ const dreamApp = DreamApp.getOrFail();
66
+ const encryptOpts = dreamApp.encryption?.columns;
67
+ if (!encryptOpts)
68
+ throw new MissingColumnEncryptionOpts();
69
+ if (data === null || data === undefined)
70
+ return null;
71
+ return Encrypt.reencrypt(data, encryptOpts.current, encryptOpts.legacy);
72
+ }
73
+ /**
74
+ * Whether `data` is ciphertext that the column keys (current, then legacy)
75
+ * open to a value `decryptColumn` returns. A missing column encryption
76
+ * config or key still throws, since that is a configuration error rather
77
+ * than a property of `data`.
78
+ */
79
+ static isDecryptableColumnCiphertext(data) {
80
+ const dreamApp = DreamApp.getOrFail();
81
+ const encryptOpts = dreamApp.encryption?.columns;
82
+ if (!encryptOpts)
83
+ throw new MissingColumnEncryptionOpts();
84
+ try {
85
+ this.doDecryption(data, encryptOpts.current, encryptOpts.legacy);
86
+ return true;
87
+ }
88
+ catch (err) {
89
+ if (err instanceof DecryptionError ||
90
+ err instanceof DecryptionParseError ||
91
+ err instanceof DecryptionRotationError)
92
+ return false;
93
+ throw err;
94
+ }
22
95
  }
23
96
  static doEncryption(data, encryptionOpts) {
24
97
  return Encrypt.encrypt(data, encryptionOpts);
25
98
  }
26
- static doDecryption(data, encryptionOpts, legacyEncryptionOpts) {
27
- return Encrypt.decrypt(data, encryptionOpts, legacyEncryptionOpts);
99
+ static doDecryption(data, encryptionOpts, legacyEncryptionOpts, callbacks = {}) {
100
+ return Encrypt.decrypt(data, encryptionOpts, legacyEncryptionOpts, callbacks);
28
101
  }
29
102
  }
@@ -0,0 +1,10 @@
1
+ import pascalize from '../helpers/pascalize.js';
2
+ /**
3
+ * The column that holds an `@deco.Encrypted` property's ciphertext when the
4
+ * decorator is not given one: the property's name, pascalized and prefixed
5
+ * with `encrypted`. `secretPhone`, or `secret_phone`, gives
6
+ * `encryptedSecretPhone`, which is the `encrypted_secret_phone` column.
7
+ */
8
+ export default function defaultEncryptedColumnName(propertyName) {
9
+ return `encrypted${pascalize(propertyName)}`;
10
+ }
@@ -41,14 +41,30 @@ export default class Encrypt {
41
41
  *
42
42
  * `MissingEncryptionKey` propagates from either form when a key is missing.
43
43
  *
44
+ * **`onLegacyKeyUsed`** (optional fourth argument): during a key rotation,
45
+ * tells you which values still need the legacy key. It is called with no
46
+ * arguments, synchronously, before a three-arg call returns a value that
47
+ * the legacy key opened. It is not called when the current key opens the
48
+ * value, when decryption throws, or in the two-arg form. An error it
49
+ * throws propagates from `decrypt` as is.
50
+ *
51
+ * ```ts
52
+ * const value = Encrypt.decrypt<string>(
53
+ * encrypted,
54
+ * { algorithm: 'aes-256-gcm', key: newKey },
55
+ * { algorithm: 'aes-256-gcm', key: oldKey },
56
+ * { onLegacyKeyUsed: () => console.warn('this value is still encrypted with the old key') }
57
+ * )
58
+ * ```
59
+ *
44
60
  * @throws MissingEncryptionKey
45
61
  * @throws DecryptionError
46
62
  * @throws DecryptionParseError
47
63
  * @throws DecryptionRotationError
48
64
  */
49
- static decrypt(encrypted, { algorithm, key }, legacyOpts) {
65
+ static decrypt(encrypted, { algorithm, key }, legacyOpts, { onLegacyKeyUsed } = {}) {
50
66
  if (legacyOpts)
51
- return this.attemptDecryptionWithLegacyKeys(encrypted, { algorithm, key }, legacyOpts);
67
+ return this.attemptDecryptionWithLegacyKeys(encrypted, { algorithm, key }, legacyOpts, onLegacyKeyUsed);
52
68
  if (!key)
53
69
  throw new MissingEncryptionKey();
54
70
  if ([null, undefined].includes(encrypted))
@@ -65,7 +81,31 @@ export default class Encrypt {
65
81
  }
66
82
  }
67
83
  }
68
- static attemptDecryptionWithLegacyKeys(encrypted, currentOpts, legacyOpts) {
84
+ /**
85
+ * Opens `encrypted` as {@link Encrypt.decrypt} does, with `currentOpts` and,
86
+ * when they do not open it, `legacyOpts`, and encrypts the value again with
87
+ * `currentOpts`, so that reading it no longer needs the legacy key. Use it
88
+ * during a key rotation for a value stored outside an `@deco.Encrypted`
89
+ * column, such as a cookie; `Model.reencryptEncryptedColumns()` does this
90
+ * for every value of a model's encrypted properties.
91
+ *
92
+ * ```ts
93
+ * const rewritten = Encrypt.reencrypt(
94
+ * storedValue,
95
+ * { algorithm: 'aes-256-gcm', key: newKey },
96
+ * { algorithm: 'aes-256-gcm', key: oldKey }
97
+ * )
98
+ * ```
99
+ *
100
+ * @throws MissingEncryptionKey
101
+ * @throws DecryptionError
102
+ * @throws DecryptionParseError
103
+ * @throws DecryptionRotationError
104
+ */
105
+ static reencrypt(encrypted, currentOpts, legacyOpts) {
106
+ return this.encrypt(this.decrypt(encrypted, currentOpts, legacyOpts), currentOpts);
107
+ }
108
+ static attemptDecryptionWithLegacyKeys(encrypted, currentOpts, legacyOpts, onLegacyKeyUsed) {
69
109
  let currentKeyError;
70
110
  try {
71
111
  return this.decrypt(encrypted, currentOpts);
@@ -75,14 +115,19 @@ export default class Encrypt {
75
115
  throw err;
76
116
  currentKeyError = err;
77
117
  }
118
+ let decrypted;
78
119
  try {
79
- return this.decrypt(encrypted, legacyOpts);
120
+ decrypted = this.decrypt(encrypted, legacyOpts);
80
121
  }
81
122
  catch (err) {
82
123
  if (!(err instanceof DecryptionError))
83
124
  throw err;
84
125
  throw new DecryptionRotationError(currentKeyError, err);
85
126
  }
127
+ // called outside the try above, so an error the callback throws is never
128
+ // mistaken for the legacy key failing
129
+ onLegacyKeyUsed?.();
130
+ return decrypted;
86
131
  }
87
132
  /**
88
133
  * Generates a base64-encoded random key suitable for the given algorithm.
@@ -100,11 +145,26 @@ export default class Encrypt {
100
145
  * to decrypt via `legacy` fallback.
101
146
  * 4. For cookies, wait at least the cookie `maxAge` so all in-flight
102
147
  * cookies have either expired or been re-issued under the new key. For
103
- * `@Encrypted` columns, re-encrypt every existing row under the new
104
- * key (read each row and write it back; the setter re-encrypts with
105
- * `current`).
148
+ * `@Encrypted` columns, re-encrypt every existing row under the new key
149
+ * by calling `reencryptEncryptedColumns()` on each model that has
150
+ * encrypted properties (`await User.reencryptEncryptedColumns()`), in
151
+ * each environment, from a console or a script rather than a migration.
152
+ * Run it only once every server in that environment runs with the new
153
+ * `current`: a server still on the old `current` keeps writing old-key
154
+ * values that its single pass over the table can miss. Writing old-key
155
+ * ciphertext straight into a backing column (`setAttribute`,
156
+ * `setAttributes` or `updateAttributes` given the backing column) after
157
+ * it has run also puts an old-key value back. For any other value
158
+ * encrypted with these keys, {@link Encrypt.reencrypt} rewrites it.
106
159
  * 5. Drop `legacy` from config and deploy again.
107
160
  *
161
+ * To see which values still need the old key while `legacy` is
162
+ * configured, register `dreamApp.on('encryption:legacy-key-used', ...)`,
163
+ * which fires each time reading an `@Encrypted` property needed `legacy`
164
+ * to open the value, or pass `onLegacyKeyUsed` to {@link Encrypt.decrypt}
165
+ * for values you decrypt yourself. A value nobody reads never fires
166
+ * either, so silence alone does not show that `legacy` is safe to drop.
167
+ *
108
168
  * ## When to rotate
109
169
  *
110
170
  * - On a scheduled cadence (90–180 days is a reasonable policy default).
@@ -116,8 +176,15 @@ export default class Encrypt {
116
176
  * - For cookies: at least the cookie `maxAge`, so in-flight sessions are
117
177
  * not forced to re-authenticate.
118
178
  * - For `@Encrypted` columns: until every existing row has been
119
- * re-encrypted under the new key. Dropping `legacy` early will cause
120
- * `DecryptionRotationError` on any not-yet-rewritten row.
179
+ * re-encrypted under the new key. Once `legacy` is dropped, reading the
180
+ * `@Encrypted` property of a row that is still under the old key throws
181
+ * `DecryptionError`, the same error a tampered or corrupted value
182
+ * throws, so the error class alone cannot show that `legacy` was
183
+ * dropped too early. `DecryptionRotationError` is thrown only while
184
+ * `legacy` is configured and neither key opens the value. Neither class
185
+ * extends the other, so code that catches `DecryptionRotationError`
186
+ * during a rotation must also catch `DecryptionError` to handle such a
187
+ * read once `legacy` is dropped.
121
188
  */
122
189
  static generateKey(algorithm) {
123
190
  switch (algorithm) {
@@ -13,6 +13,10 @@ export default class DoNotSetEncryptedFieldsDirectly extends Error {
13
13
  Do not set @Encrypted columns directly. Instead, set their accessors, so that
14
14
  those fields can be encrypted by Dream internally.
15
15
 
16
+ Writes that bypass setters, such as setAttribute, setAttributes and
17
+ updateAttributes, accept a value for an @Encrypted column only when it is
18
+ ciphertext that decrypts with this app's encryption keys, or null.
19
+
16
20
  Dream class: ${this.dreamClass.sanitizedName}
17
21
  Problematic setter: ${this.encryptedColumnName}
18
22
  Setter to be used instead: ${this.encryptedProperty}`;
@@ -0,0 +1,25 @@
1
+ export default class UnrecognizedAssociationConditionKeys extends Error {
2
+ associationName;
3
+ unrecognizedKeys;
4
+ acceptedKeys;
5
+ methodName;
6
+ constructor(associationName, unrecognizedKeys, acceptedKeys, methodName) {
7
+ super();
8
+ this.associationName = associationName;
9
+ this.unrecognizedKeys = unrecognizedKeys;
10
+ this.acceptedKeys = acceptedKeys;
11
+ this.methodName = methodName;
12
+ }
13
+ get message() {
14
+ const source = this.methodName
15
+ ? `in the object passed to ${this.methodName}('${this.associationName}')`
16
+ : `in the condition on the "${this.associationName}" association`;
17
+ return `
18
+ Unrecognized key${this.unrecognizedKeys.length === 1 ? '' : 's'} ${source}: ${this.unrecognizedKeys.join(', ')}
19
+ Accepted keys: ${this.acceptedKeys.join(', ')}
20
+
21
+ Where-clauses on an association go under \`and\`, \`andNot\` or \`andAny\`, e.g.:
22
+ { and: { email: 'how@yadoin' } }
23
+ `;
24
+ }
25
+ }
@@ -34,6 +34,22 @@ export default class Env {
34
34
  const parsedVal = parseInt(val, 10);
35
35
  return (parsedVal.toString() === val ? parsedVal : undefined);
36
36
  }
37
+ /**
38
+ * Reads a boolean environment variable. Only the exact string `"1"` is
39
+ * true. Every other value is false, including `"true"`, `"TRUE"`, `"yes"`
40
+ * and `""`, and so is an unset variable.
41
+ *
42
+ * ```ts
43
+ * // FEATURE_FLAG=1
44
+ * AppEnv.boolean('FEATURE_FLAG') // true
45
+ *
46
+ * // FEATURE_FLAG=true
47
+ * AppEnv.boolean('FEATURE_FLAG') // false
48
+ * ```
49
+ *
50
+ * @param env - the name of the environment variable
51
+ * @returns `true` when the variable is exactly `"1"`, otherwise `false`
52
+ */
37
53
  boolean(env) {
38
54
  return this.optional(env) === '1';
39
55
  }
@@ -51,9 +67,21 @@ export default class Env {
51
67
  unsetInteger(env) {
52
68
  delete process.env[env];
53
69
  }
70
+ /**
71
+ * Sets a boolean environment variable to `"1"`, the only value
72
+ * {@link Env.boolean} reads as true.
73
+ *
74
+ * @param env - the name of the environment variable
75
+ */
54
76
  setBoolean(env) {
55
77
  process.env[env] = '1';
56
78
  }
79
+ /**
80
+ * Deletes a boolean environment variable. {@link Env.boolean} reads an
81
+ * unset variable as false, since only the exact string `"1"` is true.
82
+ *
83
+ * @param env - the name of the environment variable
84
+ */
57
85
  unsetBoolean(env) {
58
86
  delete process.env[env];
59
87
  }
@@ -1,5 +1,6 @@
1
1
  import pluralize from 'pluralize-esm';
2
2
  import serializerGlobalNameFromFullyQualifiedModelName from '../../serializer/helpers/serializerGlobalNameFromFullyQualifiedModelName.js';
3
+ import defaultEncryptedColumnName from '../../encrypt/defaultEncryptedColumnName.js';
3
4
  import camelize from '../camelize.js';
4
5
  import globalClassNameFromFullyQualifiedModelName from '../globalClassNameFromFullyQualifiedModelName.js';
5
6
  import pascalize from '../pascalize.js';
@@ -189,7 +190,7 @@ public ${associationForeignKey}: DreamColumn<${modelClassName}, '${associationNa
189
190
  };
190
191
  }
191
192
  export function createEncryptedAttribute(attributeName, attribute, modelClassName) {
192
- const encryptedColumnName = camelize(`encrypted_${attributeName}`);
193
+ const encryptedColumnName = defaultEncryptedColumnName(attributeName);
193
194
  const content = `
194
195
  @deco.Encrypted()
195
196
  public ${camelize(attributeName)}: DreamColumn<${modelClassName}, '${encryptedColumnName}'>`;
@@ -46,7 +46,7 @@ export default function generateFactoryContent({ fullyQualifiedModelName, column
46
46
  switch (safeAttributeType) {
47
47
  case 'belongsto': {
48
48
  // When `Model@alias:belongs_to`, the factory property uses the alias;
49
- // otherwise it uses the model's last namespace segment (legacy form).
49
+ // otherwise it uses the model's last namespace segment.
50
50
  const attributeVariable = aliasName ? camelize(aliasName) : camelize(attributeName.split('/').pop());
51
51
  const fullyQualifiedAssociatedModelName = standardizeFullyQualifiedModelName(attributeName);
52
52
  const associationModelName = globalClassNameFromFullyQualifiedModelName(fullyQualifiedAssociatedModelName);
@@ -4,6 +4,7 @@ import Query from '../../dream/Query.js';
4
4
  import InvalidDecimalFieldPassedToGenerator from '../../errors/InvalidDecimalFieldPassedToGenerator.js';
5
5
  import NoColumnsToAlterMigration from '../../errors/NoColumnsToAlterMigration.js';
6
6
  import UnparseableMigrationColumn from '../../errors/UnparseableMigrationColumn.js';
7
+ import defaultEncryptedColumnName from '../../encrypt/defaultEncryptedColumnName.js';
7
8
  import camelize from '../camelize.js';
8
9
  import compact from '../compact.js';
9
10
  import globalClassNameFromFullyQualifiedModelName from '../globalClassNameFromFullyQualifiedModelName.js';
@@ -141,7 +142,7 @@ export default function generateMigrationContent({ connectionName = 'default', t
141
142
  columnDefs.push(generateBooleanStr(attributeName, { omitInlineNonNull }));
142
143
  break;
143
144
  case 'encrypted':
144
- attributeName = `encrypted_${attributeName}`;
145
+ attributeName = snakeify(defaultEncryptedColumnName(attributeName));
145
146
  columnDefs.push(generateColumnStr(attributeName, 'text', descriptors, {
146
147
  omitInlineNonNull,
147
148
  skipUniqueHeuristic: true,
@@ -183,13 +184,12 @@ export default function generateMigrationContent({ connectionName = 'default', t
183
184
  // time rather than generation time. Fail loudly here instead.
184
185
  //
185
186
  // This guard is scoped to the standalone `g:migration` `-to-`/`-from-` flow
186
- // only (`stiChildClassName` is unset there). `g:sti-child` legitimately
187
- // calls this function in alter mode with zero `columnsWithTypes` — a
188
- // zero-attribute STI child is a documented use case (see `g:sti-child`'s
189
- // help example in src/cli/index.ts), and `generateStiMigrationContent`
190
- // handles the type-column/check-constraint machinery separately from the
191
- // `columnDefs` this check inspects, so an empty `columnDefs` there is
192
- // expected, not a sign of a mistyped/unparseable column list.
187
+ // only (`stiChildClassName` is unset there); STI child migrations skip it.
188
+ // `g:sti-child` writes no migration for a child declared with no columns
189
+ // (see `generateDream`), so an STI child migration reaches this point with
190
+ // an empty `columnDefs` only when none of its declarations produced a
191
+ // column (e.g. only `has_one`/`has_many` ones), and it then emits the bare
192
+ // `.alterTable(...).execute()` described above.
193
193
  if (altering &&
194
194
  columnDefs.length === 0 &&
195
195
  table !== MIGRATION_TABLE_NAME_PLACEHOLDER &&
@@ -472,7 +472,7 @@ function generateBelongsToStr(connectionName, associationName, { primaryKeyType,
472
472
  const dataType = dbDriverClass.foreignKeyTypeFromPrimaryKey(primaryKeyType);
473
473
  const references = lookupReferencesTable(associationName, originalAssociationName);
474
474
  // When the user passed `Model@alias:belongs_to`, the column name comes from
475
- // the alias; otherwise it's the model's last segment (existing behavior).
475
+ // the alias; otherwise it's the model's last segment.
476
476
  const columnNameSource = aliasName ? snakeify(aliasName) : associationName.split('/').pop();
477
477
  return `.addColumn('${associationNameToForeignKey(columnNameSource)}', '${dataType}', col => col.references('${references}.id').onDelete('restrict')${optional ? '' : '.notNull()'})`;
478
478
  }
@@ -40,6 +40,16 @@ export default function generateSerializerContent({ fullyQualifiedModelName, col
40
40
  ? `import { ${uniq(dreamImports).join(', ')} } from '@rvoh/dream'\n`
41
41
  : '';
42
42
  const additionalImportsStr = uniq(additionalImports).join('');
43
+ // An STI base renders the `type` discriminator in its summary serializers,
44
+ // so each default serializer, and each STI child serializer, inherits it.
45
+ const rendersStiType = stiBaseSerializer &&
46
+ columnsWithTypes.some(attr => {
47
+ const [name, type] = attr.split(':');
48
+ return name === 'type' && !isAssociationType(type);
49
+ });
50
+ const stiTypeAttribute = rendersStiType
51
+ ? `\n .attribute('type', { openapi: { type: 'string', enum: [(StiChildClass ?? ${modelClassName}).sanitizedName] } })`
52
+ : '';
43
53
  const buildSerializerPair = (variantSuffix) => {
44
54
  const summaryName = variantSerializerClassName(localSerializerBase, variantSuffix, 'summary');
45
55
  const defaultName = variantSerializerClassName(localSerializerBase, variantSuffix, 'default');
@@ -50,15 +60,17 @@ export default function generateSerializerContent({ fullyQualifiedModelName, col
50
60
  ? `${variantSerializerClassName(parentSerializerBase, variantSuffix, 'default')}(${modelClassName}, ${modelSerializerArgs})`
51
61
  : `${summaryName}(${stiBaseSerializer ? 'StiChildClass, ' : ''}${modelSerializerArgs})`;
52
62
  const summary = `export const ${summaryName} = ${modelSerializerSignature} =>
53
- ${summaryExtends}${isSTI ? '' : `\n .attribute('id')`}`;
63
+ ${summaryExtends}${isSTI ? '' : `\n .attribute('id')`}${stiTypeAttribute}`;
54
64
  const defaultBody = columnsWithTypes
55
65
  .map(attr => {
56
66
  const [name, type] = attr.split(':');
57
67
  if (name === undefined)
58
68
  return '';
59
- if (['belongsto', 'hasone', 'hasmany'].includes(camelize(type)?.toLowerCase()))
69
+ if (isAssociationType(type))
60
70
  return '';
61
- return `\n ${attribute(modelClassName, name, type, attr, stiBaseSerializer)}`;
71
+ if (rendersStiType && name === 'type')
72
+ return '';
73
+ return `\n ${attribute(name, type, attr)}`;
62
74
  })
63
75
  .join('');
64
76
  const def = `export const ${defaultName} = ${modelSerializerSignature} =>
@@ -75,16 +87,17 @@ function variantSerializerClassName(serializerBase, variantSuffix, kind) {
75
87
  ? `${serializerBase}${variantSuffix}SummarySerializer`
76
88
  : `${serializerBase}${variantSuffix}Serializer`;
77
89
  }
78
- function attribute(modelClassName, name, type, attr, stiBaseSerializer) {
79
- if (name === 'type' && stiBaseSerializer) {
80
- return `.attribute('type', { openapi: { type: 'string', enum: [(StiChildClass ?? ${modelClassName}).sanitizedName] } })`;
81
- }
90
+ function isAssociationType(type) {
91
+ return ['belongsto', 'hasone', 'hasmany'].includes(camelize(type)?.toLowerCase());
92
+ }
93
+ function attribute(name, type, attr) {
82
94
  switch (type) {
83
95
  case 'json':
84
96
  case 'jsonb':
97
+ return `.attribute('${camelize(name)}', { openapi: { type: 'object', properties: { } } })`;
85
98
  case 'json[]':
86
99
  case 'jsonb[]':
87
- return `.attribute('${camelize(name)}', { openapi: { type: 'object', properties: { } } })`;
100
+ return `.attribute('${camelize(name)}', { openapi: { type: 'array', items: { type: 'object', properties: { } } } })`;
88
101
  default:
89
102
  return `.attribute('${camelize(name)}'${attributeOptionsSpecifier(type, attr)})`;
90
103
  }
@@ -1,3 +1,4 @@
1
+ import Dream from '../Dream.js';
1
2
  import RendersManyMustReceiveArray from '../errors/serializers/RendersManyMustReceiveArray.js';
2
3
  import compact from '../helpers/compact.js';
3
4
  import round from '../helpers/round.js';
@@ -8,7 +9,7 @@ import { DateTime } from '../utils/datetime/DateTime.js';
8
9
  import DreamSerializerBuilder from './builders/DreamSerializerBuilder.js';
9
10
  import ObjectSerializerBuilder from './builders/ObjectSerializerBuilder.js';
10
11
  import inferSerializerFromDreamOrViewModel from './helpers/inferSerializerFromDreamOrViewModel.js';
11
- import { serializerForAssociatedClass } from './helpers/serializerForAssociatedClass.js';
12
+ import { serializersForAssociatedClass } from './helpers/serializersForAssociatedClass.js';
12
13
  export default class SerializerRenderer {
13
14
  serializerBuilder;
14
15
  passthroughData;
@@ -95,19 +96,16 @@ export default class SerializerRenderer {
95
96
  case 'rendersOne': {
96
97
  const outputAttributeName = this.setCase(attribute.options.as ?? attribute.name);
97
98
  const associatedObject = data[attribute.name];
98
- let serializer = null;
99
- if (associatedObject) {
100
- serializer = serializerForAssociatedObject(associatedObject, attribute.options, renderTimeResolutionContext(attribute.options, 'rendersOne', attribute.name));
101
- }
102
- else if (attribute.options.flatten) {
103
- /**
104
- * Only used when flatten: true, and the associated model is null, in which case,
105
- * we need something to determine the keys that will be flattened into the
106
- * rendering serializer
107
- */
108
- serializer = serializerForAssociatedClass(data, attribute.name, attribute.options);
99
+ if (!associatedObject && attribute.options.flatten) {
100
+ return {
101
+ ...accumulator,
102
+ ...this.nullFlattenedAttributes(serializersForAssociatedClass(data instanceof Dream ? data.constructor : null, attribute.name, attribute.options), passthroughData),
103
+ };
109
104
  }
110
- const serializerBuilder = serializer?.(attribute.options.flatten ? (associatedObject ?? {}) : associatedObject,
105
+ const serializer = associatedObject
106
+ ? serializerForAssociatedObject(associatedObject, attribute.options, renderTimeResolutionContext(attribute.options, 'rendersOne', attribute.name))
107
+ : null;
108
+ const serializerBuilder = serializer?.(associatedObject,
111
109
  // passthrough data going into the serializer is the argument that gets
112
110
  // used in the custom attribute callback function
113
111
  passthroughData);
@@ -178,6 +176,60 @@ export default class SerializerRenderer {
178
176
  }, renderedAttributes);
179
177
  return renderedAttributes;
180
178
  }
179
+ /**
180
+ * The keys a flattened `rendersOne` adds to the parent's payload when its associated object is
181
+ * null: each key any of `serializers` declares, set to `null`. There are several serializers when
182
+ * the association is a polymorphic BelongsTo, one for each target class, or when the associated
183
+ * class is an STI base, one for each child, and their keys are unioned.
184
+ *
185
+ * Each serializer is built with `undefined` data to read its declarations, as Psychic's OpenAPI
186
+ * generation and `preloadFor` build it, and nothing is rendered: no attribute callback runs and
187
+ * no association of the missing object is read. A flattened `rendersOne` it declares adds its
188
+ * own serializers' keys the same way. A flattened `customAttribute` adds none, since only its
189
+ * callback knows its keys.
190
+ */
191
+ nullFlattenedAttributes(serializers, passthroughData, serializersBeingFlattened = new Set()) {
192
+ return serializers.reduce((flattenedKeys, serializer) => {
193
+ // a serializer reached again, e.g. one that flattens itself directly or through others, or
194
+ // one that two polymorphic targets or two STI children both flatten, adds no keys the first
195
+ // walk through it did not
196
+ if (serializersBeingFlattened.has(serializer))
197
+ return flattenedKeys;
198
+ serializersBeingFlattened.add(serializer);
199
+ const serializerBuilder = serializer(undefined, passthroughData);
200
+ const dreamClass = serializerBuilder instanceof DreamSerializerBuilder
201
+ ? serializerBuilder['$typeForOpenapi']
202
+ : null;
203
+ return serializerBuilder['attributes'].reduce((keys, attribute) => {
204
+ const attributeType = attribute.type;
205
+ switch (attributeType) {
206
+ case 'attribute':
207
+ case 'delegatedAttribute':
208
+ case 'rendersMany':
209
+ keys[this.setCase(attribute.options?.as ?? attribute.name)] = null;
210
+ return keys;
211
+ case 'customAttribute':
212
+ if (!attribute.options.flatten)
213
+ keys[this.setCase(attribute.name)] = null;
214
+ return keys;
215
+ case 'rendersOne':
216
+ if (!attribute.options.flatten) {
217
+ keys[this.setCase(attribute.options.as ?? attribute.name)] = null;
218
+ return keys;
219
+ }
220
+ return {
221
+ ...keys,
222
+ ...this.nullFlattenedAttributes(serializersForAssociatedClass(dreamClass, attribute.name, attribute.options), passthroughData, serializersBeingFlattened),
223
+ };
224
+ default: {
225
+ // protection so that if a new ValidationType is ever added, this will throw a type error at build time
226
+ const _never = attributeType;
227
+ throw new Error(`Unhandled serializer attribute type: ${_never}`);
228
+ }
229
+ }
230
+ }, flattenedKeys);
231
+ }, {});
232
+ }
181
233
  setCase(attr) {
182
234
  switch (this.renderOpts.casing) {
183
235
  case 'camel':