@rvoh/psychic 0.37.0-beta.4 → 0.37.0-beta.6

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 (22) hide show
  1. package/dist/cjs/src/error/openapi/AttemptedToDeriveDescendentSerializersFromNonSerializer.js +1 -0
  2. package/dist/cjs/src/error/openapi/NonSerializerPassedToSerializerOpenapiRenderer.js +1 -0
  3. package/dist/cjs/src/error/openapi/NonSerializerSerializerOverrideProvided.js +5 -1
  4. package/dist/cjs/src/openapi-renderer/SerializerOpenapiRenderer.js +40 -34
  5. package/dist/cjs/src/openapi-renderer/body-segment.js +52 -15
  6. package/dist/cjs/src/openapi-renderer/endpoint.js +2 -8
  7. package/dist/cjs/src/openapi-renderer/helpers/allSerializersFromHandWrittenOpenapi.js +47 -3
  8. package/dist/cjs/src/openapi-renderer/helpers/allSerializersToRefsInOpenapi.js +68 -6
  9. package/dist/esm/src/error/openapi/AttemptedToDeriveDescendentSerializersFromNonSerializer.js +1 -0
  10. package/dist/esm/src/error/openapi/NonSerializerPassedToSerializerOpenapiRenderer.js +1 -0
  11. package/dist/esm/src/error/openapi/NonSerializerSerializerOverrideProvided.js +5 -1
  12. package/dist/esm/src/openapi-renderer/SerializerOpenapiRenderer.js +40 -34
  13. package/dist/esm/src/openapi-renderer/body-segment.js +51 -14
  14. package/dist/esm/src/openapi-renderer/endpoint.js +9 -15
  15. package/dist/esm/src/openapi-renderer/helpers/allSerializersFromHandWrittenOpenapi.js +47 -3
  16. package/dist/esm/src/openapi-renderer/helpers/allSerializersToRefsInOpenapi.js +68 -6
  17. package/dist/types/src/helpers/typeHelpers.d.ts +2 -0
  18. package/dist/types/src/openapi-renderer/body-segment.d.ts +51 -14
  19. package/dist/types/src/openapi-renderer/endpoint.d.ts +2 -1
  20. package/dist/types/src/openapi-renderer/helpers/allSerializersFromHandWrittenOpenapi.d.ts +39 -0
  21. package/dist/types/src/openapi-renderer/helpers/allSerializersToRefsInOpenapi.d.ts +44 -0
  22. package/package.json +2 -2
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const node_util_1 = require("node:util");
4
4
  class AttemptedToDeriveDescendentSerializersFromNonSerializer extends Error {
5
5
  serializer;
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
7
  constructor(serializer) {
7
8
  super();
8
9
  this.serializer = serializer;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const node_util_1 = require("node:util");
4
4
  class NonSerializerPassedToSerializerOpenapiRenderer extends Error {
5
5
  serializer;
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
7
  constructor(serializer) {
7
8
  super();
8
9
  this.serializer = serializer;
@@ -4,7 +4,11 @@ const node_util_1 = require("node:util");
4
4
  class NonSerializerSerializerOverrideProvided extends Error {
5
5
  rendersAssociation;
6
6
  serializer;
7
- constructor(rendersAssociation, serializer) {
7
+ constructor(
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ rendersAssociation,
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ serializer) {
8
12
  super();
9
13
  this.rendersAssociation = rendersAssociation;
10
14
  this.serializer = serializer;
@@ -108,10 +108,6 @@ class SerializerOpenapiRenderer {
108
108
  const DataTypeForOpenapi = $typeForOpenapi;
109
109
  let referencedSerializers = [];
110
110
  let renderedOpenapi = {};
111
- const openapiRenderingOpts = {
112
- casing: this.casing,
113
- suppressResponseEnums: this.suppressResponseEnums,
114
- };
115
111
  renderedOpenapi = this.serializerBuilder['attributes'].reduce((accumulator, attribute) => {
116
112
  const attributeType = attribute.type;
117
113
  let newlyReferencedSerializers = [];
@@ -171,10 +167,11 @@ class SerializerOpenapiRenderer {
171
167
  case 'rendersOne': {
172
168
  try {
173
169
  const outputAttributeName = this.setCase(attribute.options.as ?? attribute.name);
174
- const referencedSerializersAndOpenapiSchemaBodyShorthand = associationOpenapi(attribute, DataTypeForOpenapi, alreadyExtractedDescendantSerializers, openapiRenderingOpts);
170
+ const { associationOpts, referencedSerializersAndOpenapiSchemaBodyShorthand } = associationOpenapi(attribute, DataTypeForOpenapi, alreadyExtractedDescendantSerializers);
171
+ const optional = attribute.options.optional ?? associationOpts.optional;
175
172
  newlyReferencedSerializers =
176
173
  referencedSerializersAndOpenapiSchemaBodyShorthand.referencedSerializers;
177
- if (attribute.options.flatten && attribute.options.optional) {
174
+ if (attribute.options.flatten && optional) {
178
175
  this.allOfSiblings.push({
179
176
  anyOf: [referencedSerializersAndOpenapiSchemaBodyShorthand.openapi, NULL_OBJECT_OPENAPI],
180
177
  });
@@ -184,7 +181,7 @@ class SerializerOpenapiRenderer {
184
181
  this.allOfSiblings.push(referencedSerializersAndOpenapiSchemaBodyShorthand.openapi);
185
182
  //
186
183
  }
187
- else if (attribute.options.optional) {
184
+ else if (optional) {
188
185
  accumulator[outputAttributeName] = {
189
186
  anyOf: [referencedSerializersAndOpenapiSchemaBodyShorthand.openapi, NULL_OBJECT_OPENAPI],
190
187
  };
@@ -211,7 +208,7 @@ class SerializerOpenapiRenderer {
211
208
  case 'rendersMany': {
212
209
  try {
213
210
  const outputAttributeName = this.setCase(attribute.options.as ?? attribute.name);
214
- const referencedSerializersAndOpenapiSchemaBodyShorthand = associationOpenapi(attribute, DataTypeForOpenapi, alreadyExtractedDescendantSerializers, openapiRenderingOpts);
211
+ const { referencedSerializersAndOpenapiSchemaBodyShorthand } = associationOpenapi(attribute, DataTypeForOpenapi, alreadyExtractedDescendantSerializers);
215
212
  newlyReferencedSerializers =
216
213
  referencedSerializersAndOpenapiSchemaBodyShorthand.referencedSerializers;
217
214
  accumulator[outputAttributeName] = {
@@ -238,7 +235,7 @@ class SerializerOpenapiRenderer {
238
235
  }
239
236
  }
240
237
  })();
241
- const recursiveNewlyReferencedSerializers = newlyReferencedSerializers.flatMap(serializer => descendantSerializers(serializer, alreadyExtractedDescendantSerializers, openapiRenderingOpts));
238
+ const recursiveNewlyReferencedSerializers = newlyReferencedSerializers.flatMap(serializer => descendantSerializers(serializer, alreadyExtractedDescendantSerializers));
242
239
  referencedSerializers = [
243
240
  ...referencedSerializers,
244
241
  ...newlyReferencedSerializers,
@@ -266,16 +263,19 @@ class SerializerOpenapiRenderer {
266
263
  }
267
264
  }
268
265
  exports.default = SerializerOpenapiRenderer;
269
- function associationOpenapi(attribute, DataTypeForOpenapi, alreadyExtractedDescendantSerializers, opts) {
266
+ function associationOpenapi(attribute, DataTypeForOpenapi, alreadyExtractedDescendantSerializers) {
270
267
  const serializerOverride = attribute.options.serializer;
271
268
  if (serializerOverride) {
272
269
  try {
273
270
  return {
274
- referencedSerializers: [
275
- serializerOverride,
276
- ...descendantSerializers(serializerOverride, alreadyExtractedDescendantSerializers, opts),
277
- ],
278
- openapi: new SerializerOpenapiRenderer(serializerOverride, opts).serializerRef,
271
+ associationOpts: { optional: false },
272
+ referencedSerializersAndOpenapiSchemaBodyShorthand: {
273
+ referencedSerializers: [
274
+ serializerOverride,
275
+ ...descendantSerializers(serializerOverride, alreadyExtractedDescendantSerializers),
276
+ ],
277
+ openapi: new SerializerOpenapiRenderer(serializerOverride).serializerRef,
278
+ },
279
279
  };
280
280
  }
281
281
  catch (error) {
@@ -288,8 +288,8 @@ function associationOpenapi(attribute, DataTypeForOpenapi, alreadyExtractedDesce
288
288
  const association = DataTypeForOpenapi?.isDream &&
289
289
  // eslint-disable-next-line @typescript-eslint/no-unsafe-call
290
290
  DataTypeForOpenapi['getAssociationMetadata'](attribute.name);
291
+ const optional = !!association?.optional;
291
292
  if (association) {
292
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call
293
293
  associatedClasses = (0, dream_1.expandStiClasses)(association.modelCB());
294
294
  //
295
295
  }
@@ -315,30 +315,36 @@ function associationOpenapi(attribute, DataTypeForOpenapi, alreadyExtractedDesce
315
315
  associatedClasses = [associatedClass];
316
316
  }
317
317
  }
318
- const serializersOpenapi = associatedClasses.flatMap(associatedClass => (0, dream_1.inferSerializersFromDreamClassOrViewModelClass)(associatedClass, attribute.options.serializerKey));
319
- if (serializersOpenapi.length === 0)
318
+ const serializers = associatedClasses.flatMap(associatedClass => (0, dream_1.inferSerializersFromDreamClassOrViewModelClass)(associatedClass, attribute.options.serializerKey));
319
+ if (serializers.length === 0)
320
320
  throw new NoSerializerFoundForRendersOneAndMany_js_1.default(attribute.name);
321
- if (serializersOpenapi.length === 1) {
322
- const serializer = serializersOpenapi[0];
321
+ if (serializers.length === 1) {
322
+ const serializer = serializers[0];
323
323
  return {
324
- referencedSerializers: [
325
- serializer,
326
- ...descendantSerializers(serializer, alreadyExtractedDescendantSerializers, opts),
327
- ],
328
- openapi: new SerializerOpenapiRenderer(serializer, opts).serializerRef,
324
+ associationOpts: { optional },
325
+ referencedSerializersAndOpenapiSchemaBodyShorthand: {
326
+ referencedSerializers: [
327
+ serializer,
328
+ ...descendantSerializers(serializer, alreadyExtractedDescendantSerializers),
329
+ ],
330
+ openapi: new SerializerOpenapiRenderer(serializer).serializerRef,
331
+ },
329
332
  };
330
333
  }
331
334
  return {
332
- referencedSerializers: [
333
- ...serializersOpenapi,
334
- ...serializersOpenapi.flatMap(serializer => descendantSerializers(serializer, alreadyExtractedDescendantSerializers, opts)),
335
- ],
336
- openapi: {
337
- anyOf: (0, dream_1.sortBy)((0, dream_1.uniq)(serializersOpenapi.map(serializer => new SerializerOpenapiRenderer(serializer, opts).serializerRef), ref => ref['$ref']), ref => (ref['$ref'] ? ref['$ref'] : (0, node_util_1.inspect)(ref, { depth: 2 }))),
335
+ associationOpts: { optional },
336
+ referencedSerializersAndOpenapiSchemaBodyShorthand: {
337
+ referencedSerializers: [
338
+ ...serializers,
339
+ ...serializers.flatMap(serializer => descendantSerializers(serializer, alreadyExtractedDescendantSerializers)),
340
+ ],
341
+ openapi: {
342
+ anyOf: (0, dream_1.sortBy)((0, dream_1.uniq)(serializers.map(serializer => new SerializerOpenapiRenderer(serializer).serializerRef), ref => ref['$ref']), ref => (ref['$ref'] ? ref['$ref'] : (0, node_util_1.inspect)(ref, { depth: 2 }))),
343
+ },
338
344
  },
339
345
  };
340
346
  }
341
- function descendantSerializers(serializer, alreadyExtractedDescendantSerializers, opts) {
347
+ function descendantSerializers(serializer, alreadyExtractedDescendantSerializers) {
342
348
  // alreadyExtractedDescendantSerializers is used not only to avoid duplicate
343
349
  // work (and thereby speed up OpenAPI spec generation), but also to avoid
344
350
  // infinite loops (a recursive OpenAPI structure is valid)
@@ -346,10 +352,10 @@ function descendantSerializers(serializer, alreadyExtractedDescendantSerializers
346
352
  return [];
347
353
  if (!(0, dream_1.isDreamSerializer)(serializer))
348
354
  throw new AttemptedToDeriveDescendentSerializersFromNonSerializer_js_1.default(serializer);
349
- const immediateDescendantSerializers = new SerializerOpenapiRenderer(serializer, opts).renderedOpenapi(alreadyExtractedDescendantSerializers).referencedSerializers;
355
+ const immediateDescendantSerializers = new SerializerOpenapiRenderer(serializer).renderedOpenapi(alreadyExtractedDescendantSerializers).referencedSerializers;
350
356
  return [
351
357
  ...immediateDescendantSerializers,
352
- ...immediateDescendantSerializers.flatMap(descendantSerializer => descendantSerializers(descendantSerializer, alreadyExtractedDescendantSerializers, opts)),
358
+ ...immediateDescendantSerializers.flatMap(descendantSerializer => descendantSerializers(descendantSerializer, alreadyExtractedDescendantSerializers)),
353
359
  ];
354
360
  }
355
361
  // When attempting to expand STI children, we might call `.serializers` on
@@ -12,20 +12,57 @@ const maybeNullOpenapiShorthandToOpenapiShorthand_js_1 = __importDefault(require
12
12
  const primitiveOpenapiStatementToOpenapi_js_1 = __importDefault(require("./helpers/primitiveOpenapiStatementToOpenapi.js"));
13
13
  const schemaToRef_js_1 = __importDefault(require("./helpers/schemaToRef.js"));
14
14
  const SerializerOpenapiRenderer_js_1 = __importDefault(require("./SerializerOpenapiRenderer.js"));
15
- class OpenapiBodySegmentRenderer {
15
+ /**
16
+ * @internal
17
+ *
18
+ * Internal class used to recursively expand OpenAPI shorthand notation into full OpenAPI schema objects.
19
+ *
20
+ * This class handles the transformation of various shorthand formats:
21
+ * - Primitive shorthands like `'string'` → `{ type: 'string' }`
22
+ * - Nullable shorthands like `['string', 'null']` → `{ type: ['string', 'null'] }`
23
+ * - Array shorthands like `'string[]'` → `{ type: 'array', items: { type: 'string' } }`
24
+ * - Nullable array shorthands like `['string[]', 'null']` → `{ type: ['array', 'null'], items: { type: 'string' } }`
25
+ * - Serializer references like `{ $serializer: SomeSerializer }` → `{ $ref: '#/components/schemas/SerializerOpenapiName' }`
26
+ * - Serializable references like `{ $serializable: SomeModel, key: 'summary' }` → resolved serializer reference
27
+ *
28
+ * The class recursively processes nested structures (objects, arrays, unions) and maintains
29
+ * a collection of referenced serializers that need to be included in the final OpenAPI document.
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * // Input shorthand
34
+ * {
35
+ * type: 'object',
36
+ * properties: {
37
+ * name: 'string',
38
+ * tags: 'string[]',
39
+ * user: { $serializer: UserSerializer }
40
+ * }
41
+ * }
42
+ *
43
+ * // Output expanded
44
+ * {
45
+ * type: 'object',
46
+ * properties: {
47
+ * name: { type: 'string' },
48
+ * tags: { type: 'array', items: { type: 'string' } },
49
+ * user: { $ref: '#/components/schemas/UserSerializer' }
50
+ * }
51
+ * }
52
+ * ```
53
+ */
54
+ class OpenapiSegmentExpander {
16
55
  bodySegment;
17
56
  casing;
18
57
  suppressResponseEnums;
19
58
  target;
20
- openapiName;
21
59
  /**
22
60
  * @internal
23
61
  *
24
- * Used to recursively parse nested object structures
62
+ * Used to recursively expand nested object structures
25
63
  * within nested openapi objects
26
64
  */
27
- constructor(bodySegment, { openapiName, renderOpts, target }) {
28
- this.openapiName = openapiName;
65
+ constructor(bodySegment, { renderOpts, target }) {
29
66
  this.bodySegment = bodySegment;
30
67
  this.casing = renderOpts.casing;
31
68
  this.suppressResponseEnums = renderOpts.suppressResponseEnums;
@@ -41,7 +78,7 @@ class OpenapiBodySegmentRenderer {
41
78
  /**
42
79
  * @internal
43
80
  *
44
- * Recursively parses nested objects and arrays,
81
+ * Recursively expand nested objects and arrays,
45
82
  * as well as primitive types
46
83
  */
47
84
  recursivelyParseBody(bodySegment) {
@@ -167,7 +204,7 @@ class OpenapiBodySegmentRenderer {
167
204
  /**
168
205
  * @internal
169
206
  *
170
- * recursively parses a oneOf statement
207
+ * recursively expands a oneOf statement
171
208
  */
172
209
  oneOfStatement(bodySegment) {
173
210
  const oneOfBodySegment = bodySegment;
@@ -191,7 +228,7 @@ class OpenapiBodySegmentRenderer {
191
228
  /**
192
229
  * @internal
193
230
  *
194
- * recursively parses an anyOf statement
231
+ * recursively expand an anyOf statement
195
232
  */
196
233
  anyOfStatement(bodySegment) {
197
234
  const anyOfBodySegment = bodySegment;
@@ -215,7 +252,7 @@ class OpenapiBodySegmentRenderer {
215
252
  /**
216
253
  * @internal
217
254
  *
218
- * recursively parses an allOf statement
255
+ * recursively expand an allOf statement
219
256
  */
220
257
  allOfStatement(bodySegment) {
221
258
  const allOfBodySegment = bodySegment;
@@ -239,7 +276,7 @@ class OpenapiBodySegmentRenderer {
239
276
  /**
240
277
  * @internal
241
278
  *
242
- * recursively parses an array statement
279
+ * recursively expand an array statement
243
280
  */
244
281
  arrayStatement(bodySegment) {
245
282
  const results = this.recursivelyParseBody(bodySegment.items);
@@ -256,7 +293,7 @@ class OpenapiBodySegmentRenderer {
256
293
  /**
257
294
  * @internal
258
295
  *
259
- * recursively parses an object statement
296
+ * recursively expand an object statement
260
297
  */
261
298
  objectStatement(bodySegment) {
262
299
  const objectBodySegment = bodySegment;
@@ -293,7 +330,7 @@ class OpenapiBodySegmentRenderer {
293
330
  /**
294
331
  * @internal
295
332
  *
296
- * parses either the `properties` or `additionalProperties` values
333
+ * expand either the `properties` or `additionalProperties` values
297
334
  * on an object
298
335
  */
299
336
  parseObjectPropertyStatement(propertySegment) {
@@ -316,7 +353,7 @@ class OpenapiBodySegmentRenderer {
316
353
  /**
317
354
  * @internal
318
355
  *
319
- * recursively parses a primitive literal type (i.e. string or boolean[])
356
+ * recursively expand a primitive literal type (i.e. string or boolean[])
320
357
  */
321
358
  primitiveLiteralStatement(bodySegment) {
322
359
  return (0, primitiveOpenapiStatementToOpenapi_js_1.default)(bodySegment);
@@ -324,7 +361,7 @@ class OpenapiBodySegmentRenderer {
324
361
  /**
325
362
  * @internal
326
363
  *
327
- * recursively parses a primitive object type (i.e. { type: 'string[]' })
364
+ * recursively expand a primitive object type (i.e. { type: 'string[]' })
328
365
  */
329
366
  primitiveObjectStatement(bodySegment) {
330
367
  const safeBodySegment = bodySegment;
@@ -454,4 +491,4 @@ The following values will be allowed:
454
491
  return returnObj;
455
492
  }
456
493
  }
457
- exports.default = OpenapiBodySegmentRenderer;
494
+ exports.default = OpenapiSegmentExpander;
@@ -283,7 +283,7 @@ class OpenapiEndpointRenderer {
283
283
  * Generates the header portion of the openapi payload's
284
284
  * "parameters" field for a single endpoint.
285
285
  */
286
- queryArray({ openapiName, renderOpts, }) {
286
+ queryArray({ renderOpts, }) {
287
287
  const queryParams = Object.keys(this.query || {}).map((queryName) => {
288
288
  const queryParam = this.query[queryName];
289
289
  let output = {
@@ -307,7 +307,6 @@ class OpenapiEndpointRenderer {
307
307
  ...output,
308
308
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
309
309
  schema: new body_segment_js_1.default(queryParam.schema, {
310
- openapiName,
311
310
  renderOpts,
312
311
  target: 'request',
313
312
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -347,7 +346,6 @@ class OpenapiEndpointRenderer {
347
346
  return this.generateRequestBodyForModel({ openapiName, renderOpts });
348
347
  }
349
348
  let schema = new body_segment_js_1.default(this.requestBody, {
350
- openapiName,
351
349
  renderOpts,
352
350
  target: 'request',
353
351
  }).render().openapi;
@@ -412,7 +410,7 @@ class OpenapiEndpointRenderer {
412
410
  * that model that are safe to ingest will be automatically added to
413
411
  * the request body.
414
412
  */
415
- generateRequestBodyForModel({ openapiName, renderOpts, }) {
413
+ generateRequestBodyForModel({ renderOpts, }) {
416
414
  const forDreamClass = this.requestBody?.for;
417
415
  const dreamClass = forDreamClass || this.getSingleDreamModelClass();
418
416
  if (!dreamClass)
@@ -525,7 +523,6 @@ class OpenapiEndpointRenderer {
525
523
  paramsShape.properties = {
526
524
  ...paramsShape.properties,
527
525
  [columnName]: new body_segment_js_1.default(metadata.type, {
528
- openapiName,
529
526
  renderOpts,
530
527
  target: 'request',
531
528
  }).render().openapi,
@@ -570,7 +567,6 @@ class OpenapiEndpointRenderer {
570
567
  }
571
568
  }
572
569
  let processedSchema = new body_segment_js_1.default(paramsShape, {
573
- openapiName,
574
570
  renderOpts,
575
571
  target: 'request',
576
572
  }).render().openapi;
@@ -594,7 +590,6 @@ class OpenapiEndpointRenderer {
594
590
  parseResponses({ openapiName, renderOpts, }) {
595
591
  let responseData = {};
596
592
  const rendererOpts = {
597
- openapiName,
598
593
  renderOpts,
599
594
  target: 'response',
600
595
  };
@@ -986,7 +981,6 @@ function serializersToSchemaObjects(controllerClass, actionName, serializers, {
986
981
  const renderer = new SerializerOpenapiRenderer_js_1.default(serializer, renderOpts);
987
982
  const results = renderer.renderedOpenapi(alreadyExtractedDescendantSerializers);
988
983
  const segmentRendererResults = new body_segment_js_1.default(results.openapi, {
989
- openapiName,
990
984
  renderOpts,
991
985
  target: 'response',
992
986
  }).render();
@@ -4,7 +4,49 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.default = allSerializersFromHandWrittenOpenapi;
7
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
8
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
9
+ const dream_1 = require("@rvoh/dream");
7
10
  const isObject_js_1 = __importDefault(require("../../helpers/isObject.js"));
11
+ /**
12
+ * @internal
13
+ *
14
+ * Recursively scans an OpenAPI schema structure and returns an array of all Serializers referenced within it.
15
+ *
16
+ * This function traverses the provided OpenAPI schema definition looking for `$serializer` properties
17
+ * and collects all unique serializer references found. It performs a deep scan of nested objects and
18
+ * arrays to find all serializer references at any level of nesting.
19
+ *
20
+ * **Important**: This function does _not_ recurse into the OpenAPI schemas generated by the discovered
21
+ * Serializers themselves. It only extracts serializers from the hand-written OpenAPI structure provided
22
+ * as input.
23
+ *
24
+ * @param openapi - The OpenAPI schema definition to scan for serializer references
25
+ * @returns An array of unique serializers found in the schema structure
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * const schema = {
30
+ * type: 'object',
31
+ * properties: {
32
+ * user: { $serializer: UserSerializer },
33
+ * posts: {
34
+ * type: 'array',
35
+ * items: { $serializer: PostSerializer }
36
+ * },
37
+ * metadata: {
38
+ * type: 'object',
39
+ * properties: {
40
+ * author: { $serializer: UserSerializer } // Duplicate, will be deduplicated
41
+ * }
42
+ * }
43
+ * }
44
+ * }
45
+ *
46
+ * const serializers = allSerializersFromHandWrittenOpenapi(schema)
47
+ * // Returns: [UserSerializer, PostSerializer]
48
+ * ```
49
+ */
8
50
  function allSerializersFromHandWrittenOpenapi(openapi) {
9
51
  const serializers = new Set();
10
52
  extractSerializers(openapi, serializers);
@@ -15,15 +57,17 @@ function extractSerializers(
15
57
  value, serializers) {
16
58
  if (!value)
17
59
  return;
18
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
19
60
  if (value.$serializer) {
20
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
21
61
  serializers.add(value.$serializer);
22
62
  //
23
63
  }
64
+ else if (value.$serializable) {
65
+ const foundSerializers = (0, dream_1.inferSerializersFromDreamClassOrViewModelClass)(value.$serializable, value.$serializableSerializerKey);
66
+ foundSerializers.forEach(serializer => serializers.add(serializer));
67
+ //
68
+ }
24
69
  else if ((0, isObject_js_1.default)(value)) {
25
70
  // Recurse into objects
26
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
27
71
  Object.values(value).forEach(val => extractSerializers(val, serializers));
28
72
  //
29
73
  }
@@ -4,8 +4,57 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.default = allSerializersToRefsInOpenapi;
7
+ /* eslint-disable @typescript-eslint/no-unsafe-return */
8
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
9
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
10
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
11
+ const dream_1 = require("@rvoh/dream");
7
12
  const isObject_js_1 = __importDefault(require("../../helpers/isObject.js"));
8
13
  const SerializerOpenapiRenderer_js_1 = __importDefault(require("../SerializerOpenapiRenderer.js"));
14
+ /**
15
+ * @internal
16
+ *
17
+ * Transforms OpenAPI schema definitions by replacing serializer shorthand references with proper `$ref` statements.
18
+ *
19
+ * This function is used by the SerializerOpenapiRenderer to ensure that the schemas it generates from
20
+ * Serializers contain `$ref` statements instead of `$serializer` or `$serializable` statements. This
21
+ * transformation enables consistent ordering of `anyOf` statements since `anyOf` arrays are sorted by
22
+ * the `$ref` value, which follows the pattern `'#/components/schemas/TheSerializerOpenapiName'`.
23
+ *
24
+ * The function recursively traverses the schema and transforms:
25
+ * - `{ $serializer: SomeSerializer }` → `{ $ref: '#/components/schemas/SerializerOpenapiName' }`
26
+ * - `{ $serializable: SomeModel, key: 'summary' }` → `{ $ref: '#/components/schemas/ModelSummarySerializer' }`
27
+ *
28
+ * @param openapi - The OpenAPI schema definition that may contain serializer shorthand references
29
+ * @returns The transformed schema with `$ref` statements replacing serializer shorthands
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * const input = {
34
+ * type: 'object',
35
+ * properties: {
36
+ * user: { $serializer: UserSerializer },
37
+ * posts: {
38
+ * type: 'array',
39
+ * items: { $serializer: PostSerializer }
40
+ * }
41
+ * }
42
+ * }
43
+ *
44
+ * const output = allSerializersToRefsInOpenapi(input)
45
+ * // Returns:
46
+ * // {
47
+ * // type: 'object',
48
+ * // properties: {
49
+ * // user: { $ref: '#/components/schemas/UserSerializer' },
50
+ * // posts: {
51
+ * // type: 'array',
52
+ * // items: { $ref: '#/components/schemas/PostSerializer' }
53
+ * // }
54
+ * // }
55
+ * // }
56
+ * ```
57
+ */
9
58
  function allSerializersToRefsInOpenapi(openapi) {
10
59
  if (!openapi)
11
60
  return {};
@@ -16,24 +65,38 @@ function transformValue(value) {
16
65
  if (!value)
17
66
  return value;
18
67
  // If this is an object with a $serializer property, replace it with $ref
19
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
20
68
  if (value.$serializer) {
21
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
22
69
  const { $serializer, ...rest } = value;
23
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
24
70
  const openapiRenderer = new SerializerOpenapiRenderer_js_1.default($serializer).serializerRef;
25
71
  return {
26
72
  ...rest,
27
73
  ...openapiRenderer,
28
74
  };
75
+ //
76
+ }
77
+ else if (value.$serializable) {
78
+ const { $serializable, $serializableSerializerKey, ...rest } = value;
79
+ const foundSerializers = (0, dream_1.inferSerializersFromDreamClassOrViewModelClass)($serializable, $serializableSerializerKey);
80
+ const refs = foundSerializers.map(serializer => new SerializerOpenapiRenderer_js_1.default(serializer).serializerRef);
81
+ if (refs.length === 0)
82
+ return rest;
83
+ if (refs.length === 1) {
84
+ return {
85
+ ...rest,
86
+ ...refs[0],
87
+ };
88
+ }
89
+ return {
90
+ ...rest,
91
+ anyOf: refs,
92
+ };
93
+ //
29
94
  }
30
95
  else if ((0, isObject_js_1.default)(value)) {
31
96
  // Recurse into objects
32
97
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
98
  const transformed = {};
34
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
35
99
  for (const [key, val] of Object.entries(value)) {
36
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
37
100
  transformed[key] = transformValue(val);
38
101
  }
39
102
  return transformed;
@@ -41,7 +104,6 @@ function transformValue(value) {
41
104
  }
42
105
  else if (Array.isArray(value)) {
43
106
  // Recurse into arrays
44
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
45
107
  return value.map(val => transformValue(val));
46
108
  //
47
109
  }
@@ -1,6 +1,7 @@
1
1
  import { inspect } from 'node:util';
2
2
  export default class AttemptedToDeriveDescendentSerializersFromNonSerializer extends Error {
3
3
  serializer;
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
5
  constructor(serializer) {
5
6
  super();
6
7
  this.serializer = serializer;
@@ -1,6 +1,7 @@
1
1
  import { inspect } from 'node:util';
2
2
  export default class NonSerializerPassedToSerializerOpenapiRenderer extends Error {
3
3
  serializer;
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
5
  constructor(serializer) {
5
6
  super();
6
7
  this.serializer = serializer;
@@ -2,7 +2,11 @@ import { inspect } from 'node:util';
2
2
  export default class NonSerializerSerializerOverrideProvided extends Error {
3
3
  rendersAssociation;
4
4
  serializer;
5
- constructor(rendersAssociation, serializer) {
5
+ constructor(
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ rendersAssociation,
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ serializer) {
6
10
  super();
7
11
  this.rendersAssociation = rendersAssociation;
8
12
  this.serializer = serializer;