@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
@@ -68,44 +68,56 @@ function buildColumnsWithTypesDescription(belongsToDescription) {
68
68
  return `${baseColumnsWithTypesDescription}
69
69
  ${belongsToDescription}`;
70
70
  }
71
- const columnsWithTypesDescription = buildColumnsWithTypesDescription(`
71
+ /**
72
+ * The `[columnsWithTypes...]` argument description for `g:model`. Exported so
73
+ * that spec/unit/cli/generateMigrationContent.spec.ts can run each
74
+ * `:belongs_to` example in this text through the migration generator and check
75
+ * the FK column the example's comment names: an example that drifts from what
76
+ * the generator actually produces then fails the suite instead of shipping.
77
+ */
78
+ export const columnsWithTypesDescription = buildColumnsWithTypesDescription(`
72
79
  ${INDENT}
73
80
  ${INDENT} - belongs_to:
74
81
  ${INDENT} ALWAYS use this instead of adding a raw uuid column for foreign keys. It creates the FK column, adds a database index,
75
82
  ${INDENT} AND generates the @deco.BelongsTo association and typed property on the model. A raw uuid column does none of this.
76
83
  ${INDENT}
77
- ${INDENT} use the fully qualified model name (matching its path under src/app/models/):
84
+ ${INDENT} use the fully qualified model name (matching its path under src/app/models/). The last segment of that
85
+ ${INDENT} path names the FK column, the typed FK property and the association:
78
86
  ${INDENT} User:belongs_to # creates user_id column + BelongsTo association
79
- ${INDENT} Health/Coach:belongs_to # creates health_coach_id column + BelongsTo association
87
+ ${INDENT} Health/Coach:belongs_to # creates coach_id column, coachId property, coach association
80
88
  ${INDENT} User:belongs_to:optional # nullable foreign key (for optional associations)
81
89
  ${INDENT}
82
- ${INDENT} rename the association with Model@alias — the snake_case alias drives the FK column name AND the
83
- ${INDENT} @deco.BelongsTo association + typed FK property on the generated model:
90
+ ${INDENT} rename the association with Model@alias — the snake_case alias takes the last segment's place, naming the
91
+ ${INDENT} FK column <alias>_id AND the @deco.BelongsTo association + typed FK property on the generated model:
84
92
  ${INDENT} InternalUser@canceled_by:belongs_to:optional # canceled_by_id column, canceledById property, canceledBy association,
85
93
  ${INDENT} # @deco.BelongsTo('InternalUser', { on: 'canceledById', optional: true })
86
- ${INDENT} Messaging/Template@template:belongs_to # template_id column, templateId property, template association
87
- ${INDENT} # (strips the namespace from the property/association names while keeping
88
- ${INDENT} # the namespaced model reference intact)
94
+ ${INDENT} Health/Coach@health_coach:belongs_to # health_coach_id column, healthCoachId property, healthCoach association,
95
+ ${INDENT} # @deco.BelongsTo('Health/Coach', { on: 'healthCoachId' })
89
96
  ${INDENT} Aliasing also lets you declare multiple FKs to the same model in one generator call without column collisions.`);
90
97
  export const columnsWithTypesDescriptionForStiChild = buildColumnsWithTypesDescription(`
91
98
  ${INDENT}
92
99
  ${INDENT} - belongs_to:
93
100
  ${INDENT} NOT supported for STI children. STI children cannot declare associations; declare all BelongsTo,
94
101
  ${INDENT} HasOne, and HasMany associations on the STI parent model instead.`);
95
- const columnsWithTypesDescriptionForMigration = buildColumnsWithTypesDescription(`
102
+ /**
103
+ * The `[columnsWithTypes...]` argument description for `g:migration`. Exported
104
+ * for the same `:belongs_to` example check as `columnsWithTypesDescription`.
105
+ */
106
+ export const columnsWithTypesDescriptionForMigration = buildColumnsWithTypesDescription(`
96
107
  ${INDENT}
97
108
  ${INDENT} - belongs_to:
98
109
  ${INDENT} ALWAYS use this instead of adding a raw uuid column for foreign keys. It creates the FK column with an index.
99
110
  ${INDENT} Unlike in g:model/g:resource, this does NOT add a BelongsTo association (no model is generated).
100
111
  ${INDENT}
101
- ${INDENT} use the fully qualified model name (matching its path under src/app/models/):
112
+ ${INDENT} use the fully qualified model name (matching its path under src/app/models/). The last segment of that
113
+ ${INDENT} path names the FK column:
102
114
  ${INDENT} User:belongs_to # creates user_id column with index
103
- ${INDENT} Health/Coach:belongs_to # creates health_coach_id column with index
115
+ ${INDENT} Health/Coach:belongs_to # creates coach_id column with index
104
116
  ${INDENT} User:belongs_to:optional # nullable foreign key
105
117
  ${INDENT}
106
- ${INDENT} rename the FK column with Model@alias (snake_case alias becomes the column name):
118
+ ${INDENT} rename the FK column with Model@alias (the column is <alias>_id, with the alias snake_cased):
107
119
  ${INDENT} InternalUser@canceled_by:belongs_to:optional # canceled_by_id column with index
108
- ${INDENT} Messaging/Template@template:belongs_to # template_id column (strips the namespace from the column name)`);
120
+ ${INDENT} Health/Coach@health_coach:belongs_to # health_coach_id column with index`);
109
121
  /**
110
122
  * The `<migrationName>` argument description for `g:migration`. Exported so
111
123
  * that spec/unit/helpers/cli/generateMigration.spec.ts can read the example
@@ -217,7 +229,7 @@ ${INDENT} # STI parent model (use with g:sti-child for children)
217
229
  ${INDENT} pnpm psy g:model --sti-base-serializer Room Place:belongs_to type:enum:room_types:Bathroom,Bedroom deleted_at:datetime:optional`)
218
230
  .option('--no-serializer', 'skip serializer generation. Useful for internal models that will never be serialized in an API response (e.g., join tables, audit logs)')
219
231
  .option('--connection-name <connectionName>', 'the name of the database connection to use for the model. Only needed for multi-database setups; defaults to "default"', 'default')
220
- .option('--sti-base-serializer', `Creates generically typed base serializers (default and summary) that accept a \`StiChildClass\` parameter and include the \`type\` attribute with a per-child enum constraint. This allows consuming applications to determine the response shape based on the STI type discriminator.
232
+ .option('--sti-base-serializer', `Creates generically typed base serializers (summary and default, plus each Admin and Internal pair requested with --admin-serializers or --internal-serializers) that accept a \`StiChildClass\` parameter. When a \`type\` column is given, each summary serializer renders \`type\` right after \`id\`, with an OpenAPI enum of just the STI child's class name, and the default serializer inherits it from the summary. This allows consuming applications to determine the response shape based on the STI type discriminator.
221
233
  ${INDENT}
222
234
  ${INDENT}Use this when generating the parent model of an STI hierarchy. After generating the parent, use g:sti-child for each child type.
223
235
  ${INDENT}
@@ -255,9 +267,9 @@ ${INDENT} Settings/CommunicationPreferences # src/app/models/Settings/Communi
255
267
  program
256
268
  .command('generate:sti-child')
257
269
  .alias('g:sti-child')
258
- .description(`Generates an STI (Single Table Inheritance) child model that extends an existing parent model. The child shares the parent's database table (discriminated by the \`type\` column) and can add child-specific columns. Generates a child model decorated with @STI(Parent), child serializers extending the parent's base serializers, a migration that ALTERs the parent table (not a new table), check constraints, a factory, and spec skeleton.
270
+ .description(`Generates an STI (Single Table Inheritance) child model that extends an existing parent model. The child shares the parent's database table (discriminated by the \`type\` column) and can add child-specific columns. Always generates the child model decorated with @STI(Parent), a factory, and a spec skeleton; unless --no-serializer is passed, also generates child serializers extending the parent's base serializers.
259
271
  ${INDENT}
260
- ${INDENT}If the child declares no additional columns, only the model file is generated — no migration is created. STI children share the parent's table, so a no-columns child requires no schema change. Add a migration only by passing positional field:type args, in which case the generator emits one with the appropriate check constraint. STI children cannot declare associations — declare them on the parent model instead. The generator rejects belongs_to columns. STI children never receive @SoftDelete() — soft delete is enforced at the parent level only — and the generator does not accept --no-soft-delete.
272
+ ${INDENT}A migration is generated only when positional field:type args are passed. It ALTERs the parent's table (it does not create a new one), adds a check constraint requiring a value on this child's rows for each column that is not :optional, an array or a boolean, and goes in the migrations folder of the parent model's database connection, which the child shares. A child with no additional columns needs no schema change, so no migration is generated for it. STI children cannot declare associations — declare them on the parent model instead. The generator rejects belongs_to columns. STI children never receive @SoftDelete() — soft delete is enforced at the parent level only — and the generator does not accept --no-soft-delete.
261
273
  ${INDENT}
262
274
  ${INDENT}The parent must already exist (typically generated with g:model --sti-base-serializer or g:resource --sti-base-serializer).
263
275
  ${INDENT}
@@ -271,7 +283,6 @@ ${INDENT}
271
283
  ${INDENT} # Child with no additional columns
272
284
  ${INDENT} pnpm psy g:sti-child --model-name=Kitchen Room/Kitchen extends Room`)
273
285
  .option('--no-serializer', 'skip serializer generation. Useful if the child uses the parent serializer directly or serialization is handled elsewhere')
274
- .option('--connection-name', 'the name of the database connection to use for the model. Only needed for multi-database setups; defaults to "default"', 'default')
275
286
  .option('--model-name <modelName>', `Explicit model class name to use instead of the one auto-derived from the model path. Useful when the path segments don't match the desired class name.
276
287
  ${INDENT}
277
288
  ${INDENT}Example:
@@ -1,6 +1,8 @@
1
1
  import { sql } from 'kysely';
2
+ import defaultEncryptedColumnName from '../../encrypt/defaultEncryptedColumnName.js';
2
3
  import InternalEncrypt from '../../encrypt/InternalEncrypt.js';
3
4
  import DropEnumValueRetypeFailed, { quotedRelationName, } from '../../errors/DropEnumValueRetypeFailed.js';
5
+ import snakeify from '../../helpers/snakeify.js';
4
6
  export default class DreamMigrationHelpers {
5
7
  /**
6
8
  * Rename a table and its associated primary key index and sequence.
@@ -49,8 +51,8 @@ export default class DreamMigrationHelpers {
49
51
  await this.dropConstraint(db, constraintName, { table });
50
52
  await sql `
51
53
  ALTER TABLE ${sql.table(table)}
52
- ADD CONSTRAINT ${sql.table(constraintName)}
53
- UNIQUE (${sql.raw(columns.join(', '))})
54
+ ADD CONSTRAINT ${sql.ref(constraintName)}
55
+ UNIQUE (${sql.join(columns.map(column => sql.ref(column)))})
54
56
  DEFERRABLE INITIALLY DEFERRED;
55
57
  `.execute(db);
56
58
  }
@@ -65,7 +67,7 @@ export default class DreamMigrationHelpers {
65
67
  * @param __namedParameters.value - The name of the value to add to the enum
66
68
  */
67
69
  static async addEnumValue(db, { enumName, value }) {
68
- await sql `ALTER TYPE ${sql.raw(enumName)} ADD VALUE IF NOT EXISTS '${sql.raw(value)}';`.execute(db);
70
+ await sql `ALTER TYPE ${sql.ref(enumName)} ADD VALUE IF NOT EXISTS ${sql.lit(value)};`.execute(db);
69
71
  }
70
72
  /**
71
73
  * Create a database extension.
@@ -104,7 +106,7 @@ export default class DreamMigrationHelpers {
104
106
  */
105
107
  static async createGinIndex(db, indexName, { table, column }) {
106
108
  await sql `
107
- CREATE INDEX IF NOT EXISTS ${sql.raw(indexName)} ON ${sql.raw(table)} USING GIN (${sql.raw(`${column} gin_trgm_ops`)});
109
+ CREATE INDEX IF NOT EXISTS ${sql.ref(indexName)} ON ${sql.table(table)} USING GIN (${sql.ref(column)} gin_trgm_ops);
108
110
  `.execute(db);
109
111
  }
110
112
  /**
@@ -120,7 +122,7 @@ export default class DreamMigrationHelpers {
120
122
  */
121
123
  static async dropConstraint(db, constraintName, { table }) {
122
124
  await sql `
123
- ALTER TABLE ${sql.table(table)} DROP CONSTRAINT IF EXISTS ${sql.table(constraintName)};
125
+ ALTER TABLE ${sql.table(table)} DROP CONSTRAINT IF EXISTS ${sql.ref(constraintName)};
124
126
  `.execute(db);
125
127
  }
126
128
  /**
@@ -262,11 +264,11 @@ export default class DreamMigrationHelpers {
262
264
  * @param options - Configuration options
263
265
  * @param options.table - The name of the table
264
266
  * @param options.column - The current (plaintext) column name
265
- * @param options.encryptedColumnName - The target encrypted column name. Defaults to `encrypted_<column>`, matching the `@Encrypted` decorator's default; pass this when the decorator was given a custom encrypted column name.
267
+ * @param options.encryptedColumnName - The target encrypted column name. Defaults to the column the `@Encrypted` decorator reads for a property named after `column` (`encrypted_phone` for `phone`); pass this when the decorator was given a custom encrypted column name.
266
268
  * @param options.primaryKey - The primary key column used to keyset-paginate and target each row's update. Defaults to `id`.
267
269
  * @param options.batchSize - How many rows to read per batch. Defaults to `1000`.
268
270
  */
269
- static async encryptColumn(db, { table, column, encryptedColumnName = `encrypted_${column}`, primaryKey = 'id', batchSize = 1000, }) {
271
+ static async encryptColumn(db, { table, column, encryptedColumnName = snakeify(defaultEncryptedColumnName(column)), primaryKey = 'id', batchSize = 1000, }) {
270
272
  await db.schema.alterTable(table).renameColumn(column, encryptedColumnName).execute();
271
273
  await db.schema
272
274
  .alterTable(table)
@@ -301,12 +303,12 @@ export default class DreamMigrationHelpers {
301
303
  * @param options - Configuration options
302
304
  * @param options.table - The name of the table
303
305
  * @param options.column - The target (plaintext) column name to rename back to
304
- * @param options.encryptedColumnName - The current encrypted column name. Defaults to `encrypted_<column>`.
306
+ * @param options.encryptedColumnName - The current encrypted column name. Defaults to the column the `@Encrypted` decorator reads for a property named after `column` (`encrypted_phone` for `phone`).
305
307
  * @param options.primaryKey - The primary key column used to keyset-paginate and target each row's update. Defaults to `id`.
306
308
  * @param options.batchSize - How many rows to read per batch. Defaults to `1000`.
307
309
  * @param options.columnType - When provided, the restored column is converted to this type. When omitted, the column is left as `text`.
308
310
  */
309
- static async decryptColumn(db, { table, column, encryptedColumnName = `encrypted_${column}`, primaryKey = 'id', batchSize = 1000, columnType, }) {
311
+ static async decryptColumn(db, { table, column, encryptedColumnName = snakeify(defaultEncryptedColumnName(column)), primaryKey = 'id', batchSize = 1000, columnType, }) {
310
312
  await this.transformColumnInBatches(db, { table, column: encryptedColumnName, primaryKey, batchSize }, value => InternalEncrypt.decryptColumn(value));
311
313
  await db.schema.alterTable(table).renameColumn(encryptedColumnName, column).execute();
312
314
  if (columnType !== undefined) {
@@ -452,7 +454,7 @@ function constraintContextFor(byPosition, position) {
452
454
  return byPosition[position] ?? { relationResolved: false, checkConstraints: [] };
453
455
  }
454
456
  async function getEnumValues(db, enumName) {
455
- const response = await sql `SELECT unnest(enum_range(NULL::${sql.raw(enumName)}))`.execute(db);
457
+ const response = await sql `SELECT unnest(enum_range(NULL::${sql.ref(enumName)}))`.execute(db);
456
458
  return response.rows.map(row => row.unnest);
457
459
  }
458
460
  // finds any records in the specified table
@@ -466,10 +468,10 @@ async function replaceArrayValues(db, value, tableAndColumnToChange) {
466
468
  .updateTable(table)
467
469
  .set({
468
470
  [column]: tableAndColumnToChange.behavior === 'remove'
469
- ? sql.raw(`array_remove(${column}, '${value}')`)
470
- : sql.raw(`array_replace(${column}, '${value}', '${tableAndColumnToChange.replaceWith}')`),
471
+ ? sql `array_remove(${sql.ref(column)}, ${value})`
472
+ : sql `array_replace(${sql.ref(column)}, ${value}, ${tableAndColumnToChange.replaceWith})`,
471
473
  })
472
- .where(sql.raw(`'${value}'`), '=', sql.raw(`ANY(${column})`))
474
+ .where(sql `${value} = ANY(${sql.ref(column)})`)
473
475
  .execute();
474
476
  }
475
477
  // finds any records in the specified table
@@ -489,20 +491,20 @@ async function replaceNonArrayValues(db, value, tableAndColumnToChange) {
489
491
  async function updateTableColumnToNewEnumArrayType(db, enumName, tableAndColumnToChange) {
490
492
  const { table, column } = tableAndColumnToChange;
491
493
  await sql `
492
- ALTER TABLE ${sql.raw(table)}
493
- ALTER ${sql.raw(column)}
494
- TYPE ${sql.raw(enumName)}[]
495
- USING ${sql.raw(column)}::${sql.raw(enumName)}[];
494
+ ALTER TABLE ${sql.table(table)}
495
+ ALTER COLUMN ${sql.ref(column)}
496
+ TYPE ${sql.ref(enumName)}[]
497
+ USING ${sql.ref(column)}::${sql.ref(enumName)}[];
496
498
  `.execute(db);
497
499
  }
498
500
  // updates the table's targeted column to the newly-created enum
499
501
  async function updateTableColumnToNewEnumType(db, enumName, tableAndColumnToChange) {
500
502
  const { table, column } = tableAndColumnToChange;
501
503
  await sql `
502
- ALTER TABLE ${sql.raw(table)}
503
- ALTER ${sql.raw(column)}
504
- TYPE ${sql.raw(enumName)}
505
- USING ${sql.raw(column)}::${sql.raw(enumName)};
504
+ ALTER TABLE ${sql.table(table)}
505
+ ALTER COLUMN ${sql.ref(column)}
506
+ TYPE ${sql.ref(enumName)}
507
+ USING ${sql.ref(column)}::${sql.ref(enumName)};
506
508
  `.execute(db);
507
509
  }
508
510
  function computedTemporaryType(array) {
@@ -115,16 +115,38 @@ export default class Decorators {
115
115
  *
116
116
  * // automatically sets `myEncryptedPhone` to the encrypted value that
117
117
  * // `phone` is set to new/create/update, e.g., `await user.update({ phone })`
118
- * @deco.Encrypted('myEncryptedPhone)
118
+ * @deco.Encrypted('myEncryptedPhone')
119
119
  * public phone: string
120
+ *
121
+ * // automatically sets `myEncryptedMfaSettings` to the encrypted value that
122
+ * // `mfaSettings` is set to in new/create/update, and declares the
123
+ * // value's OpenAPI shape, since it is not a string
124
+ * @deco.Encrypted({
125
+ * column: 'myEncryptedMfaSettings',
126
+ * openapi: { type: ['object', 'null'], properties: { token: 'string' } },
127
+ * })
128
+ * public mfaSettings: { token: string } | null
120
129
  * }
121
130
  * ```
122
131
  *
123
- * @param column — if omitted, then 'encrypted' is prepended to the Pascal cased version of the decorated field
132
+ * The decorated property may hold any JSON-serializable value. Its OpenAPI
133
+ * shape is what Psychic uses to cast the property in params and to render
134
+ * it in request body and serializer OpenAPI. Without an `openapi` option,
135
+ * that shape is `'string'`, or `['string', 'null']` when the backing column
136
+ * allows null. Pass `openapi` when the property holds a non-string value,
137
+ * such as an object, a number, or an array; it accepts any shape
138
+ * `@deco.Virtual` accepts. The declared shape is used exactly as given and
139
+ * replaces the nullability inferred from the backing column, so when the
140
+ * backing column allows null, include `'null'` in the shape, e.g.
141
+ * `type: ['object', 'null']`.
142
+ *
143
+ * @param columnOrOptions - Either the backing column's name, or an options object:
144
+ * - `column` — the backing column's name; if omitted, then 'encrypted' is prepended to the Pascal cased version of the decorated field
145
+ * - `openapi` — the OpenAPI shape of the decorated property's value; if omitted, the property's shape is a string (see above)
124
146
  * @returns An Encrypted decorator
125
147
  */
126
- Encrypted(column) {
127
- return Encrypted(column);
148
+ Encrypted(columnOrOptions) {
149
+ return Encrypted(columnOrOptions);
128
150
  }
129
151
  /**
130
152
  * The Scope decorator decorates a static method that accepts
@@ -1,11 +1,18 @@
1
+ import DreamApp from '../../dream-app/index.js';
2
+ import defaultEncryptedColumnName from '../../encrypt/defaultEncryptedColumnName.js';
1
3
  import InternalEncrypt from '../../encrypt/InternalEncrypt.js';
2
4
  import DoNotSetEncryptedFieldsDirectly from '../../errors/DoNotSetEncryptedFieldsDirectly.js';
3
5
  import columnAllowsNull from '../../helpers/db/columnAllowsNull.js';
4
- import pascalize from '../../helpers/pascalize.js';
5
- export default function Encrypted(encryptedColumnName) {
6
+ export default function Encrypted(columnOrOptions) {
7
+ const { column, openapi } = typeof columnOrOptions === 'string' ? { column: columnOrOptions } : (columnOrOptions ?? {});
6
8
  return function (_, context) {
7
9
  const key = context.name;
8
- const encryptedKey = encryptedColumnName || `encrypted${pascalize(key)}`;
10
+ const encryptedKey = column || defaultEncryptedColumnName(key);
11
+ // records whose legacy-key listeners for this property are running; a
12
+ // read of the property on one of them returns its value without
13
+ // dispatching the event again, so a listener that reads the property
14
+ // does not recurse
15
+ const recordsDispatchingLegacyKeyUsed = new WeakSet();
9
16
  context.addInitializer(function () {
10
17
  const dreamClass = this.constructor;
11
18
  if (!dreamClass['globallyInitializingDecorators']) {
@@ -30,7 +37,7 @@ export default function Encrypted(encryptedColumnName) {
30
37
  ;
31
38
  dreamClass['virtualAttributes'].push({
32
39
  property: key,
33
- type: columnAllowsNull(dreamClass, encryptedKey) ? ['string', 'null'] : 'string',
40
+ type: openapi ?? (columnAllowsNull(dreamClass, encryptedKey) ? ['string', 'null'] : 'string'),
34
41
  });
35
42
  if (!Object.getOwnPropertyDescriptor(dreamClass, 'explicitUnsafeParamColumns')) {
36
43
  // This pattern allows `explicitUnsafeParamColumns` on a base STI class and on
@@ -56,7 +63,22 @@ export default function Encrypted(encryptedColumnName) {
56
63
  const dreamPrototype = Object.getPrototypeOf(this);
57
64
  Object.defineProperty(dreamPrototype, key, {
58
65
  get() {
59
- return InternalEncrypt.decryptColumn(this.getAttribute(encryptedKey));
66
+ const dream = this;
67
+ return InternalEncrypt.decryptColumn(this.getAttribute(encryptedKey), {
68
+ onLegacyKeyUsed: () => {
69
+ if (recordsDispatchingLegacyKeyUsed.has(dream))
70
+ return;
71
+ recordsDispatchingLegacyKeyUsed.add(dream);
72
+ try {
73
+ DreamApp.getOrFail().specialHooks.encryptionLegacyKeyUsed.forEach(fn => {
74
+ fn({ dream, property: key, encryptedColumnName: encryptedKey });
75
+ });
76
+ }
77
+ finally {
78
+ recordsDispatchingLegacyKeyUsed.delete(dream);
79
+ }
80
+ },
81
+ });
60
82
  },
61
83
  set(val) {
62
84
  /**
@@ -74,7 +96,9 @@ export default function Encrypted(encryptedColumnName) {
74
96
  */
75
97
  if (this.columnSetterGuardActivated)
76
98
  return;
77
- this.setAttribute(encryptedKey, InternalEncrypt.encryptColumn(val));
99
+ // the value was just encrypted, so it bypasses setAttribute's check
100
+ // that a backing column receives only decryptable ciphertext
101
+ this.setAttributeUnchecked(encryptedKey, InternalEncrypt.encryptColumn(val));
78
102
  },
79
103
  configurable: false,
80
104
  });
@@ -498,12 +498,12 @@ export default class DreamClassTransactionBuilder {
498
498
  * 5. associations loading associations loading associations could result in exponential amounts of data; in those cases, `.preload(...).findEach(...)` avoids instantiating massive amounts of data at once
499
499
  *
500
500
  * ```ts
501
- * const user = await User.txn(txn).leftJoinPreload('posts', 'comments', { visibilty: 'public' }, 'replies').first()
501
+ * const user = await User.txn(txn).leftJoinPreload('posts', 'comments', { and: { visibility: 'public' } }, 'replies').first()
502
502
  * console.log(user.posts[0].comments[0].replies)
503
503
  * // [Reply{id: 1}, Reply{id: 2}]
504
504
  * ```
505
505
  *
506
- * @param args - A chain of association names and where clauses
506
+ * @param args - A chain of association names and and/andNot/andAny clauses
507
507
  * @returns A query for this model with the include statement applied
508
508
  */
509
509
  leftJoinPreload(...args) {
@@ -521,13 +521,13 @@ export default class DreamClassTransactionBuilder {
521
521
  *
522
522
  * ```ts
523
523
  * await ApplicationModel.transaction(async txn => {
524
- * const user = await User.txn(txn).preload('posts', 'comments', { visibilty: 'public' }, 'replies').first()
524
+ * const user = await User.txn(txn).preload('posts', 'comments', { and: { visibility: 'public' } }, 'replies').first()
525
525
  * console.log(user.posts[0].comments[0].replies)
526
526
  * // [Reply{id: 1}, Reply{id: 2}]
527
527
  * })
528
528
  * ```
529
529
  *
530
- * @param args - A chain of association names and where clauses
530
+ * @param args - A chain of association names and and/andNot/andAny clauses
531
531
  * @returns A query for this model with the preload statement applied
532
532
  */
533
533
  preload(...args) {
@@ -591,7 +591,7 @@ export default class DreamClassTransactionBuilder {
591
591
  * })
592
592
  * ```
593
593
  *
594
- * @param args - A chain of association names and where clauses
594
+ * @param args - A chain of association names and and/andNot/andAny clauses
595
595
  * @returns A Query for this model with the inner join clause applied
596
596
  */
597
597
  innerJoin(...args) {
@@ -607,7 +607,7 @@ export default class DreamClassTransactionBuilder {
607
607
  * })
608
608
  * ```
609
609
  *
610
- * @param args - A chain of association names and where clauses
610
+ * @param args - A chain of association names and and/andNot/andAny clauses
611
611
  * @returns A Query for this model with the left join clause applied
612
612
  */
613
613
  leftJoin(...args) {
@@ -3,6 +3,7 @@ import CannotCreateAssociationOnUnpersistedDream from '../errors/associations/Ca
3
3
  import CannotDestroyAssociationOnUnpersistedDream from '../errors/associations/CannotDestroyAssociationOnUnpersistedDream.js';
4
4
  import CannotUpdateAssociationOnUnpersistedDream from '../errors/associations/CannotUpdateAssociationOnUnpersistedDream.js';
5
5
  import RecordNotFound from '../errors/RecordNotFound.js';
6
+ import assertRecognizedAssociationConditionKeys, { DESTROY_ASSOCIATION_OPTION_KEYS, UPDATE_ASSOCIATION_OPTION_KEYS, } from './internal/associations/assertRecognizedAssociationConditionKeys.js';
6
7
  import associationQuery from './internal/associations/associationQuery.js';
7
8
  import associationUpdateQuery from './internal/associations/associationUpdateQuery.js';
8
9
  import createAssociation from './internal/associations/createAssociation.js';
@@ -33,26 +34,29 @@ export default class DreamInstanceTransactionBuilder {
33
34
  /**
34
35
  * Loads the requested associations upon execution
35
36
  *
37
+ * **IMPORTANT:** `load().execute()` returns a **new clone** of the model with the
38
+ * associations loaded. It does NOT modify the original instance.
39
+ *
36
40
  * NOTE: {@link Dream#preload} is often a preferrable way of achieving the
37
41
  * same goal.
38
42
  *
39
43
  * ```ts
40
44
  * await ApplicationModel.transaction(async txn => {
41
- * await user
45
+ * const loadedUser = await user
42
46
  * .txn(txn)
43
- * .load('posts', { body: ops.ilike('%hello world%') }, 'comments', 'replies')
47
+ * .load('posts', { and: { body: ops.ilike('%hello world%') } }, 'comments', 'replies')
44
48
  * .load('images')
45
49
  * .execute()
46
50
  *
47
- * user.posts[0].comments[0].replies[0]
51
+ * loadedUser.posts[0].comments[0].replies[0]
48
52
  * // Reply{}
49
53
  *
50
- * user.images[0]
54
+ * loadedUser.images[0]
51
55
  * // Image{}
52
56
  * })
53
57
  * ```
54
58
  *
55
- * @param args - A list of associations (and optional where clauses) to load
59
+ * @param args - A chain of association names and and/andNot/andAny clauses
56
60
  * @returns A chainable LoadBuilder instance
57
61
  */
58
62
  load(...args) {
@@ -112,32 +116,34 @@ export default class DreamInstanceTransactionBuilder {
112
116
  * Load each specified association using a single SQL query.
113
117
  * See {@link #load} for loading in separate queries.
114
118
  *
115
- * Note: since leftJoinPreload loads via single query, it has
116
- * some downsides and that may be avoided using {@link #load}:
119
+ * **IMPORTANT:** Like `load()`, `leftJoinLoad().execute()` returns a **new clone** of the
120
+ * model with associations loaded. It does NOT modify the original instance.
121
+ *
122
+ * Note: since leftJoinLoad loads via single query, it has
123
+ * some downsides that may be avoided using {@link #load}:
117
124
  * 1. `limit` and `offset` will be automatically removed
118
125
  * 2. `through` associations will bring additional namespaces into the query that can conflict with through associations from other associations, creating an invalid query
119
- * 3. each nested association will result in an additional record which duplicates data from the outer record. E.g., given `.leftJoinPreload('a', 'b', 'c')`, if each `a` has 10 `b` and each `b` has 10 `c`, then for one `a`, 100 records will be returned, each of which has all of the columns of `a`. `.load('a', 'b', 'c')` would perform three separate SQL queries, but the data for a single `a` would only be returned once.
126
+ * 3. each nested association will result in an additional record which duplicates data from the outer record. E.g., given `.leftJoinLoad('a', 'b', 'c')`, if each `a` has 10 `b` and each `b` has 10 `c`, then for one `a`, 100 records will be returned, each of which has all of the columns of `a`. `.load('a', 'b', 'c')` would perform three separate SQL queries, but the data for a single `a` would only be returned once.
120
127
  * 4. the individual query becomes more complex the more associations are included
121
128
  * 5. associations loading associations loading associations could result in exponential amounts of data; in those cases, `.load(...).findEach(...)` avoids instantiating massive amounts of data at once
122
- * Loads the requested associations upon execution
123
- *
124
- * NOTE: {@link Dream#leftJoinPreload} is often a preferrable way of achieving the
125
- * same goal.
126
129
  *
127
130
  * ```ts
128
- * await user.txn(txn)
129
- * .leftJoinLoad('posts', { body: ops.ilike('%hello world%') }, 'comments', 'replies')
130
- * .leftJoinLoad('images')
131
- * .execute()
131
+ * await ApplicationModel.transaction(async txn => {
132
+ * const loadedUser = await user
133
+ * .txn(txn)
134
+ * .leftJoinLoad('posts', { and: { body: ops.ilike('%hello world%') } }, 'comments', 'replies')
135
+ * .leftJoinLoad('images')
136
+ * .execute()
132
137
  *
133
- * user.posts[0].comments[0].replies[0]
134
- * // Reply{}
138
+ * loadedUser.posts[0].comments[0].replies[0]
139
+ * // Reply{}
135
140
  *
136
- * user.images[0]
137
- * // Image{}
141
+ * loadedUser.images[0]
142
+ * // Image{}
143
+ * })
138
144
  * ```
139
145
  *
140
- * @param args - A list of associations (and optional where clauses) to load
146
+ * @param args - A chain of association names and and/andNot/andAny clauses
141
147
  * @returns A chainable LeftJoinLoadBuilder instance
142
148
  */
143
149
  leftJoinLoad(...args) {
@@ -153,7 +159,7 @@ export default class DreamInstanceTransactionBuilder {
153
159
  * })
154
160
  * ```
155
161
  *
156
- * @param args - A chain of association names and where clauses
162
+ * @param args - A chain of association names and and/andNot/andAny clauses
157
163
  * @returns A Query for this model with the inner join clause applied
158
164
  */
159
165
  innerJoin(...args) {
@@ -169,7 +175,7 @@ export default class DreamInstanceTransactionBuilder {
169
175
  * })
170
176
  * ```
171
177
  *
172
- * @param args - A chain of association names and where clauses
178
+ * @param args - A chain of association names and and/andNot/andAny clauses
173
179
  * @returns A Query for this model with the left join clause applied
174
180
  */
175
181
  leftJoin(...args) {
@@ -252,6 +258,12 @@ export default class DreamInstanceTransactionBuilder {
252
258
  * See {@link Dream.save | save} for details on
253
259
  * the side effects of saving.
254
260
  *
261
+ * A column whose value is still `undefined` after setters run is
262
+ * skipped, not written (see {@link Dream.save | save}), with two
263
+ * `@Encrypted` exceptions: `undefined` for an encrypted property (e.g.
264
+ * `secret`) is encrypted to `null`, so it clears the column, and
265
+ * `undefined` for its backing column (e.g. `encryptedSecret`) throws.
266
+ *
255
267
  * NOTE:
256
268
  * To bypass custom-defined setters, use {@link Dream.updateAttributes | updateAttributes} instead.
257
269
  *
@@ -279,6 +291,18 @@ export default class DreamInstanceTransactionBuilder {
279
291
  * See {@link Dream.save | save} for details on
280
292
  * the side effects of saving.
281
293
  *
294
+ * An encrypted property (e.g. `secret`) is still encrypted. Its
295
+ * `@Encrypted` backing column (e.g. `encryptedSecret`) accepts only
296
+ * ciphertext that decrypts with this app's column encryption keys, or
297
+ * `null`; anything else throws before anything is saved (see
298
+ * {@link Dream.setAttribute | setAttribute}).
299
+ *
300
+ * A column whose value is still `undefined` after setters run is
301
+ * skipped, not written (see {@link Dream.save | save}), with two
302
+ * `@Encrypted` exceptions: `undefined` for an encrypted property (e.g.
303
+ * `secret`) is encrypted to `null`, so it clears the column, and
304
+ * `undefined` for its backing column (e.g. `encryptedSecret`) throws.
305
+ *
282
306
  * NOTE:
283
307
  * To update the values without bypassing any custom-defined
284
308
  * setters, use {@link Dream.update | update} instead.
@@ -329,6 +353,10 @@ export default class DreamInstanceTransactionBuilder {
329
353
  * Upon updating an instance, the update timestamp (if defined)
330
354
  * will be updated on the model.
331
355
  *
356
+ * A column whose value is `undefined` is skipped, not written: a new
357
+ * record gets the column's database default, and a persisted record
358
+ * keeps the value stored for it. Set a column to `null` to clear it.
359
+ *
332
360
  * ```ts
333
361
  * const user = User.new({ email: 'how@yadoin' })
334
362
  *
@@ -386,8 +414,8 @@ export default class DreamInstanceTransactionBuilder {
386
414
  }
387
415
  /**
388
416
  * Updates all records matching the association with
389
- * the provided attributes. If a where statement is passed,
390
- * The where statement will be applied to the query
417
+ * the provided attributes. If an and, andNot or andAny
418
+ * statement is passed, it will be applied to the query
391
419
  * before updating.
392
420
  *
393
421
  * ```ts
@@ -402,13 +430,16 @@ export default class DreamInstanceTransactionBuilder {
402
430
  * @param associationName - The name of the association to update
403
431
  * @param attributes - The attributes to update on the association
404
432
  * @param updateAssociationOptions - Options for updating the association
405
- * @param updateAssociationOptions.where - Optional where statement to apply to query before updating
433
+ * @param updateAssociationOptions.and - Optional and statement to apply to query before updating
434
+ * @param updateAssociationOptions.andNot - Optional andNot statement to apply to query before updating
435
+ * @param updateAssociationOptions.andAny - Optional andAny statement to apply to query before updating
406
436
  * @param updateAssociationOptions.bypassAllDefaultScopes - If true, bypasses user-removable default scopes when updating the association; an STI child's reserved discriminator remains enforced, including when associations are loaded. Defaults to false
407
437
  * @param updateAssociationOptions.defaultScopesToBypass - An array of default scope names to bypass when updating the association. Defaults to an empty array
408
438
  * @param updateAssociationOptions.skipHooks - If true, skips applying model hooks during the update operation. Defaults to false
409
439
  * @returns The number of updated records
410
440
  */
411
441
  async updateAssociation(associationName, attributes, updateAssociationOptions) {
442
+ assertRecognizedAssociationConditionKeys(updateAssociationOptions, UPDATE_ASSOCIATION_OPTION_KEYS, associationName, 'updateAssociation');
412
443
  if (this.dreamInstance.isNewRecord)
413
444
  throw new CannotUpdateAssociationOnUnpersistedDream(this.dreamInstance, associationName);
414
445
  return await associationUpdateQuery(this.dreamInstance, this.dreamTransaction, associationName, {
@@ -465,6 +496,7 @@ export default class DreamInstanceTransactionBuilder {
465
496
  * @returns The number of records deleted
466
497
  */
467
498
  async destroyAssociation(associationName, options) {
499
+ assertRecognizedAssociationConditionKeys(options, DESTROY_ASSOCIATION_OPTION_KEYS, associationName, 'destroyAssociation');
468
500
  if (this.dreamInstance.isNewRecord)
469
501
  throw new CannotDestroyAssociationOnUnpersistedDream(this.dreamInstance, associationName);
470
502
  return await destroyAssociation(this.dreamInstance, this.dreamTransaction, associationName, {
@@ -503,6 +535,7 @@ export default class DreamInstanceTransactionBuilder {
503
535
  * @returns The number of records deleted
504
536
  */
505
537
  async reallyDestroyAssociation(associationName, options) {
538
+ assertRecognizedAssociationConditionKeys(options, DESTROY_ASSOCIATION_OPTION_KEYS, associationName, 'reallyDestroyAssociation');
506
539
  if (this.dreamInstance.isNewRecord)
507
540
  throw new CannotDestroyAssociationOnUnpersistedDream(this.dreamInstance, associationName);
508
541
  return await destroyAssociation(this.dreamInstance, this.dreamTransaction, associationName, {
@@ -521,7 +554,9 @@ export default class DreamInstanceTransactionBuilder {
521
554
  * will also be undeleted.
522
555
  *
523
556
  * ```ts
524
- * await user.undestroyAssociation('posts', { and: { body: 'hello world' } })
557
+ * await ApplicationModel.transaction(async txn => {
558
+ * await user.txn(txn).undestroyAssociation('posts', { and: { body: 'hello world' } })
559
+ * })
525
560
  * ```
526
561
  *
527
562
  * @param associationName - The name of the association to undestroy
@@ -536,6 +571,7 @@ export default class DreamInstanceTransactionBuilder {
536
571
  * @returns The number of records undestroyed
537
572
  */
538
573
  async undestroyAssociation(associationName, options) {
574
+ assertRecognizedAssociationConditionKeys(options, DESTROY_ASSOCIATION_OPTION_KEYS, associationName, 'undestroyAssociation');
539
575
  return await undestroyAssociation(this.dreamInstance, this.dreamTransaction, associationName, {
540
576
  ...undestroyOptions(options),
541
577
  joinAndStatements: {
@@ -28,13 +28,13 @@ export default class LeftJoinLoadBuilder {
28
28
  return this;
29
29
  }
30
30
  /**
31
- * Attaches a load statement to the load builder
31
+ * Attaches a leftJoinLoad statement to the load builder
32
32
  *
33
33
  * ```ts
34
34
  * const user = await User.firstOrFail()
35
35
  * await user
36
- * .load('settings')
37
- * .load('posts', 'comments', 'replies', ['image', 'localizedText'])
36
+ * .leftJoinLoad('settings')
37
+ * .leftJoinLoad('posts', 'comments', 'replies', ['image', 'localizedText'])
38
38
  * .execute()
39
39
  * ```
40
40
  */
@@ -93,7 +93,7 @@ export default class LoadBuilder {
93
93
  * .execute()
94
94
  * ```
95
95
  *
96
- * @param args - A list of associations (and optional where clauses) to load
96
+ * @param args - A chain of association names and and/andNot/andAny clauses to load
97
97
  * @returns A chainable LoadBuilder instance
98
98
  */
99
99
  load(...args) {