@rvoh/dream 2.25.0 → 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 (235) 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/internal/printSerializerHierarchyLevel.js +4 -3
  5. package/dist/cjs/src/dream/internal/resolveSerializerAssociationEdges.js +14 -1
  6. package/dist/cjs/src/errors/DropEnumValueRetypeFailed.js +208 -0
  7. package/dist/cjs/src/errors/serializers/MissingSerializersDefinition.js +16 -2
  8. package/dist/cjs/src/errors/serializers/MissingSerializersDefinitionForKey.js +5 -2
  9. package/dist/cjs/src/errors/serializers/NoGlobalSerializerForSpecifiedKey.js +5 -3
  10. package/dist/cjs/src/errors/serializers/NoSerializersResolvedForKey.js +5 -2
  11. package/dist/cjs/src/errors/serializers/NonDreamSerializerDerivedFromGlobalSerializerForSpecifiedKey.js +5 -3
  12. package/dist/cjs/src/errors/serializers/serializerResolutionContextMessage.js +46 -0
  13. package/dist/cjs/src/helpers/cli/generateFactoryContent.js +7 -1
  14. package/dist/cjs/src/helpers/cli/generateMigrationContent.js +80 -13
  15. package/dist/cjs/src/helpers/cli/syncWithFailureContext.js +41 -0
  16. package/dist/cjs/src/serializer/SerializerRenderer.js +32 -4
  17. package/dist/cjs/src/serializer/helpers/inferSerializerFromDreamOrViewModel.js +28 -10
  18. package/dist/cjs/src/serializer/helpers/serializerForAssociatedClass.js +6 -1
  19. package/dist/cjs/src/serializer/helpers/serializerGlobalName.js +16 -0
  20. package/dist/esm/src/Dream.js +10 -1
  21. package/dist/esm/src/cli/index.js +31 -5
  22. package/dist/esm/src/db/migration-helpers/DreamMigrationHelpers.js +127 -6
  23. package/dist/esm/src/dream/internal/printSerializerHierarchyLevel.js +4 -3
  24. package/dist/esm/src/dream/internal/resolveSerializerAssociationEdges.js +14 -1
  25. package/dist/esm/src/errors/DropEnumValueRetypeFailed.js +208 -0
  26. package/dist/esm/src/errors/serializers/MissingSerializersDefinition.js +16 -2
  27. package/dist/esm/src/errors/serializers/MissingSerializersDefinitionForKey.js +5 -2
  28. package/dist/esm/src/errors/serializers/NoGlobalSerializerForSpecifiedKey.js +5 -3
  29. package/dist/esm/src/errors/serializers/NoSerializersResolvedForKey.js +5 -2
  30. package/dist/esm/src/errors/serializers/NonDreamSerializerDerivedFromGlobalSerializerForSpecifiedKey.js +5 -3
  31. package/dist/esm/src/errors/serializers/serializerResolutionContextMessage.js +46 -0
  32. package/dist/esm/src/helpers/cli/generateFactoryContent.js +7 -1
  33. package/dist/esm/src/helpers/cli/generateMigrationContent.js +80 -13
  34. package/dist/esm/src/helpers/cli/syncWithFailureContext.js +41 -0
  35. package/dist/esm/src/serializer/SerializerRenderer.js +32 -4
  36. package/dist/esm/src/serializer/helpers/inferSerializerFromDreamOrViewModel.js +28 -10
  37. package/dist/esm/src/serializer/helpers/serializerForAssociatedClass.js +6 -1
  38. package/dist/esm/src/serializer/helpers/serializerGlobalName.js +16 -0
  39. package/dist/types/src/Dream.d.ts +8 -2
  40. package/dist/types/src/cli/index.d.ts +1 -1
  41. package/dist/types/src/errors/DropEnumValueRetypeFailed.d.ts +89 -0
  42. package/dist/types/src/errors/serializers/MissingSerializersDefinition.d.ts +3 -1
  43. package/dist/types/src/errors/serializers/MissingSerializersDefinitionForKey.d.ts +3 -1
  44. package/dist/types/src/errors/serializers/NoGlobalSerializerForSpecifiedKey.d.ts +3 -1
  45. package/dist/types/src/errors/serializers/NoSerializersResolvedForKey.d.ts +3 -1
  46. package/dist/types/src/errors/serializers/NonDreamSerializerDerivedFromGlobalSerializerForSpecifiedKey.d.ts +3 -1
  47. package/dist/types/src/errors/serializers/serializerResolutionContextMessage.d.ts +20 -0
  48. package/dist/types/src/helpers/cli/syncWithFailureContext.d.ts +58 -0
  49. package/dist/types/src/serializer/helpers/inferSerializerFromDreamOrViewModel.d.ts +11 -4
  50. package/dist/types/src/serializer/helpers/serializerGlobalName.d.ts +15 -0
  51. package/dist/types/src/types/serializer.d.ts +38 -0
  52. package/dist/types/src/types/types/serializer.ts +40 -0
  53. package/docs/assets/search.js +1 -1
  54. package/docs/classes/db.DreamMigrationHelpers.html +11 -11
  55. package/docs/classes/db.KyselyQueryDriver.html +40 -40
  56. package/docs/classes/db.PostgresQueryDriver.html +41 -41
  57. package/docs/classes/db.QueryDriverBase.html +39 -39
  58. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  59. package/docs/classes/errors.ColumnOverflow.html +3 -3
  60. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  61. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  62. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  63. package/docs/classes/errors.DecryptionError.html +2 -2
  64. package/docs/classes/errors.DecryptionParseError.html +2 -2
  65. package/docs/classes/errors.DecryptionRotationError.html +3 -3
  66. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  67. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  68. package/docs/classes/errors.InvalidClockTime.html +2 -2
  69. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  70. package/docs/classes/errors.InvalidDateTime.html +2 -2
  71. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  72. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  73. package/docs/classes/errors.NotNullViolation.html +3 -3
  74. package/docs/classes/errors.RecordNotFound.html +3 -3
  75. package/docs/classes/errors.ValidationError.html +3 -3
  76. package/docs/classes/index.CalendarDate.html +33 -33
  77. package/docs/classes/index.ClockTime.html +32 -32
  78. package/docs/classes/index.ClockTimeTz.html +35 -35
  79. package/docs/classes/index.DateTime.html +86 -86
  80. package/docs/classes/index.Decorators.html +19 -19
  81. package/docs/classes/index.Dream.html +123 -137
  82. package/docs/classes/index.DreamApp.html +10 -10
  83. package/docs/classes/index.DreamTransaction.html +2 -2
  84. package/docs/classes/index.Env.html +2 -2
  85. package/docs/classes/index.Query.html +64 -64
  86. package/docs/classes/system.CliFileWriter.html +4 -4
  87. package/docs/classes/system.DreamBin.html +2 -2
  88. package/docs/classes/system.DreamCLI.html +7 -7
  89. package/docs/classes/system.DreamImporter.html +2 -2
  90. package/docs/classes/system.DreamLogos.html +2 -2
  91. package/docs/classes/system.DreamSerializerBuilder.html +11 -11
  92. package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
  93. package/docs/classes/system.PathHelpers.html +3 -3
  94. package/docs/classes/utils.Encrypt.html +3 -3
  95. package/docs/classes/utils.Range.html +2 -2
  96. package/docs/functions/db.closeAllDbConnections.html +1 -1
  97. package/docs/functions/db.dreamDbConnections.html +1 -1
  98. package/docs/functions/db.untypedDb.html +1 -1
  99. package/docs/functions/db.validateColumn.html +1 -1
  100. package/docs/functions/db.validateTable.html +1 -1
  101. package/docs/functions/errors.pgErrorType.html +1 -1
  102. package/docs/functions/index.DreamSerializer.html +1 -1
  103. package/docs/functions/index.ObjectSerializer.html +1 -1
  104. package/docs/functions/index.ReplicaSafe.html +1 -1
  105. package/docs/functions/index.STI.html +1 -1
  106. package/docs/functions/index.SoftDelete.html +1 -1
  107. package/docs/functions/utils.camelize.html +1 -1
  108. package/docs/functions/utils.capitalize.html +1 -1
  109. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  110. package/docs/functions/utils.compact.html +1 -1
  111. package/docs/functions/utils.groupBy.html +1 -1
  112. package/docs/functions/utils.hyphenize.html +1 -1
  113. package/docs/functions/utils.intersection.html +1 -1
  114. package/docs/functions/utils.isEmpty.html +1 -1
  115. package/docs/functions/utils.normalizeUnicode.html +1 -1
  116. package/docs/functions/utils.pascalize.html +1 -1
  117. package/docs/functions/utils.percent.html +1 -1
  118. package/docs/functions/utils.range.html +1 -1
  119. package/docs/functions/utils.round.html +1 -1
  120. package/docs/functions/utils.sanitizeString.html +1 -1
  121. package/docs/functions/utils.snakeify.html +1 -1
  122. package/docs/functions/utils.sort.html +1 -1
  123. package/docs/functions/utils.sortBy.html +1 -1
  124. package/docs/functions/utils.sortObjectByKey.html +1 -1
  125. package/docs/functions/utils.sortObjectByValue.html +1 -1
  126. package/docs/functions/utils.uncapitalize.html +1 -1
  127. package/docs/functions/utils.uniq.html +1 -1
  128. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  129. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  130. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  131. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  132. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  133. package/docs/interfaces/types.DecoratorContext.html +2 -2
  134. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  135. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  136. package/docs/interfaces/types.DreamDbConfig.html +5 -5
  137. package/docs/interfaces/types.DurationObject.html +2 -2
  138. package/docs/interfaces/types.EncryptOptions.html +2 -2
  139. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  140. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  141. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  142. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  143. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  144. package/docs/types/openapi.OpenapiFormats.html +1 -1
  145. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  146. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  147. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  148. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  149. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  150. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  151. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  152. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  153. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  154. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
  155. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
  156. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
  157. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  158. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  159. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  160. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  161. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  162. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  163. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  164. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  165. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
  166. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
  167. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  168. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  169. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
  170. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
  171. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  172. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  173. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
  174. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
  175. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
  176. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  177. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  178. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  179. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  180. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  181. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  182. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  183. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  184. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  185. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  186. package/docs/types/types.CalendarDateObject.html +1 -1
  187. package/docs/types/types.Camelized.html +1 -1
  188. package/docs/types/types.ClockTimeObject.html +1 -1
  189. package/docs/types/types.DbConnectionType.html +1 -1
  190. package/docs/types/types.DbTypes.html +1 -1
  191. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  192. package/docs/types/types.DreamAttributes.html +1 -1
  193. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  194. package/docs/types/types.DreamClassColumn.html +1 -1
  195. package/docs/types/types.DreamColumn.html +1 -1
  196. package/docs/types/types.DreamColumnNames.html +1 -1
  197. package/docs/types/types.DreamLogLevel.html +1 -1
  198. package/docs/types/types.DreamLogger.html +2 -2
  199. package/docs/types/types.DreamModelSerializerType.html +1 -1
  200. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  201. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  202. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  203. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  204. package/docs/types/types.DreamSerializable.html +1 -1
  205. package/docs/types/types.DreamSerializableArray.html +1 -1
  206. package/docs/types/types.DreamSerializerKey.html +1 -1
  207. package/docs/types/types.DreamSerializers.html +1 -1
  208. package/docs/types/types.DreamVirtualColumns.html +1 -1
  209. package/docs/types/types.DurationUnit.html +1 -1
  210. package/docs/types/types.EncryptAlgorithm.html +1 -1
  211. package/docs/types/types.HasManyStatement.html +1 -1
  212. package/docs/types/types.HasOneStatement.html +1 -1
  213. package/docs/types/types.Hyphenized.html +1 -1
  214. package/docs/types/types.Pascalized.html +1 -1
  215. package/docs/types/types.PrimaryKeyType.html +1 -1
  216. package/docs/types/types.RoundingPrecision.html +1 -1
  217. package/docs/types/types.SerializerCasing.html +1 -1
  218. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  219. package/docs/types/types.Snakeified.html +1 -1
  220. package/docs/types/types.StrictInterface.html +1 -1
  221. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  222. package/docs/types/types.UpdateableProperties.html +1 -1
  223. package/docs/types/types.ValidationType.html +1 -1
  224. package/docs/types/types.ViewModel.html +2 -2
  225. package/docs/types/types.ViewModelClass.html +1 -1
  226. package/docs/types/types.WeekdayName.html +1 -1
  227. package/docs/types/types.WhereStatementForDream.html +1 -1
  228. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  229. package/docs/variables/index.DreamConst.html +1 -1
  230. package/docs/variables/index.ops.html +1 -1
  231. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  232. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  233. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  234. package/docs/variables/system.primaryKeyTypes.html +1 -1
  235. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { sql } from 'kysely';
