@rvoh/psychic 3.8.5 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/src/bin/helpers/OpenApiSpecDiff.js +4 -0
- package/dist/cjs/src/openapi-renderer/app.js +2 -0
- package/dist/cjs/src/openapi-renderer/body-segment.js +32 -6
- package/dist/cjs/src/openapi-renderer/endpoint.js +10 -0
- package/dist/cjs/src/openapi-renderer/helpers/OpenapiPayloadValidator.js +2 -0
- package/dist/cjs/src/openapi-renderer/helpers/allSerializersFromHandWrittenOpenapi.js +2 -3
- package/dist/cjs/src/openapi-renderer/helpers/allSerializersToRefsInOpenapi.js +3 -8
- package/dist/cjs/src/openapi-renderer/helpers/buildDreamRequestBodyShape.js +8 -2
- package/dist/cjs/src/openapi-renderer/helpers/dreamColumnOpenapiShape.js +1 -0
- package/dist/cjs/src/openapi-renderer/helpers/legacyImplicitRequestBodyParamsConfig.js +9 -0
- package/dist/cjs/src/openapi-renderer/helpers/serializersAndRefsFromSerializableRef.js +17 -0
- package/dist/esm/src/bin/helpers/OpenApiSpecDiff.js +4 -0
- package/dist/esm/src/openapi-renderer/app.js +2 -0
- package/dist/esm/src/openapi-renderer/body-segment.js +32 -6
- package/dist/esm/src/openapi-renderer/endpoint.js +10 -0
- package/dist/esm/src/openapi-renderer/helpers/OpenapiPayloadValidator.js +2 -0
- package/dist/esm/src/openapi-renderer/helpers/allSerializersFromHandWrittenOpenapi.js +2 -3
- package/dist/esm/src/openapi-renderer/helpers/allSerializersToRefsInOpenapi.js +3 -8
- package/dist/esm/src/openapi-renderer/helpers/buildDreamRequestBodyShape.js +8 -2
- package/dist/esm/src/openapi-renderer/helpers/dreamColumnOpenapiShape.js +1 -0
- package/dist/esm/src/openapi-renderer/helpers/legacyImplicitRequestBodyParamsConfig.js +9 -0
- package/dist/esm/src/openapi-renderer/helpers/serializersAndRefsFromSerializableRef.js +17 -0
- package/dist/types/src/bin/helpers/OpenApiSpecDiff.d.ts +5 -1
- package/dist/types/src/openapi-renderer/body-segment.d.ts +1 -0
- package/dist/types/src/openapi-renderer/endpoint.d.ts +1 -0
- package/dist/types/src/openapi-renderer/helpers/buildDreamRequestBodyShape.d.ts +7 -0
- package/dist/types/src/openapi-renderer/helpers/legacyImplicitRequestBodyParamsConfig.d.ts +6 -0
- package/dist/types/src/openapi-renderer/helpers/serializersAndRefsFromSerializableRef.d.ts +9 -0
- package/dist/types/src/psychic-app/index.d.ts +23 -0
- package/package.json +1 -1
|
@@ -189,6 +189,10 @@ export class OpenApiSpecDiff {
|
|
|
189
189
|
}
|
|
190
190
|
/**
|
|
191
191
|
* Retrieves head branch content for a file
|
|
192
|
+
*
|
|
193
|
+
* Marked `protected` so tests can supply a deterministic baseline instead of
|
|
194
|
+
* the live head branch. Production code never overrides this.
|
|
195
|
+
*
|
|
192
196
|
* @param absoluteFilePath - Absolute path to the file
|
|
193
197
|
*/
|
|
194
198
|
getHeadBranchContent(absoluteFilePath) {
|
|
@@ -7,6 +7,7 @@ import PsychicApp from '../psychic-app/index.js';
|
|
|
7
7
|
import { HttpMethods } from '../router/types.js';
|
|
8
8
|
import { DEFAULT_OPENAPI_COMPONENT_RESPONSES, DEFAULT_OPENAPI_COMPONENT_SCHEMAS } from './defaults.js';
|
|
9
9
|
import { MissingControllerActionPairingInRoutes, } from './endpoint.js';
|
|
10
|
+
import legacyImplicitRequestBodyParamsConfig from './helpers/legacyImplicitRequestBodyParamsConfig.js';
|
|
10
11
|
import suppressResponseEnumsConfig from './helpers/suppressResponseEnumsConfig.js';
|
|
11
12
|
const debugEnabled = debuglog('psychic').enabled;
|
|
12
13
|
export default class OpenapiAppRenderer {
|
|
@@ -47,6 +48,7 @@ export default class OpenapiAppRenderer {
|
|
|
47
48
|
const renderOpts = {
|
|
48
49
|
casing: 'camel',
|
|
49
50
|
suppressResponseEnums: suppressResponseEnumsConfig(openapiName),
|
|
51
|
+
legacyImplicitRequestBodyParams: legacyImplicitRequestBodyParamsConfig(openapiName),
|
|
50
52
|
};
|
|
51
53
|
const alreadyExtractedDescendantSerializers = {};
|
|
52
54
|
const renderedSchemasOpenapi = {};
|
|
@@ -8,6 +8,7 @@ import isBlankDescription from './helpers/isBlankDescription.js';
|
|
|
8
8
|
import maybeNullOpenapiShorthandToOpenapiShorthand from './helpers/maybeNullOpenapiShorthandToOpenapiShorthand.js';
|
|
9
9
|
import primitiveOpenapiStatementToOpenapi from './helpers/primitiveOpenapiStatementToOpenapi.js';
|
|
10
10
|
import schemaToRef from './helpers/schemaToRef.js';
|
|
11
|
+
import serializersAndRefsFromSerializableRef from './helpers/serializersAndRefsFromSerializableRef.js';
|
|
11
12
|
import SerializerOpenapiRenderer from './SerializerOpenapiRenderer.js';
|
|
12
13
|
/**
|
|
13
14
|
* @internal
|
|
@@ -53,6 +54,7 @@ export default class OpenapiSegmentExpander {
|
|
|
53
54
|
bodySegment;
|
|
54
55
|
casing;
|
|
55
56
|
suppressResponseEnums;
|
|
57
|
+
legacyImplicitRequestBodyParams;
|
|
56
58
|
target;
|
|
57
59
|
source;
|
|
58
60
|
/**
|
|
@@ -65,6 +67,7 @@ export default class OpenapiSegmentExpander {
|
|
|
65
67
|
this.bodySegment = bodySegment;
|
|
66
68
|
this.casing = renderOpts.casing;
|
|
67
69
|
this.suppressResponseEnums = renderOpts.suppressResponseEnums;
|
|
70
|
+
this.legacyImplicitRequestBodyParams = renderOpts.legacyImplicitRequestBodyParams;
|
|
68
71
|
this.target = target;
|
|
69
72
|
this.source = source ?? 'unknown';
|
|
70
73
|
}
|
|
@@ -488,19 +491,42 @@ The following values will be allowed:
|
|
|
488
491
|
including: ref.including,
|
|
489
492
|
required: ref.required,
|
|
490
493
|
combining: ref.combining,
|
|
494
|
+
legacyImplicitRequestBodyParams: this.legacyImplicitRequestBodyParams,
|
|
491
495
|
}, this.source);
|
|
492
496
|
return this.recursivelyParseBody(paramsShape);
|
|
493
497
|
}
|
|
494
498
|
serializableStatement(bodySegment) {
|
|
495
499
|
const serializableRef = bodySegment;
|
|
496
500
|
const key = serializableRef.$serializableSerializerKey || serializableRef.key || 'default';
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
return this.serializerStatement({
|
|
501
|
-
$serializer: serializer,
|
|
502
|
-
...serializableRef,
|
|
501
|
+
const { serializers, refs } = serializersAndRefsFromSerializableRef(serializableRef, {
|
|
502
|
+
casing: this.casing,
|
|
503
|
+
suppressResponseEnums: this.suppressResponseEnums,
|
|
503
504
|
});
|
|
505
|
+
if (serializers.length === 0)
|
|
506
|
+
throw new Error(`Failed to locate serializers getter from: ${serializableRef.$serializable.name} using key: ${key}`);
|
|
507
|
+
if (serializers.length === 1) {
|
|
508
|
+
return this.serializerStatement({
|
|
509
|
+
$serializer: serializers[0],
|
|
510
|
+
...serializableRef,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
const serializerAnyOf = { anyOf: refs };
|
|
514
|
+
if (serializableRef.many) {
|
|
515
|
+
return {
|
|
516
|
+
referencedSerializers: serializers,
|
|
517
|
+
openapi: {
|
|
518
|
+
type: serializableRef.maybeNull ? ['array', 'null'] : 'array',
|
|
519
|
+
items: serializerAnyOf,
|
|
520
|
+
},
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
if (serializableRef.maybeNull) {
|
|
524
|
+
return {
|
|
525
|
+
referencedSerializers: serializers,
|
|
526
|
+
openapi: { anyOf: [...refs, { type: 'null' }] },
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
return { referencedSerializers: serializers, openapi: serializerAnyOf };
|
|
504
530
|
}
|
|
505
531
|
/**
|
|
506
532
|
* @internal
|
|
@@ -529,7 +529,17 @@ export default class OpenapiEndpointRenderer {
|
|
|
529
529
|
including: including,
|
|
530
530
|
required: required,
|
|
531
531
|
combining: combining,
|
|
532
|
+
legacyImplicitRequestBodyParams: renderOpts.legacyImplicitRequestBodyParams,
|
|
532
533
|
}, source);
|
|
534
|
+
// When the model-derived shape contributes no properties and no `required`
|
|
535
|
+
// (e.g. the new none-default with no `params`/`only`/`combining`), there is
|
|
536
|
+
// nothing meaningful to advertise. Fall back to `defaultRequestBody()`, which
|
|
537
|
+
// omits the body entirely unless body-level pagination params are present.
|
|
538
|
+
const shapeProperties = paramsShape.properties;
|
|
539
|
+
const shapeRequired = paramsShape.required;
|
|
540
|
+
if (Object.keys(shapeProperties ?? {}).length === 0 && !shapeRequired) {
|
|
541
|
+
return this.defaultRequestBody();
|
|
542
|
+
}
|
|
533
543
|
let processedSchema = new OpenapiSegmentExpander(paramsShape, {
|
|
534
544
|
renderOpts,
|
|
535
545
|
target: 'request',
|
|
@@ -2,6 +2,7 @@ import OpenapiRequestValidationFailure from '../../error/openapi/OpenapiRequestV
|
|
|
2
2
|
import OpenapiResponseValidationFailure from '../../error/openapi/OpenapResponseValidationFailure.js';
|
|
3
3
|
import { createValidator, formatAjvErrors, } from '../../helpers/validateOpenApiSchema.js';
|
|
4
4
|
import PsychicApp from '../../psychic-app/index.js';
|
|
5
|
+
import legacyImplicitRequestBodyParamsConfig from './legacyImplicitRequestBodyParamsConfig.js';
|
|
5
6
|
import suppressResponseEnumsConfig from './suppressResponseEnumsConfig.js';
|
|
6
7
|
import { cacheValidator, getCachedValidator } from './validator-cache.js';
|
|
7
8
|
/**
|
|
@@ -207,6 +208,7 @@ export default class OpenapiPayloadValidator {
|
|
|
207
208
|
return {
|
|
208
209
|
casing: 'camel',
|
|
209
210
|
suppressResponseEnums: suppressResponseEnumsConfig(this.openapiName),
|
|
211
|
+
legacyImplicitRequestBodyParams: legacyImplicitRequestBodyParamsConfig(this.openapiName),
|
|
210
212
|
};
|
|
211
213
|
}
|
|
212
214
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { DreamApp } from '@rvoh/dream';
|
|
2
1
|
import isObject from '../../helpers/isObject.js';
|
|
2
|
+
import serializersAndRefsFromSerializableRef from './serializersAndRefsFromSerializableRef.js';
|
|
3
3
|
/**
|
|
4
4
|
* @internal
|
|
5
5
|
*
|
|
@@ -54,8 +54,7 @@ value, serializers) {
|
|
|
54
54
|
//
|
|
55
55
|
}
|
|
56
56
|
else if (value.$serializable) {
|
|
57
|
-
|
|
58
|
-
foundSerializers.forEach(serializer => serializers.add(serializer));
|
|
57
|
+
serializersAndRefsFromSerializableRef(value).serializers.forEach(serializer => serializers.add(serializer));
|
|
59
58
|
//
|
|
60
59
|
}
|
|
61
60
|
else if (isObject(value)) {
|
|
@@ -1,10 +1,6 @@
|
|
|
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 { DreamApp } from '@rvoh/dream';
|
|
6
1
|
import isObject from '../../helpers/isObject.js';
|
|
7
2
|
import SerializerOpenapiRenderer from '../SerializerOpenapiRenderer.js';
|
|
3
|
+
import serializersAndRefsFromSerializableRef from './serializersAndRefsFromSerializableRef.js';
|
|
8
4
|
/**
|
|
9
5
|
* @internal
|
|
10
6
|
*
|
|
@@ -69,9 +65,8 @@ function transformValue(value) {
|
|
|
69
65
|
//
|
|
70
66
|
}
|
|
71
67
|
else if (value.$serializable) {
|
|
72
|
-
const { $serializable, $serializableSerializerKey, ...rest } = value;
|
|
73
|
-
const
|
|
74
|
-
const refs = foundSerializers.map(serializer => new SerializerOpenapiRenderer(serializer).serializerRef);
|
|
68
|
+
const { $serializable, $serializableSerializerKey, key, ...rest } = value;
|
|
69
|
+
const { refs } = serializersAndRefsFromSerializableRef({ $serializable, $serializableSerializerKey, key });
|
|
75
70
|
if (refs.length === 0)
|
|
76
71
|
return rest;
|
|
77
72
|
if (refs.length === 1) {
|
|
@@ -11,9 +11,15 @@ import { dreamColumnOpenapiShape } from './dreamColumnOpenapiShape.js';
|
|
|
11
11
|
* truth keeps top-level and nested semantics identical.
|
|
12
12
|
*/
|
|
13
13
|
export default function buildDreamRequestBodyShape(dreamClass, opts, source) {
|
|
14
|
-
const { params, only, including, required, combining } = opts;
|
|
14
|
+
const { params, only, including, required, combining, legacyImplicitRequestBodyParams } = opts;
|
|
15
|
+
// When neither `params` nor `only` is provided, the default is to resolve to
|
|
16
|
+
// NO model columns (an empty allowlist), rather than implicitly exposing every
|
|
17
|
+
// param-safe column. The legacy implicit-all behavior can be re-activated via
|
|
18
|
+
// the `legacyImplicitRequestBodyParams` opt-in.
|
|
19
|
+
const resolvedOnly = params ?? only;
|
|
20
|
+
const onlyForResolution = resolvedOnly === undefined && !legacyImplicitRequestBodyParams ? [] : resolvedOnly;
|
|
15
21
|
const paramSafeColumns = openapiParamNamesForDreamClass(dreamClass, {
|
|
16
|
-
only:
|
|
22
|
+
only: onlyForResolution,
|
|
17
23
|
including,
|
|
18
24
|
});
|
|
19
25
|
const paramsShape = {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import openapiOpts from './openapiOpts.js';
|
|
2
|
+
/**
|
|
3
|
+
* returns whether the named openapi config opts in to the legacy behavior of
|
|
4
|
+
* implicitly exposing all param-safe columns when a model-derived request body
|
|
5
|
+
* is given no `params`/`only`.
|
|
6
|
+
*/
|
|
7
|
+
export default function legacyImplicitRequestBodyParamsConfig(openapiName) {
|
|
8
|
+
return !!openapiOpts(openapiName)?.legacyImplicitRequestBodyParams;
|
|
9
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { DreamApp } from '@rvoh/dream';
|
|
2
|
+
import { sortBy, uniq } from '@rvoh/dream/utils';
|
|
3
|
+
import SerializerOpenapiRenderer from '../SerializerOpenapiRenderer.js';
|
|
4
|
+
export default function serializersAndRefsFromSerializableRef(serializableRef, { casing = 'camel', suppressResponseEnums = false, } = {}) {
|
|
5
|
+
const key = serializableRef.$serializableSerializerKey || serializableRef.key || 'default';
|
|
6
|
+
const serializerRefs = sortBy(uniq(DreamApp.system.inferSerializersFromDreamClassOrViewModelClass(serializableRef.$serializable, key)).map(serializer => ({
|
|
7
|
+
serializer,
|
|
8
|
+
ref: new SerializerOpenapiRenderer(serializer, {
|
|
9
|
+
casing,
|
|
10
|
+
suppressResponseEnums,
|
|
11
|
+
}).serializerRef,
|
|
12
|
+
})), ({ ref }) => ref.$ref);
|
|
13
|
+
return {
|
|
14
|
+
serializers: serializerRefs.map(({ serializer }) => serializer),
|
|
15
|
+
refs: serializerRefs.map(({ ref }) => ref),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -189,6 +189,10 @@ export class OpenApiSpecDiff {
|
|
|
189
189
|
}
|
|
190
190
|
/**
|
|
191
191
|
* Retrieves head branch content for a file
|
|
192
|
+
*
|
|
193
|
+
* Marked `protected` so tests can supply a deterministic baseline instead of
|
|
194
|
+
* the live head branch. Production code never overrides this.
|
|
195
|
+
*
|
|
192
196
|
* @param absoluteFilePath - Absolute path to the file
|
|
193
197
|
*/
|
|
194
198
|
getHeadBranchContent(absoluteFilePath) {
|
|
@@ -7,6 +7,7 @@ import PsychicApp from '../psychic-app/index.js';
|
|
|
7
7
|
import { HttpMethods } from '../router/types.js';
|
|
8
8
|
import { DEFAULT_OPENAPI_COMPONENT_RESPONSES, DEFAULT_OPENAPI_COMPONENT_SCHEMAS } from './defaults.js';
|
|
9
9
|
import { MissingControllerActionPairingInRoutes, } from './endpoint.js';
|
|
10
|
+
import legacyImplicitRequestBodyParamsConfig from './helpers/legacyImplicitRequestBodyParamsConfig.js';
|
|
10
11
|
import suppressResponseEnumsConfig from './helpers/suppressResponseEnumsConfig.js';
|
|
11
12
|
const debugEnabled = debuglog('psychic').enabled;
|
|
12
13
|
export default class OpenapiAppRenderer {
|
|
@@ -47,6 +48,7 @@ export default class OpenapiAppRenderer {
|
|
|
47
48
|
const renderOpts = {
|
|
48
49
|
casing: 'camel',
|
|
49
50
|
suppressResponseEnums: suppressResponseEnumsConfig(openapiName),
|
|
51
|
+
legacyImplicitRequestBodyParams: legacyImplicitRequestBodyParamsConfig(openapiName),
|
|
50
52
|
};
|
|
51
53
|
const alreadyExtractedDescendantSerializers = {};
|
|
52
54
|
const renderedSchemasOpenapi = {};
|
|
@@ -8,6 +8,7 @@ import isBlankDescription from './helpers/isBlankDescription.js';
|
|
|
8
8
|
import maybeNullOpenapiShorthandToOpenapiShorthand from './helpers/maybeNullOpenapiShorthandToOpenapiShorthand.js';
|
|
9
9
|
import primitiveOpenapiStatementToOpenapi from './helpers/primitiveOpenapiStatementToOpenapi.js';
|
|
10
10
|
import schemaToRef from './helpers/schemaToRef.js';
|
|
11
|
+
import serializersAndRefsFromSerializableRef from './helpers/serializersAndRefsFromSerializableRef.js';
|
|
11
12
|
import SerializerOpenapiRenderer from './SerializerOpenapiRenderer.js';
|
|
12
13
|
/**
|
|
13
14
|
* @internal
|
|
@@ -53,6 +54,7 @@ export default class OpenapiSegmentExpander {
|
|
|
53
54
|
bodySegment;
|
|
54
55
|
casing;
|
|
55
56
|
suppressResponseEnums;
|
|
57
|
+
legacyImplicitRequestBodyParams;
|
|
56
58
|
target;
|
|
57
59
|
source;
|
|
58
60
|
/**
|
|
@@ -65,6 +67,7 @@ export default class OpenapiSegmentExpander {
|
|
|
65
67
|
this.bodySegment = bodySegment;
|
|
66
68
|
this.casing = renderOpts.casing;
|
|
67
69
|
this.suppressResponseEnums = renderOpts.suppressResponseEnums;
|
|
70
|
+
this.legacyImplicitRequestBodyParams = renderOpts.legacyImplicitRequestBodyParams;
|
|
68
71
|
this.target = target;
|
|
69
72
|
this.source = source ?? 'unknown';
|
|
70
73
|
}
|
|
@@ -488,19 +491,42 @@ The following values will be allowed:
|
|
|
488
491
|
including: ref.including,
|
|
489
492
|
required: ref.required,
|
|
490
493
|
combining: ref.combining,
|
|
494
|
+
legacyImplicitRequestBodyParams: this.legacyImplicitRequestBodyParams,
|
|
491
495
|
}, this.source);
|
|
492
496
|
return this.recursivelyParseBody(paramsShape);
|
|
493
497
|
}
|
|
494
498
|
serializableStatement(bodySegment) {
|
|
495
499
|
const serializableRef = bodySegment;
|
|
496
500
|
const key = serializableRef.$serializableSerializerKey || serializableRef.key || 'default';
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
return this.serializerStatement({
|
|
501
|
-
$serializer: serializer,
|
|
502
|
-
...serializableRef,
|
|
501
|
+
const { serializers, refs } = serializersAndRefsFromSerializableRef(serializableRef, {
|
|
502
|
+
casing: this.casing,
|
|
503
|
+
suppressResponseEnums: this.suppressResponseEnums,
|
|
503
504
|
});
|
|
505
|
+
if (serializers.length === 0)
|
|
506
|
+
throw new Error(`Failed to locate serializers getter from: ${serializableRef.$serializable.name} using key: ${key}`);
|
|
507
|
+
if (serializers.length === 1) {
|
|
508
|
+
return this.serializerStatement({
|
|
509
|
+
$serializer: serializers[0],
|
|
510
|
+
...serializableRef,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
const serializerAnyOf = { anyOf: refs };
|
|
514
|
+
if (serializableRef.many) {
|
|
515
|
+
return {
|
|
516
|
+
referencedSerializers: serializers,
|
|
517
|
+
openapi: {
|
|
518
|
+
type: serializableRef.maybeNull ? ['array', 'null'] : 'array',
|
|
519
|
+
items: serializerAnyOf,
|
|
520
|
+
},
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
if (serializableRef.maybeNull) {
|
|
524
|
+
return {
|
|
525
|
+
referencedSerializers: serializers,
|
|
526
|
+
openapi: { anyOf: [...refs, { type: 'null' }] },
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
return { referencedSerializers: serializers, openapi: serializerAnyOf };
|
|
504
530
|
}
|
|
505
531
|
/**
|
|
506
532
|
* @internal
|
|
@@ -529,7 +529,17 @@ export default class OpenapiEndpointRenderer {
|
|
|
529
529
|
including: including,
|
|
530
530
|
required: required,
|
|
531
531
|
combining: combining,
|
|
532
|
+
legacyImplicitRequestBodyParams: renderOpts.legacyImplicitRequestBodyParams,
|
|
532
533
|
}, source);
|
|
534
|
+
// When the model-derived shape contributes no properties and no `required`
|
|
535
|
+
// (e.g. the new none-default with no `params`/`only`/`combining`), there is
|
|
536
|
+
// nothing meaningful to advertise. Fall back to `defaultRequestBody()`, which
|
|
537
|
+
// omits the body entirely unless body-level pagination params are present.
|
|
538
|
+
const shapeProperties = paramsShape.properties;
|
|
539
|
+
const shapeRequired = paramsShape.required;
|
|
540
|
+
if (Object.keys(shapeProperties ?? {}).length === 0 && !shapeRequired) {
|
|
541
|
+
return this.defaultRequestBody();
|
|
542
|
+
}
|
|
533
543
|
let processedSchema = new OpenapiSegmentExpander(paramsShape, {
|
|
534
544
|
renderOpts,
|
|
535
545
|
target: 'request',
|
|
@@ -2,6 +2,7 @@ import OpenapiRequestValidationFailure from '../../error/openapi/OpenapiRequestV
|
|
|
2
2
|
import OpenapiResponseValidationFailure from '../../error/openapi/OpenapResponseValidationFailure.js';
|
|
3
3
|
import { createValidator, formatAjvErrors, } from '../../helpers/validateOpenApiSchema.js';
|
|
4
4
|
import PsychicApp from '../../psychic-app/index.js';
|
|
5
|
+
import legacyImplicitRequestBodyParamsConfig from './legacyImplicitRequestBodyParamsConfig.js';
|
|
5
6
|
import suppressResponseEnumsConfig from './suppressResponseEnumsConfig.js';
|
|
6
7
|
import { cacheValidator, getCachedValidator } from './validator-cache.js';
|
|
7
8
|
/**
|
|
@@ -207,6 +208,7 @@ export default class OpenapiPayloadValidator {
|
|
|
207
208
|
return {
|
|
208
209
|
casing: 'camel',
|
|
209
210
|
suppressResponseEnums: suppressResponseEnumsConfig(this.openapiName),
|
|
211
|
+
legacyImplicitRequestBodyParams: legacyImplicitRequestBodyParamsConfig(this.openapiName),
|
|
210
212
|
};
|
|
211
213
|
}
|
|
212
214
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { DreamApp } from '@rvoh/dream';
|
|
2
1
|
import isObject from '../../helpers/isObject.js';
|
|
2
|
+
import serializersAndRefsFromSerializableRef from './serializersAndRefsFromSerializableRef.js';
|
|
3
3
|
/**
|
|
4
4
|
* @internal
|
|
5
5
|
*
|
|
@@ -54,8 +54,7 @@ value, serializers) {
|
|
|
54
54
|
//
|
|
55
55
|
}
|
|
56
56
|
else if (value.$serializable) {
|
|
57
|
-
|
|
58
|
-
foundSerializers.forEach(serializer => serializers.add(serializer));
|
|
57
|
+
serializersAndRefsFromSerializableRef(value).serializers.forEach(serializer => serializers.add(serializer));
|
|
59
58
|
//
|
|
60
59
|
}
|
|
61
60
|
else if (isObject(value)) {
|
|
@@ -1,10 +1,6 @@
|
|
|
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 { DreamApp } from '@rvoh/dream';
|
|
6
1
|
import isObject from '../../helpers/isObject.js';
|
|
7
2
|
import SerializerOpenapiRenderer from '../SerializerOpenapiRenderer.js';
|
|
3
|
+
import serializersAndRefsFromSerializableRef from './serializersAndRefsFromSerializableRef.js';
|
|
8
4
|
/**
|
|
9
5
|
* @internal
|
|
10
6
|
*
|
|
@@ -69,9 +65,8 @@ function transformValue(value) {
|
|
|
69
65
|
//
|
|
70
66
|
}
|
|
71
67
|
else if (value.$serializable) {
|
|
72
|
-
const { $serializable, $serializableSerializerKey, ...rest } = value;
|
|
73
|
-
const
|
|
74
|
-
const refs = foundSerializers.map(serializer => new SerializerOpenapiRenderer(serializer).serializerRef);
|
|
68
|
+
const { $serializable, $serializableSerializerKey, key, ...rest } = value;
|
|
69
|
+
const { refs } = serializersAndRefsFromSerializableRef({ $serializable, $serializableSerializerKey, key });
|
|
75
70
|
if (refs.length === 0)
|
|
76
71
|
return rest;
|
|
77
72
|
if (refs.length === 1) {
|
|
@@ -11,9 +11,15 @@ import { dreamColumnOpenapiShape } from './dreamColumnOpenapiShape.js';
|
|
|
11
11
|
* truth keeps top-level and nested semantics identical.
|
|
12
12
|
*/
|
|
13
13
|
export default function buildDreamRequestBodyShape(dreamClass, opts, source) {
|
|
14
|
-
const { params, only, including, required, combining } = opts;
|
|
14
|
+
const { params, only, including, required, combining, legacyImplicitRequestBodyParams } = opts;
|
|
15
|
+
// When neither `params` nor `only` is provided, the default is to resolve to
|
|
16
|
+
// NO model columns (an empty allowlist), rather than implicitly exposing every
|
|
17
|
+
// param-safe column. The legacy implicit-all behavior can be re-activated via
|
|
18
|
+
// the `legacyImplicitRequestBodyParams` opt-in.
|
|
19
|
+
const resolvedOnly = params ?? only;
|
|
20
|
+
const onlyForResolution = resolvedOnly === undefined && !legacyImplicitRequestBodyParams ? [] : resolvedOnly;
|
|
15
21
|
const paramSafeColumns = openapiParamNamesForDreamClass(dreamClass, {
|
|
16
|
-
only:
|
|
22
|
+
only: onlyForResolution,
|
|
17
23
|
including,
|
|
18
24
|
});
|
|
19
25
|
const paramsShape = {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import openapiOpts from './openapiOpts.js';
|
|
2
|
+
/**
|
|
3
|
+
* returns whether the named openapi config opts in to the legacy behavior of
|
|
4
|
+
* implicitly exposing all param-safe columns when a model-derived request body
|
|
5
|
+
* is given no `params`/`only`.
|
|
6
|
+
*/
|
|
7
|
+
export default function legacyImplicitRequestBodyParamsConfig(openapiName) {
|
|
8
|
+
return !!openapiOpts(openapiName)?.legacyImplicitRequestBodyParams;
|
|
9
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { DreamApp } from '@rvoh/dream';
|
|
2
|
+
import { sortBy, uniq } from '@rvoh/dream/utils';
|
|
3
|
+
import SerializerOpenapiRenderer from '../SerializerOpenapiRenderer.js';
|
|
4
|
+
export default function serializersAndRefsFromSerializableRef(serializableRef, { casing = 'camel', suppressResponseEnums = false, } = {}) {
|
|
5
|
+
const key = serializableRef.$serializableSerializerKey || serializableRef.key || 'default';
|
|
6
|
+
const serializerRefs = sortBy(uniq(DreamApp.system.inferSerializersFromDreamClassOrViewModelClass(serializableRef.$serializable, key)).map(serializer => ({
|
|
7
|
+
serializer,
|
|
8
|
+
ref: new SerializerOpenapiRenderer(serializer, {
|
|
9
|
+
casing,
|
|
10
|
+
suppressResponseEnums,
|
|
11
|
+
}).serializerRef,
|
|
12
|
+
})), ({ ref }) => ref.$ref);
|
|
13
|
+
return {
|
|
14
|
+
serializers: serializerRefs.map(({ serializer }) => serializer),
|
|
15
|
+
refs: serializerRefs.map(({ ref }) => ref),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -96,9 +96,13 @@ export declare class OpenApiSpecDiff {
|
|
|
96
96
|
private createTempFilePath;
|
|
97
97
|
/**
|
|
98
98
|
* Retrieves head branch content for a file
|
|
99
|
+
*
|
|
100
|
+
* Marked `protected` so tests can supply a deterministic baseline instead of
|
|
101
|
+
* the live head branch. Production code never overrides this.
|
|
102
|
+
*
|
|
99
103
|
* @param absoluteFilePath - Absolute path to the file
|
|
100
104
|
*/
|
|
101
|
-
|
|
105
|
+
protected getHeadBranchContent(absoluteFilePath: string): string;
|
|
102
106
|
/**
|
|
103
107
|
* Compares a single OpenAPI file against head branch
|
|
104
108
|
*/
|
|
@@ -11,6 +11,7 @@ import { OpenapiBodySegment, ReferencedSerializersAndOpenapiEndpointResponse, Se
|
|
|
11
11
|
export interface OpenapiRenderOpts {
|
|
12
12
|
casing: SerializerCasing;
|
|
13
13
|
suppressResponseEnums: boolean;
|
|
14
|
+
legacyImplicitRequestBodyParams: boolean;
|
|
14
15
|
}
|
|
15
16
|
export interface ToPathObjectOpts {
|
|
16
17
|
openapiName: string;
|
|
@@ -6,6 +6,13 @@ export interface BuildDreamRequestBodyShapeOpts {
|
|
|
6
6
|
including?: readonly string[] | undefined;
|
|
7
7
|
required?: readonly string[] | undefined;
|
|
8
8
|
combining?: Record<string, unknown> | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* When true, restores the legacy behavior of resolving to ALL param-safe
|
|
11
|
+
* columns when no `params`/`only` are provided. When false/undefined (the
|
|
12
|
+
* default), an absent `params`/`only` resolves to NO model columns, so that
|
|
13
|
+
* database-level structure is not implicitly leaked into the OpenAPI shape.
|
|
14
|
+
*/
|
|
15
|
+
legacyImplicitRequestBodyParams?: boolean | undefined;
|
|
9
16
|
}
|
|
10
17
|
/**
|
|
11
18
|
* @internal
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* returns whether the named openapi config opts in to the legacy behavior of
|
|
3
|
+
* implicitly exposing all param-safe columns when a model-derived request body
|
|
4
|
+
* is given no `params`/`only`.
|
|
5
|
+
*/
|
|
6
|
+
export default function legacyImplicitRequestBodyParamsConfig(openapiName: string): boolean;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { OpenapiSchemaExpressionRef, OpenapiSchemaShorthandExpressionSerializableRef } from '@rvoh/dream/openapi';
|
|
2
|
+
import { DreamModelSerializerType, SerializerCasing, SimpleObjectSerializerType } from '@rvoh/dream/types';
|
|
3
|
+
export default function serializersAndRefsFromSerializableRef(serializableRef: OpenapiSchemaShorthandExpressionSerializableRef, { casing, suppressResponseEnums, }?: {
|
|
4
|
+
casing?: SerializerCasing;
|
|
5
|
+
suppressResponseEnums?: boolean;
|
|
6
|
+
}): {
|
|
7
|
+
serializers: (DreamModelSerializerType | SimpleObjectSerializerType)[];
|
|
8
|
+
refs: OpenapiSchemaExpressionRef[];
|
|
9
|
+
};
|
|
@@ -356,6 +356,29 @@ interface PsychicOpenapiBaseOptions {
|
|
|
356
356
|
* ```
|
|
357
357
|
*/
|
|
358
358
|
suppressResponseEnums?: boolean;
|
|
359
|
+
/**
|
|
360
|
+
* @deprecated escape hatch — prefer explicit `params`/`only` on each request body.
|
|
361
|
+
*
|
|
362
|
+
* When true, restores the legacy behavior where a model-derived `requestBody`
|
|
363
|
+
* (or nested `$dream`/`for:` segment) given no `params`/`only` implicitly
|
|
364
|
+
* exposes ALL param-safe columns of the model.
|
|
365
|
+
*
|
|
366
|
+
* As of 3.9.0 the default flipped: an absent `params`/`only` now resolves to
|
|
367
|
+
* NO model columns, so that database-level structure is not implicitly leaked
|
|
368
|
+
* into the OpenAPI shape (an attacker could otherwise infer the column set).
|
|
369
|
+
* A top-level model-derived `requestBody` with nothing to contribute is
|
|
370
|
+
* omitted entirely; a nested `$dream`/`for:` segment renders an empty object.
|
|
371
|
+
*
|
|
372
|
+
* Set this to true to keep the pre-3.9.0 implicit-all behavior while you
|
|
373
|
+
* migrate to explicit allowlists.
|
|
374
|
+
*
|
|
375
|
+
* ```ts
|
|
376
|
+
* psy.set('openapi', {
|
|
377
|
+
* legacyImplicitRequestBodyParams: true,
|
|
378
|
+
* })
|
|
379
|
+
* ```
|
|
380
|
+
*/
|
|
381
|
+
legacyImplicitRequestBodyParams?: boolean;
|
|
359
382
|
/**
|
|
360
383
|
* When true, psychic will use the `openapi-typescript` package
|
|
361
384
|
* to read this openapi.json file and create typescript interfaces
|