@rvoh/dream 2.37.0 → 2.39.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 (213) hide show
  1. package/dist/cjs/src/dream-app/index.js +6 -0
  2. package/dist/cjs/src/serializer/DreamSerializer.js +23 -0
  3. package/dist/cjs/src/serializer/ObjectSerializer.js +22 -0
  4. package/dist/cjs/src/serializer/SerializerRenderer.js +34 -59
  5. package/dist/cjs/src/serializer/builders/DreamSerializerBuilder.js +46 -17
  6. package/dist/cjs/src/serializer/builders/ObjectSerializerBuilder.js +44 -17
  7. package/dist/cjs/src/serializer/helpers/nullFlattenedSerializerKeys.js +233 -0
  8. package/dist/cjs/src/serializer/helpers/serializersForAssociatedClass.js +4 -3
  9. package/dist/esm/src/dream-app/index.js +6 -0
  10. package/dist/esm/src/serializer/DreamSerializer.js +23 -0
  11. package/dist/esm/src/serializer/ObjectSerializer.js +22 -0
  12. package/dist/esm/src/serializer/SerializerRenderer.js +34 -59
  13. package/dist/esm/src/serializer/builders/DreamSerializerBuilder.js +46 -17
  14. package/dist/esm/src/serializer/builders/ObjectSerializerBuilder.js +44 -17
  15. package/dist/esm/src/serializer/helpers/nullFlattenedSerializerKeys.js +233 -0
  16. package/dist/esm/src/serializer/helpers/serializersForAssociatedClass.js +4 -3
  17. package/dist/types/src/dream-app/index.d.ts +6 -0
  18. package/dist/types/src/serializer/DreamSerializer.d.ts +23 -0
  19. package/dist/types/src/serializer/ObjectSerializer.d.ts +22 -0
  20. package/dist/types/src/serializer/SerializerRenderer.d.ts +1 -13
  21. package/dist/types/src/serializer/builders/DreamSerializerBuilder.d.ts +49 -19
  22. package/dist/types/src/serializer/builders/ObjectSerializerBuilder.d.ts +47 -19
  23. package/dist/types/src/serializer/helpers/nullFlattenedSerializerKeys.d.ts +85 -0
  24. package/dist/types/src/serializer/helpers/serializersForAssociatedClass.d.ts +4 -3
  25. package/docs/classes/db.DreamMigrationHelpers.html +11 -11
  26. package/docs/classes/db.KyselyQueryDriver.html +49 -49
  27. package/docs/classes/db.PostgresQueryDriver.html +49 -49
  28. package/docs/classes/db.QueryDriverBase.html +49 -49
  29. package/docs/classes/errors.CannotSaveMissingDream.html +2 -2
  30. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  31. package/docs/classes/errors.ColumnOverflow.html +3 -3
  32. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  33. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  34. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  35. package/docs/classes/errors.DecryptionError.html +2 -2
  36. package/docs/classes/errors.DecryptionParseError.html +2 -2
  37. package/docs/classes/errors.DecryptionRotationError.html +3 -3
  38. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  39. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  40. package/docs/classes/errors.InvalidClockTime.html +2 -2
  41. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  42. package/docs/classes/errors.InvalidDateTime.html +2 -2
  43. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  44. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  45. package/docs/classes/errors.NotNullViolation.html +3 -3
  46. package/docs/classes/errors.RecordNotFound.html +3 -3
  47. package/docs/classes/errors.SortableScopeDidNotStabilize.html +3 -3
  48. package/docs/classes/errors.SortableScopeLockWaitTimedOut.html +3 -3
  49. package/docs/classes/errors.ValidationError.html +3 -3
  50. package/docs/classes/index.CalendarDate.html +33 -33
  51. package/docs/classes/index.ClockTime.html +32 -32
  52. package/docs/classes/index.ClockTimeTz.html +35 -35
  53. package/docs/classes/index.DateTime.html +86 -86
  54. package/docs/classes/index.Decorators.html +19 -19
  55. package/docs/classes/index.Dream.html +125 -125
  56. package/docs/classes/index.DreamApp.html +11 -11
  57. package/docs/classes/index.DreamTransaction.html +3 -3
  58. package/docs/classes/index.Env.html +5 -5
  59. package/docs/classes/index.Query.html +65 -65
  60. package/docs/classes/system.CliFileWriter.html +4 -4
  61. package/docs/classes/system.DreamBin.html +3 -3
  62. package/docs/classes/system.DreamCLI.html +7 -7
  63. package/docs/classes/system.DreamImporter.html +2 -2
  64. package/docs/classes/system.DreamLogos.html +2 -2
  65. package/docs/classes/system.DreamSerializerBuilder.html +57 -28
  66. package/docs/classes/system.ObjectSerializerBuilder.html +55 -27
  67. package/docs/classes/system.PathHelpers.html +3 -3
  68. package/docs/classes/utils.Encrypt.html +4 -4
  69. package/docs/classes/utils.Range.html +2 -2
  70. package/docs/functions/db.closeAllDbConnections.html +1 -1
  71. package/docs/functions/db.dreamDbConnections.html +1 -1
  72. package/docs/functions/db.untypedDb.html +1 -1
  73. package/docs/functions/db.validateColumn.html +1 -1
  74. package/docs/functions/db.validateTable.html +1 -1
  75. package/docs/functions/errors.pgErrorType.html +1 -1
  76. package/docs/functions/index.DreamSerializer.html +18 -1
  77. package/docs/functions/index.ObjectSerializer.html +17 -1
  78. package/docs/functions/index.ReplicaSafe.html +1 -1
  79. package/docs/functions/index.STI.html +1 -1
  80. package/docs/functions/index.SoftDelete.html +1 -1
  81. package/docs/functions/utils.camelize.html +1 -1
  82. package/docs/functions/utils.capitalize.html +1 -1
  83. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  84. package/docs/functions/utils.compact.html +1 -1
  85. package/docs/functions/utils.groupBy.html +1 -1
  86. package/docs/functions/utils.hyphenize.html +1 -1
  87. package/docs/functions/utils.intersection.html +1 -1
  88. package/docs/functions/utils.isEmpty.html +1 -1
  89. package/docs/functions/utils.normalizeUnicode.html +1 -1
  90. package/docs/functions/utils.pascalize.html +1 -1
  91. package/docs/functions/utils.percent.html +1 -1
  92. package/docs/functions/utils.range.html +1 -1
  93. package/docs/functions/utils.round.html +1 -1
  94. package/docs/functions/utils.sanitizeString.html +1 -1
  95. package/docs/functions/utils.snakeify.html +1 -1
  96. package/docs/functions/utils.sort.html +1 -1
  97. package/docs/functions/utils.sortBy.html +1 -1
  98. package/docs/functions/utils.sortObjectByKey.html +1 -1
  99. package/docs/functions/utils.sortObjectByValue.html +1 -1
  100. package/docs/functions/utils.uncapitalize.html +1 -1
  101. package/docs/functions/utils.uniq.html +1 -1
  102. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  103. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  104. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  105. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  106. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  107. package/docs/interfaces/types.DecoratorContext.html +2 -2
  108. package/docs/interfaces/types.DecryptCallbacks.html +2 -2
  109. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  110. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  111. package/docs/interfaces/types.DreamDbConfig.html +6 -6
  112. package/docs/interfaces/types.DurationObject.html +2 -2
  113. package/docs/interfaces/types.EncryptOptions.html +2 -2
  114. package/docs/interfaces/types.EncryptionLegacyKeyUsedEvent.html +4 -4
  115. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  116. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  117. package/docs/interfaces/types.IntrospectedColumn.html +7 -7
  118. package/docs/interfaces/types.IntrospectedDatabase.html +2 -2
  119. package/docs/interfaces/types.IntrospectedEnum.html +2 -2
  120. package/docs/interfaces/types.IntrospectedTable.html +5 -5
  121. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  122. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  123. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  124. package/docs/types/openapi.OpenapiFormats.html +1 -1
  125. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  126. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  127. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  128. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  129. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  130. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  131. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  132. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  133. package/docs/types/openapi.OpenapiSchemaCombinatorFields.html +1 -1
  134. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  135. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +1 -1
  136. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +1 -1
  137. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +1 -1
  138. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  139. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  140. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  141. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  142. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  143. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  144. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  145. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  146. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  147. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  148. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  149. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  150. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +1 -1
  151. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +1 -1
  152. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +1 -1
  153. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  154. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  155. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  156. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  157. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  158. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  159. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  160. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  161. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  162. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  163. package/docs/types/types.CalendarDateObject.html +1 -1
  164. package/docs/types/types.Camelized.html +1 -1
  165. package/docs/types/types.ClockTimeObject.html +1 -1
  166. package/docs/types/types.DbConnectionType.html +1 -1
  167. package/docs/types/types.DbTypes.html +1 -1
  168. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  169. package/docs/types/types.DreamAttributes.html +1 -1
  170. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  171. package/docs/types/types.DreamClassColumn.html +1 -1
  172. package/docs/types/types.DreamColumn.html +1 -1
  173. package/docs/types/types.DreamColumnNames.html +1 -1
  174. package/docs/types/types.DreamLogLevel.html +1 -1
  175. package/docs/types/types.DreamLogger.html +2 -2
  176. package/docs/types/types.DreamModelSerializerType.html +1 -1
  177. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  178. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  179. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  180. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  181. package/docs/types/types.DreamSerializable.html +1 -1
  182. package/docs/types/types.DreamSerializableArray.html +1 -1
  183. package/docs/types/types.DreamSerializerKey.html +1 -1
  184. package/docs/types/types.DreamSerializers.html +1 -1
  185. package/docs/types/types.DreamVirtualColumns.html +1 -1
  186. package/docs/types/types.DurationUnit.html +1 -1
  187. package/docs/types/types.EncryptAlgorithm.html +1 -1
  188. package/docs/types/types.HasManyStatement.html +1 -1
  189. package/docs/types/types.HasOneStatement.html +1 -1
  190. package/docs/types/types.Hyphenized.html +1 -1
  191. package/docs/types/types.IntrospectedValueType.html +1 -1
  192. package/docs/types/types.Pascalized.html +1 -1
  193. package/docs/types/types.PrimaryKeyType.html +1 -1
  194. package/docs/types/types.RoundingPrecision.html +1 -1
  195. package/docs/types/types.SerializerCasing.html +1 -1
  196. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  197. package/docs/types/types.Snakeified.html +1 -1
  198. package/docs/types/types.StrictInterface.html +1 -1
  199. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  200. package/docs/types/types.UpdateableProperties.html +1 -1
  201. package/docs/types/types.ValidationType.html +1 -1
  202. package/docs/types/types.ViewModel.html +2 -2
  203. package/docs/types/types.ViewModelClass.html +1 -1
  204. package/docs/types/types.WeekdayName.html +1 -1
  205. package/docs/types/types.WhereStatementForDream.html +1 -1
  206. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  207. package/docs/variables/index.DreamConst.html +1 -1
  208. package/docs/variables/index.ops.html +1 -1
  209. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  210. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  211. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  212. package/docs/variables/system.primaryKeyTypes.html +1 -1
  213. package/package.json +1 -1
