@rvoh/dream 2.24.1 → 2.26.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 (244) hide show
  1. package/dist/cjs/src/Dream.js +10 -1
  2. package/dist/cjs/src/cli/index.js +31 -5
  3. package/dist/cjs/src/db/migration-helpers/DreamMigrationHelpers.js +127 -6
  4. package/dist/cjs/src/dream/Query.js +247 -67
  5. package/dist/cjs/src/dream/internal/printSerializerHierarchyLevel.js +4 -3
  6. package/dist/cjs/src/dream/internal/resolveSerializerAssociationEdges.js +14 -1
  7. package/dist/cjs/src/errors/BatchingIncompatibleWithLimitOrOffset.js +25 -0
  8. package/dist/cjs/src/errors/DropEnumValueRetypeFailed.js +208 -0
  9. package/dist/cjs/src/errors/MissingRequiredLockOptionForUpdateCallback.js +16 -0
  10. package/dist/cjs/src/errors/serializers/MissingSerializersDefinition.js +16 -2
  11. package/dist/cjs/src/errors/serializers/MissingSerializersDefinitionForKey.js +5 -2
  12. package/dist/cjs/src/errors/serializers/NoGlobalSerializerForSpecifiedKey.js +5 -3
  13. package/dist/cjs/src/errors/serializers/NoSerializersResolvedForKey.js +5 -2
  14. package/dist/cjs/src/errors/serializers/NonDreamSerializerDerivedFromGlobalSerializerForSpecifiedKey.js +5 -3
  15. package/dist/cjs/src/errors/serializers/serializerResolutionContextMessage.js +46 -0
  16. package/dist/cjs/src/helpers/cli/generateFactoryContent.js +7 -1
  17. package/dist/cjs/src/helpers/cli/generateMigrationContent.js +80 -13
  18. package/dist/cjs/src/helpers/cli/syncWithFailureContext.js +41 -0
  19. package/dist/cjs/src/serializer/SerializerRenderer.js +32 -4
  20. package/dist/cjs/src/serializer/helpers/inferSerializerFromDreamOrViewModel.js +28 -10
  21. package/dist/cjs/src/serializer/helpers/serializerForAssociatedClass.js +6 -1
  22. package/dist/cjs/src/serializer/helpers/serializerGlobalName.js +16 -0
  23. package/dist/esm/src/Dream.js +10 -1
  24. package/dist/esm/src/cli/index.js +31 -5
  25. package/dist/esm/src/db/migration-helpers/DreamMigrationHelpers.js +127 -6
  26. package/dist/esm/src/dream/Query.js +247 -67
  27. package/dist/esm/src/dream/internal/printSerializerHierarchyLevel.js +4 -3
  28. package/dist/esm/src/dream/internal/resolveSerializerAssociationEdges.js +14 -1
  29. package/dist/esm/src/errors/BatchingIncompatibleWithLimitOrOffset.js +25 -0
  30. package/dist/esm/src/errors/DropEnumValueRetypeFailed.js +208 -0
  31. package/dist/esm/src/errors/MissingRequiredLockOptionForUpdateCallback.js +16 -0
  32. package/dist/esm/src/errors/serializers/MissingSerializersDefinition.js +16 -2
  33. package/dist/esm/src/errors/serializers/MissingSerializersDefinitionForKey.js +5 -2
  34. package/dist/esm/src/errors/serializers/NoGlobalSerializerForSpecifiedKey.js +5 -3
  35. package/dist/esm/src/errors/serializers/NoSerializersResolvedForKey.js +5 -2
  36. package/dist/esm/src/errors/serializers/NonDreamSerializerDerivedFromGlobalSerializerForSpecifiedKey.js +5 -3
  37. package/dist/esm/src/errors/serializers/serializerResolutionContextMessage.js +46 -0
  38. package/dist/esm/src/helpers/cli/generateFactoryContent.js +7 -1
  39. package/dist/esm/src/helpers/cli/generateMigrationContent.js +80 -13
  40. package/dist/esm/src/helpers/cli/syncWithFailureContext.js +41 -0
  41. package/dist/esm/src/serializer/SerializerRenderer.js +32 -4
  42. package/dist/esm/src/serializer/helpers/inferSerializerFromDreamOrViewModel.js +28 -10
  43. package/dist/esm/src/serializer/helpers/serializerForAssociatedClass.js +6 -1
  44. package/dist/esm/src/serializer/helpers/serializerGlobalName.js +16 -0
  45. package/dist/types/src/Dream.d.ts +8 -2
  46. package/dist/types/src/cli/index.d.ts +1 -1
  47. package/dist/types/src/dream/Query.d.ts +316 -40
  48. package/dist/types/src/errors/BatchingIncompatibleWithLimitOrOffset.d.ts +3 -0
  49. package/dist/types/src/errors/DropEnumValueRetypeFailed.d.ts +89 -0
  50. package/dist/types/src/errors/MissingRequiredLockOptionForUpdateCallback.d.ts +3 -0
  51. package/dist/types/src/errors/serializers/MissingSerializersDefinition.d.ts +3 -1
  52. package/dist/types/src/errors/serializers/MissingSerializersDefinitionForKey.d.ts +3 -1
  53. package/dist/types/src/errors/serializers/NoGlobalSerializerForSpecifiedKey.d.ts +3 -1
  54. package/dist/types/src/errors/serializers/NoSerializersResolvedForKey.d.ts +3 -1
  55. package/dist/types/src/errors/serializers/NonDreamSerializerDerivedFromGlobalSerializerForSpecifiedKey.d.ts +3 -1
  56. package/dist/types/src/errors/serializers/serializerResolutionContextMessage.d.ts +20 -0
  57. package/dist/types/src/helpers/cli/syncWithFailureContext.d.ts +58 -0
  58. package/dist/types/src/serializer/helpers/inferSerializerFromDreamOrViewModel.d.ts +11 -4
  59. package/dist/types/src/serializer/helpers/serializerGlobalName.d.ts +15 -0
  60. package/dist/types/src/types/serializer.d.ts +38 -0
  61. package/dist/types/src/types/types/serializer.ts +40 -0
  62. package/docs/assets/search.js +1 -1
  63. package/docs/classes/db.DreamMigrationHelpers.html +11 -11
  64. package/docs/classes/db.KyselyQueryDriver.html +40 -40
  65. package/docs/classes/db.PostgresQueryDriver.html +41 -41
  66. package/docs/classes/db.QueryDriverBase.html +39 -39
  67. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  68. package/docs/classes/errors.ColumnOverflow.html +3 -3
  69. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  70. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  71. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  72. package/docs/classes/errors.DecryptionError.html +2 -2
  73. package/docs/classes/errors.DecryptionParseError.html +2 -2
  74. package/docs/classes/errors.DecryptionRotationError.html +3 -3
  75. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  76. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  77. package/docs/classes/errors.InvalidClockTime.html +2 -2
  78. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  79. package/docs/classes/errors.InvalidDateTime.html +2 -2
  80. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  81. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  82. package/docs/classes/errors.NotNullViolation.html +3 -3
  83. package/docs/classes/errors.RecordNotFound.html +3 -3
  84. package/docs/classes/errors.ValidationError.html +3 -3
  85. package/docs/classes/index.CalendarDate.html +33 -33
  86. package/docs/classes/index.ClockTime.html +32 -32
  87. package/docs/classes/index.ClockTimeTz.html +35 -35
  88. package/docs/classes/index.DateTime.html +86 -86
  89. package/docs/classes/index.Decorators.html +19 -19
  90. package/docs/classes/index.Dream.html +123 -137
  91. package/docs/classes/index.DreamApp.html +10 -10
  92. package/docs/classes/index.DreamTransaction.html +2 -2
  93. package/docs/classes/index.Env.html +2 -2
  94. package/docs/classes/index.Query.html +735 -73
  95. package/docs/classes/system.CliFileWriter.html +4 -4
  96. package/docs/classes/system.DreamBin.html +2 -2
  97. package/docs/classes/system.DreamCLI.html +7 -7
  98. package/docs/classes/system.DreamImporter.html +2 -2
  99. package/docs/classes/system.DreamLogos.html +2 -2
  100. package/docs/classes/system.DreamSerializerBuilder.html +11 -11
  101. package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
  102. package/docs/classes/system.PathHelpers.html +3 -3
  103. package/docs/classes/utils.Encrypt.html +3 -3
  104. package/docs/classes/utils.Range.html +2 -2
  105. package/docs/functions/db.closeAllDbConnections.html +1 -1
  106. package/docs/functions/db.dreamDbConnections.html +1 -1
  107. package/docs/functions/db.untypedDb.html +1 -1
  108. package/docs/functions/db.validateColumn.html +1 -1
  109. package/docs/functions/db.validateTable.html +1 -1
  110. package/docs/functions/errors.pgErrorType.html +1 -1
  111. package/docs/functions/index.DreamSerializer.html +1 -1
  112. package/docs/functions/index.ObjectSerializer.html +1 -1
  113. package/docs/functions/index.ReplicaSafe.html +1 -1
  114. package/docs/functions/index.STI.html +1 -1
  115. package/docs/functions/index.SoftDelete.html +1 -1
  116. package/docs/functions/utils.camelize.html +1 -1
  117. package/docs/functions/utils.capitalize.html +1 -1
  118. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  119. package/docs/functions/utils.compact.html +1 -1
  120. package/docs/functions/utils.groupBy.html +1 -1
  121. package/docs/functions/utils.hyphenize.html +1 -1
  122. package/docs/functions/utils.intersection.html +1 -1
  123. package/docs/functions/utils.isEmpty.html +1 -1
  124. package/docs/functions/utils.normalizeUnicode.html +1 -1
  125. package/docs/functions/utils.pascalize.html +1 -1
  126. package/docs/functions/utils.percent.html +1 -1
  127. package/docs/functions/utils.range.html +1 -1
  128. package/docs/functions/utils.round.html +1 -1
  129. package/docs/functions/utils.sanitizeString.html +1 -1
  130. package/docs/functions/utils.snakeify.html +1 -1
  131. package/docs/functions/utils.sort.html +1 -1
  132. package/docs/functions/utils.sortBy.html +1 -1
  133. package/docs/functions/utils.sortObjectByKey.html +1 -1
  134. package/docs/functions/utils.sortObjectByValue.html +1 -1
  135. package/docs/functions/utils.uncapitalize.html +1 -1
  136. package/docs/functions/utils.uniq.html +1 -1
  137. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  138. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  139. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  140. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  141. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  142. package/docs/interfaces/types.DecoratorContext.html +2 -2
  143. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  144. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  145. package/docs/interfaces/types.DreamDbConfig.html +5 -5
  146. package/docs/interfaces/types.DurationObject.html +2 -2
  147. package/docs/interfaces/types.EncryptOptions.html +2 -2
  148. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  149. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  150. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  151. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  152. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  153. package/docs/types/openapi.OpenapiFormats.html +1 -1
  154. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  155. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  156. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  157. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  158. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  159. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  160. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  161. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  162. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  163. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
  164. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
  165. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
  166. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  167. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  168. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  169. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  170. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  171. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  172. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  173. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  174. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
  175. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
  176. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  177. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  178. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
  179. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
  180. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  181. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  182. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
  183. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
  184. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
  185. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  186. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  187. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  188. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  189. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  190. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  191. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  192. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  193. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  194. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  195. package/docs/types/types.CalendarDateObject.html +1 -1
  196. package/docs/types/types.Camelized.html +1 -1
  197. package/docs/types/types.ClockTimeObject.html +1 -1
  198. package/docs/types/types.DbConnectionType.html +1 -1
  199. package/docs/types/types.DbTypes.html +1 -1
  200. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  201. package/docs/types/types.DreamAttributes.html +1 -1
  202. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  203. package/docs/types/types.DreamClassColumn.html +1 -1
  204. package/docs/types/types.DreamColumn.html +1 -1
  205. package/docs/types/types.DreamColumnNames.html +1 -1
  206. package/docs/types/types.DreamLogLevel.html +1 -1
  207. package/docs/types/types.DreamLogger.html +2 -2
  208. package/docs/types/types.DreamModelSerializerType.html +1 -1
  209. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  210. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  211. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  212. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  213. package/docs/types/types.DreamSerializable.html +1 -1
  214. package/docs/types/types.DreamSerializableArray.html +1 -1
  215. package/docs/types/types.DreamSerializerKey.html +1 -1
  216. package/docs/types/types.DreamSerializers.html +1 -1
  217. package/docs/types/types.DreamVirtualColumns.html +1 -1
  218. package/docs/types/types.DurationUnit.html +1 -1
  219. package/docs/types/types.EncryptAlgorithm.html +1 -1
  220. package/docs/types/types.HasManyStatement.html +1 -1
  221. package/docs/types/types.HasOneStatement.html +1 -1
  222. package/docs/types/types.Hyphenized.html +1 -1
  223. package/docs/types/types.Pascalized.html +1 -1
  224. package/docs/types/types.PrimaryKeyType.html +1 -1
  225. package/docs/types/types.RoundingPrecision.html +1 -1
  226. package/docs/types/types.SerializerCasing.html +1 -1
  227. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  228. package/docs/types/types.Snakeified.html +1 -1
  229. package/docs/types/types.StrictInterface.html +1 -1
  230. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  231. package/docs/types/types.UpdateableProperties.html +1 -1
  232. package/docs/types/types.ValidationType.html +1 -1
  233. package/docs/types/types.ViewModel.html +2 -2
  234. package/docs/types/types.ViewModelClass.html +1 -1
  235. package/docs/types/types.WeekdayName.html +1 -1
  236. package/docs/types/types.WhereStatementForDream.html +1 -1
  237. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  238. package/docs/variables/index.DreamConst.html +1 -1
  239. package/docs/variables/index.ops.html +1 -1
  240. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  241. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  242. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  243. package/docs/variables/system.primaryKeyTypes.html +1 -1
  244. package/package.json +1 -1
