@rvoh/psychic 3.8.5 → 3.9.0

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