@@ -19,6 +19,7 @@ import standardizeFullyQualifiedModelName from '../helpers/standardizeFullyQuali
19
19
  import expandStiClasses from '../helpers/sti/expandStiClasses.js';
20
20
  import inferSerializerFromDreamOrViewModel, { inferSerializersFromDreamClassOrViewModelClass, } from '../serializer/helpers/inferSerializerFromDreamOrViewModel.js';
21
21
  import isDreamSerializer from '../serializer/helpers/isDreamSerializer.js';
22
+ import { flattenedOpenapiVariants, nullFlattenedOpenapiKeys, nullFlattenedOpenapiKeysOfOtherVariants, nullFlattenedSerializerKeys, nullFlattenedSerializerKeysOfOtherVariants, } from '../serializer/helpers/nullFlattenedSerializerKeys.js';
22
23
  import serializerNameFromFullyQualifiedModelName from '../serializer/helpers/serializerNameFromFullyQualifiedModelName.js';
23
24
  import BaseClockTime from '../utils/datetime/BaseClockTime.js';
24
25
  import CalendarDate from '../utils/datetime/CalendarDate.js';
@@ -103,6 +104,11 @@ export default class DreamApp {
103
104
  inferSerializerFromDreamOrViewModel,
104
105
  inferSerializersFromDreamClassOrViewModelClass,
105
106
  isDreamSerializer,
107
+ nullFlattenedSerializerKeys,
108
+ nullFlattenedSerializerKeysOfOtherVariants,
109
+ nullFlattenedOpenapiKeys,
110
+ nullFlattenedOpenapiKeysOfOtherVariants,
111
+ flattenedOpenapiVariants,
106
112
  serializerNameFromFullyQualifiedModelName,
107
113
  autogeneratedFileDisclaimer,
108
114
  };
