@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.
- package/dist/cjs/src/error/openapi/AttemptedToDeriveDescendentSerializersFromNonSerializer.js +1 -0
- package/dist/cjs/src/error/openapi/NonSerializerPassedToSerializerOpenapiRenderer.js +1 -0
- package/dist/cjs/src/error/openapi/NonSerializerSerializerOverrideProvided.js +5 -1
- package/dist/cjs/src/openapi-renderer/SerializerOpenapiRenderer.js +40 -34
- package/dist/cjs/src/openapi-renderer/body-segment.js +52 -15
- package/dist/cjs/src/openapi-renderer/endpoint.js +2 -8
- package/dist/cjs/src/openapi-renderer/helpers/allSerializersFromHandWrittenOpenapi.js +47 -3
- package/dist/cjs/src/openapi-renderer/helpers/allSerializersToRefsInOpenapi.js +68 -6
- package/dist/esm/src/error/openapi/AttemptedToDeriveDescendentSerializersFromNonSerializer.js +1 -0
- package/dist/esm/src/error/openapi/NonSerializerPassedToSerializerOpenapiRenderer.js +1 -0
- package/dist/esm/src/error/openapi/NonSerializerSerializerOverrideProvided.js +5 -1
- package/dist/esm/src/openapi-renderer/SerializerOpenapiRenderer.js +40 -34
- package/dist/esm/src/openapi-renderer/body-segment.js +51 -14
- package/dist/esm/src/openapi-renderer/endpoint.js +9 -15
- package/dist/esm/src/openapi-renderer/helpers/allSerializersFromHandWrittenOpenapi.js +47 -3
- package/dist/esm/src/openapi-renderer/helpers/allSerializersToRefsInOpenapi.js +68 -6
- package/dist/types/src/helpers/typeHelpers.d.ts +2 -0
- package/dist/types/src/openapi-renderer/body-segment.d.ts +51 -14
- package/dist/types/src/openapi-renderer/endpoint.d.ts +2 -1
- package/dist/types/src/openapi-renderer/helpers/allSerializersFromHandWrittenOpenapi.d.ts +39 -0
- package/dist/types/src/openapi-renderer/helpers/allSerializersToRefsInOpenapi.d.ts +44 -0
- package/package.json +2 -2
|
@@ -103,10 +103,6 @@ export default class SerializerOpenapiRenderer {
|
|
|
103
103
|
const DataTypeForOpenapi = $typeForOpenapi;
|
|
104
104
|
let referencedSerializers = [];
|
|
105
105
|
let renderedOpenapi = {};
|
|
106
|
-
const openapiRenderingOpts = {
|
|
107
|
-
casing: this.casing,
|
|
108
|
-
suppressResponseEnums: this.suppressResponseEnums,
|
|
109
|
-
};
|
|
110
106
|
renderedOpenapi = this.serializerBuilder['attributes'].reduce((accumulator, attribute) => {
|
|
111
107
|
const attributeType = attribute.type;
|
|
112
108
|
let newlyReferencedSerializers = [];
|
|
@@ -166,10 +162,11 @@ export default class SerializerOpenapiRenderer {
|
|
|
166
162
|
case 'rendersOne': {
|
|
167
163
|
try {
|
|
168
164
|
const outputAttributeName = this.setCase(attribute.options.as ?? attribute.name);
|
|
169
|
-
const referencedSerializersAndOpenapiSchemaBodyShorthand = associationOpenapi(attribute, DataTypeForOpenapi, alreadyExtractedDescendantSerializers
|
|
165
|
+
const { associationOpts, referencedSerializersAndOpenapiSchemaBodyShorthand } = associationOpenapi(attribute, DataTypeForOpenapi, alreadyExtractedDescendantSerializers);
|
|
166
|
+
const optional = attribute.options.optional ?? associationOpts.optional;
|
|
170
167
|
newlyReferencedSerializers =
|
|
171
168
|
referencedSerializersAndOpenapiSchemaBodyShorthand.referencedSerializers;
|
|
172
|
-
if (attribute.options.flatten &&
|
|
169
|
+
if (attribute.options.flatten && optional) {
|
|
173
170
|
this.allOfSiblings.push({
|
|
174
171
|
anyOf: [referencedSerializersAndOpenapiSchemaBodyShorthand.openapi, NULL_OBJECT_OPENAPI],
|
|
175
172
|
});
|
|
@@ -179,7 +176,7 @@ export default class SerializerOpenapiRenderer {
|
|
|
179
176
|
this.allOfSiblings.push(referencedSerializersAndOpenapiSchemaBodyShorthand.openapi);
|
|
180
177
|
//
|
|
181
178
|
}
|
|
182
|
-
else if (
|
|
179
|
+
else if (optional) {
|
|
183
180
|
accumulator[outputAttributeName] = {
|
|
184
181
|
anyOf: [referencedSerializersAndOpenapiSchemaBodyShorthand.openapi, NULL_OBJECT_OPENAPI],
|
|
185
182
|
};
|
|
@@ -206,7 +203,7 @@ export default class SerializerOpenapiRenderer {
|
|
|
206
203
|
case 'rendersMany': {
|
|
207
204
|
try {
|
|
208
205
|
const outputAttributeName = this.setCase(attribute.options.as ?? attribute.name);
|
|
209
|
-
const referencedSerializersAndOpenapiSchemaBodyShorthand = associationOpenapi(attribute, DataTypeForOpenapi, alreadyExtractedDescendantSerializers
|
|
206
|
+
const { referencedSerializersAndOpenapiSchemaBodyShorthand } = associationOpenapi(attribute, DataTypeForOpenapi, alreadyExtractedDescendantSerializers);
|
|
210
207
|
newlyReferencedSerializers =
|
|
211
208
|
referencedSerializersAndOpenapiSchemaBodyShorthand.referencedSerializers;
|
|
212
209
|
accumulator[outputAttributeName] = {
|
|
@@ -233,7 +230,7 @@ export default class SerializerOpenapiRenderer {
|
|
|
233
230
|
}
|
|
234
231
|
}
|
|
235
232
|
})();
|
|
236
|
-
const recursiveNewlyReferencedSerializers = newlyReferencedSerializers.flatMap(serializer => descendantSerializers(serializer, alreadyExtractedDescendantSerializers
|
|
233
|
+
const recursiveNewlyReferencedSerializers = newlyReferencedSerializers.flatMap(serializer => descendantSerializers(serializer, alreadyExtractedDescendantSerializers));
|
|
237
234
|
referencedSerializers = [
|
|
238
235
|
...referencedSerializers,
|
|
239
236
|
...newlyReferencedSerializers,
|
|
@@ -260,16 +257,19 @@ export default class SerializerOpenapiRenderer {
|
|
|
260
257
|
}
|
|
261
258
|
}
|
|
262
259
|
}
|
|
263
|
-
function associationOpenapi(attribute, DataTypeForOpenapi, alreadyExtractedDescendantSerializers
|
|
260
|
+
function associationOpenapi(attribute, DataTypeForOpenapi, alreadyExtractedDescendantSerializers) {
|
|
264
261
|
const serializerOverride = attribute.options.serializer;
|
|
265
262
|
if (serializerOverride) {
|
|
266
263
|
try {
|
|
267
264
|
return {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
265
|
+
associationOpts: { optional: false },
|
|
266
|
+
referencedSerializersAndOpenapiSchemaBodyShorthand: {
|
|
267
|
+
referencedSerializers: [
|
|
268
|
+
serializerOverride,
|
|
269
|
+
...descendantSerializers(serializerOverride, alreadyExtractedDescendantSerializers),
|
|
270
|
+
],
|
|
271
|
+
openapi: new SerializerOpenapiRenderer(serializerOverride).serializerRef,
|
|
272
|
+
},
|
|
273
273
|
};
|
|
274
274
|
}
|
|
275
275
|
catch (error) {
|
|
@@ -282,8 +282,8 @@ function associationOpenapi(attribute, DataTypeForOpenapi, alreadyExtractedDesce
|
|
|
282
282
|
const association = DataTypeForOpenapi?.isDream &&
|
|
283
283
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
284
284
|
DataTypeForOpenapi['getAssociationMetadata'](attribute.name);
|
|
285
|
+
const optional = !!association?.optional;
|
|
285
286
|
if (association) {
|
|
286
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
287
287
|
associatedClasses = expandStiClasses(association.modelCB());
|
|
288
288
|
//
|
|
289
289
|
}
|
|
@@ -309,30 +309,36 @@ function associationOpenapi(attribute, DataTypeForOpenapi, alreadyExtractedDesce
|
|
|
309
309
|
associatedClasses = [associatedClass];
|
|
310
310
|
}
|
|
311
311
|
}
|
|
312
|
-
const
|
|
313
|
-
if (
|
|
312
|
+
const serializers = associatedClasses.flatMap(associatedClass => inferSerializersFromDreamClassOrViewModelClass(associatedClass, attribute.options.serializerKey));
|
|
313
|
+
if (serializers.length === 0)
|
|
314
314
|
throw new NoSerializerFoundForRendersOneAndMany(attribute.name);
|
|
315
|
-
if (
|
|
316
|
-
const serializer =
|
|
315
|
+
if (serializers.length === 1) {
|
|
316
|
+
const serializer = serializers[0];
|
|
317
317
|
return {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
318
|
+
associationOpts: { optional },
|
|
319
|
+
referencedSerializersAndOpenapiSchemaBodyShorthand: {
|
|
320
|
+
referencedSerializers: [
|
|
321
|
+
serializer,
|
|
322
|
+
...descendantSerializers(serializer, alreadyExtractedDescendantSerializers),
|
|
323
|
+
],
|
|
324
|
+
openapi: new SerializerOpenapiRenderer(serializer).serializerRef,
|
|
325
|
+
},
|
|
323
326
|
};
|
|
324
327
|
}
|
|
325
328
|
return {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
329
|
+
associationOpts: { optional },
|
|
330
|
+
referencedSerializersAndOpenapiSchemaBodyShorthand: {
|
|
331
|
+
referencedSerializers: [
|
|
332
|
+
...serializers,
|
|
333
|
+
...serializers.flatMap(serializer => descendantSerializers(serializer, alreadyExtractedDescendantSerializers)),
|
|
334
|
+
],
|
|
335
|
+
openapi: {
|
|
336
|
+
anyOf: sortBy(uniq(serializers.map(serializer => new SerializerOpenapiRenderer(serializer).serializerRef), ref => ref['$ref']), ref => (ref['$ref'] ? ref['$ref'] : inspect(ref, { depth: 2 }))),
|
|
337
|
+
},
|
|
332
338
|
},
|
|
333
339
|
};
|
|
334
340
|
}
|
|
335
|
-
function descendantSerializers(serializer, alreadyExtractedDescendantSerializers
|
|
341
|
+
function descendantSerializers(serializer, alreadyExtractedDescendantSerializers) {
|
|
336
342
|
// alreadyExtractedDescendantSerializers is used not only to avoid duplicate
|
|
337
343
|
// work (and thereby speed up OpenAPI spec generation), but also to avoid
|
|
338
344
|
// infinite loops (a recursive OpenAPI structure is valid)
|
|
@@ -340,10 +346,10 @@ function descendantSerializers(serializer, alreadyExtractedDescendantSerializers
|
|
|
340
346
|
return [];
|
|
341
347
|
if (!isDreamSerializer(serializer))
|
|
342
348
|
throw new AttemptedToDeriveDescendentSerializersFromNonSerializer(serializer);
|
|
343
|
-
const immediateDescendantSerializers = new SerializerOpenapiRenderer(serializer
|
|
349
|
+
const immediateDescendantSerializers = new SerializerOpenapiRenderer(serializer).renderedOpenapi(alreadyExtractedDescendantSerializers).referencedSerializers;
|
|
344
350
|
return [
|
|
345
351
|
...immediateDescendantSerializers,
|
|
346
|
-
...immediateDescendantSerializers.flatMap(descendantSerializer => descendantSerializers(descendantSerializer, alreadyExtractedDescendantSerializers
|
|
352
|
+
...immediateDescendantSerializers.flatMap(descendantSerializer => descendantSerializers(descendantSerializer, alreadyExtractedDescendantSerializers)),
|
|
347
353
|
];
|
|
348
354
|
}
|
|
349
355
|
// When attempting to expand STI children, we might call `.serializers` on
|
|
@@ -7,20 +7,57 @@ import maybeNullOpenapiShorthandToOpenapiShorthand from './helpers/maybeNullOpen
|
|
|
7
7
|
import primitiveOpenapiStatementToOpenapi from './helpers/primitiveOpenapiStatementToOpenapi.js';
|
|
8
8
|
import schemaToRef from './helpers/schemaToRef.js';
|
|
9
9
|
import SerializerOpenapiRenderer from './SerializerOpenapiRenderer.js';
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* @internal
|
|
12
|
+
*
|
|
13
|
+
* Internal class used to recursively expand OpenAPI shorthand notation into full OpenAPI schema objects.
|
|
14
|
+
*
|
|
15
|
+
* This class handles the transformation of various shorthand formats:
|
|
16
|
+
* - Primitive shorthands like `'string'` → `{ type: 'string' }`
|
|
17
|
+
* - Nullable shorthands like `['string', 'null']` → `{ type: ['string', 'null'] }`
|
|
18
|
+
* - Array shorthands like `'string[]'` → `{ type: 'array', items: { type: 'string' } }`
|
|
19
|
+
* - Nullable array shorthands like `['string[]', 'null']` → `{ type: ['array', 'null'], items: { type: 'string' } }`
|
|
20
|
+
* - Serializer references like `{ $serializer: SomeSerializer }` → `{ $ref: '#/components/schemas/SerializerOpenapiName' }`
|
|
21
|
+
* - Serializable references like `{ $serializable: SomeModel, key: 'summary' }` → resolved serializer reference
|
|
22
|
+
*
|
|
23
|
+
* The class recursively processes nested structures (objects, arrays, unions) and maintains
|
|
24
|
+
* a collection of referenced serializers that need to be included in the final OpenAPI document.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* // Input shorthand
|
|
29
|
+
* {
|
|
30
|
+
* type: 'object',
|
|
31
|
+
* properties: {
|
|
32
|
+
* name: 'string',
|
|
33
|
+
* tags: 'string[]',
|
|
34
|
+
* user: { $serializer: UserSerializer }
|
|
35
|
+
* }
|
|
36
|
+
* }
|
|
37
|
+
*
|
|
38
|
+
* // Output expanded
|
|
39
|
+
* {
|
|
40
|
+
* type: 'object',
|
|
41
|
+
* properties: {
|
|
42
|
+
* name: { type: 'string' },
|
|
43
|
+
* tags: { type: 'array', items: { type: 'string' } },
|
|
44
|
+
* user: { $ref: '#/components/schemas/UserSerializer' }
|
|
45
|
+
* }
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export default class OpenapiSegmentExpander {
|
|
11
50
|
bodySegment;
|
|
12
51
|
casing;
|
|
13
52
|
suppressResponseEnums;
|
|
14
53
|
target;
|
|
15
|
-
openapiName;
|
|
16
54
|
/**
|
|
17
55
|
* @internal
|
|
18
56
|
*
|
|
19
|
-
* Used to recursively
|
|
57
|
+
* Used to recursively expand nested object structures
|
|
20
58
|
* within nested openapi objects
|
|
21
59
|
*/
|
|
22
|
-
constructor(bodySegment, {
|
|
23
|
-
this.openapiName = openapiName;
|
|
60
|
+
constructor(bodySegment, { renderOpts, target }) {
|
|
24
61
|
this.bodySegment = bodySegment;
|
|
25
62
|
this.casing = renderOpts.casing;
|
|
26
63
|
this.suppressResponseEnums = renderOpts.suppressResponseEnums;
|
|
@@ -36,7 +73,7 @@ export default class OpenapiBodySegmentRenderer {
|
|
|
36
73
|
/**
|
|
37
74
|
* @internal
|
|
38
75
|
*
|
|
39
|
-
* Recursively
|
|
76
|
+
* Recursively expand nested objects and arrays,
|
|
40
77
|
* as well as primitive types
|
|
41
78
|
*/
|
|
42
79
|
recursivelyParseBody(bodySegment) {
|
|
@@ -162,7 +199,7 @@ export default class OpenapiBodySegmentRenderer {
|
|
|
162
199
|
/**
|
|
163
200
|
* @internal
|
|
164
201
|
*
|
|
165
|
-
* recursively
|
|
202
|
+
* recursively expands a oneOf statement
|
|
166
203
|
*/
|
|
167
204
|
oneOfStatement(bodySegment) {
|
|
168
205
|
const oneOfBodySegment = bodySegment;
|
|
@@ -186,7 +223,7 @@ export default class OpenapiBodySegmentRenderer {
|
|
|
186
223
|
/**
|
|
187
224
|
* @internal
|
|
188
225
|
*
|
|
189
|
-
* recursively
|
|
226
|
+
* recursively expand an anyOf statement
|
|
190
227
|
*/
|
|
191
228
|
anyOfStatement(bodySegment) {
|
|
192
229
|
const anyOfBodySegment = bodySegment;
|
|
@@ -210,7 +247,7 @@ export default class OpenapiBodySegmentRenderer {
|
|
|
210
247
|
/**
|
|
211
248
|
* @internal
|
|
212
249
|
*
|
|
213
|
-
* recursively
|
|
250
|
+
* recursively expand an allOf statement
|
|
214
251
|
*/
|
|
215
252
|
allOfStatement(bodySegment) {
|
|
216
253
|
const allOfBodySegment = bodySegment;
|
|
@@ -234,7 +271,7 @@ export default class OpenapiBodySegmentRenderer {
|
|
|
234
271
|
/**
|
|
235
272
|
* @internal
|
|
236
273
|
*
|
|
237
|
-
* recursively
|
|
274
|
+
* recursively expand an array statement
|
|
238
275
|
*/
|
|
239
276
|
arrayStatement(bodySegment) {
|
|
240
277
|
const results = this.recursivelyParseBody(bodySegment.items);
|
|
@@ -251,7 +288,7 @@ export default class OpenapiBodySegmentRenderer {
|
|
|
251
288
|
/**
|
|
252
289
|
* @internal
|
|
253
290
|
*
|
|
254
|
-
* recursively
|
|
291
|
+
* recursively expand an object statement
|
|
255
292
|
*/
|
|
256
293
|
objectStatement(bodySegment) {
|
|
257
294
|
const objectBodySegment = bodySegment;
|
|
@@ -288,7 +325,7 @@ export default class OpenapiBodySegmentRenderer {
|
|
|
288
325
|
/**
|
|
289
326
|
* @internal
|
|
290
327
|
*
|
|
291
|
-
*
|
|
328
|
+
* expand either the `properties` or `additionalProperties` values
|
|
292
329
|
* on an object
|
|
293
330
|
*/
|
|
294
331
|
parseObjectPropertyStatement(propertySegment) {
|
|
@@ -311,7 +348,7 @@ export default class OpenapiBodySegmentRenderer {
|
|
|
311
348
|
/**
|
|
312
349
|
* @internal
|
|
313
350
|
*
|
|
314
|
-
* recursively
|
|
351
|
+
* recursively expand a primitive literal type (i.e. string or boolean[])
|
|
315
352
|
*/
|
|
316
353
|
primitiveLiteralStatement(bodySegment) {
|
|
317
354
|
return primitiveOpenapiStatementToOpenapi(bodySegment);
|
|
@@ -319,7 +356,7 @@ export default class OpenapiBodySegmentRenderer {
|
|
|
319
356
|
/**
|
|
320
357
|
* @internal
|
|
321
358
|
*
|
|
322
|
-
* recursively
|
|
359
|
+
* recursively expand a primitive object type (i.e. { type: 'string[]' })
|
|
323
360
|
*/
|
|
324
361
|
primitiveObjectStatement(bodySegment) {
|
|
325
362
|
const safeBodySegment = bodySegment;
|
|
@@ -3,7 +3,7 @@ import OpenApiFailedToLookupSerializerForEndpoint from '../error/openapi/FailedT
|
|
|
3
3
|
import NonSerializerDerivedInOpenapiEndpointRenderer from '../error/openapi/NonSerializerDerivedInOpenapiEndpointRenderer.js';
|
|
4
4
|
import NonSerializerDerivedInToSchemaObjects from '../error/openapi/NonSerializerDerivedInToSchemaObjects.js';
|
|
5
5
|
import OpenApiSerializerForEndpointNotAFunction from '../error/openapi/SerializerForEndpointNotAFunction.js';
|
|
6
|
-
import
|
|
6
|
+
import OpenapiSegmentExpander from './body-segment.js';
|
|
7
7
|
import { DEFAULT_OPENAPI_RESPONSES } from './defaults.js';
|
|
8
8
|
import openapiOpts from './helpers/openapiOpts.js';
|
|
9
9
|
import openapiRoute from './helpers/openapiRoute.js';
|
|
@@ -277,7 +277,7 @@ export default class OpenapiEndpointRenderer {
|
|
|
277
277
|
* Generates the header portion of the openapi payload's
|
|
278
278
|
* "parameters" field for a single endpoint.
|
|
279
279
|
*/
|
|
280
|
-
queryArray({
|
|
280
|
+
queryArray({ renderOpts, }) {
|
|
281
281
|
const queryParams = Object.keys(this.query || {}).map((queryName) => {
|
|
282
282
|
const queryParam = this.query[queryName];
|
|
283
283
|
let output = {
|
|
@@ -300,8 +300,7 @@ export default class OpenapiEndpointRenderer {
|
|
|
300
300
|
output = {
|
|
301
301
|
...output,
|
|
302
302
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
303
|
-
schema: new
|
|
304
|
-
openapiName,
|
|
303
|
+
schema: new OpenapiSegmentExpander(queryParam.schema, {
|
|
305
304
|
renderOpts,
|
|
306
305
|
target: 'request',
|
|
307
306
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -340,8 +339,7 @@ export default class OpenapiEndpointRenderer {
|
|
|
340
339
|
if (this.shouldAutogenerateBody()) {
|
|
341
340
|
return this.generateRequestBodyForModel({ openapiName, renderOpts });
|
|
342
341
|
}
|
|
343
|
-
let schema = new
|
|
344
|
-
openapiName,
|
|
342
|
+
let schema = new OpenapiSegmentExpander(this.requestBody, {
|
|
345
343
|
renderOpts,
|
|
346
344
|
target: 'request',
|
|
347
345
|
}).render().openapi;
|
|
@@ -406,7 +404,7 @@ export default class OpenapiEndpointRenderer {
|
|
|
406
404
|
* that model that are safe to ingest will be automatically added to
|
|
407
405
|
* the request body.
|
|
408
406
|
*/
|
|
409
|
-
generateRequestBodyForModel({
|
|
407
|
+
generateRequestBodyForModel({ renderOpts, }) {
|
|
410
408
|
const forDreamClass = this.requestBody?.for;
|
|
411
409
|
const dreamClass = forDreamClass || this.getSingleDreamModelClass();
|
|
412
410
|
if (!dreamClass)
|
|
@@ -518,8 +516,7 @@ export default class OpenapiEndpointRenderer {
|
|
|
518
516
|
if (metadata?.type) {
|
|
519
517
|
paramsShape.properties = {
|
|
520
518
|
...paramsShape.properties,
|
|
521
|
-
[columnName]: new
|
|
522
|
-
openapiName,
|
|
519
|
+
[columnName]: new OpenapiSegmentExpander(metadata.type, {
|
|
523
520
|
renderOpts,
|
|
524
521
|
target: 'request',
|
|
525
522
|
}).render().openapi,
|
|
@@ -563,8 +560,7 @@ export default class OpenapiEndpointRenderer {
|
|
|
563
560
|
}
|
|
564
561
|
}
|
|
565
562
|
}
|
|
566
|
-
let processedSchema = new
|
|
567
|
-
openapiName,
|
|
563
|
+
let processedSchema = new OpenapiSegmentExpander(paramsShape, {
|
|
568
564
|
renderOpts,
|
|
569
565
|
target: 'request',
|
|
570
566
|
}).render().openapi;
|
|
@@ -588,7 +584,6 @@ export default class OpenapiEndpointRenderer {
|
|
|
588
584
|
parseResponses({ openapiName, renderOpts, }) {
|
|
589
585
|
let responseData = {};
|
|
590
586
|
const rendererOpts = {
|
|
591
|
-
openapiName,
|
|
592
587
|
renderOpts,
|
|
593
588
|
target: 'response',
|
|
594
589
|
};
|
|
@@ -623,7 +618,7 @@ export default class OpenapiEndpointRenderer {
|
|
|
623
618
|
const response = cloneDeepSafe(this.responses[statusCodeInt], obj => obj);
|
|
624
619
|
responseData[statusCodeInt] ||= { description: statusDescription(statusCodeInt) };
|
|
625
620
|
const statusResponse = responseData[statusCodeInt];
|
|
626
|
-
const results = new
|
|
621
|
+
const results = new OpenapiSegmentExpander(response, rendererOpts).render();
|
|
627
622
|
serializersAppearingInHandWrittenOpenapi = [
|
|
628
623
|
...serializersAppearingInHandWrittenOpenapi,
|
|
629
624
|
...results.referencedSerializers,
|
|
@@ -977,8 +972,7 @@ function serializersToSchemaObjects(controllerClass, actionName, serializers, {
|
|
|
977
972
|
serializers.forEach(serializer => {
|
|
978
973
|
const renderer = new SerializerOpenapiRenderer(serializer, renderOpts);
|
|
979
974
|
const results = renderer.renderedOpenapi(alreadyExtractedDescendantSerializers);
|
|
980
|
-
const segmentRendererResults = new
|
|
981
|
-
openapiName,
|
|
975
|
+
const segmentRendererResults = new OpenapiSegmentExpander(results.openapi, {
|
|
982
976
|
renderOpts,
|
|
983
977
|
target: 'response',
|
|
984
978
|
}).render();
|
|
@@ -1,4 +1,46 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
3
|
+
import { inferSerializersFromDreamClassOrViewModelClass, } from '@rvoh/dream';
|
|
1
4
|
import isObject from '../../helpers/isObject.js';
|
|
5
|
+
/**
|
|
6
|
+
* @internal
|
|
7
|
+
*
|
|
8
|
+
* Recursively scans an OpenAPI schema structure and returns an array of all Serializers referenced within it.
|
|
9
|
+
*
|
|
10
|
+
* This function traverses the provided OpenAPI schema definition looking for `$serializer` properties
|
|
11
|
+
* and collects all unique serializer references found. It performs a deep scan of nested objects and
|
|
12
|
+
* arrays to find all serializer references at any level of nesting.
|
|
13
|
+
*
|
|
14
|
+
* **Important**: This function does _not_ recurse into the OpenAPI schemas generated by the discovered
|
|
15
|
+
* Serializers themselves. It only extracts serializers from the hand-written OpenAPI structure provided
|
|
16
|
+
* as input.
|
|
17
|
+
*
|
|
18
|
+
* @param openapi - The OpenAPI schema definition to scan for serializer references
|
|
19
|
+
* @returns An array of unique serializers found in the schema structure
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* const schema = {
|
|
24
|
+
* type: 'object',
|
|
25
|
+
* properties: {
|
|
26
|
+
* user: { $serializer: UserSerializer },
|
|
27
|
+
* posts: {
|
|
28
|
+
* type: 'array',
|
|
29
|
+
* items: { $serializer: PostSerializer }
|
|
30
|
+
* },
|
|
31
|
+
* metadata: {
|
|
32
|
+
* type: 'object',
|
|
33
|
+
* properties: {
|
|
34
|
+
* author: { $serializer: UserSerializer } // Duplicate, will be deduplicated
|
|
35
|
+
* }
|
|
36
|
+
* }
|
|
37
|
+
* }
|
|
38
|
+
* }
|
|
39
|
+
*
|
|
40
|
+
* const serializers = allSerializersFromHandWrittenOpenapi(schema)
|
|
41
|
+
* // Returns: [UserSerializer, PostSerializer]
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
2
44
|
export default function allSerializersFromHandWrittenOpenapi(openapi) {
|
|
3
45
|
const serializers = new Set();
|
|
4
46
|
extractSerializers(openapi, serializers);
|
|
@@ -9,15 +51,17 @@ function extractSerializers(
|
|
|
9
51
|
value, serializers) {
|
|
10
52
|
if (!value)
|
|
11
53
|
return;
|
|
12
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
13
54
|
if (value.$serializer) {
|
|
14
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
|
|
15
55
|
serializers.add(value.$serializer);
|
|
16
56
|
//
|
|
17
57
|
}
|
|
58
|
+
else if (value.$serializable) {
|
|
59
|
+
const foundSerializers = inferSerializersFromDreamClassOrViewModelClass(value.$serializable, value.$serializableSerializerKey);
|
|
60
|
+
foundSerializers.forEach(serializer => serializers.add(serializer));
|
|
61
|
+
//
|
|
62
|
+
}
|
|
18
63
|
else if (isObject(value)) {
|
|
19
64
|
// Recurse into objects
|
|
20
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
21
65
|
Object.values(value).forEach(val => extractSerializers(val, serializers));
|
|
22
66
|
//
|
|
23
67
|
}
|
|
@@ -1,5 +1,54 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
3
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
4
|
+
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
5
|
+
import { inferSerializersFromDreamClassOrViewModelClass, } from '@rvoh/dream';
|
|
1
6
|
import isObject from '../../helpers/isObject.js';
|
|
2
7
|
import SerializerOpenapiRenderer from '../SerializerOpenapiRenderer.js';
|
|
8
|
+
/**
|
|
9
|
+
* @internal
|
|
10
|
+
*
|
|
11
|
+
* Transforms OpenAPI schema definitions by replacing serializer shorthand references with proper `$ref` statements.
|
|
12
|
+
*
|
|
13
|
+
* This function is used by the SerializerOpenapiRenderer to ensure that the schemas it generates from
|
|
14
|
+
* Serializers contain `$ref` statements instead of `$serializer` or `$serializable` statements. This
|
|
15
|
+
* transformation enables consistent ordering of `anyOf` statements since `anyOf` arrays are sorted by
|
|
16
|
+
* the `$ref` value, which follows the pattern `'#/components/schemas/TheSerializerOpenapiName'`.
|
|
17
|
+
*
|
|
18
|
+
* The function recursively traverses the schema and transforms:
|
|
19
|
+
* - `{ $serializer: SomeSerializer }` → `{ $ref: '#/components/schemas/SerializerOpenapiName' }`
|
|
20
|
+
* - `{ $serializable: SomeModel, key: 'summary' }` → `{ $ref: '#/components/schemas/ModelSummarySerializer' }`
|
|
21
|
+
*
|
|
22
|
+
* @param openapi - The OpenAPI schema definition that may contain serializer shorthand references
|
|
23
|
+
* @returns The transformed schema with `$ref` statements replacing serializer shorthands
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const input = {
|
|
28
|
+
* type: 'object',
|
|
29
|
+
* properties: {
|
|
30
|
+
* user: { $serializer: UserSerializer },
|
|
31
|
+
* posts: {
|
|
32
|
+
* type: 'array',
|
|
33
|
+
* items: { $serializer: PostSerializer }
|
|
34
|
+
* }
|
|
35
|
+
* }
|
|
36
|
+
* }
|
|
37
|
+
*
|
|
38
|
+
* const output = allSerializersToRefsInOpenapi(input)
|
|
39
|
+
* // Returns:
|
|
40
|
+
* // {
|
|
41
|
+
* // type: 'object',
|
|
42
|
+
* // properties: {
|
|
43
|
+
* // user: { $ref: '#/components/schemas/UserSerializer' },
|
|
44
|
+
* // posts: {
|
|
45
|
+
* // type: 'array',
|
|
46
|
+
* // items: { $ref: '#/components/schemas/PostSerializer' }
|
|
47
|
+
* // }
|
|
48
|
+
* // }
|
|
49
|
+
* // }
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
3
52
|
export default function allSerializersToRefsInOpenapi(openapi) {
|
|
4
53
|
if (!openapi)
|
|
5
54
|
return {};
|
|
@@ -10,24 +59,38 @@ function transformValue(value) {
|
|
|
10
59
|
if (!value)
|
|
11
60
|
return value;
|
|
12
61
|
// If this is an object with a $serializer property, replace it with $ref
|
|
13
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
14
62
|
if (value.$serializer) {
|
|
15
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
16
63
|
const { $serializer, ...rest } = value;
|
|
17
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
18
64
|
const openapiRenderer = new SerializerOpenapiRenderer($serializer).serializerRef;
|
|
19
65
|
return {
|
|
20
66
|
...rest,
|
|
21
67
|
...openapiRenderer,
|
|
22
68
|
};
|
|
69
|
+
//
|
|
70
|
+
}
|
|
71
|
+
else if (value.$serializable) {
|
|
72
|
+
const { $serializable, $serializableSerializerKey, ...rest } = value;
|
|
73
|
+
const foundSerializers = inferSerializersFromDreamClassOrViewModelClass($serializable, $serializableSerializerKey);
|
|
74
|
+
const refs = foundSerializers.map(serializer => new SerializerOpenapiRenderer(serializer).serializerRef);
|
|
75
|
+
if (refs.length === 0)
|
|
76
|
+
return rest;
|
|
77
|
+
if (refs.length === 1) {
|
|
78
|
+
return {
|
|
79
|
+
...rest,
|
|
80
|
+
...refs[0],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
...rest,
|
|
85
|
+
anyOf: refs,
|
|
86
|
+
};
|
|
87
|
+
//
|
|
23
88
|
}
|
|
24
89
|
else if (isObject(value)) {
|
|
25
90
|
// Recurse into objects
|
|
26
91
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
92
|
const transformed = {};
|
|
28
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
29
93
|
for (const [key, val] of Object.entries(value)) {
|
|
30
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
|
31
94
|
transformed[key] = transformValue(val);
|
|
32
95
|
}
|
|
33
96
|
return transformed;
|
|
@@ -35,7 +98,6 @@ function transformValue(value) {
|
|
|
35
98
|
}
|
|
36
99
|
else if (Array.isArray(value)) {
|
|
37
100
|
// Recurse into arrays
|
|
38
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
39
101
|
return value.map(val => transformValue(val));
|
|
40
102
|
//
|
|
41
103
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { Dream, DreamOrViewModelClassSerializerKey, DreamSerializableArray, ViewModelClass } from '@rvoh/dream';
|
|
1
2
|
export type FunctionProperties<T> = {
|
|
2
3
|
[K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: T[K];
|
|
3
4
|
};
|
|
4
5
|
export type FunctionPropertyNames<T> = keyof FunctionProperties<T> & string;
|
|
5
6
|
export type StringToInt<T extends string> = T extends `${infer N extends number}` ? N : never;
|
|
7
|
+
export type DreamOrViewModelClassSerializerArrayKeys<T extends DreamSerializableArray> = T['length'] extends 0 ? string : T[0] extends typeof Dream | ViewModelClass ? DreamOrViewModelClassSerializerKey<T[0]> & DreamOrViewModelClassSerializerArrayKeys<T extends [any, ...infer Rest] ? Rest : never> : DreamOrViewModelClassSerializerArrayKeys<T extends [any, ...infer Rest] ? Rest : never>;
|