@@ -0,0 +1,208 @@
1
+ /**
2
+ * SQLSTATEs Postgres raises when it cannot re-derive a stored expression against a column's
3
+ * new type. `42883 undefined_function` covers both `operator does not exist: text <> my_enum`
4
+ * (the widen) and `function char_length(my_enum) does not exist` (the restore); `42804
5
+ * datatype_mismatch` covers the argument-type variant and
6
+ * `default for column "x" cannot be cast automatically to type my_enum`.
7
+ *
8
+ * A retype can fail for reasons that have nothing to do with a stored expression — most
9
+ * obviously `22P02 invalid input value for enum` when a value written by `replacements` is not
10
+ * a member of the recreated enum. Those codes are deliberately absent, so the constraints found
11
+ * on the column are reported as context rather than as the diagnosis.
12
+ *
13
+ * These codes say *an* expression could not be re-derived. They do not say **which** one, and
14
+ * the message must not pretend otherwise: a check constraint, a partial index's `WHERE`
15
+ * predicate, an expression index and an uncastable column `DEFAULT` all raise them, and
16
+ * `dropEnumValue` reads only the check constraints. Verified against Postgres 18 —
17
+ * `CREATE INDEX i ON t (id) WHERE val <> 'a'` followed by `ALTER TABLE t ALTER COLUMN val TYPE
18
+ * text` raises exactly `42883 operator does not exist: text <> my_enum`, the same code and the
19
+ * same wording a blocking check constraint produces.
20
+ */
21
+ const EXPRESSION_RETYPE_SQLSTATES = new Set(['42804', '42883']);
22
+ /**
23
+ * Quote every segment of a possibly schema-qualified table name the way Kysely quotes the
24
+ * identifiers it emits, so a `to_regclass` argument resolves exactly the relation the retype
25
+ * targeted. Exported because `DreamMigrationHelpers`' catalog read and the diagnostic query in
26
+ * this message must name the relation identically; it lives here because that helper already
27
+ * imports this module, and the reverse import would be a cycle.
28
+ */
29
+ export function quotedRelationName(table) {
30
+ return table
31
+ .split('.')
32
+ .map(segment => `"${segment.replace(/"/g, '""')}"`)
33
+ .join('.');
34
+ }
35
+ /**
36
+ * Raised when one of the two column retypes inside
37
+ * `DreamMigrationHelpers.dropEnumValue` fails.
38
+ *
39
+ * The raw Postgres error for the common cause of this - a check constraint on the
40
+ * column that references the enum - is `operator does not exist: text <> my_enum`.
41
+ * It names no table, no column and no constraint, and the `text` in it comes from
42
+ * `dropEnumValue`'s own internal widen step, so it appears nowhere in the
43
+ * migration the developer wrote. This error supplies all of that, and keeps the
44
+ * original error reachable through both `originalError` and `cause`.
45
+ *
46
+ * It never claims a check constraint caused a failure it cannot tie to one. The SQLSTATE gate
47
+ * separates "a stored expression could not be re-derived" from "a bad value" — but it cannot
48
+ * say *which* expression, because an index predicate raises the identical code, so even on the
49
+ * gated path the constraints stay context and the message names the other candidates.
50
+ */
51
+ export default class DropEnumValueRetypeFailed extends Error {
52
+ enumName;
53
+ value;
54
+ table;
55
+ column;
56
+ retypeDirection;
57
+ relationResolved;
58
+ checkConstraints;
59
+ originalError;
60
+ constructor({ enumName, value, table, column, retypeDirection, relationResolved, checkConstraints, originalError, }) {
61
+ super(undefined, { cause: originalError });
62
+ this.enumName = enumName;
63
+ this.value = value;
64
+ this.table = table;
65
+ this.column = column;
66
+ this.retypeDirection = retypeDirection;
67
+ this.relationResolved = relationResolved;
68
+ this.checkConstraints = checkConstraints;
69
+ this.originalError = originalError;
70
+ }
71
+ get message() {
72
+ return `
73
+ DreamMigrationHelpers.dropEnumValue failed while retyping ${this.table}.${this.column}.
74
+
75
+ enum: ${this.enumName}
76
+ value being dropped: '${this.value}'
77
+ table: ${this.table}
78
+ column: ${this.column}
79
+ failing step: ${this.failingStepDescription}
80
+
81
+ ${this.stepExplanation}
82
+
83
+ ${this.checkConstraintsSection}
84
+
85
+ ${this.guidanceSection}
86
+
87
+ The original database error was:
88
+
89
+ ${this.originalError.message}
90
+ `;
91
+ }
92
+ get failingStepDescription() {
93
+ return this.retypeDirection === 'toText'
94
+ ? `widening ${this.column} to text`
95
+ : `restoring ${this.column} to ${this.enumName}`;
96
+ }
97
+ get stepExplanation() {
98
+ if (this.retypeDirection === 'toText') {
99
+ return `dropEnumValue temporarily retypes every column listed in \`replacements\` to text so that
100
+ the ${this.enumName} type can be dropped and recreated without '${this.value}'. That text type is
101
+ dropEnumValue's own internal step, which is why the database error below can name a
102
+ type your migration never mentioned.`;
103
+ }
104
+ return `dropEnumValue recreated ${this.enumName} without '${this.value}' and was retyping every column
105
+ listed in \`replacements\` back to it.`;
106
+ }
107
+ /**
108
+ * True when the original error is one Postgres raises because a stored expression on the
109
+ * column could not be re-derived against its new type. A blocking check constraint is one
110
+ * source of that; a partial index predicate, an expression index and an uncastable column
111
+ * default are others. Anything else (a bad value, a missing relation, a permission error) is
112
+ * not evidence that any stored expression was involved.
113
+ */
114
+ get originalErrorBlockedAnExpression() {
115
+ const code = this.originalError.code;
116
+ return typeof code === 'string' && EXPRESSION_RETYPE_SQLSTATES.has(code);
117
+ }
118
+ get checkConstraintsSection() {
119
+ if (!this.relationResolved) {
120
+ return `The check constraints on ${this.table}.${this.column} could not be read: ${this.table} did not
121
+ resolve to a table when dropEnumValue looked it up, so whether a check constraint blocked
122
+ this retype is unknown. Read the original database error below.`;
123
+ }
124
+ if (this.checkConstraints.length === 0) {
125
+ return `No check constraint is defined on ${this.table}.${this.column}, so a check constraint is not
126
+ what blocked this retype. Read the original database error below.`;
127
+ }
128
+ const constraints = this.checkConstraints
129
+ .map(constraint => ` - ${constraint.name}\n ${constraint.definition}`)
130
+ .join('\n');
131
+ return `Check constraints defined on ${this.table}.${this.column}:
132
+
133
+ ${constraints}`;
134
+ }
135
+ get guidanceSection() {
136
+ if (this.originalErrorBlockedAnExpression) {
137
+ return [this.expressionBlockerSection, this.constraintRemedySection].filter(Boolean).join('\n\n');
138
+ }
139
+ if (!this.relationResolved)
140
+ return 'No check constraints could be read, so none can be reconciled here.';
141
+ if (this.checkConstraints.length === 0)
142
+ return 'No check constraints to reconcile.';
143
+ return `The database error below is not the "an expression on this column could not be re-derived
144
+ against its new type" failure that a blocking check constraint produces, so the constraints
145
+ above are listed as context rather than as the diagnosis: they may or may not have anything
146
+ to do with this. Read the original database error below first.`;
147
+ }
148
+ /**
149
+ * What the SQLSTATE does and does not establish, and every expression that raises it. The
150
+ * candidates are listed rather than diagnosed on purpose: `dropEnumValue` reads the column's
151
+ * check constraints and nothing else, so naming one of them as the cause would be a guess
152
+ * that sends the caller to drop a constraint that changes nothing.
153
+ */
154
+ get expressionBlockerSection() {
155
+ return `The database error below is the one Postgres raises when an expression stored against
156
+ ${this.column} could not be re-derived for the column's new type. The retype re-evaluates every
157
+ such expression at once, and the error names none of them, so it does not say on its own which
158
+ one blocked the retype. It is one of:
159
+
160
+ - a check constraint on the column — ${this.checkConstraintCandidateNote}
161
+ - a partial index whose WHERE predicate references the column (not read)
162
+ - an expression index over the column (not read)
163
+ - the column's DEFAULT, when it cannot be cast to the new type (not read, though that variant
164
+ names the column and the word "default" in the error below)
165
+
166
+ A view, a rule or a generated column that reads ${this.column} blocks the retype with a different
167
+ error that names itself ("cannot alter type of a column used by ..."), so none of those is what
168
+ happened here.
169
+
170
+ The indexes are what dropEnumValue cannot see. List them with:
171
+
172
+ SELECT indexrelid::regclass AS index, pg_get_indexdef(indexrelid) AS definition
173
+ FROM pg_index WHERE indrelid = to_regclass('${quotedRelationName(this.table)}');`;
174
+ }
175
+ get checkConstraintCandidateNote() {
176
+ if (!this.relationResolved)
177
+ return 'dropEnumValue could not read these, as noted above';
178
+ if (this.checkConstraints.length === 0)
179
+ return 'there are none on this column, so this one is ruled out';
180
+ return this.checkConstraints.length === 1 ? 'the one listed above' : 'the ones listed above';
181
+ }
182
+ /**
183
+ * The copy-pasteable remedy, offered only when there is a constraint to drop and phrased as
184
+ * conditional on that constraint actually being the blocker — which this error cannot know.
185
+ */
186
+ get constraintRemedySection() {
187
+ if (!this.relationResolved || this.checkConstraints.length === 0)
188
+ return '';
189
+ const dropCalls = this.checkConstraints
190
+ .map(constraint => ` await DreamMigrationHelpers.dropConstraint(db, '${constraint.name}', { table: '${this.table}' })`)
191
+ .join('\n');
192
+ const them = this.checkConstraints.length === 1 ? 'it' : 'them';
193
+ return `If one of the check constraints above is the blocker, drop ${them} before calling dropEnumValue,
194
+ then re-add the ones that should survive. dropEnumValue deliberately does not do this for you:
195
+ only your migration knows which of these three outcomes each constraint needs.
196
+
197
+ 1. a constraint that exists only to forbid '${this.value}' should be dropped and never re-added
198
+ 2. a constraint that never mentions '${this.value}' should be re-added exactly as it was
199
+ 3. a constraint that mentions '${this.value}' alongside other values should be re-added with
200
+ '${this.value}' struck from it
201
+
202
+ Drop ${them} with:
203
+
204
+ ${dropCalls}
205
+
206
+ If dropping ${them} changes nothing, the blocker is one of the other expressions above.`;
207
+ }
208
+ }
@@ -0,0 +1,16 @@
1
+ export default class MissingRequiredLockOptionForUpdateCallback extends Error {
2
+ get message() {
3
+ return `
4
+ When passing a callback to Query#update, the options object and its \`lock\`
5
+ key are required, and \`lock\` must be an explicit boolean:
6
+
7
+ await User.query().update(user => ({ ... }), { lock: true })
8
+ await User.query().update(user => ({ ... }), { lock: false })
9
+
10
+ \`lock: true\` runs the callback on each record after re-selecting it under an
11
+ exclusive row lock (a compare-then-write). \`lock: false\` actively opts out of
12
+ locking: the callback runs on each record with no lock and no compare-and-set
13
+ guarantee. Requiring the boolean keeps that choice visible at the call site.
14
+ `;
15
+ }
16
+ }
@@ -1,8 +1,22 @@
1
+ import serializerResolutionContextMessage from './serializerResolutionContextMessage.js';
1
2
  export default class MissingSerializersDefinition extends Error {
2
3
  viewModel;
3
- constructor(viewModel) {
4
+ /**
5
+ * ECMAScript-private (`#`) rather than TypeScript-`private`, unlike the four internal siblings of
6
+ * this error. A TypeScript `private` parameter property is a real, enumerable own property at
7
+ * runtime, and this is the one serializer-resolution error the package exports
8
+ * (`src/package-exports/errors.ts`) — so `Object.keys(err)` and `JSON.stringify(err)` on a
9
+ * consumer's error-logging path would newly report the diagnostic state threaded in here. `#`
10
+ * fields are invisible to both, so the exported error's observable runtime shape is unchanged.
11
+ * `viewModel` keeps its existing form: it predates this and narrowing it is not this change.
12
+ */
13
+ #serializerKey;
14
+ #resolutionContext;
15
+ constructor(viewModel, serializerKey, resolutionContext) {
4
16
  super();
5
17
  this.viewModel = viewModel;
18
+ this.#serializerKey = serializerKey;
19
+ this.#resolutionContext = resolutionContext;
6
20
  }
7
21
  get message() {
8
22
  const className = this.viewModel?.sanitizedConstructorName ?? this.viewModel.constructor.name;
@@ -18,6 +32,6 @@ class ${className} {
18
32
  }
19
33
  }
20
34
  ...
21
- }`;
35
+ }${serializerResolutionContextMessage(this.#serializerKey, this.#resolutionContext)}`;
22
36
  }