@@ -1,4 +1,27 @@
1
1
  import DreamSerializerBuilder from './builders/DreamSerializerBuilder.js';
2
+ /**
3
+ * Builds a serializer for a Dream model. A serializer is a function that takes the
4
+ * data to render and declares the keys to render it with:
5
+ *
6
+ * ```typescript
7
+ * export const UserSerializer = (user: User) =>
8
+ * DreamSerializer(User, user).attribute('email').rendersMany('posts')
9
+ * ```
10
+ *
11
+ * A serializer must not read its data while being built: what it declares may not
12
+ * depend on its data. Dream and Psychic build it without data to read its shape (the
13
+ * OpenAPI schema, and the keys a flattened association or `customAttribute` that uses
14
+ * it renders as `null` where its value does not supply them), so a serializer that
15
+ * reads its data while being built throws when a flattened association or
16
+ * `customAttribute` that uses it is rendered. Read the data inside a `customAttribute`
17
+ * callback, which runs only when the serializer renders, not while it is built.
18
+ *
19
+ * @param dreamClass - The Dream model class the serializer renders
20
+ * @param data - The Dream instance to render
21
+ * @param passthroughData - Data available to this serializer and passed down to the serializers
22
+ * it renders
23
+ * @returns The serializer builder for method chaining
24
+ */
2
25
  export default function DreamSerializer(dreamClass, data, passthroughData) {
3
26
  return new DreamSerializerBuilder(dreamClass, data, passthroughData);
4
27
  }
@@ -1,4 +1,26 @@
1
1
  import ObjectSerializerBuilder from './builders/ObjectSerializerBuilder.js';
2
+ /**
3
+ * Builds a serializer for a plain object or view model. A serializer is a function
4
+ * that takes the data to render and declares the keys to render it with:
5
+ *
6
+ * ```typescript
7
+ * export const AddressSerializer = (address: Address) =>
8
+ * ObjectSerializer(address).attribute('city', { openapi: 'string' })
9
+ * ```
10
+ *
11
+ * A serializer must not read its data while being built: what it declares may not
12
+ * depend on its data. Dream and Psychic build it without data to read its shape (the
13
+ * OpenAPI schema, and the keys a flattened association or `customAttribute` that uses
14
+ * it renders as `null` where its value does not supply them), so a serializer that
15
+ * reads its data while being built throws when a flattened association or
16
+ * `customAttribute` that uses it is rendered. Read the data inside a `customAttribute`
17
+ * callback, which runs only when the serializer renders, not while it is built.
18
+ *
19
+ * @param data - The object to render
20
+ * @param passthroughData - Data available to this serializer and passed down to the serializers
21
+ * it renders
22
+ * @returns The serializer builder for method chaining
23
+ */
2
24
  export default function ObjectSerializer(data, passthroughData) {
3
25
  return new ObjectSerializerBuilder(data, passthroughData);
4
26
  }
@@ -9,6 +9,7 @@ import { DateTime } from '../utils/datetime/DateTime.js';
9
9
  import DreamSerializerBuilder from './builders/DreamSerializerBuilder.js';
10
10
  import ObjectSerializerBuilder from './builders/ObjectSerializerBuilder.js';
11
11
  import inferSerializerFromDreamOrViewModel from './helpers/inferSerializerFromDreamOrViewModel.js';
12
+ import { nullFlattenedOpenapiKeys, nullFlattenedSerializerKeys, } from './helpers/nullFlattenedSerializerKeys.js';
12
13
  import { serializersForAssociatedClass } from './helpers/serializersForAssociatedClass.js';
