@rvoh/psychic 3.7.0 → 3.8.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 +2 -2
- package/dist/cjs/src/generate/helpers/generateControllerContent.js +2 -2
- package/dist/cjs/src/openapi-renderer/body-segment.js +2 -1
- package/dist/cjs/src/openapi-renderer/endpoint.js +4 -1
- package/dist/cjs/src/openapi-renderer/helpers/buildDreamRequestBodyShape.js +5 -2
- package/dist/cjs/src/server/params.js +127 -10
- package/dist/esm/src/controller/decorators.js +2 -2
- package/dist/esm/src/generate/helpers/generateControllerContent.js +2 -2
- package/dist/esm/src/openapi-renderer/body-segment.js +2 -1
- package/dist/esm/src/openapi-renderer/endpoint.js +4 -1
- package/dist/esm/src/openapi-renderer/helpers/buildDreamRequestBodyShape.js +5 -2
- package/dist/esm/src/server/params.js +127 -10
- package/dist/types/src/controller/decorators.d.ts +7 -2
- package/dist/types/src/openapi-renderer/body-segment.d.ts +1 -1
- package/dist/types/src/openapi-renderer/endpoint.d.ts +29 -16
- package/dist/types/src/openapi-renderer/helpers/buildDreamRequestBodyShape.d.ts +1 -0
- package/dist/types/src/server/helpers/openapiParamNamesForDreamClass.d.ts +1 -1
- package/dist/types/src/server/helpers/paramNamesForDreamClass.d.ts +1 -1
- package/dist/types/src/server/params.d.ts +2 -0
- package/package.json +1 -1
|
@@ -78,9 +78,9 @@ export function OpenAPI(modelOrSerializer, _opts) {
|
|
|
78
78
|
(function (OpenAPI) {
|
|
79
79
|
/**
|
|
80
80
|
* Type-narrowed helper for nesting a Dream-model-driven shape inside a
|
|
81
|
-
* request body. Returns the same `{ for,
|
|
81
|
+
* request body. Returns the same `{ for, params, including, required,
|
|
82
82
|
* combining }` shape the renderer recognizes natively, but typed via a
|
|
83
|
-
* function-level generic so `
|
|
83
|
+
* function-level generic so `params` / `including` / `required` are
|
|
84
84
|
* constrained to columns of the model passed as the first argument.
|
|
85
85
|
*
|
|
86
86
|
* ```ts
|
|
@@ -51,7 +51,7 @@ export default function generateControllerContent({ ancestorName, ancestorImport
|
|
|
51
51
|
description: 'Create ${aOrAnDreamModelName(modelClassName)}',${defaultOpenapiSerializerKeyProperty}
|
|
52
52
|
fastJsonStringify: true,
|
|
53
53
|
requestBody: {
|
|
54
|
-
|
|
54
|
+
params: paramSafeColumns,
|
|
55
55
|
},
|
|
56
56
|
})
|
|
57
57
|
public async create() {
|
|
@@ -130,7 +130,7 @@ export default function generateControllerContent({ ancestorName, ancestorImport
|
|
|
130
130
|
description: 'Update ${aOrAnDreamModelName(modelClassName)}',
|
|
131
131
|
fastJsonStringify: true,
|
|
132
132
|
requestBody: {
|
|
133
|
-
|
|
133
|
+
params: paramSafeColumns,
|
|
134
134
|
},
|
|
135
135
|
})
|
|
136
136
|
public async update() {
|
|
@@ -21,7 +21,7 @@ import SerializerOpenapiRenderer from './SerializerOpenapiRenderer.js';
|
|
|
21
21
|
* - Nullable array shorthands like `['string[]', 'null']` → `{ type: ['array', 'null'], items: { type: 'string' } }`
|
|
22
22
|
* - Serializer references like `{ $serializer: SomeSerializer }` → `{ $ref: '#/components/schemas/SerializerOpenapiName' }`
|
|
23
23
|
* - Serializable references like `{ $serializable: SomeModel, key: 'summary' }` → resolved serializer reference
|
|
24
|
-
* - Nested model-derived request-body references like `{ for: SomeModel,
|
|
24
|
+
* - Nested model-derived request-body references like `{ for: SomeModel, params, including, required, combining }` → an inline object schema derived from the model's param-safe columns (request-only)
|
|
25
25
|
*
|
|
26
26
|
* The class recursively processes nested structures (objects, arrays, unions) and maintains
|
|
27
27
|
* a collection of referenced serializers that need to be included in the final OpenAPI document.
|
|
@@ -483,6 +483,7 @@ The following values will be allowed:
|
|
|
483
483
|
}
|
|
484
484
|
const ref = bodySegment;
|
|
485
485
|
const paramsShape = buildDreamRequestBodyShape(ref.for, {
|
|
486
|
+
params: ref.params,
|
|
486
487
|
only: ref.only,
|
|
487
488
|
including: ref.including,
|
|
488
489
|
required: ref.required,
|
|
@@ -492,6 +492,8 @@ export default class OpenapiEndpointRenderer {
|
|
|
492
492
|
const body = this.requestBody;
|
|
493
493
|
if (!body)
|
|
494
494
|
return true;
|
|
495
|
+
if (body.params)
|
|
496
|
+
return true;
|
|
495
497
|
if (body.only)
|
|
496
498
|
return true;
|
|
497
499
|
if (body.including)
|
|
@@ -518,10 +520,11 @@ export default class OpenapiEndpointRenderer {
|
|
|
518
520
|
const dreamClass = forDreamClass || this.getSingleDreamModelClass();
|
|
519
521
|
if (!dreamClass)
|
|
520
522
|
return this.defaultRequestBody();
|
|
521
|
-
const { only, including, required, combining } = (this.requestBody ||
|
|
523
|
+
const { params, only, including, required, combining } = (this.requestBody ||
|
|
522
524
|
{});
|
|
523
525
|
const source = this.controllerClass.controllerActionPath(this.action);
|
|
524
526
|
const paramsShape = buildDreamRequestBodyShape(dreamClass, {
|
|
527
|
+
params: params,
|
|
525
528
|
only: only,
|
|
526
529
|
including: including,
|
|
527
530
|
required: required,
|
|
@@ -11,8 +11,11 @@ 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 { only, including, required, combining } = opts;
|
|
15
|
-
const paramSafeColumns = openapiParamNamesForDreamClass(dreamClass, {
|
|
14
|
+
const { params, only, including, required, combining } = opts;
|
|
15
|
+
const paramSafeColumns = openapiParamNamesForDreamClass(dreamClass, {
|
|
16
|
+
only: params ?? only,
|
|
17
|
+
including,
|
|
18
|
+
});
|
|
16
19
|
const paramsShape = {
|
|
17
20
|
type: 'object',
|
|
18
21
|
properties: {},
|
|
@@ -53,7 +53,8 @@ export default class Params {
|
|
|
53
53
|
returnObj[columnName] = this.cast(params, columnName.toString(), castType, { allowNull: columnMetadata.allowNull });
|
|
54
54
|
}
|
|
55
55
|
else if (dreamClass.isVirtualColumn(columnName)) {
|
|
56
|
-
|
|
56
|
+
const virtualCast = virtualAttributeCast(dreamClass, columnName);
|
|
57
|
+
returnObj[columnName] = this.cast(params, columnName.toString(), virtualCast.expectedType, { allowNull: virtualCast.allowNull });
|
|
57
58
|
}
|
|
58
59
|
else if (columnMetadata?.enumValues) {
|
|
59
60
|
const paramValue = params[columnName];
|
|
@@ -167,9 +168,10 @@ export default class Params {
|
|
|
167
168
|
if (paramValue === null || paramValue === undefined) {
|
|
168
169
|
if (expectedType === 'null')
|
|
169
170
|
return null;
|
|
170
|
-
if (
|
|
171
|
-
this.
|
|
171
|
+
if (this.shouldUseOpenapiValidation(expectedType)) {
|
|
172
|
+
return this.validateOpenapiOrThrow(paramName, paramValue, expectedType);
|
|
172
173
|
}
|
|
174
|
+
this.throwUnlessAllowNull(paramName, paramValue, typeToError(expectedType), opts);
|
|
173
175
|
return paramValue;
|
|
174
176
|
}
|
|
175
177
|
const integerRegexp = /^-?\d+$/;
|
|
@@ -298,13 +300,7 @@ export default class Params {
|
|
|
298
300
|
return paramValue.map(param => this.cast(paramName, param, arrayTypeToNonArrayType(expectedType), { ...opts, allowNull: true }));
|
|
299
301
|
default:
|
|
300
302
|
if (this.shouldUseOpenapiValidation(expectedType)) {
|
|
301
|
-
|
|
302
|
-
if (res.isValid) {
|
|
303
|
-
return res.data;
|
|
304
|
-
}
|
|
305
|
-
else {
|
|
306
|
-
throw new ParamValidationError(paramName, res.errors?.map(err => err.message) || ['openapi validation failed']);
|
|
307
|
-
}
|
|
303
|
+
return this.validateOpenapiOrThrow(paramName, paramValue, expectedType);
|
|
308
304
|
}
|
|
309
305
|
// TODO: serialize/sanitize before printing, handle array types
|
|
310
306
|
throw new Error(`Unexpected point reached in code. need to handle type for ${expectedType}`);
|
|
@@ -313,6 +309,12 @@ export default class Params {
|
|
|
313
309
|
shouldUseOpenapiValidation(expectedType) {
|
|
314
310
|
return isObject(expectedType);
|
|
315
311
|
}
|
|
312
|
+
validateOpenapiOrThrow(paramName, paramValue, expectedType) {
|
|
313
|
+
const res = validateObject(paramValue, expectedType, { coerceTypes: true });
|
|
314
|
+
if (res.isValid)
|
|
315
|
+
return res.data;
|
|
316
|
+
throw new ParamValidationError(paramName, res.errors?.map(err => err.message) || ['openapi validation failed']);
|
|
317
|
+
}
|
|
316
318
|
restrict(allowed) {
|
|
317
319
|
const params = this.$params;
|
|
318
320
|
const permitted = {};
|
|
@@ -403,6 +405,121 @@ const DB_TYPE_TO_CAST_TYPE = {
|
|
|
403
405
|
numeric: 'number',
|
|
404
406
|
'numeric[]': 'number[]',
|
|
405
407
|
};
|
|
408
|
+
function virtualAttributeCast(dreamClass, columnName) {
|
|
409
|
+
const metadata = dreamClass['virtualAttributes'].find(statement => statement.property === columnName);
|
|
410
|
+
const primitiveCast = primitiveCastFromOpenapi(metadata?.type);
|
|
411
|
+
if (!primitiveCast)
|
|
412
|
+
throw new Error(`Unable to infer params cast type for virtual column: ${columnName}`);
|
|
413
|
+
return primitiveCast;
|
|
414
|
+
}
|
|
415
|
+
function primitiveCastFromOpenapi(openapi) {
|
|
416
|
+
if (!openapi)
|
|
417
|
+
return null;
|
|
418
|
+
if (Array.isArray(openapi)) {
|
|
419
|
+
const nonNullOpenapi = openapi.find(type => type !== 'null');
|
|
420
|
+
const primitiveCast = primitiveCastFromOpenapi(nonNullOpenapi);
|
|
421
|
+
if (!primitiveCast)
|
|
422
|
+
return null;
|
|
423
|
+
return { ...primitiveCast, allowNull: true };
|
|
424
|
+
}
|
|
425
|
+
if (typeof openapi === 'string') {
|
|
426
|
+
const expectedType = shorthandToCastType(openapi);
|
|
427
|
+
return expectedType ? { expectedType, allowNull: false } : null;
|
|
428
|
+
}
|
|
429
|
+
if (!('type' in openapi))
|
|
430
|
+
return null;
|
|
431
|
+
const openapiType = openapi.type;
|
|
432
|
+
const allowNull = Array.isArray(openapiType) && openapiType.includes('null');
|
|
433
|
+
const nonNullType = Array.isArray(openapiType) ? openapiType.find(type => type !== 'null') : openapiType;
|
|
434
|
+
if (nonNullType === 'array') {
|
|
435
|
+
if (!('items' in openapi))
|
|
436
|
+
return null;
|
|
437
|
+
const item = openapi.items;
|
|
438
|
+
const itemType = item && 'type' in item ? item.type : undefined;
|
|
439
|
+
const itemFormat = item && 'format' in item ? item.format : undefined;
|
|
440
|
+
if (Array.isArray(itemType))
|
|
441
|
+
return null;
|
|
442
|
+
const itemCastType = openapiTypeToCastType(itemType, itemFormat);
|
|
443
|
+
if (!itemCastType)
|
|
444
|
+
return null;
|
|
445
|
+
return { expectedType: `${itemCastType}[]`, allowNull };
|
|
446
|
+
}
|
|
447
|
+
const expectedType = openapiTypeToCastType(nonNullType, 'format' in openapi ? openapi.format : undefined);
|
|
448
|
+
return expectedType ? { expectedType, allowNull } : null;
|
|
449
|
+
}
|
|
450
|
+
function shorthandToCastType(openapi) {
|
|
451
|
+
switch (openapi) {
|
|
452
|
+
case 'date-time':
|
|
453
|
+
return 'datetime';
|
|
454
|
+
case 'date-time[]':
|
|
455
|
+
return 'datetime[]';
|
|
456
|
+
case 'decimal':
|
|
457
|
+
return 'number';
|
|
458
|
+
case 'decimal[]':
|
|
459
|
+
return 'number[]';
|
|
460
|
+
case 'json':
|
|
461
|
+
return 'json';
|
|
462
|
+
case 'null':
|
|
463
|
+
return 'null';
|
|
464
|
+
default:
|
|
465
|
+
return PARAM_CAST_TYPES.includes(openapi)
|
|
466
|
+
? openapi
|
|
467
|
+
: null;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function openapiTypeToCastType(openapiType, format) {
|
|
471
|
+
switch (openapiType) {
|
|
472
|
+
case 'boolean':
|
|
473
|
+
return 'boolean';
|
|
474
|
+
case 'integer':
|
|
475
|
+
return 'integer';
|
|
476
|
+
case 'number':
|
|
477
|
+
return 'number';
|
|
478
|
+
case 'null':
|
|
479
|
+
return 'null';
|
|
480
|
+
case 'object':
|
|
481
|
+
return 'json';
|
|
482
|
+
case 'string':
|
|
483
|
+
switch (format) {
|
|
484
|
+
case 'date':
|
|
485
|
+
return 'date';
|
|
486
|
+
case 'date-time':
|
|
487
|
+
return 'datetime';
|
|
488
|
+
case 'uuid':
|
|
489
|
+
return 'uuid';
|
|
490
|
+
default:
|
|
491
|
+
return 'string';
|
|
492
|
+
}
|
|
493
|
+
default:
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
const PARAM_CAST_TYPES = [
|
|
498
|
+
'bigint',
|
|
499
|
+
'bigint[]',
|
|
500
|
+
'boolean',
|
|
501
|
+
'boolean[]',
|
|
502
|
+
'date',
|
|
503
|
+
'date[]',
|
|
504
|
+
'datetime',
|
|
505
|
+
'datetime[]',
|
|
506
|
+
'integer',
|
|
507
|
+
'integer[]',
|
|
508
|
+
'json',
|
|
509
|
+
'json[]',
|
|
510
|
+
'null',
|
|
511
|
+
'null[]',
|
|
512
|
+
'number',
|
|
513
|
+
'number[]',
|
|
514
|
+
'string',
|
|
515
|
+
'string[]',
|
|
516
|
+
'time',
|
|
517
|
+
'time[]',
|
|
518
|
+
'timetz',
|
|
519
|
+
'timetz[]',
|
|
520
|
+
'uuid',
|
|
521
|
+
'uuid[]',
|
|
522
|
+
];
|
|
406
523
|
const typeToErrorMap = {
|
|
407
524
|
bigint: 'expected bigint',
|
|
408
525
|
boolean: 'expected boolean',
|
|
@@ -78,9 +78,9 @@ export function OpenAPI(modelOrSerializer, _opts) {
|
|
|
78
78
|
(function (OpenAPI) {
|
|
79
79
|
/**
|
|
80
80
|
* Type-narrowed helper for nesting a Dream-model-driven shape inside a
|
|
81
|
-
* request body. Returns the same `{ for,
|
|
81
|
+
* request body. Returns the same `{ for, params, including, required,
|
|
82
82
|
* combining }` shape the renderer recognizes natively, but typed via a
|
|
83
|
-
* function-level generic so `
|
|
83
|
+
* function-level generic so `params` / `including` / `required` are
|
|
84
84
|
* constrained to columns of the model passed as the first argument.
|
|
85
85
|
*
|
|
86
86
|
* ```ts
|
|
@@ -51,7 +51,7 @@ export default function generateControllerContent({ ancestorName, ancestorImport
|
|
|
51
51
|
description: 'Create ${aOrAnDreamModelName(modelClassName)}',${defaultOpenapiSerializerKeyProperty}
|
|
52
52
|
fastJsonStringify: true,
|
|
53
53
|
requestBody: {
|
|
54
|
-
|
|
54
|
+
params: paramSafeColumns,
|
|
55
55
|
},
|
|
56
56
|
})
|
|
57
57
|
public async create() {
|
|
@@ -130,7 +130,7 @@ export default function generateControllerContent({ ancestorName, ancestorImport
|
|
|
130
130
|
description: 'Update ${aOrAnDreamModelName(modelClassName)}',
|
|
131
131
|
fastJsonStringify: true,
|
|
132
132
|
requestBody: {
|
|
133
|
-
|
|
133
|
+
params: paramSafeColumns,
|
|
134
134
|
},
|
|
135
135
|
})
|
|
136
136
|
public async update() {
|
|
@@ -21,7 +21,7 @@ import SerializerOpenapiRenderer from './SerializerOpenapiRenderer.js';
|
|
|
21
21
|
* - Nullable array shorthands like `['string[]', 'null']` → `{ type: ['array', 'null'], items: { type: 'string' } }`
|
|
22
22
|
* - Serializer references like `{ $serializer: SomeSerializer }` → `{ $ref: '#/components/schemas/SerializerOpenapiName' }`
|
|
23
23
|
* - Serializable references like `{ $serializable: SomeModel, key: 'summary' }` → resolved serializer reference
|
|
24
|
-
* - Nested model-derived request-body references like `{ for: SomeModel,
|
|
24
|
+
* - Nested model-derived request-body references like `{ for: SomeModel, params, including, required, combining }` → an inline object schema derived from the model's param-safe columns (request-only)
|
|
25
25
|
*
|
|
26
26
|
* The class recursively processes nested structures (objects, arrays, unions) and maintains
|
|
27
27
|
* a collection of referenced serializers that need to be included in the final OpenAPI document.
|
|
@@ -483,6 +483,7 @@ The following values will be allowed:
|
|
|
483
483
|
}
|
|
484
484
|
const ref = bodySegment;
|
|
485
485
|
const paramsShape = buildDreamRequestBodyShape(ref.for, {
|
|
486
|
+
params: ref.params,
|
|
486
487
|
only: ref.only,
|
|
487
488
|
including: ref.including,
|
|
488
489
|
required: ref.required,
|
|
@@ -492,6 +492,8 @@ export default class OpenapiEndpointRenderer {
|
|
|
492
492
|
const body = this.requestBody;
|
|
493
493
|
if (!body)
|
|
494
494
|
return true;
|
|
495
|
+
if (body.params)
|
|
496
|
+
return true;
|
|
495
497
|
if (body.only)
|
|
496
498
|
return true;
|
|
497
499
|
if (body.including)
|
|
@@ -518,10 +520,11 @@ export default class OpenapiEndpointRenderer {
|
|
|
518
520
|
const dreamClass = forDreamClass || this.getSingleDreamModelClass();
|
|
519
521
|
if (!dreamClass)
|
|
520
522
|
return this.defaultRequestBody();
|
|
521
|
-
const { only, including, required, combining } = (this.requestBody ||
|
|
523
|
+
const { params, only, including, required, combining } = (this.requestBody ||
|
|
522
524
|
{});
|
|
523
525
|
const source = this.controllerClass.controllerActionPath(this.action);
|
|
524
526
|
const paramsShape = buildDreamRequestBodyShape(dreamClass, {
|
|
527
|
+
params: params,
|
|
525
528
|
only: only,
|
|
526
529
|
including: including,
|
|
527
530
|
required: required,
|
|
@@ -11,8 +11,11 @@ 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 { only, including, required, combining } = opts;
|
|
15
|
-
const paramSafeColumns = openapiParamNamesForDreamClass(dreamClass, {
|
|
14
|
+
const { params, only, including, required, combining } = opts;
|
|
15
|
+
const paramSafeColumns = openapiParamNamesForDreamClass(dreamClass, {
|
|
16
|
+
only: params ?? only,
|
|
17
|
+
including,
|
|
18
|
+
});
|
|
16
19
|
const paramsShape = {
|
|
17
20
|
type: 'object',
|
|
18
21
|
properties: {},
|
|
@@ -53,7 +53,8 @@ export default class Params {
|
|
|
53
53
|
returnObj[columnName] = this.cast(params, columnName.toString(), castType, { allowNull: columnMetadata.allowNull });
|
|
54
54
|
}
|
|
55
55
|
else if (dreamClass.isVirtualColumn(columnName)) {
|
|
56
|
-
|
|
56
|
+
const virtualCast = virtualAttributeCast(dreamClass, columnName);
|
|
57
|
+
returnObj[columnName] = this.cast(params, columnName.toString(), virtualCast.expectedType, { allowNull: virtualCast.allowNull });
|
|
57
58
|
}
|
|
58
59
|
else if (columnMetadata?.enumValues) {
|
|
59
60
|
const paramValue = params[columnName];
|
|
@@ -167,9 +168,10 @@ export default class Params {
|
|
|
167
168
|
if (paramValue === null || paramValue === undefined) {
|
|
168
169
|
if (expectedType === 'null')
|
|
169
170
|
return null;
|
|
170
|
-
if (
|
|
171
|
-
this.
|
|
171
|
+
if (this.shouldUseOpenapiValidation(expectedType)) {
|
|
172
|
+
return this.validateOpenapiOrThrow(paramName, paramValue, expectedType);
|
|
172
173
|
}
|
|
174
|
+
this.throwUnlessAllowNull(paramName, paramValue, typeToError(expectedType), opts);
|
|
173
175
|
return paramValue;
|
|
174
176
|
}
|
|
175
177
|
const integerRegexp = /^-?\d+$/;
|
|
@@ -298,13 +300,7 @@ export default class Params {
|
|
|
298
300
|
return paramValue.map(param => this.cast(paramName, param, arrayTypeToNonArrayType(expectedType), { ...opts, allowNull: true }));
|
|
299
301
|
default:
|
|
300
302
|
if (this.shouldUseOpenapiValidation(expectedType)) {
|
|
301
|
-
|
|
302
|
-
if (res.isValid) {
|
|
303
|
-
return res.data;
|
|
304
|
-
}
|
|
305
|
-
else {
|
|
306
|
-
throw new ParamValidationError(paramName, res.errors?.map(err => err.message) || ['openapi validation failed']);
|
|
307
|
-
}
|
|
303
|
+
return this.validateOpenapiOrThrow(paramName, paramValue, expectedType);
|
|
308
304
|
}
|
|
309
305
|
// TODO: serialize/sanitize before printing, handle array types
|
|
310
306
|
throw new Error(`Unexpected point reached in code. need to handle type for ${expectedType}`);
|
|
@@ -313,6 +309,12 @@ export default class Params {
|
|
|
313
309
|
shouldUseOpenapiValidation(expectedType) {
|
|
314
310
|
return isObject(expectedType);
|
|
315
311
|
}
|
|
312
|
+
validateOpenapiOrThrow(paramName, paramValue, expectedType) {
|
|
313
|
+
const res = validateObject(paramValue, expectedType, { coerceTypes: true });
|
|
314
|
+
if (res.isValid)
|
|
315
|
+
return res.data;
|
|
316
|
+
throw new ParamValidationError(paramName, res.errors?.map(err => err.message) || ['openapi validation failed']);
|
|
317
|
+
}
|
|
316
318
|
restrict(allowed) {
|
|
317
319
|
const params = this.$params;
|
|
318
320
|
const permitted = {};
|
|
@@ -403,6 +405,121 @@ const DB_TYPE_TO_CAST_TYPE = {
|
|
|
403
405
|
numeric: 'number',
|
|
404
406
|
'numeric[]': 'number[]',
|
|
405
407
|
};
|
|
408
|
+
function virtualAttributeCast(dreamClass, columnName) {
|
|
409
|
+
const metadata = dreamClass['virtualAttributes'].find(statement => statement.property === columnName);
|
|
410
|
+
const primitiveCast = primitiveCastFromOpenapi(metadata?.type);
|
|
411
|
+
if (!primitiveCast)
|
|
412
|
+
throw new Error(`Unable to infer params cast type for virtual column: ${columnName}`);
|
|
413
|
+
return primitiveCast;
|
|
414
|
+
}
|
|
415
|
+
function primitiveCastFromOpenapi(openapi) {
|
|
416
|
+
if (!openapi)
|
|
417
|
+
return null;
|
|
418
|
+
if (Array.isArray(openapi)) {
|
|
419
|
+
const nonNullOpenapi = openapi.find(type => type !== 'null');
|
|
420
|
+
const primitiveCast = primitiveCastFromOpenapi(nonNullOpenapi);
|
|
421
|
+
if (!primitiveCast)
|
|
422
|
+
return null;
|
|
423
|
+
return { ...primitiveCast, allowNull: true };
|
|
424
|
+
}
|
|
425
|
+
if (typeof openapi === 'string') {
|
|
426
|
+
const expectedType = shorthandToCastType(openapi);
|
|
427
|
+
return expectedType ? { expectedType, allowNull: false } : null;
|
|
428
|
+
}
|
|
429
|
+
if (!('type' in openapi))
|
|
430
|
+
return null;
|
|
431
|
+
const openapiType = openapi.type;
|
|
432
|
+
const allowNull = Array.isArray(openapiType) && openapiType.includes('null');
|
|
433
|
+
const nonNullType = Array.isArray(openapiType) ? openapiType.find(type => type !== 'null') : openapiType;
|
|
434
|
+
if (nonNullType === 'array') {
|
|
435
|
+
if (!('items' in openapi))
|
|
436
|
+
return null;
|
|
437
|
+
const item = openapi.items;
|
|
438
|
+
const itemType = item && 'type' in item ? item.type : undefined;
|
|
439
|
+
const itemFormat = item && 'format' in item ? item.format : undefined;
|
|
440
|
+
if (Array.isArray(itemType))
|
|
441
|
+
return null;
|
|
442
|
+
const itemCastType = openapiTypeToCastType(itemType, itemFormat);
|
|
443
|
+
if (!itemCastType)
|
|
444
|
+
return null;
|
|
445
|
+
return { expectedType: `${itemCastType}[]`, allowNull };
|
|
446
|
+
}
|
|
447
|
+
const expectedType = openapiTypeToCastType(nonNullType, 'format' in openapi ? openapi.format : undefined);
|
|
448
|
+
return expectedType ? { expectedType, allowNull } : null;
|
|
449
|
+
}
|
|
450
|
+
function shorthandToCastType(openapi) {
|
|
451
|
+
switch (openapi) {
|
|
452
|
+
case 'date-time':
|
|
453
|
+
return 'datetime';
|
|
454
|
+
case 'date-time[]':
|
|
455
|
+
return 'datetime[]';
|
|
456
|
+
case 'decimal':
|
|
457
|
+
return 'number';
|
|
458
|
+
case 'decimal[]':
|
|
459
|
+
return 'number[]';
|
|
460
|
+
case 'json':
|
|
461
|
+
return 'json';
|
|
462
|
+
case 'null':
|
|
463
|
+
return 'null';
|
|
464
|
+
default:
|
|
465
|
+
return PARAM_CAST_TYPES.includes(openapi)
|
|
466
|
+
? openapi
|
|
467
|
+
: null;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function openapiTypeToCastType(openapiType, format) {
|
|
471
|
+
switch (openapiType) {
|
|
472
|
+
case 'boolean':
|
|
473
|
+
return 'boolean';
|
|
474
|
+
case 'integer':
|
|
475
|
+
return 'integer';
|
|
476
|
+
case 'number':
|
|
477
|
+
return 'number';
|
|
478
|
+
case 'null':
|
|
479
|
+
return 'null';
|
|
480
|
+
case 'object':
|
|
481
|
+
return 'json';
|
|
482
|
+
case 'string':
|
|
483
|
+
switch (format) {
|
|
484
|
+
case 'date':
|
|
485
|
+
return 'date';
|
|
486
|
+
case 'date-time':
|
|
487
|
+
return 'datetime';
|
|
488
|
+
case 'uuid':
|
|
489
|
+
return 'uuid';
|
|
490
|
+
default:
|
|
491
|
+
return 'string';
|
|
492
|
+
}
|
|
493
|
+
default:
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
const PARAM_CAST_TYPES = [
|
|
498
|
+
'bigint',
|
|
499
|
+
'bigint[]',
|
|
500
|
+
'boolean',
|
|
501
|
+
'boolean[]',
|
|
502
|
+
'date',
|
|
503
|
+
'date[]',
|
|
504
|
+
'datetime',
|
|
505
|
+
'datetime[]',
|
|
506
|
+
'integer',
|
|
507
|
+
'integer[]',
|
|
508
|
+
'json',
|
|
509
|
+
'json[]',
|
|
510
|
+
'null',
|
|
511
|
+
'null[]',
|
|
512
|
+
'number',
|
|
513
|
+
'number[]',
|
|
514
|
+
'string',
|
|
515
|
+
'string[]',
|
|
516
|
+
'time',
|
|
517
|
+
'time[]',
|
|
518
|
+
'timetz',
|
|
519
|
+
'timetz[]',
|
|
520
|
+
'uuid',
|
|
521
|
+
'uuid[]',
|
|
522
|
+
];
|
|
406
523
|
const typeToErrorMap = {
|
|
407
524
|
bigint: 'expected bigint',
|
|
408
525
|
boolean: 'expected boolean',
|
|
@@ -12,9 +12,9 @@ export declare function OpenAPI<const I extends DreamSerializable | DreamSeriali
|
|
|
12
12
|
export declare namespace OpenAPI {
|
|
13
13
|
/**
|
|
14
14
|
* Type-narrowed helper for nesting a Dream-model-driven shape inside a
|
|
15
|
-
* request body. Returns the same `{ for,
|
|
15
|
+
* request body. Returns the same `{ for, params, including, required,
|
|
16
16
|
* combining }` shape the renderer recognizes natively, but typed via a
|
|
17
|
-
* function-level generic so `
|
|
17
|
+
* function-level generic so `params` / `including` / `required` are
|
|
18
18
|
* constrained to columns of the model passed as the first argument.
|
|
19
19
|
*
|
|
20
20
|
* ```ts
|
|
@@ -41,6 +41,11 @@ export declare namespace OpenAPI {
|
|
|
41
41
|
* ```
|
|
42
42
|
*/
|
|
43
43
|
function forDream<const M extends typeof Dream>(model: M, opts?: {
|
|
44
|
+
params?: readonly (keyof DreamParamSafeAttributes<InstanceType<M>>)[];
|
|
45
|
+
/**
|
|
46
|
+
* @deprecated Use `params` instead. `only` remains as a compatibility alias
|
|
47
|
+
* for apps written before `extractParams` made request params explicit.
|
|
48
|
+
*/
|
|
44
49
|
only?: readonly (keyof DreamParamSafeAttributes<InstanceType<M>>)[];
|
|
45
50
|
including?: readonly Exclude<keyof UpdateableProperties<InstanceType<M>>, keyof DreamParamSafeAttributes<InstanceType<M>>>[];
|
|
46
51
|
required?: readonly (keyof UpdateableProperties<InstanceType<M>>)[];
|
|
@@ -24,7 +24,7 @@ export interface OpenapiBodySegmentRendererOpts {
|
|
|
24
24
|
* - Nullable array shorthands like `['string[]', 'null']` → `{ type: ['array', 'null'], items: { type: 'string' } }`
|
|
25
25
|
* - Serializer references like `{ $serializer: SomeSerializer }` → `{ $ref: '#/components/schemas/SerializerOpenapiName' }`
|
|
26
26
|
* - Serializable references like `{ $serializable: SomeModel, key: 'summary' }` → resolved serializer reference
|
|
27
|
-
* - Nested model-derived request-body references like `{ for: SomeModel,
|
|
27
|
+
* - Nested model-derived request-body references like `{ for: SomeModel, params, including, required, combining }` → an inline object schema derived from the model's param-safe columns (request-only)
|
|
28
28
|
*
|
|
29
29
|
* The class recursively processes nested structures (objects, arrays, unions) and maintains
|
|
30
30
|
* a collection of referenced serializers that need to be included in the final OpenAPI document.
|
|
@@ -784,8 +784,13 @@ type DreamShorthandPrev = [never, 0, 1, 2, 3];
|
|
|
784
784
|
*/
|
|
785
785
|
export type OpenapiSchemaNestedFor<Depth extends 0 | 1 | 2 | 3 = 3> = {
|
|
786
786
|
for: typeof Dream;
|
|
787
|
-
|
|
787
|
+
params?: readonly string[];
|
|
788
|
+
/**
|
|
789
|
+
* @deprecated Use `params` instead. `only` remains as a compatibility alias
|
|
790
|
+
* for apps written before `extractParams` made request params explicit.
|
|
791
|
+
*/
|
|
788
792
|
only?: readonly string[];
|
|
793
|
+
including?: readonly string[];
|
|
789
794
|
required?: readonly string[];
|
|
790
795
|
combining?: Depth extends 0 ? OpenapiSchemaPropertiesShorthand : OpenapiSchemaRequestPropertiesShorthandWithFor<DreamShorthandPrev[Depth] & (0 | 1 | 2 | 3)>;
|
|
791
796
|
};
|
|
@@ -837,15 +842,14 @@ export interface OpenapiSchemaRequestBodyForDreamClass<ForOption extends typeof
|
|
|
837
842
|
*/
|
|
838
843
|
for: ForOption;
|
|
839
844
|
/**
|
|
840
|
-
*
|
|
841
|
-
*
|
|
842
|
-
* will be derived from
|
|
843
|
-
*
|
|
844
|
-
* the OpenAPI decorator, provided it is a dream model.
|
|
845
|
+
* Explicitly list which request params to include from the model. If a `for`
|
|
846
|
+
* option is provided, the params will be derived from that model class.
|
|
847
|
+
* Otherwise, they will be derived from the first argument passed to the
|
|
848
|
+
* OpenAPI decorator, provided it is a Dream model.
|
|
845
849
|
*
|
|
846
850
|
* ```ts
|
|
847
851
|
* @OpenAPI({
|
|
848
|
-
* requestBody: { for: Pet,
|
|
852
|
+
* requestBody: { for: Pet, params: ['species', 'name'] }
|
|
849
853
|
* })
|
|
850
854
|
* public create() {
|
|
851
855
|
* ...
|
|
@@ -856,13 +860,18 @@ export interface OpenapiSchemaRequestBodyForDreamClass<ForOption extends typeof
|
|
|
856
860
|
*
|
|
857
861
|
* ```ts
|
|
858
862
|
* @OpenAPI(Pet, {
|
|
859
|
-
* requestBody: {
|
|
863
|
+
* requestBody: { params: ['species', 'name'] }
|
|
860
864
|
* })
|
|
861
865
|
* public create() {
|
|
862
866
|
* ...
|
|
863
867
|
* }
|
|
864
868
|
* ```
|
|
865
869
|
*/
|
|
870
|
+
params?: (keyof DreamParamSafeAttributes<InstanceType<ForOption>>)[];
|
|
871
|
+
/**
|
|
872
|
+
* @deprecated Use `params` instead. `only` remains as a compatibility alias
|
|
873
|
+
* for apps written before `extractParams` made request params explicit.
|
|
874
|
+
*/
|
|
866
875
|
only?: (keyof DreamParamSafeAttributes<InstanceType<ForOption>>)[];
|
|
867
876
|
/**
|
|
868
877
|
* expand the included fields to allow fields that
|
|
@@ -911,7 +920,7 @@ export interface OpenapiSchemaRequestBodyForDreamClass<ForOption extends typeof
|
|
|
911
920
|
* `combining` values may also be a nested `for:` sentinel — the same
|
|
912
921
|
* shape as the top-level model-driven `requestBody`, just nested. It
|
|
913
922
|
* derives an object schema from another Dream model's param-safe
|
|
914
|
-
* columns and accepts the same `
|
|
923
|
+
* columns and accepts the same `params` / `including` / `required` /
|
|
915
924
|
* `combining` siblings.
|
|
916
925
|
*
|
|
917
926
|
* ```ts
|
|
@@ -965,21 +974,25 @@ export interface OpenapiSchemaRequestBodyForDreamClass<ForOption extends typeof
|
|
|
965
974
|
export interface OpenapiSchemaRequestBodyForBaseDreamClass<Serializable extends DreamSerializable | DreamSerializableArray | undefined, T extends Serializable extends typeof Dream ? Serializable : undefined = Serializable extends typeof Dream ? Serializable : undefined, I extends T extends typeof Dream ? InstanceType<T> : undefined = T extends typeof Dream ? InstanceType<T> : undefined, Only extends I extends Dream ? readonly (keyof DreamParamSafeAttributes<I>)[] : string[] = I extends Dream ? readonly (keyof DreamParamSafeAttributes<I>)[] : string[], Including extends I extends Dream ? Exclude<keyof DreamAttributes<I>, keyof DreamParamSafeAttributes<I>>[] : string[] = I extends Dream ? Exclude<keyof DreamAttributes<I>, keyof DreamParamSafeAttributes<I>>[] : string[], RequiredFields extends I extends Dream ? readonly (keyof DreamAttributes<I>)[] : string[] = I extends Dream ? readonly (keyof DreamAttributes<I>)[] : string[]> {
|
|
966
975
|
for?: never;
|
|
967
976
|
/**
|
|
968
|
-
*
|
|
969
|
-
*
|
|
970
|
-
* will be derived from
|
|
971
|
-
*
|
|
972
|
-
* the OpenAPI decorator, provided it is a dream model.
|
|
977
|
+
* Explicitly list which request params to include from the model. If a `for`
|
|
978
|
+
* option is provided, the params will be derived from that model class.
|
|
979
|
+
* Otherwise, they will be derived from the first argument passed to the
|
|
980
|
+
* OpenAPI decorator, provided it is a Dream model.
|
|
973
981
|
*
|
|
974
982
|
* ```ts
|
|
975
983
|
* @OpenAPI(Pet, {
|
|
976
|
-
* requestBody: {
|
|
984
|
+
* requestBody: { params: ['species', 'name'] }
|
|
977
985
|
* })
|
|
978
986
|
* public create() {
|
|
979
987
|
* ...
|
|
980
988
|
* }
|
|
981
989
|
* ```
|
|
982
990
|
*/
|
|
991
|
+
params?: Only;
|
|
992
|
+
/**
|
|
993
|
+
* @deprecated Use `params` instead. `only` remains as a compatibility alias
|
|
994
|
+
* for apps written before `extractParams` made request params explicit.
|
|
995
|
+
*/
|
|
983
996
|
only?: Only;
|
|
984
997
|
/**
|
|
985
998
|
* expand the included fields to allow fields that
|
|
@@ -1017,7 +1030,7 @@ export interface OpenapiSchemaRequestBodyForBaseDreamClass<Serializable extends
|
|
|
1017
1030
|
* `combining` values may also be a nested `for:` sentinel — the same
|
|
1018
1031
|
* shape as the top-level model-driven `requestBody`, just nested. It
|
|
1019
1032
|
* derives an object schema from another Dream model's param-safe
|
|
1020
|
-
* columns and accepts the same `
|
|
1033
|
+
* columns and accepts the same `params` / `including` / `required` /
|
|
1021
1034
|
* `combining` siblings.
|
|
1022
1035
|
*
|
|
1023
1036
|
* ```ts
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Dream } from '@rvoh/dream';
|
|
2
2
|
import { OpenapiSchemaObject } from '@rvoh/dream/openapi';
|
|
3
3
|
export interface BuildDreamRequestBodyShapeOpts {
|
|
4
|
+
params?: readonly string[] | undefined;
|
|
4
5
|
only?: readonly string[] | undefined;
|
|
5
6
|
including?: readonly string[] | undefined;
|
|
6
7
|
required?: readonly string[] | undefined;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Dream } from '@rvoh/dream';
|
|
2
2
|
import { DreamParamSafeAttributes, DreamParamSafeColumnNames, StrictInterface, UpdateableProperties } from '@rvoh/dream/types';
|
|
3
|
-
import { OpenAPIDreamModelRequestBodyModifications } from '../params.js';
|
|
3
|
+
import type { OpenAPIDreamModelRequestBodyModifications } from '../params.js';
|
|
4
4
|
export default function openapiParamNamesForDreamClass<T extends typeof Dream, I extends InstanceType<T>, const OnlyArray extends readonly (keyof DreamParamSafeAttributes<I>)[], const IncludingArray extends Exclude<keyof UpdateableProperties<I>, OnlyArray[number]>[], ForOpts extends StrictInterface<ForOpts, OpenAPIDreamModelRequestBodyModifications<OnlyArray, IncludingArray>>, ParamSafeColumnsOverride extends I['paramSafeColumns' & keyof I] extends never ? undefined : I['paramSafeColumns' & keyof I] & string[], ParamSafeColumns extends ParamSafeColumnsOverride extends string[] | Readonly<string[]> ? Extract<DreamParamSafeColumnNames<I>, ParamSafeColumnsOverride[number] & DreamParamSafeColumnNames<I>>[] : DreamParamSafeColumnNames<I>[], ParamSafeAttrs extends DreamParamSafeAttributes<InstanceType<T>>, ReturnPartialType extends ForOpts['only'] extends readonly (keyof DreamParamSafeAttributes<InstanceType<T>>)[] ? Partial<{
|
|
5
5
|
[K in ForOpts['only'][number] & keyof ParamSafeAttrs]: ParamSafeAttrs[K & keyof ParamSafeAttrs];
|
|
6
6
|
}> : Partial<{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Dream } from '@rvoh/dream';
|
|
2
2
|
import { DreamParamSafeAttributes, DreamParamSafeColumnNames, StrictInterface } from '@rvoh/dream/types';
|
|
3
|
-
import { ParamsForOpts } from '../params.js';
|
|
3
|
+
import type { ParamsForOpts } from '../params.js';
|
|
4
4
|
export default function paramNamesForDreamClass<T extends typeof Dream, I extends InstanceType<T>, const OnlyArray extends readonly (keyof DreamParamSafeAttributes<I>)[], ForOpts extends StrictInterface<ForOpts, ParamsForOpts<OnlyArray>>, ParamSafeColumnsOverride extends I['paramSafeColumns' & keyof I] extends never ? undefined : I['paramSafeColumns' & keyof I] & string[], ParamSafeColumns extends ParamSafeColumnsOverride extends string[] | Readonly<string[]> ? Extract<DreamParamSafeColumnNames<I>, ParamSafeColumnsOverride[number] & DreamParamSafeColumnNames<I>>[] : DreamParamSafeColumnNames<I>[], ParamSafeAttrs extends DreamParamSafeAttributes<InstanceType<T>>, ReturnPartialType extends ForOpts['only'] extends readonly (keyof DreamParamSafeAttributes<InstanceType<T>>)[] ? Partial<{
|
|
5
5
|
[K in ForOpts['only'][number] & keyof ParamSafeAttrs]: ParamSafeAttrs[K & keyof ParamSafeAttrs];
|
|
6
6
|
}> : Partial<{
|
|
@@ -74,6 +74,7 @@ export default class Params {
|
|
|
74
74
|
casing(casing: 'snake' | 'camel'): this;
|
|
75
75
|
cast<EnumType extends readonly string[], OptsType extends StrictInterface<OptsType, ParamsCastOptions<EnumType>>, ExpectedType extends PsychicParamsPrimitiveLiteral | RegExp | OpenapiSchemaBody, ValidatedType extends ValidatedReturnType<ExpectedType, OptsType>, AllowNullOrUndefined extends ValidatedAllowsNull<ExpectedType, OptsType>, ReturnType extends AllowNullOrUndefined extends true ? ValidatedType | null | undefined : ValidatedType>(paramName: string, paramValue: PsychicParamsPrimitive | PsychicParamsDictionary | PsychicParamsDictionary[], expectedType: ExpectedType, opts?: OptsType): AllowNullOrUndefined extends true ? ValidatedType | null | undefined : ValidatedType;
|
|
76
76
|
private shouldUseOpenapiValidation;
|
|
77
|
+
private validateOpenapiOrThrow;
|
|
77
78
|
restrict(allowed: string[]): PsychicParamsDictionary;
|
|
78
79
|
private matchRegexOrThrow;
|
|
79
80
|
private throwUnlessAllowNull;
|
|
@@ -130,6 +131,7 @@ export interface ExtractParamsOpts {
|
|
|
130
131
|
key?: string;
|
|
131
132
|
}
|
|
132
133
|
export interface OpenAPIDreamModelRequestBodyModifications<OnlyArray, IncludingArray> extends ParamsForOptsBase<OnlyArray> {
|
|
134
|
+
params?: OnlyArray;
|
|
133
135
|
combining?: OpenapiSchemaPropertiesShorthand;
|
|
134
136
|
including?: IncludingArray;
|
|
135
137
|
}
|