2
2
  import InternalEncrypt from '../../encrypt/InternalEncrypt.js';
3
+ import DropEnumValueRetypeFailed, { quotedRelationName, } from '../../errors/DropEnumValueRetypeFailed.js';
3
4
  export default class DreamMigrationHelpers {
4
5
  /**
5
6
  * Rename a table and its associated primary key index and sequence.
@@ -169,14 +170,29 @@ export default class DreamMigrationHelpers {
169
170
  * @param __namedParameters.replacements - Details about which table and column to change and which value to replace the dropped value with (or remove it if the column is an array)
170
171
  */
171
172
  static async dropEnumValue(db, { enumName, value, replacements }) {
173
+ // Read the check constraints on every replacement column up front, before anything is
174
+ // retyped. They are only ever *used* from the catch blocks below, but they cannot be read
175
+ // from there: dropEnumValue runs inside a migration transaction (runMigration.ts routes any
176
+ // migration mentioning it through `newTransaction`), and once a statement in a transaction
177
+ // block errors, Postgres aborts the block and fails every subsequent statement with
178
+ // `25P02 current transaction is aborted`. A pg_constraint SELECT issued from a catch block
179
+ // would therefore throw itself and bury the very error we are trying to explain.
180
+ const checkConstraintsByPosition = await checkConstraintsOnColumns(db, replacements);
172
181
  // temporarily set all table columns depending on this enum to an acceptable alternate type
173
- for (const tableAndColumnToChange of replacements) {
182
+ for (const [position, tableAndColumnToChange] of replacements.entries()) {
174
183
  const tableAndColumnToChangeAsArray = tableAndColumnToChange;
175
184
  const isArray = tableAndColumnToChangeAsArray.array || false;
176
- await db.schema
185
+ await retypeOrExplain(() => db.schema
177
186
  .alterTable(tableAndColumnToChange.table)
178
187
  .alterColumn(tableAndColumnToChange.column, col => col.setDataType(computedTemporaryType(isArray)))
179
- .execute();
188
+ .execute(), {
189
+ enumName,
190
+ value,
191
+ table: tableAndColumnToChange.table,
192
+ column: tableAndColumnToChange.column,
193
+ retypeDirection: 'toText',
194
+ ...constraintContextFor(checkConstraintsByPosition, position),
195
+ });
180
196
  }
181
197
  // collect enum values before dropping type
182
198
  const allEnumValues = await getEnumValues(db, enumName);
@@ -187,15 +203,23 @@ export default class DreamMigrationHelpers {
187
203
  .createType(enumName)
188
204
  .asEnum(allEnumValues.filter(val => val !== value))
189
205
  .execute();
190
- for (const tableAndColumnToChange of replacements) {
206
+ for (const [position, tableAndColumnToChange] of replacements.entries()) {
191
207
  const isArray = tableAndColumnToChange.array || false;
208
+ const retypeContext = {
209
+ enumName,
210
+ value,
211
+ table: tableAndColumnToChange.table,
212
+ column: tableAndColumnToChange.column,
213
+ retypeDirection: 'toEnum',
214
+ ...constraintContextFor(checkConstraintsByPosition, position),
215
+ };
192
216
  if (isArray) {
193
217
  await replaceArrayValues(db, value, tableAndColumnToChange);
194
- await updateTableColumnToNewEnumArrayType(db, enumName, tableAndColumnToChange);
218
+ await retypeOrExplain(() => updateTableColumnToNewEnumArrayType(db, enumName, tableAndColumnToChange), retypeContext);
195
219
  }
196
220
  else {
197
221
  await replaceNonArrayValues(db, value, tableAndColumnToChange);
198
- await updateTableColumnToNewEnumType(db, enumName, tableAndColumnToChange);
222
+ await retypeOrExplain(() => updateTableColumnToNewEnumType(db, enumName, tableAndColumnToChange), retypeContext);
199
223
  }
200
224
  }
201
225
  }
@@ -330,6 +354,103 @@ export default class DreamMigrationHelpers {
330
354
  }
331
355
  }
332
356
  }
357
+ /**
358
+ * Run one of dropEnumValue's internal column retypes, and on failure re-raise it as a
359
+ * {@link DropEnumValueRetypeFailed} naming the table, the column and the check constraints
360
+ * that were read before any retype ran. The original error is kept reachable underneath.
361
+ *
362
+ * A caught throwable is normalized to an `Error` rather than cast to one: the error's
363
+ * message getter dereferences `originalError.message`, so a driver (or a mock) that rejects
364
+ * with a non-Error would otherwise blow up inside the getter and destroy the very
365
+ * diagnostic this wrapping exists to produce.
366
+ */
367
+ async function retypeOrExplain(retype, context) {
368
+ try {
369
+ await retype();
370
+ }
371
+ catch (error) {
372
+ throw new DropEnumValueRetypeFailed({
373
+ ...context,
374
+ originalError: error instanceof Error ? error : new Error(String(error)),
375
+ });
376
+ }
377
+ }
378
+ /**
379
+ * The check constraints defined on each replacement's column, indexed by the replacement's
380
+ * position in `replacements` (both of dropEnumValue's loops walk the same array in order, so
381
+ * the position identifies a replacement exactly and no key string has to be kept in sync).
382
+ *
383
+ * Position, rather than the replacement object, because the same object may legitimately
384
+ * appear at two positions — `const r = {...}; replacements: [r, r]` — and a Map keyed on the
385
+ * object would collapse those two entries onto one, reporting each column's constraints twice
386
+ * and emitting a duplicated dropConstraint line for every one of them.
387
+ *
388
+ * One catalog query covers every replacement: the (relation, column) pairs are passed as two
389
+ * arrays and unnested `WITH ORDINALITY`, so each row carries the 1-based position of the
390
+ * replacement it belongs to. Issuing one query per replacement would cost a round trip per
391
+ * column on every successful `dropEnumValue`, and Kysely serializes them all on the
392
+ * migration's single connection.
393
+ *
394
+ * `relationResolved` is selected separately from the constraints, because "the table has no
395
+ * check constraints" and "the table name did not resolve" both produce zero constraint rows
396
+ * and the error message must not confuse the two.
397
+ *
398
+ * `to_regclass` rather than a `::regclass` cast so that an unknown table yields no rows
399
+ * instead of throwing, leaving the retype itself to report a missing table exactly as it
400
+ * does today. The name handed to it is quoted per segment, because Kysely quotes the
401
+ * identifiers it emits (`alterTable('MixedCase')` -> `"MixedCase"`) while an unquoted
402
+ * `to_regclass` argument case-folds and would silently miss every non-lowercase table.
403
+ *
404
+ * The selected aliases are deliberately single words: a `CamelCasePlugin` on the connection
405
+ * rewrites result keys, and `ord`/`resolved`/`name`/`definition` survive that rewrite
406
+ * unchanged.
407
+ */
408
+ async function checkConstraintsOnColumns(db, replacements) {
409
+ const byPosition = replacements.map(() => ({
410
+ relationResolved: false,
411
+ checkConstraints: [],
412
+ }));
413
+ if (replacements.length === 0)
414
+ return byPosition;
415
+ const relationNames = replacements.map(replacement => quotedRelationName(replacement.table));
416
+ const columnNames = replacements.map(replacement => replacement.column);
417
+ const response = await sql `
418
+ SELECT
419
+ t.ord::int AS ord,
420
+ (to_regclass(t.relname) IS NOT NULL) AS resolved,
421
+ con.name AS name,
422
+ con.definition AS definition
423
+ FROM unnest(${relationNames}::text[], ${columnNames}::text[])
424
+ WITH ORDINALITY AS t(relname, colname, ord)
425
+ LEFT JOIN LATERAL (
426
+ SELECT c.conname AS name, pg_get_constraintdef(c.oid) AS definition
427
+ FROM pg_constraint c
428
+ JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
429
+ WHERE c.contype = 'c'
430
+ AND c.conrelid = to_regclass(t.relname)
431
+ AND a.attname = t.colname
432
+ ) con ON true
433
+ ORDER BY t.ord, con.name
434
+ `.execute(db);
435
+ for (const row of response.rows) {
436
+ const entry = byPosition[Number(row.ord) - 1];
437
+ if (entry === undefined)
438
+ continue;
439
+ entry.relationResolved = row.resolved;
440
+ if (row.name !== null && row.definition !== null) {
441
+ entry.checkConstraints.push({ name: row.name, definition: row.definition });
442
+ }
443
+ }
444
+ return byPosition;
445
+ }
446
+ /**
447
+ * The constraint metadata for the replacement at `position`. A position with no entry (which
448
+ * cannot happen today, but would be a silent lie if it did) reads as "not read", never as
449
+ * "none defined".
450
+ */
451
+ function constraintContextFor(byPosition, position) {
452
+ return byPosition[position] ?? { relationResolved: false, checkConstraints: [] };
453
+ }
333
454
  async function getEnumValues(db, enumName) {
334
455
  const response = await sql `SELECT unnest(enum_range(NULL::${sql.raw(enumName)}))`.execute(db);
335
456
  return response.rows.map(row => row.unnest);
@@ -1,5 +1,6 @@
1
1
  import yoctocolors from 'yoctocolors';
2
2
  import { indent } from '../../helpers/indent.js';
3
+ import serializerGlobalName from '../../serializer/helpers/serializerGlobalName.js';
3
4
  /**
4
5
  * @internal
5
6
  *
@@ -30,10 +31,10 @@ export default function printSerializerHierarchyLevel({ targetSerializers, forDi
30
31
  // eslint-disable-next-line no-console
31
32
  console.log(indentation + `${prefix}${type} ${yoctocolors.cyan(serializerAssociationName)}`);
32
33
  serializers.forEach(serializer => {
34
+ // Interpolated rather than defaulted so the printed line is unchanged for a serializer with no
35
+ // globalName; naming inline serializers in this tree is its own change.
33
36
  // eslint-disable-next-line no-console
34
- console.log(yoctocolors.gray(indentation +
35
- indent(prefix.length, { tabWidth: 1 }) +
36
- serializer.globalName));
37
+ console.log(yoctocolors.gray(indentation + indent(prefix.length, { tabWidth: 1 }) + `${serializerGlobalName(serializer)}`));
37
38
  });
38
39
  });
39
40
  }
@@ -1,5 +1,6 @@
1
1
  import compact from '../../helpers/compact.js';
2
2
  import { inferSerializersFromDreamClassOrViewModelClass } from '../../serializer/helpers/inferSerializerFromDreamOrViewModel.js';
3
+ import serializerGlobalName from '../../serializer/helpers/serializerGlobalName.js';
3
4
  /**
4
5
  * Keyed on the two arguments. Weak on both, so an entry is collectable once either the Dream class
5
6
  * or the serializer is. The cached arrays are handed out by reference and must be treated as
@@ -102,7 +103,19 @@ function buildSerializerAssociationEdges(dreamClass, serializer) {
102
103
  // nothing wrong with it.
103
104
  const serializers = serializerAssociation.options.serializer
104
105
  ? compact([serializerAssociation.options.serializer])
105
- : compact(inferSerializersFromDreamClassOrViewModelClass(associatedClass, serializerAssociation.options.serializerKey));
106
+ : compact(inferSerializersFromDreamClassOrViewModelClass(associatedClass, serializerAssociation.options.serializerKey,
107
+ // Error-only, and derived entirely from this function's two arguments, so nothing
108
+ // route-dependent is introduced: it never enters the memo key in
109
+ // `resolveSerializerAssociationEdges` and is never stored in the returned edges.
110
+ {
111
+ edge: {
112
+ type: serializerAssociation.type,
113
+ associationName: serializerAssociationName,
114
+ // Undefined for a serializer built inline, which is never stamped with a
115
+ // globalName — the message simply drops the declarer.
116
+ declaredBy: serializerGlobalName(serializer),
117
+ },
118
+ }));
106
119
  return serializers.map(associatedSerializer => ({
107
120
  dreamClass: associatedClass,
108
121
  serializer: associatedSerializer,
@@ -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
+ }
@@ -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':