13
14
  export default class SerializerRenderer {
14
15
  serializerBuilder;
@@ -77,9 +78,28 @@ export default class SerializerRenderer {
77
78
  if (value === undefined && attribute.options?.required === false)
78
79
  return accumulator;
79
80
  if (attribute.options.flatten) {
81
+ const flattenOptions = { casing: this.renderOpts.casing, passthroughData };
82
+ // a flattened customAttribute's keys are the parent's own keys, cased like them, and
83
+ // always include each key any shape of its openapi requires: a callback that returns
84
+ // null or undefined, or a serializer built over null or undefined data (which renders
85
+ // null), renders each of them as null, as a null flattened rendersOne does, and a
86
+ // present value renders its own keys over them, so a required key it leaves out is null
87
+ const valueIsSerializerBuilder = value instanceof DreamSerializerBuilder || value instanceof ObjectSerializerBuilder;
88
+ const valueRendersNull = value === null ||
89
+ value === undefined ||
90
+ (valueIsSerializerBuilder &&
91
+ (value['data'] ?? null) === null);
92
+ const nullRequiredKeys = nullFlattenedOpenapiKeys(attribute.options.openapi, flattenOptions);
93
+ if (valueRendersNull)
94
+ return { ...accumulator, ...nullRequiredKeys };
95
+ const renderedValue = applyRenderingOptionsToAttribute(null, value, attribute.name, {}, this.passthroughData, this.renderOpts);
80
96
  return {
81
97
  ...accumulator,
82
- ...applyRenderingOptionsToAttribute(null, value, attribute.name, {}, this.passthroughData, this.renderOpts),
98
+ ...nullRequiredKeys,
99
+ // a returned serializer renders its keys in the render's casing; a returned plain
100
+ // object's top-level keys are cased here, its nested values left as returned, and a
101
+ // key it sets to undefined is skipped, so a required key renders null beneath it
102
+ ...(valueIsSerializerBuilder ? renderedValue : this.setCaseOfDefinedKeys(renderedValue)),
83
103
  };
84
104
  }
85
105
  else {
@@ -96,12 +116,6 @@ export default class SerializerRenderer {
96
116
  case 'rendersOne': {
97
117
  const outputAttributeName = this.setCase(attribute.options.as ?? attribute.name);
98
118
  const associatedObject = data[attribute.name];
99
- if (!associatedObject && attribute.options.flatten) {
100
- return {
101
- ...accumulator,
102
- ...this.nullFlattenedAttributes(serializersForAssociatedClass(data instanceof Dream ? data.constructor : null, attribute.name, attribute.options), passthroughData),
103
- };
104
- }
105
119
  const serializer = associatedObject
106
120
  ? serializerForAssociatedObject(associatedObject, attribute.options, renderTimeResolutionContext(attribute.options, 'rendersOne', attribute.name))
107
121
  : null;
@@ -112,6 +126,11 @@ export default class SerializerRenderer {
112
126
  if (attribute.options.flatten) {
113
127
  return {
114
128
  ...accumulator,
129
+ // a flattened rendersOne always renders each key any serializer the association may
130
+ // render with (each STI child, each polymorphic target) requires: as null when the
131
+ // associated object is null, and beneath the rendered serializer's own keys when it is
132
+ // present, so a required key the rendered serializer leaves out renders as null
133
+ ...nullFlattenedSerializerKeys(serializersForAssociatedClass(data instanceof Dream ? data.constructor : null, attribute.name, attribute.options), { casing: this.renderOpts.casing, passthroughData }),
115
134
  // passthrough data must be passed both into the serializer and render
116
135
  // because, if the serializer does accept passthrough data, then passing it in is how
117
136
  // it gets into the serializer, but if it does not accept passthrough data, and therefore
@@ -176,58 +195,14 @@ export default class SerializerRenderer {
176
195
  }, renderedAttributes);
177
196
  return renderedAttributes;
178
197
  }
179
- /**
180
- * The keys a flattened `rendersOne` adds to the parent's payload when its associated object is
181
- * null: each key any of `serializers` declares, set to `null`. There are several serializers when
182
- * the association is a polymorphic BelongsTo, one for each target class, or when the associated
183
- * class is an STI base, one for each child, and their keys are unioned.
184
- *
185
- * Each serializer is built with `undefined` data to read its declarations, as Psychic's OpenAPI
186
- * generation and `preloadFor` build it, and nothing is rendered: no attribute callback runs and
187
- * no association of the missing object is read. A flattened `rendersOne` it declares adds its
188
- * own serializers' keys the same way. A flattened `customAttribute` adds none, since only its
189
- * callback knows its keys.
190
- */
191
- nullFlattenedAttributes(serializers, passthroughData, serializersBeingFlattened = new Set()) {
192
- return serializers.reduce((flattenedKeys, serializer) => {
193
- // a serializer reached again, e.g. one that flattens itself directly or through others, or
194
- // one that two polymorphic targets or two STI children both flatten, adds no keys the first
195
- // walk through it did not
196
- if (serializersBeingFlattened.has(serializer))
197
- return flattenedKeys;
198
- serializersBeingFlattened.add(serializer);
199
- const serializerBuilder = serializer(undefined, passthroughData);
200
- const dreamClass = serializerBuilder instanceof DreamSerializerBuilder
201
- ? serializerBuilder['$typeForOpenapi']
202
- : null;
203
- return serializerBuilder['attributes'].reduce((keys, attribute) => {
204
- const attributeType = attribute.type;
205
- switch (attributeType) {
206
- case 'attribute':
207
- case 'delegatedAttribute':
208
- case 'rendersMany':
209
- keys[this.setCase(attribute.options?.as ?? attribute.name)] = null;
210
- return keys;
211
- case 'customAttribute':
212
- if (!attribute.options.flatten)
213
- keys[this.setCase(attribute.name)] = null;
214
- return keys;
215
- case 'rendersOne':
216
- if (!attribute.options.flatten) {
217
- keys[this.setCase(attribute.options.as ?? attribute.name)] = null;
218
- return keys;
219
- }
220
- return {
221
- ...keys,
222
- ...this.nullFlattenedAttributes(serializersForAssociatedClass(dreamClass, attribute.name, attribute.options), passthroughData, serializersBeingFlattened),
223
- };
224
- default: {
225
- // protection so that if a new ValidationType is ever added, this will throw a type error at build time
226
- const _never = attributeType;
227
- throw new Error(`Unhandled serializer attribute type: ${_never}`);
228
- }
229
- }
230
- }, flattenedKeys);
198
+ setCaseOfDefinedKeys(value) {
199
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
200
+ return value;
201
+ return Object.keys(value).reduce((casedValue, key) => {
202
+ const keyValue = value[key];
203
+ if (keyValue !== undefined)
204
+ casedValue[this.setCase(key)] = keyValue;
205
+ return casedValue;
231
206
  }, {});
232
207
  }
233
208
  setCase(attr) {
@@ -110,15 +110,29 @@ export default class DreamSerializerBuilder {
110
110
  * - `openapi` - (required) OpenAPI schema definition for the computed value
111
111
  * - `flatten` - When `true`, spreads the returned object's properties directly into the
112
112
  * parent serialized output instead of nesting them under `name`. The `openapi` option
113
- * then describes the whole returned object, not each property, because Psychic adds it
114
- * to the parent's schema as an `allOf` branch: either an object schema with full
115
- * property bodies (`type: 'object'`, `properties`, and `required` listing the keys that
116
- * are always present), or `{ $serializer: SomeSerializer }` when the callback returns
117
- * what that serializer renders. A serializer ref with `many: true` does not type-check
118
- * here, since a list has no properties to spread. A `null` or `undefined` return adds no
119
- * keys. Attributes render in declaration order, so a key the returned object shares with
120
- * another attribute takes the value of whichever is declared later; under the `allOf`,
121
- * that value must match both schemas
113
+ * then describes the whole returned object, not each property: either an object schema
114
+ * with full property bodies (`type: 'object'`, `properties`, and `required` listing the
115
+ * keys that are always present), or `{ $serializer: SomeSerializer }` when the callback
116
+ * returns what that serializer renders. A serializer ref with `many: true` does not
117
+ * type-check here, since a list has no properties to spread. Whatever the callback
118
+ * returns, each key any shape of the `openapi` requires renders, as `null` wherever the
119
+ * value does not supply it (a returned plain object's key set to `undefined` does not
120
+ * supply it): a key an object shape lists in its `required` array, a key a `$serializer` or
121
+ * `$serializable` ref's serializer declares without `required: false` (for an STI base,
122
+ * any child's serializer), and each such key of any `allOf`, `anyOf` or `oneOf` member. So
123
+ * a `null` return, a serializer built over `null` or `undefined` data, or an `undefined`
124
+ * return without `required: false` renders each of them as `null`, and a present value
125
+ * renders its own keys over them, so a key another shape requires renders as `null` even
126
+ * when the value's own shape leaves it optional; the payload always includes the same
127
+ * keys whichever shape the value takes and whether it is null. An optional key no shape
128
+ * requires stays absent unless the value supplies it, and an `undefined` return with
129
+ * `required: false` adds no keys.
130
+ * Every key it adds follows the render's casing like the parent's own keys: a returned
131
+ * plain object's top-level keys and the `openapi`'s `properties` keys are cased (so
132
+ * `casing: 'snake'` renders `phoneNumber` as `phone_number`, present or `null`), while
133
+ * nested values render as returned. Attributes render in declaration order, so a key the
134
+ * returned object shares with another attribute takes the value of whichever is declared
135
+ * later, and that value must match both the attribute's declaration and the `openapi`'s
122
136
  * - `required` - Set to `false` to mark the attribute as optional in the OpenAPI schema;
123
137
  * when omitted, attributes are required by default
124
138
  * @returns The serializer builder for method chaining
@@ -168,14 +182,29 @@ export default class DreamSerializerBuilder {
168
182
  * declaration order, so when the parent and the association share a key (e.g., `id`),
169
183
  * whichever is declared later wins: the association's value replaces an attribute declared
170
184
  * before the `rendersOne`, and an attribute declared after it replaces the association's.
171
- * When the association is `null`, every key its serializer declares renders as `null`
172
- * (for a polymorphic `BelongsTo`, every key the serializer of any of its target classes
173
- * declares, and for an association to an STI base, every key the serializer of any of its
174
- * STI children declares), in the same order (so an `id` declared before the `rendersOne` becomes
175
- * `null`); nothing in that serializer is rendered, so its custom attribute callbacks do
176
- * not run, and a flattened `customAttribute` in it adds no keys. Psychic adds the association's OpenAPI
177
- * schema to the parent's as an `allOf` branch, so a shared key's value must match both
178
- * schemas, whichever declaration wins
185
+ * Each key its serializer requires always renders, as `null` wherever the association
186
+ * does not supply it: an attribute, delegatedAttribute or customAttribute declared without
187
+ * `required: false`, and every non-flattened `rendersOne` and `rendersMany` (for a
188
+ * polymorphic `BelongsTo`, each key the serializer of any of its target classes requires,
189
+ * and for an association to an STI base, each key the serializer of any of its STI children
190
+ * requires), in the same order (so an `id` declared before the `rendersOne` becomes `null`
191
+ * when the association is `null`). A flattened `customAttribute` in that serializer adds the
192
+ * keys its `openapi` requires: a key an object shape lists in its `required` array, a key a
193
+ * `$serializer` or `$serializable` ref's serializer declares without `required: false`, and
194
+ * each such key of an `allOf`, `anyOf` or `oneOf` member; a ref with `many: true` adds none,
195
+ * and a flattened `customAttribute` declared with `required: false` adds none. When the
196
+ * association is `null`, each of these keys renders as `null` and nothing in its serializer
197
+ * is rendered, so its custom attribute callbacks do not run. When it is present, the held
198
+ * object's serializer renders its own keys over them, so a key another STI child's or
199
+ * target's serializer requires renders as `null` where the held one's serializer does not
200
+ * render it, including a key that serializer declares with `required: false`; the payload
201
+ * always includes the same keys whichever is held and, for an association or a property
202
+ * given a `serializer` or `viewModelClass` option, whether it is `null` (a `null` property
203
+ * that is not an association and has neither option renders none of these keys, since no
204
+ * serializer is known for it). A key declared with `required: false` that no serializer
205
+ * requires stays absent unless it renders. The association's OpenAPI schema describes the
206
+ * parent's payload too, so a shared key's value must match both the parent's declaration
207
+ * and the association's, whichever declaration wins
179
208
  * - `optional` - When `true`, allows the association to be null/missing without causing
180
209
  * an `OpenapiResponseValidationFailure` during Psychic controller unit specs. By default,
181
210
  * `rendersOne` expects the association to be present (mirroring the `optional` option on
@@ -146,15 +146,29 @@ export default class ObjectSerializerBuilder {
146
146
  * - `openapi` - (required) OpenAPI schema definition for the computed value
147
147
  * - `flatten` - When `true`, spreads the returned object's properties directly into the
148
148
  * parent serialized output instead of nesting them under `name`. The `openapi` option
149
- * then describes the whole returned object, not each property, because Psychic adds it
150
- * to the parent's schema as an `allOf` branch: either an object schema with full
151
- * property bodies (`type: 'object'`, `properties`, and `required` listing the keys that
152
- * are always present), or `{ $serializer: SomeSerializer }` when the callback returns
153
- * what that serializer renders. A serializer ref with `many: true` does not type-check
154
- * here, since a list has no properties to spread. A `null` or `undefined` return adds no
155
- * keys. Attributes render in declaration order, so a key the returned object shares with
156
- * another attribute takes the value of whichever is declared later; under the `allOf`,
157
- * that value must match both schemas
149
+ * then describes the whole returned object, not each property: either an object schema
150
+ * with full property bodies (`type: 'object'`, `properties`, and `required` listing the
151
+ * keys that are always present), or `{ $serializer: SomeSerializer }` when the callback
152
+ * returns what that serializer renders. A serializer ref with `many: true` does not
153
+ * type-check here, since a list has no properties to spread. Whatever the callback
154
+ * returns, each key any shape of the `openapi` requires renders, as `null` wherever the
155
+ * value does not supply it (a returned plain object's key set to `undefined` does not
156
+ * supply it): a key an object shape lists in its `required` array, a key a `$serializer` or
157
+ * `$serializable` ref's serializer declares without `required: false` (for an STI base,
158
+ * any child's serializer), and each such key of any `allOf`, `anyOf` or `oneOf` member. So
159
+ * a `null` return, a serializer built over `null` or `undefined` data, or an `undefined`
160
+ * return without `required: false` renders each of them as `null`, and a present value
161
+ * renders its own keys over them, so a key another shape requires renders as `null` even
162
+ * when the value's own shape leaves it optional; the payload always includes the same
163
+ * keys whichever shape the value takes and whether it is null. An optional key no shape
164
+ * requires stays absent unless the value supplies it, and an `undefined` return with
165
+ * `required: false` adds no keys.
166
+ * Every key it adds follows the render's casing like the parent's own keys: a returned
167
+ * plain object's top-level keys and the `openapi`'s `properties` keys are cased (so
168
+ * `casing: 'snake'` renders `phoneNumber` as `phone_number`, present or `null`), while
169
+ * nested values render as returned. Attributes render in declaration order, so a key the
170
+ * returned object shares with another attribute takes the value of whichever is declared
171
+ * later, and that value must match both the attribute's declaration and the `openapi`'s
158
172
  * - `required` - Set to `false` to mark the attribute as optional in the OpenAPI schema;
159
173
  * when omitted, attributes are required by default
160
174
  * @returns The serializer builder for method chaining
@@ -204,14 +218,27 @@ export default class ObjectSerializerBuilder {
204
218
  * declaration order, so when the parent and the association share a key (e.g., `id`),
205
219
  * whichever is declared later wins: the association's value replaces an attribute declared
206
220
  * before the `rendersOne`, and an attribute declared after it replaces the association's.
207
- * When the association is `null`, every key its serializer declares renders as `null`
208
- * (for a polymorphic `BelongsTo`, every key the serializer of any of its target classes
209
- * declares, and for an association to an STI base, every key the serializer of any of its
210
- * STI children declares), in the same order (so an `id` declared before the `rendersOne` becomes
211
- * `null`); nothing in that serializer is rendered, so its custom attribute callbacks do
212
- * not run, and a flattened `customAttribute` in it adds no keys. Psychic adds the association's OpenAPI
213
- * schema to the parent's as an `allOf` branch, so a shared key's value must match both
214
- * schemas, whichever declaration wins
221
+ * Each key its serializer requires always renders, as `null` wherever the association
222
+ * does not supply it: an attribute, delegatedAttribute or customAttribute declared without
223
+ * `required: false`, and every non-flattened `rendersOne` and `rendersMany` (for a
224
+ * polymorphic `BelongsTo`, each key the serializer of any of its target classes requires,
225
+ * and for an association to an STI base, each key the serializer of any of its STI children
226
+ * requires), in the same order (so an `id` declared before the `rendersOne` becomes `null`
227
+ * when the association is `null`). A flattened `customAttribute` in that serializer adds the
228
+ * keys its `openapi` requires: a key an object shape lists in its `required` array, a key a
229
+ * `$serializer` or `$serializable` ref's serializer declares without `required: false`, and
230
+ * each such key of an `allOf`, `anyOf` or `oneOf` member; a ref with `many: true` adds none,
231
+ * and a flattened `customAttribute` declared with `required: false` adds none. When the
232
+ * association is `null`, each of these keys renders as `null` and nothing in its serializer
233
+ * is rendered, so its custom attribute callbacks do not run. When it is present, the held
234
+ * object's serializer renders its own keys over them, so a key another STI child's or
235
+ * target's serializer requires renders as `null` where the held one's serializer does not
236
+ * render it, including a key that serializer declares with `required: false`; the payload
237
+ * always includes the same keys whichever is held and whether the association is `null`. A
238
+ * key declared with `required: false` that no serializer requires stays absent unless it
239
+ * renders. The association's OpenAPI schema describes the parent's payload too, so a shared
240
+ * key's value must match both the parent's declaration and the association's, whichever
241
+ * declaration wins
215
242
  * - `optional` - When `true`, allows the association to be null/missing without causing
216
243
  * an `OpenapiResponseValidationFailure` during Psychic controller unit specs. By default,
217
244
  * `rendersOne` expects the association to be present (mirroring the `optional` option on
@@ -0,0 +1,233 @@
1
+ import snakeify from '../../helpers/snakeify.js';
2
+ import DreamSerializerBuilder from '../builders/DreamSerializerBuilder.js';
3
+ import { inferSerializersFromDreamClassOrViewModelClass } from './inferSerializerFromDreamOrViewModel.js';
4
+ import { serializersForAssociatedClass } from './serializersForAssociatedClass.js';
5
+ /**
6
+ * The keys a flattened `rendersOne` always adds to the parent's payload, returned as an object
7
+ * with each key set to `null`, ready to spread into the parent's payload (`Object.keys` of it for
8
+ * the key names alone): each key one of `serializers` requires. When the associated object is
9
+ * null, these are the keys it renders; when it is present, the rendered serializer's own keys
10
+ * render over them, so each of these it leaves out stays `null` and the payload always includes
11
+ * these keys, whichever serializer renders and whether the object is null. As a property read on
12
+ * an empty object is `undefined`, a null value carries no key it may omit: an attribute,
13
+ * delegatedAttribute or customAttribute declared with `required: false` adds no key unless another
14
+ * of `serializers` requires it. Every key is cased per `casing`, as the renderer cases it. There
15
+ * are several serializers when the association is a polymorphic BelongsTo, one for each target
16
+ * class, or when the associated class is an STI base, one for each child, and their required keys
17
+ * are unioned.
18
+ *
19
+ * Each serializer is built with `undefined` data to read its declarations, as Psychic's OpenAPI
20
+ * generation and `preloadFor` build it, and nothing is rendered: no attribute callback runs and
21
+ * no association of the missing object is read. A non-flattened `rendersOne` or a `rendersMany`
22
+ * always adds its key. A flattened `rendersOne` it declares adds its own serializers' required keys
23
+ * the same way. A flattened `customAttribute` adds the keys its `openapi` requires: each key of
24
+ * its `properties` that the shape's `required` array lists, the required keys of the serializer a
25
+ * `$serializer` ref names or of every serializer a `$serializable` ref resolves to (one per child
26
+ * for an STI base), walked the same way, and the union of the keys every member of an `allOf`,
27
+ * `anyOf` or `oneOf` requires, with each key an object shape's own `required` array lists beside
28
+ * its members; one declared with `required: false` adds none. A ref with `many: true` adds none,
29
+ * since a list has no keys to spread. A serializer reached again within its own walk, as when one
30
+ * flattens itself, adds nothing, so the walk always terminates; one reached again along another
31
+ * path is walked again, so each path adds its keys and their requirements.
32
+ *
33
+ * `passthroughData` is handed to each serializer as it is built, as when rendering; outside a
34
+ * render it may be left out.
35
+ */
36
+ export function nullFlattenedSerializerKeys(serializers, options = {}) {
37
+ return nullRequiredKeys(walkSerializers(serializers, newWalk(options)));
38
+ }
39
+ /**
40
+ * The keys a flattened `customAttribute` with this `openapi` always adds to the parent's payload,
41
+ * each set to `null`: each key any shape of `openapi` requires, walked as
42
+ * `nullFlattenedSerializerKeys` walks a flattened `customAttribute` and cased per `casing`. When
43
+ * its callback returns null (or `undefined` without `required: false`), or a serializer built over
44
+ * null data, these are the keys it renders; when it returns any other value, that value's own keys
45
+ * render over them, so each of these the value leaves out, or a returned plain object sets to
46
+ * `undefined`, stays `null`. With `required: false`, a callback that returns `undefined` adds none
47
+ * of them.
48
+ */
49
+ export function nullFlattenedOpenapiKeys(openapi, options = {}) {
50
+ return nullRequiredKeys(walkOpenapi(openapi, newWalk(options)));
51
+ }
52
+ /**
53
+ * The keys only the other variants of a flattened `rendersOne` contribute to its payload, each set
54
+ * to `null`: each key one of `serializers` (every serializer the association may render with)
55
+ * requires that `renderedSerializer` does not declare at all. A key `renderedSerializer` declares
56
+ * is left out even when it declares it with `required: false` and another serializer requires it;
57
+ * the renderer fills that key with `null` too, since it renders every key
58
+ * `nullFlattenedSerializerKeys` returns beneath the rendered serializer's own keys.
59
+ */
60
+ export function nullFlattenedSerializerKeysOfOtherVariants(serializers, renderedSerializer, options = {}) {
61
+ return nullKeysRequiredByOthers(walkSerializers(serializers, newWalk(options)), Object.keys(walkSerializers([renderedSerializer], newWalk(options))));
62
+ }
63
+ /**
64
+ * The keys only the other variants of a flattened `customAttribute` with this `openapi` contribute
65
+ * to its payload, each set to `null`: each key a variant of `openapi` requires that
66
+ * `renderedVariant` does not declare. `renderedVariant` is any `openapi` shorthand: one member of
67
+ * an `anyOf` or `oneOf`, or `{ $serializer: childSerializer }` for one child of a `$serializable`
68
+ * ref to an STI base. A key requires its value when an object shape's `required` array lists it,
69
+ * whether the shape's own `properties` declare it or a member of its `allOf`, `anyOf` or `oneOf`
70
+ * does, or when a serializer declares it without `required: false`. A key `renderedVariant`
71
+ * declares is left out even when it is optional there and another variant requires it; the
72
+ * renderer fills that key with `null` too, since it renders every key `nullFlattenedOpenapiKeys`
73
+ * returns beneath the value's own keys.
74
+ */
75
+ export function nullFlattenedOpenapiKeysOfOtherVariants(openapi, renderedVariant, options = {}) {
76
+ return nullKeysRequiredByOthers(walkOpenapi(openapi, newWalk(options)), Object.keys(walkOpenapi(renderedVariant, newWalk(options))));
77
+ }
78
+ /**
79
+ * Lists the single shapes a value of a flattened `customAttribute` with this `openapi` may take,
80
+ * which a consumer can union into one description of the flattened payload: a `$serializer` ref to
81
+ * each serializer a `$serializable` ref resolves to (one per child for an STI base), or the members
82
+ * of an `anyOf` or `oneOf` (an `anyOf`'s members listed before a sibling `oneOf`'s, not paired with
83
+ * them), each member that itself has several shapes (a nested `anyOf` or `oneOf`, or a
84
+ * `$serializable` ref to an STI base) contributing those shapes in its place. An `allOf` beside
85
+ * the `anyOf` or `oneOf` describes every value, so each shape is that `allOf` with the shape added
86
+ * to its members. Anything else (a single shape, a ref with `many: true`, or a primitive) has no
87
+ * variants and returns an empty list. Each shape may be passed back as the `renderedVariant` of
88
+ * `nullFlattenedOpenapiKeysOfOtherVariants`. The renderer does not read a value's shape from this
89
+ * list: whatever shape the value takes, it renders every key `nullFlattenedOpenapiKeys` returns.
90
+ */
91
+ export function flattenedOpenapiVariants(openapi) {
92
+ if (typeof openapi !== 'object' || Array.isArray(openapi))
93
+ return [];
94
+ if ('$serializable' in openapi)
95
+ return openapi.many
96
+ ? []
97
+ : inferSerializersFromDreamClassOrViewModelClass(openapi.$serializable, openapi.$serializableSerializerKey ?? openapi.key).map(serializer => ({ $serializer: serializer }));
98
+ const variants = [
99
+ ...(('anyOf' in openapi && openapi.anyOf) || []),
100
+ ...(('oneOf' in openapi && openapi.oneOf) || []),
101
+ ].flatMap(member => {
102
+ const memberVariants = flattenedOpenapiVariants(member);
103
+ return memberVariants.length > 1 ? memberVariants : [member];
104
+ });
105
+ const sharedMembers = ('allOf' in openapi && openapi.allOf) || [];
106
+ return sharedMembers.length ? variants.map(variant => ({ allOf: [...sharedMembers, variant] })) : variants;
107
+ }
108
+ function newWalk({ casing = 'camel', passthroughData = {}, }) {
109
+ return { casing, passthroughData, serializersBeingFlattened: new Set() };
110
+ }
111
+ function nullRequiredKeys(requirements) {
112
+ return Object.keys(requirements).reduce((keys, key) => {
113
+ if (requirements[key])
114
+ keys[key] = null;
115
+ return keys;
116
+ }, {});
117
+ }
118
+ function nullKeysRequiredByOthers(requirements, renderedKeys) {
119
+ return Object.keys(requirements).reduce((keys, key) => {
120
+ if (requirements[key] && !renderedKeys.includes(key))
121
+ keys[key] = null;
122
+ return keys;
123
+ }, {});
124
+ }
125
+ /**
126
+ * A key is required when any of its declarations requires it.
127
+ */
128
+ function mergeRequirements(requirements, other) {
129
+ Object.keys(other).forEach(key => {
130
+ requirements[key] = (requirements[key] ?? false) || other[key];
131
+ });
132
+ return requirements;
133
+ }
134
+ function walkSerializers(serializers, walk) {
135
+ return serializers.reduce((flattenedKeys, serializer) => {
136
+ // a serializer that flattens itself, directly or through others, adds no keys its walk in
137
+ // progress does not; one reached again along another path is walked again, since a path that
138
+ // requires its keys must not lose them to an earlier path that left them optional
139
+ if (walk.serializersBeingFlattened.has(serializer))
140
+ return flattenedKeys;
141
+ walk.serializersBeingFlattened.add(serializer);
142
+ const serializerBuilder = serializer(undefined, walk.passthroughData);
143
+ const serializerKeys = walkSerializerBuilder(serializerBuilder, walk);
144
+ walk.serializersBeingFlattened.delete(serializer);
145
+ return mergeRequirements(flattenedKeys, serializerKeys);
146
+ }, {});
147
+ }
148
+ function walkSerializerBuilder(serializerBuilder, walk) {
149
+ const dreamClass = serializerBuilder instanceof DreamSerializerBuilder
150
+ ? serializerBuilder['$typeForOpenapi']
151
+ : null;
152
+ return serializerBuilder['attributes'].reduce((keys, attribute) => {
153
+ const attributeType = attribute.type;
154
+ switch (attributeType) {
155
+ case 'attribute':
156
+ case 'delegatedAttribute':
157
+ return mergeRequirements(keys, {
158
+ [setCase(attribute.options?.as ?? attribute.name, walk.casing)]: attribute.options?.required !== false,
159
+ });
160
+ case 'rendersMany':
161
+ return mergeRequirements(keys, {
162
+ [setCase(attribute.options?.as ?? attribute.name, walk.casing)]: true,
163
+ });
164
+ case 'customAttribute': {
165
+ if (!attribute.options.flatten)
166
+ return mergeRequirements(keys, {
167
+ [setCase(attribute.name, walk.casing)]: attribute.options.required !== false,
168
+ });
169
+ const flattenedKeys = walkOpenapi(attribute.options.openapi, walk);
170
+ // a callback that may return undefined with `required: false` adds none of its keys
171
+ if (attribute.options.required === false)
172
+ Object.keys(flattenedKeys).forEach(key => (flattenedKeys[key] = false));
173
+ return mergeRequirements(keys, flattenedKeys);
174
+ }
175
+ case 'rendersOne':
176
+ if (!attribute.options.flatten)
177
+ return mergeRequirements(keys, {
178
+ [setCase(attribute.options.as ?? attribute.name, walk.casing)]: true,
179
+ });
180
+ return mergeRequirements(keys, walkSerializers(serializersForAssociatedClass(dreamClass, attribute.name, attribute.options), walk));
181
+ default: {
182
+ // protection so that if a new ValidationType is ever added, this will throw a type error at build time
183
+ const _never = attributeType;
184
+ throw new Error(`Unhandled serializer attribute type: ${_never}`);
185
+ }
186
+ }
187
+ }, {});
188
+ }
189
+ function walkOpenapi(openapi, walk) {
190
+ if (typeof openapi !== 'object' || Array.isArray(openapi))
191
+ return {};
192
+ if ('$serializer' in openapi) {
193
+ if (openapi.many)
194
+ return {};
195
+ return walkSerializers([openapi.$serializer], walk);
196
+ }
197
+ if ('$serializable' in openapi) {
198
+ if (openapi.many)
199
+ return {};
200
+ return walkSerializers(inferSerializersFromDreamClassOrViewModelClass(openapi.$serializable, openapi.$serializableSerializerKey ?? openapi.key), walk);
201
+ }
202
+ const keys = Object.keys(('properties' in openapi && openapi.properties) || {}).reduce((propertyKeys, key) => {
203
+ propertyKeys[setCase(key, walk.casing)] = false;
204
+ return propertyKeys;
205
+ }, {});
206
+ const members = [
207
+ ...(('allOf' in openapi && openapi.allOf) || []),
208
+ ...(('anyOf' in openapi && openapi.anyOf) || []),
209
+ ...(('oneOf' in openapi && openapi.oneOf) || []),
210
+ ];
211
+ const shapeKeys = members.reduce((keys, member) => mergeRequirements(keys, walkOpenapi(member, walk)), keys);
212
+ // the shape's own `required` array requires a key its `properties` declare or a member declares
213
+ const requiredKeys = ('required' in openapi && openapi.required) || [];
214
+ requiredKeys.forEach(key => {
215
+ const casedKey = setCase(key, walk.casing);
216
+ if (Object.prototype.hasOwnProperty.call(shapeKeys, casedKey))
217
+ shapeKeys[casedKey] = true;
218
+ });
219
+ return shapeKeys;
220
+ }
221
+ function setCase(attr, casing) {
222
+ switch (casing) {
223
+ case 'camel':
224
+ return attr;
225
+ case 'snake':
226
+ return snakeify(attr);
227
+ default: {
228
+ // protection so that if a new Casing is ever added, this will throw a type error at build time
229
+ const _never = casing;
230
+ throw new Error(`Unhandled Casing: ${_never}`);
231
+ }
232
+ }
233
+ }
@@ -1,9 +1,10 @@
1
1
  import uniq from '../../helpers/uniq.js';
2
2
  import { inferSerializersFromDreamClassOrViewModelClass } from './inferSerializerFromDreamOrViewModel.js';
3
3
  /**
4
- * Only used when flatten: true, and the associated object is null, in which case there is no
5
- * object to resolve a serializer from, but the renderer still needs the serializers whose keys it
6
- * renders as null in the parent's payload.
4
+ * Used by every flattened rendersOne render, whether the associated object is present or null: it
5
+ * lists every serializer the association may render with, whose required keys the renderer renders
6
+ * as null in the parent's payload, beneath the present object's own keys or alone when the object
7
+ * is null, where there is no object to resolve a serializer from.
7
8
  *
8
9
  * Resolves, in order, to the `serializer` option, the serializers of the `dreamClass` or
9
10
  * `viewModelClass` option, or the serializers of each class `parentClass` declares the association