@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.
@@ -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, only, including, required,
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 `only` / `including` / `required` are
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
- only: paramSafeColumns,
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
- only: paramSafeColumns,
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, including, only, required, combining }` → an inline object schema derived from the model's param-safe columns (request-only)
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, { only, including });
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
- returnObj[columnName] = params[columnName];
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 (!this.shouldUseOpenapiValidation(expectedType)) {
171
- this.throwUnlessAllowNull(paramName, paramValue, typeToError(expectedType), opts);
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
- const res = validateObject(paramValue, expectedType, { coerceTypes: true });
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, only, including, required,
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 `only` / `including` / `required` are
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
- only: paramSafeColumns,
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
- only: paramSafeColumns,
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, including, only, required, combining }` → an inline object schema derived from the model's param-safe columns (request-only)
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, { only, including });
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
- returnObj[columnName] = params[columnName];
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 (!this.shouldUseOpenapiValidation(expectedType)) {
171
- this.throwUnlessAllowNull(paramName, paramValue, typeToError(expectedType), opts);
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
- const res = validateObject(paramValue, expectedType, { coerceTypes: true });
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, only, including, required,
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 `only` / `including` / `required` are
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, including, only, required, combining }` → an inline object schema derived from the model's param-safe columns (request-only)
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
- including?: readonly string[];
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
- * Narrow down which fields on the model you would like
841
- * to include. If a `for` option is provided, the fields
842
- * will be derived from that model class. Otherwise, it
843
- * will be derrived from the first argument passed to
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, only: ['species', 'name'] }
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: { only: ['species', 'name'] }
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 `including` / `only` / `required` /
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
- * Narrow down which fields on the model you would like
969
- * to include. If a `for` option is provided, the fields
970
- * will be derived from that model class. Otherwise, it
971
- * will be derrived from the first argument passed to
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: { only: ['species', 'name'] }
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 `including` / `only` / `required` /
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
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "type": "module",
3
3
  "name": "@rvoh/psychic",
4
4
  "description": "Typescript web framework",
5
- "version": "3.7.0",
5
+ "version": "3.8.0",
6
6
  "author": "RVOHealth",
7
7
  "repository": {
8
8
  "type": "git",