23
37
  }
@@ -1,10 +1,13 @@
1
+ import serializerResolutionContextMessage from './serializerResolutionContextMessage.js';
1
2
  export default class MissingSerializersDefinitionForKey extends Error {
2
3
  viewModel;
3
4
  serializerKey;
4
- constructor(viewModel, serializerKey) {
5
+ resolutionContext;
6
+ constructor(viewModel, serializerKey, resolutionContext) {
5
7
  super();
6
8
  this.viewModel = viewModel;
7
9
  this.serializerKey = serializerKey;
10
+ this.resolutionContext = resolutionContext;
8
11
  }
9
12
  get message() {
10
13
  const className = this.viewModel?.sanitizedConstructorName ?? this.viewModel.constructor.name;
@@ -21,6 +24,6 @@ class ${className} {
21
24
  }
22
25
  }
23
26
  ...
24
- }`;
27
+ }${serializerResolutionContextMessage(this.serializerKey, this.resolutionContext)}`;
25
28
  }
26
29
  }
@@ -1,18 +1,20 @@
1
+ import serializerResolutionContextMessage from './serializerResolutionContextMessage.js';
1
2
  export default class NoGlobalSerializerForSpecifiedKey extends Error {
2
3
  viewModel;
3
4
  serializerKey;
4
5
  globalName;
5
- constructor(viewModel, serializerKey, globalName) {
6
+ resolutionContext;
7
+ constructor(viewModel, serializerKey, globalName, resolutionContext) {
6
8
  super();
7
9
  this.viewModel = viewModel;
8
10
  this.serializerKey = serializerKey;
9
11
  this.globalName = globalName;
12
+ this.resolutionContext = resolutionContext;
10
13
  }
11
14
  get message() {
12
15
  const className = this.viewModel?.sanitizedConstructorName ?? this.viewModel.constructor.name;
13
16
  return `
14
17
  ${className} specified a global name of "${this.globalName}" for serializer key "${this.serializerKey}",
15
- but no serializer corresponds to "${this.globalName}".
16
- `;
18
+ but no serializer corresponds to "${this.globalName}".${serializerResolutionContextMessage(this.serializerKey, this.resolutionContext)}`;
17
19
  }
18
20
  }
@@ -1,3 +1,4 @@
1
+ import serializerResolutionContextMessage from './serializerResolutionContextMessage.js';
1
2
  /**
2
3
  * Raised when resolving a serializer key against a class yields an empty set of serializers.
3
4
  *
@@ -11,10 +12,12 @@
11
12
  export default class NoSerializersResolvedForKey extends Error {
12
13
  classDef;
13
14
  serializerKey;
14
- constructor(classDef, serializerKey) {
15
+ resolutionContext;
16
+ constructor(classDef, serializerKey, resolutionContext) {
15
17
  super();
16
18
  this.classDef = classDef;
17
19
  this.serializerKey = serializerKey;
20
+ this.resolutionContext = resolutionContext;
18
21
  }
19
22
  get message() {
20
23
  const className = this.classDef === null || this.classDef === undefined
@@ -25,6 +28,6 @@ No serializers resolved for \`${className}\` under serializer key \`${this.seria
25
28
 
26
29
  This should be impossible — a serializer key that cannot be resolved throws while it is being
27
30
  resolved, before reaching here — and indicates a bug in Dream rather than a problem with your
28
- application. Please report it.`;
31
+ application. Please report it.${serializerResolutionContextMessage(this.serializerKey, this.resolutionContext)}`;
29
32
  }
30
33
  }
@@ -1,15 +1,18 @@
1
1
  import { inspect } from 'node:util';
2
+ import serializerResolutionContextMessage from './serializerResolutionContextMessage.js';
2
3
  export default class NonDreamSerializerDerivedFromGlobalSerializerForSpecifiedKey extends Error {
3
4
  viewModel;
4
5
  serializerKey;
5
6
  globalName;
6
7
  serializer;
7
- constructor(viewModel, serializerKey, globalName, serializer) {
8
+ resolutionContext;
9
+ constructor(viewModel, serializerKey, globalName, serializer, resolutionContext) {
8
10
  super();
9
11
  this.viewModel = viewModel;
10
12
  this.serializerKey = serializerKey;
11
13
  this.globalName = globalName;
12
14
  this.serializer = serializer;
15
+ this.resolutionContext = resolutionContext;
13
16
  }
14
17
  get message() {
15
18
  const className = this.viewModel?.sanitizedConstructorName ?? this.viewModel.constructor.name;
@@ -17,7 +20,6 @@ export default class NonDreamSerializerDerivedFromGlobalSerializerForSpecifiedKe
17
20
  ${className} specified a global name of "${this.globalName}" for serializer key "${this.serializerKey}",
18
21
  but the derived serializer is not a Dream serializer:
19
22
 
20
- ${inspect(this.serializer)}
21
- `;
23
+ ${inspect(this.serializer)}${serializerResolutionContextMessage(this.serializerKey, this.resolutionContext)}`;
22
24
  }
23
25
  }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @internal
3
+ *
4
+ * Renders the trailing "how did resolution reach this class" block shared by the five
5
+ * serializer-resolution errors.
6
+ *
7
+ * Every line is independently well-formed and a line is omitted entirely — no placeholder, no empty
8
+ * slot — when the path that raised the error does not have that fact. The paths differ, and
9
+ * deliberately so:
10
+ *
11
+ * - the serializer key is on every path, since it is an argument of resolution itself;
12
+ * - the render edge is present only when a `rendersOne`/`rendersMany` led to the failing class. The
13
+ * three root entry points (`Dream.serializationMap`, `Dream.displaySerialization` and
14
+ * `buildSerializerPreloadPaths`) resolve a class the caller named directly, so nothing led there;
15
+ * - the declaring serializer is present only while preload paths and serialization maps are built.
16
+ * `SerializerRenderer` holds the rendered attribute but not the serializer that declared it;
17
+ * - the STI base is present only where expanding one produced the class the error names.
18
+ */
19
+ export default function serializerResolutionContextMessage(serializerKey, context) {
20
+ const lines = [];
21
+ if (serializerKey !== undefined)
22
+ lines.push(['serializer key', serializerKey]);
23
+ const edge = context?.edge;
24
+ if (edge)
25
+ lines.push([
26
+ 'reached through',
27
+ edge.declaredBy
28
+ ? `${edge.type} \`${edge.associationName}\`, declared by ${edge.declaredBy}`
29
+ : `${edge.type} \`${edge.associationName}\``,
30
+ ]);
31
+ if (context?.stiBase)
32
+ lines.push(['expanded from', `the STI base ${context.stiBase}`]);
33
+ if (lines.length === 0)
34
+ return '';
35
+ return `
36
+
37
+ Serializer resolution context:
38
+
39
+ ${lines.map(([label, value]) => ` ${`${label}:`.padEnd(LABEL_COLUMN_WIDTH)}${value}`).join('\n')}`;
40
+ }
41
+ /**
42
+ * Fixed rather than computed from the lines actually present, so that the same fact sits in the
43
+ * same column in every one of these errors — a reader comparing two failures is not also comparing
44
+ * two layouts.
45
+ */
46
+ const LABEL_COLUMN_WIDTH = 'reached through: '.length;
@@ -54,7 +54,13 @@ export default function generateFactoryContent({ fullyQualifiedModelName, column
54
54
  belongsToNames.push(attributeVariable);
55
55
  belongsToTypedNames.push(`${attributeVariable}: ${associationModelName}`);
56
56
  additionalImports.push(associationFactoryImportStatement);
57
- associationCreationStatements.push(`${attributeVariable}: attrs.${attributeVariable} ? null : await create${associationModelName}(),`);
57
+ // The parens around `await` are what Prettier itself emits for an
58
+ // `await` operand of `??` (it parenthesizes unconditionally), and this
59
+ // string is written to the consumer's file verbatim — no formatting
60
+ // pass — so omitting them makes every freshly generated factory fail
61
+ // `prettier --check` in their project. See the Prettier fixed-point
62
+ // specs in spec/unit/cli/generateFactoryContent.spec.ts.
63
+ associationCreationStatements.push(`${attributeVariable}: attrs.${attributeVariable} ?? (await create${associationModelName}()),`);
58
64
  break;
59
65
  }
60
66
  case 'encrypted':
@@ -175,19 +175,8 @@ export default function generateMigrationContent({ connectionName = 'default', t
175
175
  }
176
176
  return acc;
177
177
  }, { columnDefs: [], columnDrops: [], indexDefs: [] });
