@rvoh/psychic 3.2.0 → 3.3.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/controller/decorators.js +37 -0
- package/dist/cjs/src/helpers/validateOpenApiSchema.js +2 -2
- package/dist/cjs/src/openapi-renderer/SerializerOpenapiRenderer.js +13 -3
- package/dist/cjs/src/openapi-renderer/body-segment.js +43 -1
- package/dist/cjs/src/openapi-renderer/endpoint.js +11 -23
- package/dist/cjs/src/openapi-renderer/helpers/buildDreamRequestBodyShape.js +34 -0
- package/dist/cjs/src/router/helpers.js +4 -1
- package/dist/cjs/src/router/index.js +2 -1
- package/dist/esm/src/controller/decorators.js +37 -0
- package/dist/esm/src/helpers/validateOpenApiSchema.js +2 -2
- package/dist/esm/src/openapi-renderer/SerializerOpenapiRenderer.js +13 -3
- package/dist/esm/src/openapi-renderer/body-segment.js +43 -1
- package/dist/esm/src/openapi-renderer/endpoint.js +11 -23
- package/dist/esm/src/openapi-renderer/helpers/buildDreamRequestBodyShape.js +34 -0
- package/dist/esm/src/router/helpers.js +4 -1
- package/dist/esm/src/router/index.js +2 -1
- package/dist/types/src/controller/decorators.d.ts +40 -2
- package/dist/types/src/helpers/validateOpenApiSchema.d.ts +4 -3
- package/dist/types/src/openapi-renderer/body-segment.d.ts +32 -1
- package/dist/types/src/openapi-renderer/endpoint.d.ts +135 -6
- package/dist/types/src/openapi-renderer/helpers/buildDreamRequestBodyShape.d.ts +19 -0
- package/package.json +1 -1
|
@@ -74,3 +74,40 @@ export function OpenAPI(modelOrSerializer, _opts) {
|
|
|
74
74
|
});
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
78
|
+
(function (OpenAPI) {
|
|
79
|
+
/**
|
|
80
|
+
* Type-narrowed helper for nesting a Dream-model-driven shape inside a
|
|
81
|
+
* request body. Returns the same `{ for, only, including, required,
|
|
82
|
+
* combining }` shape the renderer recognizes natively, but typed via a
|
|
83
|
+
* function-level generic so `only` / `including` / `required` are
|
|
84
|
+
* constrained to columns of the model passed as the first argument.
|
|
85
|
+
*
|
|
86
|
+
* ```ts
|
|
87
|
+
* @OpenAPI(ActionPlan, {
|
|
88
|
+
* requestBody: {
|
|
89
|
+
* including: ['type', 'clientId'],
|
|
90
|
+
* combining: {
|
|
91
|
+
* planItems: {
|
|
92
|
+
* type: 'array',
|
|
93
|
+
* items: OpenAPI.forDream(ActionItem, {
|
|
94
|
+
* required: ['title', 'type'],
|
|
95
|
+
* }),
|
|
96
|
+
* },
|
|
97
|
+
* },
|
|
98
|
+
* },
|
|
99
|
+
* })
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* Wrong column names raise a compile-time error:
|
|
103
|
+
*
|
|
104
|
+
* ```ts
|
|
105
|
+
* OpenAPI.forDream(Pet, { including: ['notARealColumn'] })
|
|
106
|
+
* // ^^^^^^^^^^^^^^^^^ type error
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
function forDream(model, opts = {}) {
|
|
110
|
+
return { for: model, ...opts };
|
|
111
|
+
}
|
|
112
|
+
OpenAPI.forDream = forDream;
|
|
113
|
+
})(OpenAPI || (OpenAPI = {}));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
2
2
|
import addFormats from 'ajv-formats';
|
|
3
3
|
/**
|
|
4
4
|
* @internal
|
|
@@ -99,7 +99,7 @@ export function createValidator(schema, options = {}) {
|
|
|
99
99
|
...options,
|
|
100
100
|
init: undefined,
|
|
101
101
|
};
|
|
102
|
-
const ajv = new
|
|
102
|
+
const ajv = new Ajv2020({
|
|
103
103
|
removeAdditional: false,
|
|
104
104
|
useDefaults: true,
|
|
105
105
|
strict: false,
|
|
@@ -51,11 +51,15 @@ export default class SerializerOpenapiRenderer {
|
|
|
51
51
|
alreadyExtractedDescendantSerializers[this.serializer.globalName] = true;
|
|
52
52
|
const referencedSerializersAndOpenapiSchemaBodyShorthand = this._renderedOpenapi(alreadyExtractedDescendantSerializers);
|
|
53
53
|
if (this.allOfSiblings.length) {
|
|
54
|
-
|
|
54
|
+
// Property-level locks live only at the `allOf` wrapper (never on the
|
|
55
|
+
// inline branch or the `$ref`'d siblings) so that all branches'
|
|
56
|
+
// properties are visible to `unevaluatedProperties` for the union check.
|
|
55
57
|
return {
|
|
56
58
|
...referencedSerializersAndOpenapiSchemaBodyShorthand,
|
|
57
59
|
openapi: {
|
|
58
|
-
|
|
60
|
+
type: 'object',
|
|
61
|
+
allOf: [referencedSerializersAndOpenapiSchemaBodyShorthand.openapi, ...this.allOfSiblings],
|
|
62
|
+
unevaluatedProperties: false,
|
|
59
63
|
},
|
|
60
64
|
};
|
|
61
65
|
}
|
|
@@ -98,7 +102,13 @@ export default class SerializerOpenapiRenderer {
|
|
|
98
102
|
type: 'object',
|
|
99
103
|
required: sort(uniq(requiredProperties.map(property => this.setCase(property)))),
|
|
100
104
|
properties: sortObjectByKey(referencedSerializersAndAttributes.attributes),
|
|
101
|
-
additionalProperties
|
|
105
|
+
// Property-level locks (`additionalProperties` / `unevaluatedProperties`)
|
|
106
|
+
// are not emitted on leaf schemas: when a leaf is composed via `$ref`
|
|
107
|
+
// inside an `allOf`, neither keyword sees properties contributed by
|
|
108
|
+
// sibling branches, so a per-leaf lock incorrectly rejects flattened
|
|
109
|
+
// properties. Strictness is enforced at the `allOf`-wrapper level
|
|
110
|
+
// (`unevaluatedProperties: false`) when flattening occurs, and at the
|
|
111
|
+
// validation-pipeline level for top-level schemas.
|
|
102
112
|
},
|
|
103
113
|
};
|
|
104
114
|
}
|
|
@@ -3,6 +3,7 @@ import { DreamApp } from '@rvoh/dream';
|
|
|
3
3
|
import { openapiShorthandPrimitiveTypes, } from '@rvoh/dream/openapi';
|
|
4
4
|
import NonSerializerSuppliedToSerializerBodySegment from '../error/openapi/NonSerializerSuppliedToSerializerBodySegment.js';
|
|
5
5
|
import isArrayParamName from '../helpers/isArrayParamName.js';
|
|
6
|
+
import buildDreamRequestBodyShape from './helpers/buildDreamRequestBodyShape.js';
|
|
6
7
|
import isBlankDescription from './helpers/isBlankDescription.js';
|
|
7
8
|
import maybeNullOpenapiShorthandToOpenapiShorthand from './helpers/maybeNullOpenapiShorthandToOpenapiShorthand.js';
|
|
8
9
|
import primitiveOpenapiStatementToOpenapi from './helpers/primitiveOpenapiStatementToOpenapi.js';
|
|
@@ -20,6 +21,7 @@ import SerializerOpenapiRenderer from './SerializerOpenapiRenderer.js';
|
|
|
20
21
|
* - Nullable array shorthands like `['string[]', 'null']` → `{ type: ['array', 'null'], items: { type: 'string' } }`
|
|
21
22
|
* - Serializer references like `{ $serializer: SomeSerializer }` → `{ $ref: '#/components/schemas/SerializerOpenapiName' }`
|
|
22
23
|
* - Serializable references like `{ $serializable: SomeModel, key: 'summary' }` → resolved serializer reference
|
|
24
|
+
* - Nested model-derived request-body references like `{ for: SomeModel, including, only, required, combining }` → an inline object schema derived from the model's param-safe columns (request-only)
|
|
23
25
|
*
|
|
24
26
|
* The class recursively processes nested structures (objects, arrays, unions) and maintains
|
|
25
27
|
* a collection of referenced serializers that need to be included in the final OpenAPI document.
|
|
@@ -52,17 +54,19 @@ export default class OpenapiSegmentExpander {
|
|
|
52
54
|
casing;
|
|
53
55
|
suppressResponseEnums;
|
|
54
56
|
target;
|
|
57
|
+
source;
|
|
55
58
|
/**
|
|
56
59
|
* @internal
|
|
57
60
|
*
|
|
58
61
|
* Used to recursively expand nested object structures
|
|
59
62
|
* within nested openapi objects
|
|
60
63
|
*/
|
|
61
|
-
constructor(bodySegment, { renderOpts, target }) {
|
|
64
|
+
constructor(bodySegment, { renderOpts, target, source }) {
|
|
62
65
|
this.bodySegment = bodySegment;
|
|
63
66
|
this.casing = renderOpts.casing;
|
|
64
67
|
this.suppressResponseEnums = renderOpts.suppressResponseEnums;
|
|
65
68
|
this.target = target;
|
|
69
|
+
this.source = source ?? 'unknown';
|
|
66
70
|
}
|
|
67
71
|
/**
|
|
68
72
|
* returns the shorthanded body segment, rendered
|
|
@@ -118,6 +122,8 @@ export default class OpenapiSegmentExpander {
|
|
|
118
122
|
return this.serializerStatement(bodySegment);
|
|
119
123
|
case '$serializable':
|
|
120
124
|
return this.serializableStatement(bodySegment);
|
|
125
|
+
case '$for':
|
|
126
|
+
return this.forStatement(bodySegment);
|
|
121
127
|
case 'unknown_object':
|
|
122
128
|
return {
|
|
123
129
|
referencedSerializers: [],
|
|
@@ -153,6 +159,8 @@ export default class OpenapiSegmentExpander {
|
|
|
153
159
|
return '$serializer';
|
|
154
160
|
if (serializableRefBodySegment.$serializable)
|
|
155
161
|
return '$serializable';
|
|
162
|
+
if (typeof bodySegment?.for === 'function')
|
|
163
|
+
return '$for';
|
|
156
164
|
if (refBodySegment.$ref)
|
|
157
165
|
return '$ref';
|
|
158
166
|
if (schemaRefBodySegment.$schema)
|
|
@@ -448,6 +456,40 @@ The following values will be allowed:
|
|
|
448
456
|
}
|
|
449
457
|
return { referencedSerializers: [serializer], openapi: serializerRef };
|
|
450
458
|
}
|
|
459
|
+
/**
|
|
460
|
+
* @internal
|
|
461
|
+
*
|
|
462
|
+
* Expand a nested `for:` request-body sentinel into a fully-resolved
|
|
463
|
+
* `OpenapiSchemaObject`. The sentinel takes the same shape as the
|
|
464
|
+
* top-level model-driven `requestBody`:
|
|
465
|
+
*
|
|
466
|
+
* ```ts
|
|
467
|
+
* { for: SomeModel, including?, only?, required?, combining? }
|
|
468
|
+
* ```
|
|
469
|
+
*
|
|
470
|
+
* and produces the same shape that the top-level model-driven
|
|
471
|
+
* `requestBody` produces — param-safe columns inferred from the model,
|
|
472
|
+
* with `combining` entries (which themselves may contain further `for:`
|
|
473
|
+
* sentinels) merged into the resulting properties.
|
|
474
|
+
*
|
|
475
|
+
* The `for:` sentinel is request-only at the type level. If it is
|
|
476
|
+
* encountered while expanding a response shape, this method throws —
|
|
477
|
+
* the type system should have prevented this, so reaching here indicates
|
|
478
|
+
* a misuse that bypassed type checking (e.g., via `as any`).
|
|
479
|
+
*/
|
|
480
|
+
forStatement(bodySegment) {
|
|
481
|
+
if (this.target !== 'request') {
|
|
482
|
+
throw new Error(`'for:' sentinel is only valid inside request body shorthand; encountered while expanding a response.`);
|
|
483
|
+
}
|
|
484
|
+
const ref = bodySegment;
|
|
485
|
+
const paramsShape = buildDreamRequestBodyShape(ref.for, {
|
|
486
|
+
only: ref.only,
|
|
487
|
+
including: ref.including,
|
|
488
|
+
required: ref.required,
|
|
489
|
+
combining: ref.combining,
|
|
490
|
+
}, this.source);
|
|
491
|
+
return this.recursivelyParseBody(paramsShape);
|
|
492
|
+
}
|
|
451
493
|
serializableStatement(bodySegment) {
|
|
452
494
|
const serializableRef = bodySegment;
|
|
453
495
|
const key = serializableRef.$serializableSerializerKey || serializableRef.key || 'default';
|
|
@@ -6,10 +6,9 @@ import NonSerializerDerivedInToSchemaObjects from '../error/openapi/NonSerialize
|
|
|
6
6
|
import OpenApiSerializerForEndpointNotAFunction from '../error/openapi/SerializerForEndpointNotAFunction.js';
|
|
7
7
|
import isObject from '../helpers/isObject.js';
|
|
8
8
|
import PsychicApp from '../psychic-app/index.js';
|
|
9
|
-
import openapiParamNamesForDreamClass from '../server/helpers/openapiParamNamesForDreamClass.js';
|
|
10
9
|
import OpenapiSegmentExpander from './body-segment.js';
|
|
11
10
|
import { DEFAULT_OPENAPI_RESPONSES } from './defaults.js';
|
|
12
|
-
import
|
|
11
|
+
import buildDreamRequestBodyShape from './helpers/buildDreamRequestBodyShape.js';
|
|
13
12
|
import openapiOpts from './helpers/openapiOpts.js';
|
|
14
13
|
import openapiRoute from './helpers/openapiRoute.js';
|
|
15
14
|
import safelyAttachCursorPaginationParamToRequestBodySegment from './helpers/safelyAttachCursorPaginationParamToRequestBodySegment.js';
|
|
@@ -519,30 +518,19 @@ export default class OpenapiEndpointRenderer {
|
|
|
519
518
|
const dreamClass = forDreamClass || this.getSingleDreamModelClass();
|
|
520
519
|
if (!dreamClass)
|
|
521
520
|
return this.defaultRequestBody();
|
|
522
|
-
const { only, including, combining } = (this.requestBody ||
|
|
523
|
-
|
|
524
|
-
const
|
|
525
|
-
const paramsShape = {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
paramsShape.required = required;
|
|
532
|
-
}
|
|
533
|
-
paramsShape.properties = paramSafeColumns.reduce((acc, columnName) => {
|
|
534
|
-
acc[columnName] = dreamColumnOpenapiShape(this.controllerClass.controllerActionPath(this.action), dreamClass, columnName, undefined, {
|
|
535
|
-
allowGenericJson: true,
|
|
536
|
-
});
|
|
537
|
-
return acc;
|
|
538
|
-
}, paramsShape.properties);
|
|
539
|
-
paramsShape.properties = {
|
|
540
|
-
...paramsShape.properties,
|
|
541
|
-
...(combining || {}),
|
|
542
|
-
};
|
|
521
|
+
const { only, including, required, combining } = (this.requestBody ||
|
|
522
|
+
{});
|
|
523
|
+
const source = this.controllerClass.controllerActionPath(this.action);
|
|
524
|
+
const paramsShape = buildDreamRequestBodyShape(dreamClass, {
|
|
525
|
+
only: only,
|
|
526
|
+
including: including,
|
|
527
|
+
required: required,
|
|
528
|
+
combining: combining,
|
|
529
|
+
}, source);
|
|
543
530
|
let processedSchema = new OpenapiSegmentExpander(paramsShape, {
|
|
544
531
|
renderOpts,
|
|
545
532
|
target: 'request',
|
|
533
|
+
source,
|
|
546
534
|
}).render().openapi;
|
|
547
535
|
const bodyPaginationPageParam = this.paginate?.body;
|
|
548
536
|
if (bodyPaginationPageParam) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import openapiParamNamesForDreamClass from '../../server/helpers/openapiParamNamesForDreamClass.js';
|
|
2
|
+
import { dreamColumnOpenapiShape } from './dreamColumnOpenapiShape.js';
|
|
3
|
+
/**
|
|
4
|
+
* @internal
|
|
5
|
+
*
|
|
6
|
+
* Builds an unexpanded `OpenapiSchemaObject` for a Dream model's request-body shape,
|
|
7
|
+
* resolving param-safe columns, attaching `required`, and merging `combining` entries.
|
|
8
|
+
*
|
|
9
|
+
* Used by both the top-level model-driven `requestBody` path in `OpenapiEndpointRenderer`
|
|
10
|
+
* and the `$dream` sentinel expansion inside `OpenapiSegmentExpander`. Single source of
|
|
11
|
+
* truth keeps top-level and nested semantics identical.
|
|
12
|
+
*/
|
|
13
|
+
export default function buildDreamRequestBodyShape(dreamClass, opts, source) {
|
|
14
|
+
const { only, including, required, combining } = opts;
|
|
15
|
+
const paramSafeColumns = openapiParamNamesForDreamClass(dreamClass, { only, including });
|
|
16
|
+
const paramsShape = {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {},
|
|
19
|
+
};
|
|
20
|
+
if (required) {
|
|
21
|
+
paramsShape.required = required;
|
|
22
|
+
}
|
|
23
|
+
paramsShape.properties = paramSafeColumns.reduce((acc, columnName) => {
|
|
24
|
+
acc[columnName] = dreamColumnOpenapiShape(source, dreamClass, columnName, undefined, {
|
|
25
|
+
allowGenericJson: true,
|
|
26
|
+
});
|
|
27
|
+
return acc;
|
|
28
|
+
}, paramsShape.properties);
|
|
29
|
+
paramsShape.properties = {
|
|
30
|
+
...paramsShape.properties,
|
|
31
|
+
...(combining || {}),
|
|
32
|
+
};
|
|
33
|
+
return paramsShape;
|
|
34
|
+
}
|
|
@@ -4,7 +4,10 @@ import CannotInferControllerFromTopLevelRouteError from '../error/router/cannot-
|
|
|
4
4
|
import pascalizeFileName from '../helpers/pascalizeFileName.js';
|
|
5
5
|
import PsychicApp from '../psychic-app/index.js';
|
|
6
6
|
export function routePath(routePath) {
|
|
7
|
-
|
|
7
|
+
const normalized = `/${routePath.replace(/^\//, '')}`;
|
|
8
|
+
if (normalized === '/')
|
|
9
|
+
return normalized;
|
|
10
|
+
return normalized.replace(/\/+$/, '');
|
|
8
11
|
}
|
|
9
12
|
export function resourcePath(routePath) {
|
|
10
13
|
return `/${routePath}/:id`;
|
|
@@ -72,7 +72,8 @@ export default class PsychicRouter {
|
|
|
72
72
|
prefixPathWithNamespaces(str) {
|
|
73
73
|
if (!this.currentNamespaces.length)
|
|
74
74
|
return str;
|
|
75
|
-
|
|
75
|
+
const prefix = '/' + this.currentNamespacePaths.join('/');
|
|
76
|
+
return str ? `${prefix}/${str}` : prefix;
|
|
76
77
|
}
|
|
77
78
|
crud(httpMethod, path, controllerOrMiddleware, action) {
|
|
78
79
|
this.checkPathForInvalidChars(path);
|
|
@@ -74,3 +74,40 @@ export function OpenAPI(modelOrSerializer, _opts) {
|
|
|
74
74
|
});
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
78
|
+
(function (OpenAPI) {
|
|
79
|
+
/**
|
|
80
|
+
* Type-narrowed helper for nesting a Dream-model-driven shape inside a
|
|
81
|
+
* request body. Returns the same `{ for, only, including, required,
|
|
82
|
+
* combining }` shape the renderer recognizes natively, but typed via a
|
|
83
|
+
* function-level generic so `only` / `including` / `required` are
|
|
84
|
+
* constrained to columns of the model passed as the first argument.
|
|
85
|
+
*
|
|
86
|
+
* ```ts
|
|
87
|
+
* @OpenAPI(ActionPlan, {
|
|
88
|
+
* requestBody: {
|
|
89
|
+
* including: ['type', 'clientId'],
|
|
90
|
+
* combining: {
|
|
91
|
+
* planItems: {
|
|
92
|
+
* type: 'array',
|
|
93
|
+
* items: OpenAPI.forDream(ActionItem, {
|
|
94
|
+
* required: ['title', 'type'],
|
|
95
|
+
* }),
|
|
96
|
+
* },
|
|
97
|
+
* },
|
|
98
|
+
* },
|
|
99
|
+
* })
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* Wrong column names raise a compile-time error:
|
|
103
|
+
*
|
|
104
|
+
* ```ts
|
|
105
|
+
* OpenAPI.forDream(Pet, { including: ['notARealColumn'] })
|
|
106
|
+
* // ^^^^^^^^^^^^^^^^^ type error
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
function forDream(model, opts = {}) {
|
|
110
|
+
return { for: model, ...opts };
|
|
111
|
+
}
|
|
112
|
+
OpenAPI.forDream = forDream;
|
|
113
|
+
})(OpenAPI || (OpenAPI = {}));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
2
2
|
import addFormats from 'ajv-formats';
|
|
3
3
|
/**
|
|
4
4
|
* @internal
|
|
@@ -99,7 +99,7 @@ export function createValidator(schema, options = {}) {
|
|
|
99
99
|
...options,
|
|
100
100
|
init: undefined,
|
|
101
101
|
};
|
|
102
|
-
const ajv = new
|
|
102
|
+
const ajv = new Ajv2020({
|
|
103
103
|
removeAdditional: false,
|
|
104
104
|
useDefaults: true,
|
|
105
105
|
strict: false,
|
|
@@ -51,11 +51,15 @@ export default class SerializerOpenapiRenderer {
|
|
|
51
51
|
alreadyExtractedDescendantSerializers[this.serializer.globalName] = true;
|
|
52
52
|
const referencedSerializersAndOpenapiSchemaBodyShorthand = this._renderedOpenapi(alreadyExtractedDescendantSerializers);
|
|
53
53
|
if (this.allOfSiblings.length) {
|
|
54
|
-
|
|
54
|
+
// Property-level locks live only at the `allOf` wrapper (never on the
|
|
55
|
+
// inline branch or the `$ref`'d siblings) so that all branches'
|
|
56
|
+
// properties are visible to `unevaluatedProperties` for the union check.
|
|
55
57
|
return {
|
|
56
58
|
...referencedSerializersAndOpenapiSchemaBodyShorthand,
|
|
57
59
|
openapi: {
|
|
58
|
-
|
|
60
|
+
type: 'object',
|
|
61
|
+
allOf: [referencedSerializersAndOpenapiSchemaBodyShorthand.openapi, ...this.allOfSiblings],
|
|
62
|
+
unevaluatedProperties: false,
|
|
59
63
|
},
|
|
60
64
|
};
|
|
61
65
|
}
|
|
@@ -98,7 +102,13 @@ export default class SerializerOpenapiRenderer {
|
|
|
98
102
|
type: 'object',
|
|
99
103
|
required: sort(uniq(requiredProperties.map(property => this.setCase(property)))),
|
|
100
104
|
properties: sortObjectByKey(referencedSerializersAndAttributes.attributes),
|
|
101
|
-
additionalProperties
|
|
105
|
+
// Property-level locks (`additionalProperties` / `unevaluatedProperties`)
|
|
106
|
+
// are not emitted on leaf schemas: when a leaf is composed via `$ref`
|
|
107
|
+
// inside an `allOf`, neither keyword sees properties contributed by
|
|
108
|
+
// sibling branches, so a per-leaf lock incorrectly rejects flattened
|
|
109
|
+
// properties. Strictness is enforced at the `allOf`-wrapper level
|
|
110
|
+
// (`unevaluatedProperties: false`) when flattening occurs, and at the
|
|
111
|
+
// validation-pipeline level for top-level schemas.
|
|
102
112
|
},
|
|
103
113
|
};
|
|
104
114
|
}
|
|
@@ -3,6 +3,7 @@ import { DreamApp } from '@rvoh/dream';
|
|
|
3
3
|
import { openapiShorthandPrimitiveTypes, } from '@rvoh/dream/openapi';
|
|
4
4
|
import NonSerializerSuppliedToSerializerBodySegment from '../error/openapi/NonSerializerSuppliedToSerializerBodySegment.js';
|
|
5
5
|
import isArrayParamName from '../helpers/isArrayParamName.js';
|
|
6
|
+
import buildDreamRequestBodyShape from './helpers/buildDreamRequestBodyShape.js';
|
|
6
7
|
import isBlankDescription from './helpers/isBlankDescription.js';
|
|
7
8
|
import maybeNullOpenapiShorthandToOpenapiShorthand from './helpers/maybeNullOpenapiShorthandToOpenapiShorthand.js';
|
|
8
9
|
import primitiveOpenapiStatementToOpenapi from './helpers/primitiveOpenapiStatementToOpenapi.js';
|
|
@@ -20,6 +21,7 @@ import SerializerOpenapiRenderer from './SerializerOpenapiRenderer.js';
|
|
|
20
21
|
* - Nullable array shorthands like `['string[]', 'null']` → `{ type: ['array', 'null'], items: { type: 'string' } }`
|
|
21
22
|
* - Serializer references like `{ $serializer: SomeSerializer }` → `{ $ref: '#/components/schemas/SerializerOpenapiName' }`
|
|
22
23
|
* - Serializable references like `{ $serializable: SomeModel, key: 'summary' }` → resolved serializer reference
|
|
24
|
+
* - Nested model-derived request-body references like `{ for: SomeModel, including, only, required, combining }` → an inline object schema derived from the model's param-safe columns (request-only)
|
|
23
25
|
*
|
|
24
26
|
* The class recursively processes nested structures (objects, arrays, unions) and maintains
|
|
25
27
|
* a collection of referenced serializers that need to be included in the final OpenAPI document.
|
|
@@ -52,17 +54,19 @@ export default class OpenapiSegmentExpander {
|
|
|
52
54
|
casing;
|
|
53
55
|
suppressResponseEnums;
|
|
54
56
|
target;
|
|
57
|
+
source;
|
|
55
58
|
/**
|
|
56
59
|
* @internal
|
|
57
60
|
*
|
|
58
61
|
* Used to recursively expand nested object structures
|
|
59
62
|
* within nested openapi objects
|
|
60
63
|
*/
|
|
61
|
-
constructor(bodySegment, { renderOpts, target }) {
|
|
64
|
+
constructor(bodySegment, { renderOpts, target, source }) {
|
|
62
65
|
this.bodySegment = bodySegment;
|
|
63
66
|
this.casing = renderOpts.casing;
|
|
64
67
|
this.suppressResponseEnums = renderOpts.suppressResponseEnums;
|
|
65
68
|
this.target = target;
|
|
69
|
+
this.source = source ?? 'unknown';
|
|
66
70
|
}
|
|
67
71
|
/**
|
|
68
72
|
* returns the shorthanded body segment, rendered
|
|
@@ -118,6 +122,8 @@ export default class OpenapiSegmentExpander {
|
|
|
118
122
|
return this.serializerStatement(bodySegment);
|
|
119
123
|
case '$serializable':
|
|
120
124
|
return this.serializableStatement(bodySegment);
|
|
125
|
+
case '$for':
|
|
126
|
+
return this.forStatement(bodySegment);
|
|
121
127
|
case 'unknown_object':
|
|
122
128
|
return {
|
|
123
129
|
referencedSerializers: [],
|
|
@@ -153,6 +159,8 @@ export default class OpenapiSegmentExpander {
|
|
|
153
159
|
return '$serializer';
|
|
154
160
|
if (serializableRefBodySegment.$serializable)
|
|
155
161
|
return '$serializable';
|
|
162
|
+
if (typeof bodySegment?.for === 'function')
|
|
163
|
+
return '$for';
|
|
156
164
|
if (refBodySegment.$ref)
|
|
157
165
|
return '$ref';
|
|
158
166
|
if (schemaRefBodySegment.$schema)
|
|
@@ -448,6 +456,40 @@ The following values will be allowed:
|
|
|
448
456
|
}
|
|
449
457
|
return { referencedSerializers: [serializer], openapi: serializerRef };
|
|
450
458
|
}
|
|
459
|
+
/**
|
|
460
|
+
* @internal
|
|
461
|
+
*
|
|
462
|
+
* Expand a nested `for:` request-body sentinel into a fully-resolved
|
|
463
|
+
* `OpenapiSchemaObject`. The sentinel takes the same shape as the
|
|
464
|
+
* top-level model-driven `requestBody`:
|
|
465
|
+
*
|
|
466
|
+
* ```ts
|
|
467
|
+
* { for: SomeModel, including?, only?, required?, combining? }
|
|
468
|
+
* ```
|
|
469
|
+
*
|
|
470
|
+
* and produces the same shape that the top-level model-driven
|
|
471
|
+
* `requestBody` produces — param-safe columns inferred from the model,
|
|
472
|
+
* with `combining` entries (which themselves may contain further `for:`
|
|
473
|
+
* sentinels) merged into the resulting properties.
|
|
474
|
+
*
|
|
475
|
+
* The `for:` sentinel is request-only at the type level. If it is
|
|
476
|
+
* encountered while expanding a response shape, this method throws —
|
|
477
|
+
* the type system should have prevented this, so reaching here indicates
|
|
478
|
+
* a misuse that bypassed type checking (e.g., via `as any`).
|
|
479
|
+
*/
|
|
480
|
+
forStatement(bodySegment) {
|
|
481
|
+
if (this.target !== 'request') {
|
|
482
|
+
throw new Error(`'for:' sentinel is only valid inside request body shorthand; encountered while expanding a response.`);
|
|
483
|
+
}
|
|
484
|
+
const ref = bodySegment;
|
|
485
|
+
const paramsShape = buildDreamRequestBodyShape(ref.for, {
|
|
486
|
+
only: ref.only,
|
|
487
|
+
including: ref.including,
|
|
488
|
+
required: ref.required,
|
|
489
|
+
combining: ref.combining,
|
|
490
|
+
}, this.source);
|
|
491
|
+
return this.recursivelyParseBody(paramsShape);
|
|
492
|
+
}
|
|
451
493
|
serializableStatement(bodySegment) {
|
|
452
494
|
const serializableRef = bodySegment;
|
|
453
495
|
const key = serializableRef.$serializableSerializerKey || serializableRef.key || 'default';
|
|
@@ -6,10 +6,9 @@ import NonSerializerDerivedInToSchemaObjects from '../error/openapi/NonSerialize
|
|
|
6
6
|
import OpenApiSerializerForEndpointNotAFunction from '../error/openapi/SerializerForEndpointNotAFunction.js';
|
|
7
7
|
import isObject from '../helpers/isObject.js';
|
|
8
8
|
import PsychicApp from '../psychic-app/index.js';
|
|
9
|
-
import openapiParamNamesForDreamClass from '../server/helpers/openapiParamNamesForDreamClass.js';
|
|
10
9
|
import OpenapiSegmentExpander from './body-segment.js';
|
|
11
10
|
import { DEFAULT_OPENAPI_RESPONSES } from './defaults.js';
|
|
12
|
-
import
|
|
11
|
+
import buildDreamRequestBodyShape from './helpers/buildDreamRequestBodyShape.js';
|
|
13
12
|
import openapiOpts from './helpers/openapiOpts.js';
|
|
14
13
|
import openapiRoute from './helpers/openapiRoute.js';
|
|
15
14
|
import safelyAttachCursorPaginationParamToRequestBodySegment from './helpers/safelyAttachCursorPaginationParamToRequestBodySegment.js';
|
|
@@ -519,30 +518,19 @@ export default class OpenapiEndpointRenderer {
|
|
|
519
518
|
const dreamClass = forDreamClass || this.getSingleDreamModelClass();
|
|
520
519
|
if (!dreamClass)
|
|
521
520
|
return this.defaultRequestBody();
|
|
522
|
-
const { only, including, combining } = (this.requestBody ||
|
|
523
|
-
|
|
524
|
-
const
|
|
525
|
-
const paramsShape = {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
paramsShape.required = required;
|
|
532
|
-
}
|
|
533
|
-
paramsShape.properties = paramSafeColumns.reduce((acc, columnName) => {
|
|
534
|
-
acc[columnName] = dreamColumnOpenapiShape(this.controllerClass.controllerActionPath(this.action), dreamClass, columnName, undefined, {
|
|
535
|
-
allowGenericJson: true,
|
|
536
|
-
});
|
|
537
|
-
return acc;
|
|
538
|
-
}, paramsShape.properties);
|
|
539
|
-
paramsShape.properties = {
|
|
540
|
-
...paramsShape.properties,
|
|
541
|
-
...(combining || {}),
|
|
542
|
-
};
|
|
521
|
+
const { only, including, required, combining } = (this.requestBody ||
|
|
522
|
+
{});
|
|
523
|
+
const source = this.controllerClass.controllerActionPath(this.action);
|
|
524
|
+
const paramsShape = buildDreamRequestBodyShape(dreamClass, {
|
|
525
|
+
only: only,
|
|
526
|
+
including: including,
|
|
527
|
+
required: required,
|
|
528
|
+
combining: combining,
|
|
529
|
+
}, source);
|
|
543
530
|
let processedSchema = new OpenapiSegmentExpander(paramsShape, {
|
|
544
531
|
renderOpts,
|
|
545
532
|
target: 'request',
|
|
533
|
+
source,
|
|
546
534
|
}).render().openapi;
|
|
547
535
|
const bodyPaginationPageParam = this.paginate?.body;
|
|
548
536
|
if (bodyPaginationPageParam) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import openapiParamNamesForDreamClass from '../../server/helpers/openapiParamNamesForDreamClass.js';
|
|
2
|
+
import { dreamColumnOpenapiShape } from './dreamColumnOpenapiShape.js';
|
|
3
|
+
/**
|
|
4
|
+
* @internal
|
|
5
|
+
*
|
|
6
|
+
* Builds an unexpanded `OpenapiSchemaObject` for a Dream model's request-body shape,
|
|
7
|
+
* resolving param-safe columns, attaching `required`, and merging `combining` entries.
|
|
8
|
+
*
|
|
9
|
+
* Used by both the top-level model-driven `requestBody` path in `OpenapiEndpointRenderer`
|
|
10
|
+
* and the `$dream` sentinel expansion inside `OpenapiSegmentExpander`. Single source of
|
|
11
|
+
* truth keeps top-level and nested semantics identical.
|
|
12
|
+
*/
|
|
13
|
+
export default function buildDreamRequestBodyShape(dreamClass, opts, source) {
|
|
14
|
+
const { only, including, required, combining } = opts;
|
|
15
|
+
const paramSafeColumns = openapiParamNamesForDreamClass(dreamClass, { only, including });
|
|
16
|
+
const paramsShape = {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {},
|
|
19
|
+
};
|
|
20
|
+
if (required) {
|
|
21
|
+
paramsShape.required = required;
|
|
22
|
+
}
|
|
23
|
+
paramsShape.properties = paramSafeColumns.reduce((acc, columnName) => {
|
|
24
|
+
acc[columnName] = dreamColumnOpenapiShape(source, dreamClass, columnName, undefined, {
|
|
25
|
+
allowGenericJson: true,
|
|
26
|
+
});
|
|
27
|
+
return acc;
|
|
28
|
+
}, paramsShape.properties);
|
|
29
|
+
paramsShape.properties = {
|
|
30
|
+
...paramsShape.properties,
|
|
31
|
+
...(combining || {}),
|
|
32
|
+
};
|
|
33
|
+
return paramsShape;
|
|
34
|
+
}
|
|
@@ -4,7 +4,10 @@ import CannotInferControllerFromTopLevelRouteError from '../error/router/cannot-
|
|
|
4
4
|
import pascalizeFileName from '../helpers/pascalizeFileName.js';
|
|
5
5
|
import PsychicApp from '../psychic-app/index.js';
|
|
6
6
|
export function routePath(routePath) {
|
|
7
|
-
|
|
7
|
+
const normalized = `/${routePath.replace(/^\//, '')}`;
|
|
8
|
+
if (normalized === '/')
|
|
9
|
+
return normalized;
|
|
10
|
+
return normalized.replace(/\/+$/, '');
|
|
8
11
|
}
|
|
9
12
|
export function resourcePath(routePath) {
|
|
10
13
|
return `/${routePath}/:id`;
|
|
@@ -72,7 +72,8 @@ export default class PsychicRouter {
|
|
|
72
72
|
prefixPathWithNamespaces(str) {
|
|
73
73
|
if (!this.currentNamespaces.length)
|
|
74
74
|
return str;
|
|
75
|
-
|
|
75
|
+
const prefix = '/' + this.currentNamespacePaths.join('/');
|
|
76
|
+
return str ? `${prefix}/${str}` : prefix;
|
|
76
77
|
}
|
|
77
78
|
crud(httpMethod, path, controllerOrMiddleware, action) {
|
|
78
79
|
this.checkPathForInvalidChars(path);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Dream } from '@rvoh/dream';
|
|
2
|
-
import { DreamSerializable, DreamSerializableArray } from '@rvoh/dream/types';
|
|
3
|
-
import { OpenapiEndpointRendererOpts } from '../openapi-renderer/endpoint.js';
|
|
2
|
+
import { DreamParamSafeAttributes, DreamSerializable, DreamSerializableArray, UpdateableProperties } from '@rvoh/dream/types';
|
|
3
|
+
import { OpenapiEndpointRendererOpts, OpenapiSchemaNestedFor, OpenapiSchemaRequestPropertiesShorthandWithFor } from '../openapi-renderer/endpoint.js';
|
|
4
4
|
export declare function BeforeAction(opts?: {
|
|
5
5
|
isStatic?: boolean;
|
|
6
6
|
only?: string[];
|
|
@@ -9,3 +9,41 @@ export declare function BeforeAction(opts?: {
|
|
|
9
9
|
export declare function OpenAPI(): any;
|
|
10
10
|
export declare function OpenAPI<const ForOption extends typeof Dream>(opts: OpenapiEndpointRendererOpts<undefined, ForOption>): any;
|
|
11
11
|
export declare function OpenAPI<const I extends DreamSerializable | DreamSerializableArray, const ForOption extends typeof Dream>(modelOrSerializer: I, opts?: OpenapiEndpointRendererOpts<I, ForOption>): any;
|
|
12
|
+
export declare namespace OpenAPI {
|
|
13
|
+
/**
|
|
14
|
+
* Type-narrowed helper for nesting a Dream-model-driven shape inside a
|
|
15
|
+
* request body. Returns the same `{ for, only, including, required,
|
|
16
|
+
* combining }` shape the renderer recognizes natively, but typed via a
|
|
17
|
+
* function-level generic so `only` / `including` / `required` are
|
|
18
|
+
* constrained to columns of the model passed as the first argument.
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* @OpenAPI(ActionPlan, {
|
|
22
|
+
* requestBody: {
|
|
23
|
+
* including: ['type', 'clientId'],
|
|
24
|
+
* combining: {
|
|
25
|
+
* planItems: {
|
|
26
|
+
* type: 'array',
|
|
27
|
+
* items: OpenAPI.forDream(ActionItem, {
|
|
28
|
+
* required: ['title', 'type'],
|
|
29
|
+
* }),
|
|
30
|
+
* },
|
|
31
|
+
* },
|
|
32
|
+
* },
|
|
33
|
+
* })
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* Wrong column names raise a compile-time error:
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* OpenAPI.forDream(Pet, { including: ['notARealColumn'] })
|
|
40
|
+
* // ^^^^^^^^^^^^^^^^^ type error
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
function forDream<const M extends typeof Dream>(model: M, opts?: {
|
|
44
|
+
only?: readonly (keyof DreamParamSafeAttributes<InstanceType<M>>)[];
|
|
45
|
+
including?: readonly Exclude<keyof UpdateableProperties<InstanceType<M>>, keyof DreamParamSafeAttributes<InstanceType<M>>>[];
|
|
46
|
+
required?: readonly (keyof UpdateableProperties<InstanceType<M>>)[];
|
|
47
|
+
combining?: OpenapiSchemaRequestPropertiesShorthandWithFor;
|
|
48
|
+
}): OpenapiSchemaNestedFor;
|
|
49
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type ErrorObject, type JSONSchemaType, type ValidateFunction } from 'ajv';
|
|
2
|
+
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
2
3
|
/**
|
|
3
4
|
* @internal
|
|
4
5
|
*
|
|
@@ -72,8 +73,8 @@ export interface ValidationError {
|
|
|
72
73
|
message: string;
|
|
73
74
|
params?: Record<string, unknown>;
|
|
74
75
|
}
|
|
75
|
-
export type AjvValidationOpts = ConstructorParameters<typeof
|
|
76
|
+
export type AjvValidationOpts = ConstructorParameters<typeof Ajv2020>[0];
|
|
76
77
|
export type ValidateOpenapiSchemaOptions = AjvValidationOpts & CustomOpenapiValidationOptions;
|
|
77
78
|
export interface CustomOpenapiValidationOptions {
|
|
78
|
-
init?: (ajv:
|
|
79
|
+
init?: (ajv: Ajv2020) => void;
|
|
79
80
|
}
|
|
@@ -4,6 +4,13 @@ import { OpenapiEndpointResponse, OpenapiRenderOpts, OpenapiResponses } from './
|
|
|
4
4
|
export interface OpenapiBodySegmentRendererOpts {
|
|
5
5
|
renderOpts: OpenapiRenderOpts;
|
|
6
6
|
target: OpenapiBodyTarget;
|
|
7
|
+
/**
|
|
8
|
+
* Identifier (typically a controller-action path) used to enrich error
|
|
9
|
+
* messages emitted by `dreamColumnOpenapiShape` when an unrecognized DB
|
|
10
|
+
* type is encountered while expanding a `$dream` sentinel. Optional —
|
|
11
|
+
* defaults to `'unknown'` when not supplied.
|
|
12
|
+
*/
|
|
13
|
+
source?: string;
|
|
7
14
|
}
|
|
8
15
|
/**
|
|
9
16
|
* @internal
|
|
@@ -17,6 +24,7 @@ export interface OpenapiBodySegmentRendererOpts {
|
|
|
17
24
|
* - Nullable array shorthands like `['string[]', 'null']` → `{ type: ['array', 'null'], items: { type: 'string' } }`
|
|
18
25
|
* - Serializer references like `{ $serializer: SomeSerializer }` → `{ $ref: '#/components/schemas/SerializerOpenapiName' }`
|
|
19
26
|
* - Serializable references like `{ $serializable: SomeModel, key: 'summary' }` → resolved serializer reference
|
|
27
|
+
* - Nested model-derived request-body references like `{ for: SomeModel, including, only, required, combining }` → an inline object schema derived from the model's param-safe columns (request-only)
|
|
20
28
|
*
|
|
21
29
|
* The class recursively processes nested structures (objects, arrays, unions) and maintains
|
|
22
30
|
* a collection of referenced serializers that need to be included in the final OpenAPI document.
|
|
@@ -49,13 +57,14 @@ export default class OpenapiSegmentExpander {
|
|
|
49
57
|
private casing;
|
|
50
58
|
private suppressResponseEnums;
|
|
51
59
|
private target;
|
|
60
|
+
private source;
|
|
52
61
|
/**
|
|
53
62
|
* @internal
|
|
54
63
|
*
|
|
55
64
|
* Used to recursively expand nested object structures
|
|
56
65
|
* within nested openapi objects
|
|
57
66
|
*/
|
|
58
|
-
constructor(bodySegment: OpenapiBodySegment, { renderOpts, target }: OpenapiBodySegmentRendererOpts);
|
|
67
|
+
constructor(bodySegment: OpenapiBodySegment, { renderOpts, target, source }: OpenapiBodySegmentRendererOpts);
|
|
59
68
|
/**
|
|
60
69
|
* returns the shorthanded body segment, rendered
|
|
61
70
|
* to the appropriate openapi shape
|
|
@@ -138,6 +147,28 @@ export default class OpenapiSegmentExpander {
|
|
|
138
147
|
* recursively a $ref statement
|
|
139
148
|
*/
|
|
140
149
|
private serializerStatement;
|
|
150
|
+
/**
|
|
151
|
+
* @internal
|
|
152
|
+
*
|
|
153
|
+
* Expand a nested `for:` request-body sentinel into a fully-resolved
|
|
154
|
+
* `OpenapiSchemaObject`. The sentinel takes the same shape as the
|
|
155
|
+
* top-level model-driven `requestBody`:
|
|
156
|
+
*
|
|
157
|
+
* ```ts
|
|
158
|
+
* { for: SomeModel, including?, only?, required?, combining? }
|
|
159
|
+
* ```
|
|
160
|
+
*
|
|
161
|
+
* and produces the same shape that the top-level model-driven
|
|
162
|
+
* `requestBody` produces — param-safe columns inferred from the model,
|
|
163
|
+
* with `combining` entries (which themselves may contain further `for:`
|
|
164
|
+
* sentinels) merged into the resulting properties.
|
|
165
|
+
*
|
|
166
|
+
* The `for:` sentinel is request-only at the type level. If it is
|
|
167
|
+
* encountered while expanding a response shape, this method throws —
|
|
168
|
+
* the type system should have prevented this, so reaching here indicates
|
|
169
|
+
* a misuse that bypassed type checking (e.g., via `as any`).
|
|
170
|
+
*/
|
|
171
|
+
private forStatement;
|
|
141
172
|
private serializableStatement;
|
|
142
173
|
/**
|
|
143
174
|
* @internal
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Dream } from '@rvoh/dream';
|
|
2
|
-
import { OpenapiAllTypes, OpenapiFormats, OpenapiSchemaArray, OpenapiSchemaBody, OpenapiSchemaBodyShorthand, OpenapiSchemaExpressionAllOf, OpenapiSchemaExpressionAnyOf, OpenapiSchemaExpressionOneOf, OpenapiSchemaExpressionRef, OpenapiSchemaObject, OpenapiSchemaProperties, OpenapiSchemaPropertiesShorthand } from '@rvoh/dream/openapi';
|
|
2
|
+
import { OpenapiAllTypes, OpenapiFormats, OpenapiSchemaArray, OpenapiSchemaBody, OpenapiSchemaBodyShorthand, OpenapiSchemaExpressionAllOf, OpenapiSchemaExpressionAnyOf, OpenapiSchemaExpressionOneOf, OpenapiSchemaExpressionRef, OpenapiSchemaObject, OpenapiSchemaProperties, OpenapiSchemaPropertiesShorthand, OpenapiShorthandPrimitiveTypes } from '@rvoh/dream/openapi';
|
|
3
3
|
import { DreamAttributes, DreamOrViewModelClassSerializerKey, DreamParamSafeAttributes, DreamSerializable, DreamSerializableArray, SerializerCasing, UpdateableProperties, ViewModelClass } from '@rvoh/dream/types';
|
|
4
4
|
import PsychicController from '../controller/index.js';
|
|
5
5
|
import { HttpStatusCode, HttpStatusCodeNumber } from '../error/http/status-codes.js';
|
|
@@ -453,7 +453,7 @@ export interface OpenapiEndpointRendererOpts<I extends DreamSerializable | Dream
|
|
|
453
453
|
* })
|
|
454
454
|
* ```
|
|
455
455
|
*/
|
|
456
|
-
requestBody?:
|
|
456
|
+
requestBody?: OpenapiSchemaRequestBodyShorthandWithFor | OpenapiSchemaRequestBodyForOption<I, ForOption> | null;
|
|
457
457
|
/**
|
|
458
458
|
* an array of tag names you wish to apply to this endpoint.
|
|
459
459
|
* tag names will determine placement of this request within
|
|
@@ -745,6 +745,80 @@ export interface OpenapiEndpointRendererDefaultResponseOption {
|
|
|
745
745
|
description?: string;
|
|
746
746
|
maybeNull?: boolean;
|
|
747
747
|
}
|
|
748
|
+
/**
|
|
749
|
+
* @internal
|
|
750
|
+
*
|
|
751
|
+
* Predecessor map used to decrement a depth counter inside recursive
|
|
752
|
+
* `$dream` shorthand types. Capped at 3 levels of `$dream`-aware
|
|
753
|
+
* recursion; beyond that the shape falls back to the unconstrained
|
|
754
|
+
* Dream-side `OpenapiSchemaPropertiesShorthand` and accepts but does
|
|
755
|
+
* not narrow further nesting.
|
|
756
|
+
*/
|
|
757
|
+
type DreamShorthandPrev = [never, 0, 1, 2, 3];
|
|
758
|
+
/**
|
|
759
|
+
* Nested model-derived request-body shorthand. Same shape as the
|
|
760
|
+
* top-level model-driven `requestBody` (with `for:` naming the model),
|
|
761
|
+
* but usable at any nesting site — inside `combining`, inside raw
|
|
762
|
+
* `properties` of a non-model body, and inside `items` of an array.
|
|
763
|
+
*
|
|
764
|
+
* Mirrors the response-side `$serializable` / `$serializer` keys
|
|
765
|
+
* recognized by `OpenapiSegmentExpander`, but expands via param-safe
|
|
766
|
+
* column derivation instead of serializer resolution.
|
|
767
|
+
*
|
|
768
|
+
* `for:` at this nesting position is request-only at the type level.
|
|
769
|
+
* Response shorthand types do not include it.
|
|
770
|
+
*
|
|
771
|
+
* ```ts
|
|
772
|
+
* @OpenAPI(ActionPlan, {
|
|
773
|
+
* requestBody: {
|
|
774
|
+
* including: ['type', 'clientId'],
|
|
775
|
+
* combining: {
|
|
776
|
+
* planItems: {
|
|
777
|
+
* type: 'array',
|
|
778
|
+
* items: { for: ActionItem, required: ['title', 'type'] },
|
|
779
|
+
* },
|
|
780
|
+
* },
|
|
781
|
+
* },
|
|
782
|
+
* })
|
|
783
|
+
* ```
|
|
784
|
+
*/
|
|
785
|
+
export type OpenapiSchemaNestedFor<Depth extends 0 | 1 | 2 | 3 = 3> = {
|
|
786
|
+
for: typeof Dream;
|
|
787
|
+
including?: readonly string[];
|
|
788
|
+
only?: readonly string[];
|
|
789
|
+
required?: readonly string[];
|
|
790
|
+
combining?: Depth extends 0 ? OpenapiSchemaPropertiesShorthand : OpenapiSchemaRequestPropertiesShorthandWithFor<DreamShorthandPrev[Depth] & (0 | 1 | 2 | 3)>;
|
|
791
|
+
};
|
|
792
|
+
/**
|
|
793
|
+
* Property-shorthand record (request-only) whose values may be any of
|
|
794
|
+
* the existing Dream-side body shorthands OR a nested `for:` sentinel.
|
|
795
|
+
* Used as the type of `combining` and as the value type of nested
|
|
796
|
+
* `properties` / `items` in request bodies.
|
|
797
|
+
*
|
|
798
|
+
* `for:`-aware recursion is depth-bounded (cap = 3); past the cap the
|
|
799
|
+
* value type falls back to the unconstrained Dream-side shorthand to
|
|
800
|
+
* avoid TypeScript "type instantiation is excessively deep" errors.
|
|
801
|
+
*/
|
|
802
|
+
export interface OpenapiSchemaRequestPropertiesShorthandWithFor<Depth extends 0 | 1 | 2 | 3 = 3> {
|
|
803
|
+
[key: string]: OpenapiSchemaRequestBodyShorthandWithFor<Depth> | OpenapiShorthandPrimitiveTypes;
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Body-shorthand value (request-only) — a Dream-side shorthand OR a
|
|
807
|
+
* nested `for:` sentinel OR an object/array shape whose nested values
|
|
808
|
+
* may themselves contain `for:`. Recurses through `items` and
|
|
809
|
+
* `properties` with a bounded depth.
|
|
810
|
+
*/
|
|
811
|
+
export type OpenapiSchemaRequestBodyShorthandWithFor<Depth extends 0 | 1 | 2 | 3 = 3> = OpenapiSchemaBodyShorthand | OpenapiSchemaNestedFor<Depth> | {
|
|
812
|
+
type: 'array' | readonly ['array', 'null'] | readonly ['null', 'array'];
|
|
813
|
+
items: OpenapiSchemaRequestBodyShorthandWithFor<Depth>;
|
|
814
|
+
description?: string;
|
|
815
|
+
} | {
|
|
816
|
+
type: 'object' | readonly ['object', 'null'] | readonly ['null', 'object'];
|
|
817
|
+
properties?: OpenapiSchemaRequestPropertiesShorthandWithFor<Depth>;
|
|
818
|
+
required?: readonly string[];
|
|
819
|
+
description?: string;
|
|
820
|
+
additionalProperties?: boolean | OpenapiSchemaRequestBodyShorthandWithFor<Depth>;
|
|
821
|
+
};
|
|
748
822
|
export type OpenapiSchemaRequestBodyForOption<Serializable extends DreamSerializable | DreamSerializableArray | undefined, ForOption extends typeof Dream | undefined = undefined> = OpenapiSchemaRequestBodyForDreamClass<ForOption extends typeof Dream ? ForOption : typeof Dream> | OpenapiSchemaRequestBodyForBaseDreamClass<Serializable>;
|
|
749
823
|
export interface OpenapiSchemaRequestBodyForDreamClass<ForOption extends typeof Dream> {
|
|
750
824
|
/**
|
|
@@ -818,7 +892,7 @@ export interface OpenapiSchemaRequestBodyForDreamClass<ForOption extends typeof
|
|
|
818
892
|
including?: Exclude<keyof UpdateableProperties<InstanceType<ForOption>>, keyof DreamParamSafeAttributes<InstanceType<ForOption>>>[];
|
|
819
893
|
/**
|
|
820
894
|
* expand the included fields to allow additional fields
|
|
821
|
-
* that are unrelated to the model's params
|
|
895
|
+
* that are unrelated to the model's params.
|
|
822
896
|
*
|
|
823
897
|
* ```ts
|
|
824
898
|
* @OpenAPI({
|
|
@@ -833,8 +907,35 @@ export interface OpenapiSchemaRequestBodyForDreamClass<ForOption extends typeof
|
|
|
833
907
|
* ...
|
|
834
908
|
* }
|
|
835
909
|
* ```
|
|
910
|
+
*
|
|
911
|
+
* `combining` values may also be a nested `for:` sentinel — the same
|
|
912
|
+
* shape as the top-level model-driven `requestBody`, just nested. It
|
|
913
|
+
* derives an object schema from another Dream model's param-safe
|
|
914
|
+
* columns and accepts the same `including` / `only` / `required` /
|
|
915
|
+
* `combining` siblings.
|
|
916
|
+
*
|
|
917
|
+
* ```ts
|
|
918
|
+
* @OpenAPI(ActionPlan, {
|
|
919
|
+
* requestBody: {
|
|
920
|
+
* including: ['type', 'clientId'],
|
|
921
|
+
* combining: {
|
|
922
|
+
* planItems: {
|
|
923
|
+
* type: 'array',
|
|
924
|
+
* items: {
|
|
925
|
+
* for: ActionItem,
|
|
926
|
+
* required: ['title', 'type'],
|
|
927
|
+
* },
|
|
928
|
+
* },
|
|
929
|
+
* },
|
|
930
|
+
* },
|
|
931
|
+
* })
|
|
932
|
+
* ```
|
|
933
|
+
*
|
|
934
|
+
* The nested `for:` sentinel is request-only; it is not accepted in
|
|
935
|
+
* `responses` shorthand (use the existing `$serializable` / `$serializer`
|
|
936
|
+
* keys there).
|
|
836
937
|
*/
|
|
837
|
-
combining?:
|
|
938
|
+
combining?: OpenapiSchemaRequestPropertiesShorthandWithFor;
|
|
838
939
|
/**
|
|
839
940
|
* Specify which fields are required for your openapi
|
|
840
941
|
* request body.
|
|
@@ -897,7 +998,7 @@ export interface OpenapiSchemaRequestBodyForBaseDreamClass<Serializable extends
|
|
|
897
998
|
including?: Including;
|
|
898
999
|
/**
|
|
899
1000
|
* expand the included fields to allow additional fields
|
|
900
|
-
* that are unrelated to the model's params
|
|
1001
|
+
* that are unrelated to the model's params.
|
|
901
1002
|
*
|
|
902
1003
|
* ```ts
|
|
903
1004
|
* @OpenAPI({
|
|
@@ -912,8 +1013,35 @@ export interface OpenapiSchemaRequestBodyForBaseDreamClass<Serializable extends
|
|
|
912
1013
|
* ...
|
|
913
1014
|
* }
|
|
914
1015
|
* ```
|
|
1016
|
+
*
|
|
1017
|
+
* `combining` values may also be a nested `for:` sentinel — the same
|
|
1018
|
+
* shape as the top-level model-driven `requestBody`, just nested. It
|
|
1019
|
+
* derives an object schema from another Dream model's param-safe
|
|
1020
|
+
* columns and accepts the same `including` / `only` / `required` /
|
|
1021
|
+
* `combining` siblings.
|
|
1022
|
+
*
|
|
1023
|
+
* ```ts
|
|
1024
|
+
* @OpenAPI(ActionPlan, {
|
|
1025
|
+
* requestBody: {
|
|
1026
|
+
* including: ['type', 'clientId'],
|
|
1027
|
+
* combining: {
|
|
1028
|
+
* planItems: {
|
|
1029
|
+
* type: 'array',
|
|
1030
|
+
* items: {
|
|
1031
|
+
* for: ActionItem,
|
|
1032
|
+
* required: ['title', 'type'],
|
|
1033
|
+
* },
|
|
1034
|
+
* },
|
|
1035
|
+
* },
|
|
1036
|
+
* },
|
|
1037
|
+
* })
|
|
1038
|
+
* ```
|
|
1039
|
+
*
|
|
1040
|
+
* The nested `for:` sentinel is request-only; it is not accepted in
|
|
1041
|
+
* `responses` shorthand (use the existing `$serializable` / `$serializer`
|
|
1042
|
+
* keys there).
|
|
915
1043
|
*/
|
|
916
|
-
combining?:
|
|
1044
|
+
combining?: OpenapiSchemaRequestPropertiesShorthandWithFor;
|
|
917
1045
|
/**
|
|
918
1046
|
* Specify which fields are required for your openapi
|
|
919
1047
|
* request body.
|
|
@@ -1051,3 +1179,4 @@ export interface ReferencedSerializersAndOpenapiContent {
|
|
|
1051
1179
|
referencedSerializers: SerializerArray;
|
|
1052
1180
|
openapi: OpenapiContent;
|
|
1053
1181
|
}
|
|
1182
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Dream } from '@rvoh/dream';
|
|
2
|
+
import { OpenapiSchemaObject } from '@rvoh/dream/openapi';
|
|
3
|
+
export interface BuildDreamRequestBodyShapeOpts {
|
|
4
|
+
only?: readonly string[] | undefined;
|
|
5
|
+
including?: readonly string[] | undefined;
|
|
6
|
+
required?: readonly string[] | undefined;
|
|
7
|
+
combining?: Record<string, unknown> | undefined;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* @internal
|
|
11
|
+
*
|
|
12
|
+
* Builds an unexpanded `OpenapiSchemaObject` for a Dream model's request-body shape,
|
|
13
|
+
* resolving param-safe columns, attaching `required`, and merging `combining` entries.
|
|
14
|
+
*
|
|
15
|
+
* Used by both the top-level model-driven `requestBody` path in `OpenapiEndpointRenderer`
|
|
16
|
+
* and the `$dream` sentinel expansion inside `OpenapiSegmentExpander`. Single source of
|
|
17
|
+
* truth keeps top-level and nested semantics identical.
|
|
18
|
+
*/
|
|
19
|
+
export default function buildDreamRequestBodyShape(dreamClass: typeof Dream, opts: BuildDreamRequestBodyShapeOpts, source: string): OpenapiSchemaObject;
|