@rvoh/psychic 0.36.0-beta.1 → 0.37.0-beta.3
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/controller/index.js +18 -12
- package/dist/cjs/src/openapi-renderer/app.js +16 -15
- package/dist/cjs/src/openapi-renderer/body-segment.js +3 -6
- package/dist/cjs/src/openapi-renderer/endpoint.js +56 -103
- package/dist/cjs/src/openapi-renderer/helpers/{suppressResponseEnums.js → suppressResponseEnumsConfig.js} +2 -2
- package/dist/cjs/src/psychic-app/index.js +0 -1
- package/dist/cjs/src/server/index.js +1 -35
- package/dist/esm/src/controller/index.js +19 -13
- package/dist/esm/src/openapi-renderer/app.js +17 -16
- package/dist/esm/src/openapi-renderer/body-segment.js +3 -6
- package/dist/esm/src/openapi-renderer/endpoint.js +57 -104
- package/dist/esm/src/openapi-renderer/helpers/{suppressResponseEnums.js → suppressResponseEnumsConfig.js} +1 -1
- package/dist/esm/src/psychic-app/index.js +0 -1
- package/dist/esm/src/server/index.js +1 -35
- package/dist/types/src/controller/index.d.ts +3 -1
- package/dist/types/src/openapi-renderer/body-segment.d.ts +4 -7
- package/dist/types/src/openapi-renderer/endpoint.d.ts +17 -21
- package/dist/types/src/openapi-renderer/helpers/{schemaDelimiter.d.ts → suppressResponseEnumsConfig.d.ts} +1 -1
- package/dist/types/src/psychic-app/index.d.ts +0 -53
- package/dist/types/src/server/index.d.ts +0 -1
- package/package.json +2 -5
- package/dist/cjs/src/openapi-renderer/helpers/schemaDelimiter.js +0 -13
- package/dist/esm/src/openapi-renderer/helpers/schemaDelimiter.js +0 -7
- package/dist/types/src/openapi-renderer/helpers/suppressResponseEnums.d.ts +0 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DreamApp, GlobalNameNotSet, isDreamSerializer,
|
|
1
|
+
import { DreamApp, DreamSerializerBuilder, GlobalNameNotSet, isDreamSerializer, ObjectSerializerBuilder, } from '@rvoh/dream';
|
|
2
2
|
import ParamValidationError from '../error/controller/ParamValidationError.js';
|
|
3
3
|
import HttpStatusBadGateway from '../error/http/BadGateway.js';
|
|
4
4
|
import HttpStatusBadRequest from '../error/http/BadRequest.js';
|
|
@@ -187,12 +187,20 @@ export default class PsychicController {
|
|
|
187
187
|
session;
|
|
188
188
|
config;
|
|
189
189
|
action;
|
|
190
|
+
renderOpts;
|
|
190
191
|
constructor(req, res, { config, action, }) {
|
|
191
192
|
this.req = req;
|
|
192
193
|
this.res = res;
|
|
193
194
|
this.config = config;
|
|
194
195
|
this.session = new Session(req, res);
|
|
195
196
|
this.action = action;
|
|
197
|
+
// TODO: read casing from Dream app config
|
|
198
|
+
this.renderOpts = {
|
|
199
|
+
casing: 'camel',
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
get headers() {
|
|
203
|
+
return this.req.headers;
|
|
196
204
|
}
|
|
197
205
|
get params() {
|
|
198
206
|
const params = {
|
|
@@ -256,24 +264,25 @@ export default class PsychicController {
|
|
|
256
264
|
return data;
|
|
257
265
|
const dreamApp = DreamApp.getOrFail();
|
|
258
266
|
const psychicControllerClass = this.constructor;
|
|
267
|
+
// if we already have a serializer, let's just render it
|
|
268
|
+
if (data instanceof DreamSerializerBuilder || data instanceof ObjectSerializerBuilder) {
|
|
269
|
+
return data.render(this.defaultSerializerPassthrough, this.renderOpts);
|
|
270
|
+
}
|
|
259
271
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
260
272
|
const lookup = controllerSerializerIndex.lookupModel(this.constructor, data.constructor);
|
|
261
273
|
if (lookup?.length) {
|
|
262
274
|
const serializer = lookup?.[1];
|
|
263
275
|
if (isDreamSerializer(serializer)) {
|
|
264
|
-
return new SerializerRenderer(
|
|
265
276
|
// passthrough data going into the serializer is the argument that gets
|
|
266
277
|
// used in the custom attribute callback function
|
|
267
|
-
serializer(data, this.defaultSerializerPassthrough)
|
|
268
|
-
// passthrough data must be passed both into the serializer and
|
|
278
|
+
return serializer(data, this.defaultSerializerPassthrough).render(
|
|
279
|
+
// passthrough data must be passed both into the serializer and render
|
|
269
280
|
// because, if the serializer does accept passthrough data, then passing it in is how
|
|
270
281
|
// it gets into the serializer, but if it does not accept passthrough data, and therefore
|
|
271
282
|
// does not pass it into the call to DreamSerializer/ObjectSerializer,
|
|
272
283
|
// then it would be lost to serializers rendered via rendersOne/Many, and SerializerRenderer
|
|
273
284
|
// handles passing its passthrough data into those
|
|
274
|
-
this.defaultSerializerPassthrough,
|
|
275
|
-
casing: 'camel',
|
|
276
|
-
}).render();
|
|
285
|
+
this.defaultSerializerPassthrough, this.renderOpts);
|
|
277
286
|
}
|
|
278
287
|
}
|
|
279
288
|
else {
|
|
@@ -285,19 +294,16 @@ export default class PsychicController {
|
|
|
285
294
|
if (serializerKey && Object.prototype.hasOwnProperty.call(dreamApp.serializers, serializerKey)) {
|
|
286
295
|
const serializer = dreamApp.serializers[serializerKey];
|
|
287
296
|
if (serializer && isDreamSerializer(serializer)) {
|
|
288
|
-
return new SerializerRenderer(
|
|
289
297
|
// passthrough data going into the serializer is the argument that gets
|
|
290
298
|
// used in the custom attribute callback function
|
|
291
|
-
serializer(data, this.defaultSerializerPassthrough)
|
|
292
|
-
// passthrough data must be passed both into the serializer and
|
|
299
|
+
return serializer(data, this.defaultSerializerPassthrough).render(
|
|
300
|
+
// passthrough data must be passed both into the serializer and render
|
|
293
301
|
// because, if the serializer does accept passthrough data, then passing it in is how
|
|
294
302
|
// it gets into the serializer, but if it does not accept passthrough data, and therefore
|
|
295
303
|
// does not pass it into the call to DreamSerializer/ObjectSerializer,
|
|
296
304
|
// then it would be lost to serializers rendered via rendersOne/Many, and SerializerRenderer
|
|
297
305
|
// handles passing its passthrough data into those
|
|
298
|
-
this.defaultSerializerPassthrough,
|
|
299
|
-
casing: 'camel',
|
|
300
|
-
}).render();
|
|
306
|
+
this.defaultSerializerPassthrough, this.renderOpts);
|
|
301
307
|
}
|
|
302
308
|
else {
|
|
303
309
|
throw new Error(`
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { compact, sortObjectByKey } from '@rvoh/dream';
|
|
2
|
-
import { groupBy } from 'lodash-es';
|
|
1
|
+
import { compact, groupBy, sortObjectByKey } from '@rvoh/dream';
|
|
3
2
|
import * as fs from 'node:fs/promises';
|
|
4
3
|
import { debuglog } from 'node:util';
|
|
5
4
|
import UnexpectedUndefined from '../error/UnexpectedUndefined.js';
|
|
@@ -8,8 +7,7 @@ import PsychicApp from '../psychic-app/index.js';
|
|
|
8
7
|
import { HttpMethods } from '../router/types.js';
|
|
9
8
|
import PsychicServer from '../server/index.js';
|
|
10
9
|
import { DEFAULT_OPENAPI_COMPONENT_RESPONSES, DEFAULT_OPENAPI_COMPONENT_SCHEMAS } from './defaults.js';
|
|
11
|
-
import
|
|
12
|
-
import suppressResponseEnums from './helpers/suppressResponseEnums.js';
|
|
10
|
+
import suppressResponseEnumsConfig from './helpers/suppressResponseEnumsConfig.js';
|
|
13
11
|
const debugEnabled = debuglog('psychic').enabled;
|
|
14
12
|
export default class OpenapiAppRenderer {
|
|
15
13
|
/**
|
|
@@ -48,13 +46,12 @@ export default class OpenapiAppRenderer {
|
|
|
48
46
|
return output;
|
|
49
47
|
}
|
|
50
48
|
static _toObject(routes, openapiName) {
|
|
51
|
-
const
|
|
52
|
-
openapiName,
|
|
49
|
+
const renderOpts = {
|
|
53
50
|
casing: 'camel',
|
|
54
|
-
|
|
55
|
-
suppressResponseEnums: suppressResponseEnums(openapiName),
|
|
51
|
+
suppressResponseEnums: suppressResponseEnumsConfig(openapiName),
|
|
56
52
|
};
|
|
57
|
-
|
|
53
|
+
const alreadyExtractedDescendantSerializers = {};
|
|
54
|
+
const renderedSchemasOpenapi = {};
|
|
58
55
|
const psychicApp = PsychicApp.getOrFail();
|
|
59
56
|
const controllers = psychicApp.controllers;
|
|
60
57
|
const openapiConfig = psychicApp.openapi?.[openapiName];
|
|
@@ -99,7 +96,10 @@ export default class OpenapiAppRenderer {
|
|
|
99
96
|
const renderer = controller.openapi[key];
|
|
100
97
|
if (renderer === undefined)
|
|
101
98
|
throw new UnexpectedUndefined();
|
|
102
|
-
const endpointPayloadAndReferencedSerializers = renderer.toPathObject(routes,
|
|
99
|
+
const endpointPayloadAndReferencedSerializers = renderer.toPathObject(routes, {
|
|
100
|
+
openapiName,
|
|
101
|
+
renderOpts,
|
|
102
|
+
});
|
|
103
103
|
const serializersAppearingInHandWrittenOpenapi = endpointPayloadAndReferencedSerializers.referencedSerializers;
|
|
104
104
|
const endpointPayload = endpointPayloadAndReferencedSerializers.openapi;
|
|
105
105
|
if (endpointPayload === undefined)
|
|
@@ -120,15 +120,16 @@ export default class OpenapiAppRenderer {
|
|
|
120
120
|
...finalPathObject.parameters,
|
|
121
121
|
...endpointPayloadPath.parameters,
|
|
122
122
|
]);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
renderer.toSchemaObject({
|
|
124
|
+
openapiName,
|
|
125
|
+
renderOpts,
|
|
126
|
+
renderedSchemasOpenapi,
|
|
127
|
+
alreadyExtractedDescendantSerializers,
|
|
126
128
|
serializersAppearingInHandWrittenOpenapi,
|
|
127
129
|
});
|
|
128
|
-
processedSchemas = { ...processedSchemas, ...schemaRenderingResults.processedSchemas };
|
|
129
130
|
finalOutput.components.schemas = {
|
|
130
131
|
...finalOutput.components.schemas,
|
|
131
|
-
...
|
|
132
|
+
...renderedSchemasOpenapi,
|
|
132
133
|
};
|
|
133
134
|
}
|
|
134
135
|
}
|
|
@@ -140,7 +141,7 @@ export default class OpenapiAppRenderer {
|
|
|
140
141
|
return finalOutput;
|
|
141
142
|
}
|
|
142
143
|
static combineParameters(parameters) {
|
|
143
|
-
const groupedParams = groupBy(parameters,
|
|
144
|
+
const groupedParams = groupBy(parameters, obj => obj.name);
|
|
144
145
|
return compact(Object.keys(groupedParams).map(paramName => {
|
|
145
146
|
const identicalParams = groupedParams[paramName] || [];
|
|
146
147
|
return identicalParams.reduce((compositeParam, param) => {
|
|
@@ -7,7 +7,6 @@ import primitiveOpenapiStatementToOpenapi from './helpers/primitiveOpenapiStatem
|
|
|
7
7
|
import schemaToRef from './helpers/schemaToRef.js';
|
|
8
8
|
export default class OpenapiBodySegmentRenderer {
|
|
9
9
|
bodySegment;
|
|
10
|
-
schemaDelimiter;
|
|
11
10
|
casing;
|
|
12
11
|
suppressResponseEnums;
|
|
13
12
|
target;
|
|
@@ -18,12 +17,11 @@ export default class OpenapiBodySegmentRenderer {
|
|
|
18
17
|
* Used to recursively parse nested object structures
|
|
19
18
|
* within nested openapi objects
|
|
20
19
|
*/
|
|
21
|
-
constructor(bodySegment, { openapiName,
|
|
20
|
+
constructor(bodySegment, { openapiName, renderOpts, target }) {
|
|
22
21
|
this.openapiName = openapiName;
|
|
23
22
|
this.bodySegment = bodySegment;
|
|
24
|
-
this.
|
|
25
|
-
this.
|
|
26
|
-
this.suppressResponseEnums = suppressResponseEnums;
|
|
23
|
+
this.casing = renderOpts.casing;
|
|
24
|
+
this.suppressResponseEnums = renderOpts.suppressResponseEnums;
|
|
27
25
|
this.target = target;
|
|
28
26
|
}
|
|
29
27
|
/**
|
|
@@ -385,7 +383,6 @@ The following values will be allowed:
|
|
|
385
383
|
throw new NonSerializerSuppliedToSerializerBodySegment(this.bodySegment, serializer);
|
|
386
384
|
const serializerRef = new SerializerOpenapiRenderer(serializer, {
|
|
387
385
|
casing: this.casing,
|
|
388
|
-
schemaDelimiter: this.schemaDelimiter,
|
|
389
386
|
suppressResponseEnums: this.suppressResponseEnums,
|
|
390
387
|
}).serializerRef;
|
|
391
388
|
if (serializerRefBodySegment.many) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { SerializerOpenapiRenderer, compact, inferSerializersFromDreamClassOrViewModelClass, isDreamSerializer, sortBy, } from '@rvoh/dream';
|
|
2
|
-
import { cloneDeep } from 'lodash-es';
|
|
1
|
+
import { SerializerOpenapiRenderer, cloneDeepSafe, compact, inferSerializersFromDreamClassOrViewModelClass, isDreamSerializer, sortBy, } from '@rvoh/dream';
|
|
3
2
|
import OpenApiFailedToLookupSerializerForEndpoint from '../error/openapi/FailedToLookupSerializerForEndpoint.js';
|
|
4
3
|
import NonSerializerDerivedInOpenapiEndpointRenderer from '../error/openapi/NonSerializerDerivedInOpenapiEndpointRenderer.js';
|
|
5
4
|
import NonSerializerDerivedInToSchemaObjects from '../error/openapi/NonSerializerDerivedInToSchemaObjects.js';
|
|
@@ -15,10 +14,6 @@ export default class OpenapiEndpointRenderer {
|
|
|
15
14
|
dreamsOrSerializers;
|
|
16
15
|
controllerClass;
|
|
17
16
|
action;
|
|
18
|
-
openapiName;
|
|
19
|
-
casing;
|
|
20
|
-
schemaDelimiter;
|
|
21
|
-
suppressResponseEnums;
|
|
22
17
|
many;
|
|
23
18
|
paginate;
|
|
24
19
|
responses;
|
|
@@ -90,18 +85,18 @@ export default class OpenapiEndpointRenderer {
|
|
|
90
85
|
* `#toPathObject` specifically builds the `paths` portion of the
|
|
91
86
|
* final openapi.json output
|
|
92
87
|
*/
|
|
93
|
-
toPathObject(routes, { openapiName,
|
|
94
|
-
this.openapiName = openapiName;
|
|
95
|
-
this.casing = casing;
|
|
96
|
-
this.schemaDelimiter = schemaDelimiter;
|
|
97
|
-
this.suppressResponseEnums = suppressResponseEnums;
|
|
88
|
+
toPathObject(routes, { openapiName, renderOpts }) {
|
|
98
89
|
const path = this.computedPath(routes);
|
|
99
90
|
const method = this.computedMethod(routes);
|
|
100
|
-
const requestBody = this.computedRequestBody(routes);
|
|
101
|
-
const responsesAndReferencedSerializers = this.parseResponses();
|
|
91
|
+
const requestBody = this.computedRequestBody(routes, { openapiName, renderOpts });
|
|
92
|
+
const responsesAndReferencedSerializers = this.parseResponses({ openapiName, renderOpts });
|
|
102
93
|
const output = {
|
|
103
94
|
[path]: {
|
|
104
|
-
parameters: [
|
|
95
|
+
parameters: [
|
|
96
|
+
...this.headersArray({ openapiName }),
|
|
97
|
+
...this.pathParamsArray(routes),
|
|
98
|
+
...this.queryArray({ openapiName, renderOpts }),
|
|
99
|
+
],
|
|
105
100
|
[method]: {
|
|
106
101
|
tags: this.tags || [],
|
|
107
102
|
},
|
|
@@ -143,18 +138,13 @@ export default class OpenapiEndpointRenderer {
|
|
|
143
138
|
* final openapi.json output, adding any relevant entries that were uncovered
|
|
144
139
|
* while parsing the responses and provided callback function.
|
|
145
140
|
*/
|
|
146
|
-
toSchemaObject({ openapiName,
|
|
147
|
-
this.openapiName = openapiName;
|
|
148
|
-
this.casing = casing;
|
|
149
|
-
this.schemaDelimiter = schemaDelimiter;
|
|
150
|
-
this.suppressResponseEnums = suppressResponseEnums;
|
|
141
|
+
toSchemaObject({ openapiName, renderOpts, alreadyExtractedDescendantSerializers, renderedSchemasOpenapi, serializersAppearingInHandWrittenOpenapi, }) {
|
|
151
142
|
const serializers = this.getSerializerClasses() ?? [];
|
|
152
|
-
|
|
153
|
-
openapiName
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
processedSchemas,
|
|
143
|
+
serializersToSchemaObjects(this.controllerClass, this.action, [...serializers, ...serializersAppearingInHandWrittenOpenapi], {
|
|
144
|
+
openapiName,
|
|
145
|
+
renderOpts,
|
|
146
|
+
alreadyExtractedDescendantSerializers,
|
|
147
|
+
renderedSchemasOpenapi,
|
|
158
148
|
});
|
|
159
149
|
}
|
|
160
150
|
/**
|
|
@@ -258,10 +248,8 @@ export default class OpenapiEndpointRenderer {
|
|
|
258
248
|
* Generates the header portion of the openapi payload's
|
|
259
249
|
* "parameters" field for a single endpoint.
|
|
260
250
|
*/
|
|
261
|
-
headersArray() {
|
|
262
|
-
const defaultHeaders = this.omitDefaultHeaders
|
|
263
|
-
? {}
|
|
264
|
-
: openapiOpts(this.openapiName)?.defaults?.headers || {};
|
|
251
|
+
headersArray({ openapiName }) {
|
|
252
|
+
const defaultHeaders = this.omitDefaultHeaders ? {} : openapiOpts(openapiName)?.defaults?.headers || {};
|
|
265
253
|
const headers = { ...defaultHeaders, ...(this.headers || []) };
|
|
266
254
|
return (compact(Object.keys(headers).map((headerName) => {
|
|
267
255
|
const header = headers[headerName];
|
|
@@ -288,7 +276,7 @@ export default class OpenapiEndpointRenderer {
|
|
|
288
276
|
* Generates the header portion of the openapi payload's
|
|
289
277
|
* "parameters" field for a single endpoint.
|
|
290
278
|
*/
|
|
291
|
-
queryArray() {
|
|
279
|
+
queryArray({ openapiName, renderOpts, }) {
|
|
292
280
|
const queryParams = Object.keys(this.query || {}).map((queryName) => {
|
|
293
281
|
const queryParam = this.query[queryName];
|
|
294
282
|
let output = {
|
|
@@ -312,10 +300,8 @@ export default class OpenapiEndpointRenderer {
|
|
|
312
300
|
...output,
|
|
313
301
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
314
302
|
schema: new OpenapiBodySegmentRenderer(queryParam.schema, {
|
|
315
|
-
openapiName
|
|
316
|
-
|
|
317
|
-
casing: this.casing,
|
|
318
|
-
suppressResponseEnums: this.suppressResponseEnums,
|
|
303
|
+
openapiName,
|
|
304
|
+
renderOpts,
|
|
319
305
|
target: 'request',
|
|
320
306
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
321
307
|
}).render().openapi,
|
|
@@ -343,7 +329,7 @@ export default class OpenapiEndpointRenderer {
|
|
|
343
329
|
*
|
|
344
330
|
* Generates the requestBody portion of the endpoint
|
|
345
331
|
*/
|
|
346
|
-
computedRequestBody(routes) {
|
|
332
|
+
computedRequestBody(routes, { openapiName, renderOpts, }) {
|
|
347
333
|
const method = this.computedMethod(routes);
|
|
348
334
|
if (this.requestBody === null)
|
|
349
335
|
return this.defaultRequestBody();
|
|
@@ -351,13 +337,11 @@ export default class OpenapiEndpointRenderer {
|
|
|
351
337
|
if (!httpMethodsThatAllowBody.includes(method))
|
|
352
338
|
return this.defaultRequestBody();
|
|
353
339
|
if (this.shouldAutogenerateBody()) {
|
|
354
|
-
return this.generateRequestBodyForModel();
|
|
340
|
+
return this.generateRequestBodyForModel({ openapiName, renderOpts });
|
|
355
341
|
}
|
|
356
342
|
let schema = new OpenapiBodySegmentRenderer(this.requestBody, {
|
|
357
|
-
openapiName
|
|
358
|
-
|
|
359
|
-
casing: this.casing,
|
|
360
|
-
suppressResponseEnums: this.suppressResponseEnums,
|
|
343
|
+
openapiName,
|
|
344
|
+
renderOpts,
|
|
361
345
|
target: 'request',
|
|
362
346
|
}).render().openapi;
|
|
363
347
|
const bodyPageParam = this.paginate?.body;
|
|
@@ -421,7 +405,7 @@ export default class OpenapiEndpointRenderer {
|
|
|
421
405
|
* that model that are safe to ingest will be automatically added to
|
|
422
406
|
* the request body.
|
|
423
407
|
*/
|
|
424
|
-
generateRequestBodyForModel() {
|
|
408
|
+
generateRequestBodyForModel({ openapiName, renderOpts, }) {
|
|
425
409
|
const forDreamClass = this.requestBody?.for;
|
|
426
410
|
const dreamClass = forDreamClass || this.getSingleDreamModelClass();
|
|
427
411
|
if (!dreamClass)
|
|
@@ -534,10 +518,8 @@ export default class OpenapiEndpointRenderer {
|
|
|
534
518
|
paramsShape.properties = {
|
|
535
519
|
...paramsShape.properties,
|
|
536
520
|
[columnName]: new OpenapiBodySegmentRenderer(metadata.type, {
|
|
537
|
-
openapiName
|
|
538
|
-
|
|
539
|
-
casing: this.casing,
|
|
540
|
-
suppressResponseEnums: this.suppressResponseEnums,
|
|
521
|
+
openapiName,
|
|
522
|
+
renderOpts,
|
|
541
523
|
target: 'request',
|
|
542
524
|
}).render().openapi,
|
|
543
525
|
};
|
|
@@ -581,10 +563,8 @@ export default class OpenapiEndpointRenderer {
|
|
|
581
563
|
}
|
|
582
564
|
}
|
|
583
565
|
let processedSchema = new OpenapiBodySegmentRenderer(paramsShape, {
|
|
584
|
-
openapiName
|
|
585
|
-
|
|
586
|
-
casing: this.casing,
|
|
587
|
-
suppressResponseEnums: this.suppressResponseEnums,
|
|
566
|
+
openapiName,
|
|
567
|
+
renderOpts,
|
|
588
568
|
target: 'request',
|
|
589
569
|
}).render().openapi;
|
|
590
570
|
const bodyPageParam = this.paginate?.body;
|
|
@@ -604,13 +584,11 @@ export default class OpenapiEndpointRenderer {
|
|
|
604
584
|
*
|
|
605
585
|
* Generates the responses portion of the endpoint
|
|
606
586
|
*/
|
|
607
|
-
parseResponses() {
|
|
587
|
+
parseResponses({ openapiName, renderOpts, }) {
|
|
608
588
|
let responseData = {};
|
|
609
589
|
const rendererOpts = {
|
|
610
|
-
openapiName
|
|
611
|
-
|
|
612
|
-
casing: this.casing,
|
|
613
|
-
suppressResponseEnums: this.suppressResponseEnums,
|
|
590
|
+
openapiName,
|
|
591
|
+
renderOpts,
|
|
614
592
|
target: 'response',
|
|
615
593
|
};
|
|
616
594
|
const computedStatus = this.status || this.defaultStatus;
|
|
@@ -626,7 +604,7 @@ export default class OpenapiEndpointRenderer {
|
|
|
626
604
|
};
|
|
627
605
|
}
|
|
628
606
|
else {
|
|
629
|
-
const parsingResults = this.parseSerializerResponseShape();
|
|
607
|
+
const parsingResults = this.parseSerializerResponseShape({ renderOpts });
|
|
630
608
|
serializersAppearingInHandWrittenOpenapi = [
|
|
631
609
|
...serializersAppearingInHandWrittenOpenapi,
|
|
632
610
|
...parsingResults.referencedSerializers,
|
|
@@ -641,7 +619,7 @@ export default class OpenapiEndpointRenderer {
|
|
|
641
619
|
}
|
|
642
620
|
Object.keys(this.responses || {}).forEach(statusCode => {
|
|
643
621
|
const statusCodeInt = parseInt(statusCode);
|
|
644
|
-
const response =
|
|
622
|
+
const response = cloneDeepSafe(this.responses[statusCodeInt], obj => obj);
|
|
645
623
|
responseData[statusCodeInt] ||= { description: statusDescription(statusCodeInt) };
|
|
646
624
|
const statusResponse = responseData[statusCodeInt];
|
|
647
625
|
const results = new OpenapiBodySegmentRenderer(response, rendererOpts).render();
|
|
@@ -657,13 +635,13 @@ export default class OpenapiEndpointRenderer {
|
|
|
657
635
|
});
|
|
658
636
|
const defaultResponses = this.omitDefaultResponses
|
|
659
637
|
? {}
|
|
660
|
-
: openapiOpts(
|
|
638
|
+
: openapiOpts(openapiName)?.defaults?.responses || {};
|
|
661
639
|
const psychicAndConfigLevelDefaults = this.omitDefaultResponses
|
|
662
640
|
? {}
|
|
663
|
-
:
|
|
641
|
+
: cloneDeepSafe({
|
|
664
642
|
...DEFAULT_OPENAPI_RESPONSES,
|
|
665
643
|
...defaultResponses,
|
|
666
|
-
});
|
|
644
|
+
}, obj => obj);
|
|
667
645
|
Object.keys(psychicAndConfigLevelDefaults).forEach(key => {
|
|
668
646
|
if (!responseData[key]) {
|
|
669
647
|
const data = psychicAndConfigLevelDefaults[key];
|
|
@@ -702,7 +680,7 @@ export default class OpenapiEndpointRenderer {
|
|
|
702
680
|
* returns a ref object for the callback passed to the
|
|
703
681
|
* Openapi decorator.
|
|
704
682
|
*/
|
|
705
|
-
parseSerializerResponseShape() {
|
|
683
|
+
parseSerializerResponseShape({ renderOpts, }) {
|
|
706
684
|
const serializerClasses = this.getSerializerClasses();
|
|
707
685
|
if (!serializerClasses)
|
|
708
686
|
return {
|
|
@@ -710,9 +688,9 @@ export default class OpenapiEndpointRenderer {
|
|
|
710
688
|
openapi: { description: 'no content' },
|
|
711
689
|
};
|
|
712
690
|
if (serializerClasses.length > 1) {
|
|
713
|
-
return this.parseMultiEntitySerializerResponseShape(serializerClasses);
|
|
691
|
+
return this.parseMultiEntitySerializerResponseShape(serializerClasses, { renderOpts });
|
|
714
692
|
}
|
|
715
|
-
return this.parseSingleEntitySerializerResponseShape(serializerClasses[0]);
|
|
693
|
+
return this.parseSingleEntitySerializerResponseShape(serializerClasses[0], { renderOpts });
|
|
716
694
|
}
|
|
717
695
|
/**
|
|
718
696
|
* @internal
|
|
@@ -725,16 +703,14 @@ export default class OpenapiEndpointRenderer {
|
|
|
725
703
|
* public show() {...}
|
|
726
704
|
* ```
|
|
727
705
|
*/
|
|
728
|
-
parseSingleEntitySerializerResponseShape(serializer) {
|
|
706
|
+
parseSingleEntitySerializerResponseShape(serializer, { renderOpts, }) {
|
|
729
707
|
if (serializer === undefined) {
|
|
730
708
|
throw new OpenApiFailedToLookupSerializerForEndpoint(this.controllerClass, this.action);
|
|
731
709
|
}
|
|
732
710
|
if (!isDreamSerializer(serializer)) {
|
|
733
711
|
throw new OpenApiSerializerForEndpointNotAFunction(this.controllerClass, this.action, serializer);
|
|
734
712
|
}
|
|
735
|
-
const serializerOpenapiRenderer = new SerializerOpenapiRenderer(serializer,
|
|
736
|
-
schemaDelimiter: this.schemaDelimiter,
|
|
737
|
-
});
|
|
713
|
+
const serializerOpenapiRenderer = new SerializerOpenapiRenderer(serializer, renderOpts);
|
|
738
714
|
const finalOutput = {
|
|
739
715
|
content: {
|
|
740
716
|
'application/json': {
|
|
@@ -801,18 +777,16 @@ export default class OpenapiEndpointRenderer {
|
|
|
801
777
|
* public responses() {...}
|
|
802
778
|
* ```
|
|
803
779
|
*/
|
|
804
|
-
parseMultiEntitySerializerResponseShape(serializers) {
|
|
780
|
+
parseMultiEntitySerializerResponseShape(serializers, { renderOpts, }) {
|
|
805
781
|
const anyOf = { anyOf: [] };
|
|
806
782
|
serializers.forEach(serializer => {
|
|
807
783
|
if (!isDreamSerializer(serializer))
|
|
808
784
|
throw new NonSerializerDerivedInOpenapiEndpointRenderer(this.controllerClass, this.action, serializer);
|
|
809
785
|
});
|
|
810
|
-
const sortedSerializerClasses = sortBy(serializers, serializer => new SerializerOpenapiRenderer(serializer,
|
|
786
|
+
const sortedSerializerClasses = sortBy(serializers, serializer => new SerializerOpenapiRenderer(serializer, renderOpts).openapiName);
|
|
811
787
|
let referencedSerializers = [];
|
|
812
788
|
sortedSerializerClasses.forEach(serializer => {
|
|
813
|
-
const serializerOpenapiRenderer = new SerializerOpenapiRenderer(serializer,
|
|
814
|
-
schemaDelimiter: this.schemaDelimiter,
|
|
815
|
-
});
|
|
789
|
+
const serializerOpenapiRenderer = new SerializerOpenapiRenderer(serializer, renderOpts);
|
|
816
790
|
anyOf.anyOf.push(serializerOpenapiRenderer.serializerRef);
|
|
817
791
|
referencedSerializers = [
|
|
818
792
|
...referencedSerializers,
|
|
@@ -990,55 +964,34 @@ function statusDescription(status) {
|
|
|
990
964
|
return `Status ${status}`;
|
|
991
965
|
}
|
|
992
966
|
}
|
|
993
|
-
function serializersToSchemaObjects(controllerClass, actionName, serializers, {
|
|
967
|
+
function serializersToSchemaObjects(controllerClass, actionName, serializers, { renderOpts, openapiName, alreadyExtractedDescendantSerializers, renderedSchemasOpenapi, }) {
|
|
994
968
|
serializers.forEach(serializer => {
|
|
995
969
|
if (!isDreamSerializer(serializer))
|
|
996
970
|
throw new NonSerializerDerivedInToSchemaObjects(controllerClass, actionName, serializer);
|
|
997
971
|
});
|
|
998
|
-
serializers = serializers.filter(serializer =>
|
|
999
|
-
const serializerOpenapiRenderer = new SerializerOpenapiRenderer(serializer, {
|
|
1000
|
-
casing,
|
|
1001
|
-
schemaDelimiter,
|
|
1002
|
-
suppressResponseEnums,
|
|
1003
|
-
});
|
|
1004
|
-
return !processedSchemas[serializerOpenapiRenderer.globalName];
|
|
1005
|
-
});
|
|
972
|
+
serializers = serializers.filter(serializer => !renderedSchemasOpenapi[new SerializerOpenapiRenderer(serializer, renderOpts).openapiName]);
|
|
1006
973
|
if (!serializers.length)
|
|
1007
|
-
return
|
|
1008
|
-
const renderedSchemas = {};
|
|
974
|
+
return;
|
|
1009
975
|
let dependentOnSerializers = [];
|
|
1010
976
|
serializers.forEach(serializer => {
|
|
1011
|
-
const renderer = new SerializerOpenapiRenderer(serializer,
|
|
1012
|
-
|
|
1013
|
-
schemaDelimiter,
|
|
1014
|
-
suppressResponseEnums,
|
|
1015
|
-
});
|
|
1016
|
-
const globalName = renderer.globalName;
|
|
1017
|
-
processedSchemas = { ...processedSchemas, [globalName]: true };
|
|
1018
|
-
const results = renderer.renderedOpenapi(processedSchemas);
|
|
977
|
+
const renderer = new SerializerOpenapiRenderer(serializer, renderOpts);
|
|
978
|
+
const results = renderer.renderedOpenapi(alreadyExtractedDescendantSerializers);
|
|
1019
979
|
const segmentRendererResults = new OpenapiBodySegmentRenderer(results.openapi, {
|
|
1020
980
|
openapiName,
|
|
1021
|
-
|
|
1022
|
-
schemaDelimiter,
|
|
1023
|
-
suppressResponseEnums,
|
|
981
|
+
renderOpts,
|
|
1024
982
|
target: 'response',
|
|
1025
983
|
}).render();
|
|
1026
|
-
|
|
984
|
+
renderedSchemasOpenapi[renderer.openapiName] = segmentRendererResults.openapi;
|
|
1027
985
|
dependentOnSerializers = [
|
|
1028
986
|
...dependentOnSerializers,
|
|
1029
987
|
...results.referencedSerializers,
|
|
1030
|
-
...segmentRendererResults.referencedSerializers,
|
|
988
|
+
...segmentRendererResults.referencedSerializers, // should always be empty
|
|
1031
989
|
];
|
|
1032
990
|
});
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
suppressResponseEnums,
|
|
1036
|
-
schemaDelimiter,
|
|
991
|
+
serializersToSchemaObjects(controllerClass, actionName, dependentOnSerializers, {
|
|
992
|
+
renderOpts,
|
|
1037
993
|
openapiName,
|
|
1038
|
-
|
|
994
|
+
alreadyExtractedDescendantSerializers,
|
|
995
|
+
renderedSchemasOpenapi,
|
|
1039
996
|
});
|
|
1040
|
-
return {
|
|
1041
|
-
processedSchemas: { ...processedSchemas, ...recursiveResults.processedSchemas },
|
|
1042
|
-
renderedSchemas: { ...renderedSchemas, ...recursiveResults.renderedSchemas },
|
|
1043
|
-
};
|
|
1044
997
|
}
|
|
@@ -2,6 +2,6 @@ import openapiOpts from './openapiOpts.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* returns either the delimiter set in the app config, or else a blank string
|
|
4
4
|
*/
|
|
5
|
-
export default function
|
|
5
|
+
export default function suppressResponseEnumsConfig(openapiName) {
|
|
6
6
|
return !!openapiOpts(openapiName)?.suppressResponseEnums;
|
|
7
7
|
}
|
|
@@ -2,14 +2,10 @@ import { closeAllDbConnections, DreamLogos } from '@rvoh/dream';
|
|
|
2
2
|
import * as cookieParser from 'cookie-parser';
|
|
3
3
|
import * as cors from 'cors';
|
|
4
4
|
import * as express from 'express';
|
|
5
|
-
import * as OpenApiValidator from 'express-openapi-validator';
|
|
6
|
-
import * as path from 'node:path';
|
|
7
|
-
import { debuglog, inspect } from 'node:util';
|
|
8
|
-
import isOpenapiError from '../helpers/isOpenapiError.js';
|
|
9
5
|
import PsychicApp from '../psychic-app/index.js';
|
|
10
6
|
import PsychicRouter from '../router/index.js';
|
|
11
7
|
import startPsychicServer, { createPsychicHttpInstance, } from './helpers/startPsychicServer.js';
|
|
12
|
-
const debugEnabled = debuglog('psychic').enabled
|
|
8
|
+
// const debugEnabled = debuglog('psychic').enabled
|
|
13
9
|
export default class PsychicServer {
|
|
14
10
|
static async startPsychicServer(opts) {
|
|
15
11
|
return await startPsychicServer(opts);
|
|
@@ -64,7 +60,6 @@ export default class PsychicServer {
|
|
|
64
60
|
for (const serverInitAfterMiddlewareHook of this.config.specialHooks.serverInitAfterMiddleware) {
|
|
65
61
|
await serverInitAfterMiddlewareHook(this);
|
|
66
62
|
}
|
|
67
|
-
this.initializeOpenapiValidation();
|
|
68
63
|
await this.buildRoutes();
|
|
69
64
|
for (const afterRoutesHook of this.config.specialHooks.serverInitAfterRoutes) {
|
|
70
65
|
await afterRoutesHook(this);
|
|
@@ -143,35 +138,6 @@ export default class PsychicServer {
|
|
|
143
138
|
initializeJSON() {
|
|
144
139
|
this.expressApp.use(express.json(this.config.jsonOptions));
|
|
145
140
|
}
|
|
146
|
-
initializeOpenapiValidation() {
|
|
147
|
-
const psychicApp = PsychicApp.getOrFail();
|
|
148
|
-
for (const openapiName in psychicApp.openapi) {
|
|
149
|
-
const openapiOpts = psychicApp.openapi[openapiName];
|
|
150
|
-
if (openapiOpts?.validation) {
|
|
151
|
-
const opts = openapiOpts.validation;
|
|
152
|
-
opts.apiSpec ||= path.join(psychicApp.apiRoot, 'openapi.json');
|
|
153
|
-
this.expressApp.use(OpenApiValidator.middleware(opts));
|
|
154
|
-
this.expressApp.use((err, req, res, next) => {
|
|
155
|
-
if (isOpenapiError(err)) {
|
|
156
|
-
if (debugEnabled) {
|
|
157
|
-
PsychicApp.log(inspect(err));
|
|
158
|
-
console.trace();
|
|
159
|
-
}
|
|
160
|
-
res.status(err.status).json({
|
|
161
|
-
message: err.message,
|
|
162
|
-
errors: err.errors,
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
if (debugEnabled) {
|
|
167
|
-
PsychicApp.logWithLevel('error', err);
|
|
168
|
-
}
|
|
169
|
-
next();
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
141
|
async buildRoutes() {
|
|
176
142
|
const r = new PsychicRouter(this.expressApp, this.config);
|
|
177
143
|
const psychicApp = PsychicApp.getOrFail();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Dream, DreamModelSerializerType, DreamParamSafeAttributes, SimpleObjectSerializerType } from '@rvoh/dream';
|
|
1
|
+
import { Dream, DreamModelSerializerType, DreamParamSafeAttributes, SerializerRendererOpts, SimpleObjectSerializerType } from '@rvoh/dream';
|
|
2
2
|
import { Request, Response } from 'express';
|
|
3
3
|
import { ControllerHook } from '../controller/hooks.js';
|
|
4
4
|
import { HttpStatusCodeInt, HttpStatusSymbol } from '../error/http/status-codes.js';
|
|
@@ -119,10 +119,12 @@ export default class PsychicController {
|
|
|
119
119
|
session: Session;
|
|
120
120
|
config: PsychicApp;
|
|
121
121
|
action: string;
|
|
122
|
+
renderOpts: SerializerRendererOpts;
|
|
122
123
|
constructor(req: Request, res: Response, { config, action, }: {
|
|
123
124
|
config: PsychicApp;
|
|
124
125
|
action: string;
|
|
125
126
|
});
|
|
127
|
+
get headers(): import("http").IncomingHttpHeaders;
|
|
126
128
|
get params(): PsychicParamsDictionary;
|
|
127
129
|
param<ReturnType = string>(key: string): ReturnType;
|
|
128
130
|
castParam<const EnumType extends readonly string[], OptsType extends ParamsCastOptions<EnumType>, ExpectedType extends (typeof PsychicParamsPrimitiveLiterals)[number] | RegExp>(key: string, expectedType: ExpectedType, opts?: OptsType): ValidatedAllowsNull<ExpectedType, OptsType> extends infer T ? T extends ValidatedAllowsNull<ExpectedType, OptsType> ? T extends true ? ValidatedReturnType<ExpectedType, OptsType> | null | undefined : ValidatedReturnType<ExpectedType, OptsType> : never : never;
|
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
import { DreamModelSerializerType, OpenapiSchemaBody, OpenapiSchemaBodyShorthand, OpenapiShorthandPrimitiveTypes,
|
|
2
|
-
import { OpenapiEndpointResponse, OpenapiResponses } from './endpoint.js';
|
|
1
|
+
import { DreamModelSerializerType, OpenapiSchemaBody, OpenapiSchemaBodyShorthand, OpenapiShorthandPrimitiveTypes, SimpleObjectSerializerType } from '@rvoh/dream';
|
|
2
|
+
import { OpenapiEndpointResponse, OpenapiRenderOpts, OpenapiResponses } from './endpoint.js';
|
|
3
3
|
export interface OpenapiBodySegmentRendererOpts {
|
|
4
4
|
openapiName: string;
|
|
5
|
-
|
|
6
|
-
casing: SerializerCasing;
|
|
7
|
-
suppressResponseEnums: boolean;
|
|
5
|
+
renderOpts: OpenapiRenderOpts;
|
|
8
6
|
target: OpenapiBodyTarget;
|
|
9
7
|
}
|
|
10
8
|
export default class OpenapiBodySegmentRenderer {
|
|
11
9
|
private bodySegment;
|
|
12
|
-
private schemaDelimiter;
|
|
13
10
|
private casing;
|
|
14
11
|
private suppressResponseEnums;
|
|
15
12
|
private target;
|
|
@@ -20,7 +17,7 @@ export default class OpenapiBodySegmentRenderer {
|
|
|
20
17
|
* Used to recursively parse nested object structures
|
|
21
18
|
* within nested openapi objects
|
|
22
19
|
*/
|
|
23
|
-
constructor(bodySegment: OpenapiBodySegment, { openapiName,
|
|
20
|
+
constructor(bodySegment: OpenapiBodySegment, { openapiName, renderOpts, target }: OpenapiBodySegmentRendererOpts);
|
|
24
21
|
/**
|
|
25
22
|
* returns the shorthanded body segment, rendered
|
|
26
23
|
* to the appropriate openapi shape
|