178
- if (!table) {
179
- return `\
180
- import { Kysely, sql } from 'kysely'
181
-
182
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
183
- export async function up(db: Kysely<any>): Promise<void> {
184
- }
185
-
186
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
187
- export async function down(db: Kysely<any>): Promise<void> {
188
- }\
189
- `;
190
- }
178
+ if (!table)
179
+ return emptyMigrationContent();
191
180
  // An alter migration (either `-to-`/add or `-from-`/remove) with no valid
192
181
  // columns to add/drop would otherwise emit a bare `.alterTable(...).execute()`
193
182
  // with no column operation at all, which Postgres rejects at migration-run
@@ -207,6 +196,22 @@ export async function down(db: Kysely<any>): Promise<void> {
207
196
  !stiChildClassName) {
208
197
  throw new NoColumnsToAlterMigration(table, alterDirection, migrationName, alterMarker);
209
198
  }
199
+ // The placeholder table is the standalone `g:migration` markerless path (see
200
+ // MIGRATION_TABLE_NAME_PLACEHOLDER above): the migration name resolved no
201
+ // table, so there is nothing to alter. With no columns declared either, there
202
+ // is no column operation to scaffold, so emit genuinely empty `up`/`down`
203
+ // bodies for the developer to fill in rather than an
204
+ // `alterTable('<table-name>')` statement that cannot run as generated.
205
+ //
206
+ // The test is `columnsWithTypes`, deliberately, and not `columnDefs`: the two
207
+ // diverge. A declaration the reduce above filters out entirely (e.g.
208
+ // `comments:has_many`) leaves `columnDefs` empty while the user did type a
209
+ // column, and emptying the bodies there would silently discard it. Markerless
210
+ // *with* columns therefore keeps the placeholder scaffold — the table name is
211
+ // the only part the generator cannot work out, and `g:migration`'s help
212
+ // already documents that case as a stub to hand-edit.
213
+ if (altering && table === MIGRATION_TABLE_NAME_PLACEHOLDER && columnsWithTypes.length === 0)
214
+ return emptyMigrationContent();
210
215
  const citextExtension = requireCitextExtension
211
216
  ? ` await DreamMigrationHelpers.createExtension(db, 'citext')\n\n`
212
217
  : '';
@@ -273,6 +278,68 @@ ${downBody}
273
278
  }\
274
279
  `;
275
280
  }
281
+ /**
282
+ * A migration file with nothing in it: correct imports, the standard `up`/`down`
283
+ * signatures, and empty bodies.
284
+ *
285
+ * Three deliberate departures from the scaffolded migrations above. All three
286
+ * exist so that the file this generator writes is clean the instant it lands in
287
+ * a consumer's project — under their formatter as well as their linter. See
288
+ * `spec/unit/cli/generateMigrationContent.spec.ts`
289
+ * ("the generated empty migration is clean under a consumer's tooling"), which
290
+ * runs this exact output through Prettier and through ESLint's
291
+ * `recommended` + `typescript-eslint recommended` and pins zero of both.
292
+ *
293
+ * - The bodies are `{}`, closed on the signature's own line, not `{\n}`, and
294
+ * the file ends in a newline. Prettier collapses an empty block onto one line
295
+ * and requires a final newline, so either spelling is a formatting violation
296
+ * the moment it is written and fails `prettier --check` in the consumer's CI
297
+ * (and this repo's own `pnpm lint`, which ends in `pnpm prettier . --check`);
298
+ * `writeGeneratedFile` writes this string byte-for-byte with no formatting
299
+ * pass. The final newline is why this template does not end with the
300
+ * backslash line-continuation the scaffolded return above uses. (Those
301
+ * scaffolds are missing their own final newline, which is pre-existing and
302
+ * pinned by ~90 `toEqual` expectations across three spec files; it is not
303
+ * this template's to fix. `generateFactoryContent` and
304
+ * `generateUnitSpecContent` already terminate their output the way this one
305
+ * now does.)
306
+ * - `sql` is left out of the import list. Every other generated migration
307
+ * imports it unconditionally because most bodies need it, but an empty body
308
+ * never does, and an unused import is an error under
309
+ * `@typescript-eslint/no-unused-vars` in a file that would otherwise have
310
+ * nothing to report.
311
+ * - `db` keeps its name — the developer's first keystroke in the body is going
312
+ * to be `db.`, and dropping the parameter (with the `Kysely` import that only
313
+ * it uses) would make them type both back — and the existing
314
+ * `no-explicit-any` directive is widened to disable `no-unused-vars` too.
315
+ * `db` is unused until the body is written, and `no-unused-vars` is
316
+ * *error*-level under `typescript-eslint`'s `recommended` with
317
+ * `args: 'after-used'`, so without the directive `psy g:migration <name> &&
318
+ * pnpm lint` fails on a brand-new, untouched file. (The `await`-less `async`
319
+ * needs nothing: `require-await` deliberately ignores empty function bodies.)
320
+ * An `_db` rename is not a substitute: `no-unused-vars` sets no
321
+ * `argsIgnorePattern` by default, so `_db` reports too, and the developer
322
+ * then has to rename it back.
323
+ *
324
+ * The cost of the widened directive is that the `no-unused-vars` half goes
325
+ * stale once the body uses `db`, which ESLint 9 reports via its default
326
+ * `reportUnusedDisableDirectives`. That is a *warning* rather than the two
327
+ * *errors* it replaces, it lands in the file the developer is already
328
+ * editing, and `eslint --fix` removes just that rule name from the directive
329
+ * and leaves `no-explicit-any` in place — so it is strictly the cheaper of
330
+ * the two, in severity and in effort.
331
+ */
332
+ function emptyMigrationContent() {
333
+ return `\
334
+ import { Kysely } from 'kysely'
335
+
336
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
337
+ export async function up(db: Kysely<any>): Promise<void> {}
338
+
339
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
340
+ export async function down(db: Kysely<any>): Promise<void> {}
341
+ `;
342
+ }
276
343
  function getAttributeType(attributeType, descriptors) {
277
344
  switch (attributeType) {
278
345
  case 'string':
@@ -0,0 +1,41 @@
1
+ import DreamCLI from '../../cli/index.js';
2
+ /**
3
+ * Runs the type/schema sync a command performs after its own work has already
4
+ * succeeded, and — when that sync fails — logs a message naming what completed
5
+ * and naming the sync as the step that failed, before re-raising the original
6
+ * error untouched.
7
+ *
8
+ * Without this, a failing sync surfaces a bare sync error immediately after
9
+ * e.g. `db:rollback`, which reads as a failed rollback even though the
10
+ * rollback itself is already committed.
11
+ *
12
+ * The original error is re-thrown rather than wrapped, so the error a caller
13
+ * sees, and the command's non-zero exit status, are exactly what they were
14
+ * before. Callers keep their own `EnvInternal.isTest` / `--skip-sync` guards:
15
+ * this wrapper takes the sync call, not the decision to make it.
16
+ */
17
+ export default async function syncWithFailureContext({ commandName, completedWork, didNotRun, recoveryAdvice }, sync) {
18
+ try {
19
+ await sync();
20
+ }
21
+ catch (error) {
22
+ DreamCLI.logger.log(syncFailureMessage({ commandName, completedWork, didNotRun, recoveryAdvice }));
23
+ throw error;
24
+ }
25
+ }
26
+ /**
27
+ * @internal
28
+ *
29
+ * Builds the explanatory message logged when a post-work sync fails.
30
+ */
31
+ export function syncFailureMessage({ commandName, completedWork, didNotRun, recoveryAdvice, }) {
32
+ const skipped = didNotRun ? `; ${didNotRun}` : '';
33
+ const recovery = recoveryAdvice ?? 'run `sync` on its own';
34
+ return `
35
+ ${commandName}: the sync step failed — not ${commandName}'s own work.
36
+
37
+ ${completedWork}. What failed afterward is the sync that ${commandName} runs to regenerate the auto-generated type and schema files${skipped}.
38
+
39
+ The sync error itself is printed alongside this message. Once it is resolved, ${recovery}.
40
+ `;
41
+ }
@@ -97,7 +97,7 @@ export default class SerializerRenderer {
97
97
  const associatedObject = data[attribute.name];
98
98
  let serializer = null;
99
99
  if (associatedObject) {
100
- serializer = serializerForAssociatedObject(associatedObject, attribute.options);
100
+ serializer = serializerForAssociatedObject(associatedObject, attribute.options, renderTimeResolutionContext(attribute.options, 'rendersOne', attribute.name));
101
101
  }
102
102
  else if (attribute.options.flatten) {
103
103
  /**
@@ -146,8 +146,12 @@ export default class SerializerRenderer {
146
146
  const associatedObjects = data[attribute.name];
147
147
  if (!associatedObjects)
148
148
  throw new RendersManyMustReceiveArray(attribute, associatedObjects);
149
+ // Hoisted out of the map: the context is identical for every element, is immutable, and is
150
+ // read only while an error message is being built, so one object serves the whole
151
+ // collection rather than one per element.
152
+ const resolutionContext = renderTimeResolutionContext(attribute.options, 'rendersMany', attribute.name);
149
153
  accumulator[outputAttributeName] = compact(associatedObjects).map(associatedObject => {
150
- const serializer = serializerForAssociatedObject(associatedObject, attribute.options);
154
+ const serializer = serializerForAssociatedObject(associatedObject, attribute.options, resolutionContext);
151
155
  return (
152
156
  // passthrough data going into the serializer is the argument that gets
153
157
  // used in the custom attribute callback function
@@ -206,8 +210,32 @@ function _applyRenderingOptionsToAttribute(data, value, attributeName, options,
206
210
  return round(value, precision);
207
211
  return value ?? null;
208
212
  }
209
- function serializerForAssociatedObject(associatedObject, options) {
213
+ /**
214
+ * `resolutionContext` is error-only: it names the rendersOne/rendersMany this object is being
215
+ * rendered through, so that a serializer that cannot be resolved for it says which association led
216
+ * here rather than only which class failed. It is `undefined` whenever no resolution will happen —
217
+ * see `renderTimeResolutionContext`.
218
+ */
219
+ function serializerForAssociatedObject(associatedObject, options, resolutionContext) {
210
220
  if (options.serializer)
211
221
  return options.serializer;
212
- return inferSerializerFromDreamOrViewModel(associatedObject, options.serializerKey);
222
+ return inferSerializerFromDreamOrViewModel(associatedObject, options.serializerKey, resolutionContext);
223
+ }
224
+ /**
225
+ * Builds the error-only resolution context for one rendered association, or `undefined` when the
226
+ * render will not resolve a serializer at all.
227
+ *
228
+ * Rendering is a hot path — once per rendered association per request, and `rendersMany` renders one
229
+ * element at a time from a collection — so this deliberately allocates nothing it cannot use:
230
+ * `serializerForAssociatedObject` returns `options.serializer` before resolving anything when the
231
+ * association declares its own serializer, and in that case there is no resolution to describe.
232
+ * Callers build this once per rendered association and share it across every element.
233
+ *
234
+ * The renderer holds no serializer object, so the edge carries no `declaredBy` — see
235
+ * `SerializerResolutionContext`.
236
+ */
237
+ function renderTimeResolutionContext(options, type, associationName) {
238
+ if (options.serializer)
239
+ return undefined;
240
+ return { edge: { type, associationName } };
213
241
  }