@rvoh/psychic 0.32.0 → 0.33.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 (26) hide show
  1. package/dist/cjs/src/controller/decorators.js +13 -7
  2. package/dist/cjs/src/error/openapi/OpenApiDecoratorModelMissingSerializer.js +20 -0
  3. package/dist/cjs/src/error/openapi/OpenApiDecoratorModelMissingSerializerGetter.js +17 -0
  4. package/dist/cjs/src/helpers/alternateParamName.js +13 -0
  5. package/dist/cjs/src/helpers/isArrayParamName.js +9 -0
  6. package/dist/cjs/src/openapi-renderer/body-segment.js +2 -1
  7. package/dist/cjs/src/openapi-renderer/endpoint.js +31 -13
  8. package/dist/cjs/src/router/index.js +4 -5
  9. package/dist/cjs/src/server/params.js +8 -2
  10. package/dist/esm/src/controller/decorators.js +13 -7
  11. package/dist/esm/src/error/openapi/OpenApiDecoratorModelMissingSerializer.js +17 -0
  12. package/dist/esm/src/error/openapi/OpenApiDecoratorModelMissingSerializerGetter.js +14 -0
  13. package/dist/esm/src/helpers/alternateParamName.js +7 -0
  14. package/dist/esm/src/helpers/isArrayParamName.js +6 -0
  15. package/dist/esm/src/openapi-renderer/body-segment.js +2 -1
  16. package/dist/esm/src/openapi-renderer/endpoint.js +32 -14
  17. package/dist/esm/src/router/index.js +4 -5
  18. package/dist/esm/src/server/params.js +8 -2
  19. package/dist/types/src/controller/decorators.d.ts +3 -22
  20. package/dist/types/src/error/openapi/OpenApiDecoratorModelMissingSerializer.d.ts +7 -0
  21. package/dist/types/src/error/openapi/OpenApiDecoratorModelMissingSerializerGetter.d.ts +6 -0
  22. package/dist/types/src/helpers/alternateParamName.d.ts +1 -0
  23. package/dist/types/src/helpers/isArrayParamName.d.ts +1 -0
  24. package/dist/types/src/openapi-renderer/endpoint.d.ts +6 -12
  25. package/dist/types/src/server/params.d.ts +1 -1
  26. package/package.json +2 -2
@@ -41,7 +41,7 @@ function BeforeAction(opts = {}) {
41
41
  * @param tags - Optional. string array
42
42
  * @param uri - Optional. A list of uri segments that this endpoint uses
43
43
  */
44
- function OpenAPI(modelOrSerializer, opts) {
44
+ function OpenAPI(modelOrSerializer, _opts) {
45
45
  return function (_, context) {
46
46
  const methodName = context.name;
47
47
  context.addInitializer(function () {
@@ -54,19 +54,25 @@ function OpenAPI(modelOrSerializer, opts) {
54
54
  psychicControllerClass.openapi = {};
55
55
  if (!Object.getOwnPropertyDescriptor(psychicControllerClass, 'controllerActionMetadata'))
56
56
  psychicControllerClass['controllerActionMetadata'] = {};
57
- if (opts) {
58
- psychicControllerClass.openapi[methodNameString] = new endpoint_js_1.default(modelOrSerializer, psychicControllerClass, methodNameString, opts);
57
+ if (_opts) {
58
+ const opts = _opts;
59
+ psychicControllerClass.openapi[methodNameString] = new endpoint_js_1.default(
60
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
61
+ modelOrSerializer, psychicControllerClass, methodNameString, opts);
59
62
  psychicControllerClass['controllerActionMetadata'][methodNameString] ||= {};
60
- psychicControllerClass['controllerActionMetadata'][methodNameString]['serializerKey'] =
61
- opts.serializerKey;
63
+ psychicControllerClass['controllerActionMetadata'][methodNameString]['serializerKey'] = opts.serializerKey;
62
64
  //
63
65
  }
64
66
  else {
65
67
  if (isSerializable(modelOrSerializer)) {
66
- psychicControllerClass.openapi[methodNameString] = new endpoint_js_1.default(modelOrSerializer, psychicControllerClass, methodNameString, undefined);
68
+ psychicControllerClass.openapi[methodNameString] = new endpoint_js_1.default(
69
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
+ modelOrSerializer, psychicControllerClass, methodNameString, undefined);
67
71
  }
68
72
  else {
69
- psychicControllerClass.openapi[methodNameString] = new endpoint_js_1.default(null, psychicControllerClass, methodNameString, modelOrSerializer);
73
+ psychicControllerClass.openapi[methodNameString] = new endpoint_js_1.default(null, psychicControllerClass, methodNameString,
74
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
+ modelOrSerializer);
70
76
  }
71
77
  }
72
78
  });
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ class OpenApiDecoratorModelMissingSerializer extends Error {
4
+ modelClass;
5
+ serializerKey;
6
+ constructor(modelClass, serializerKey) {
7
+ super();
8
+ this.modelClass = modelClass;
9
+ this.serializerKey = serializerKey;
10
+ }
11
+ get message() {
12
+ return `
13
+ The specified class does not have a serializer for the specified serializer key:
14
+
15
+ class: ${this.modelClass.name}
16
+ serializer key: ${this.serializerKey.toString()}
17
+ `;
18
+ }
19
+ }
20
+ exports.default = OpenApiDecoratorModelMissingSerializer;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ class OpenApiDecoratorModelMissingSerializerGetter extends Error {
4
+ modelClass;
5
+ constructor(modelClass) {
6
+ super();
7
+ this.modelClass = modelClass;
8
+ }
9
+ get message() {
10
+ return `
11
+ Model passed to @OpenAPI decorator is missing a serializers getter:
12
+
13
+ class: ${this.modelClass.name}
14
+ `;
15
+ }
16
+ }
17
+ exports.default = OpenApiDecoratorModelMissingSerializerGetter;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = alternateParamName;
7
+ const isArrayParamName_js_1 = __importDefault(require("./isArrayParamName.js"));
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ function alternateParamName(paramName) {
10
+ if (typeof paramName !== 'string')
11
+ throw new Error(`paramName is not a string. received: ${paramName}`);
12
+ return (0, isArrayParamName_js_1.default)(paramName) ? paramName.replace(/\[\]$/, '') : `${paramName}[]`;
13
+ }
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = isArrayParamName;
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
+ function isArrayParamName(paramName) {
6
+ if (typeof paramName !== 'string')
7
+ return false;
8
+ return /\[\]$/.test(paramName);
9
+ }
@@ -32,6 +32,7 @@ const isBlankDescription_js_1 = __importDefault(require("./helpers/isBlankDescri
32
32
  const primitiveOpenapiStatementToOpenapi_js_1 = __importStar(require("./helpers/primitiveOpenapiStatementToOpenapi.js"));
33
33
  const schemaToRef_js_1 = __importDefault(require("./helpers/schemaToRef.js"));
34
34
  const serializer_js_1 = __importDefault(require("./serializer.js"));
35
+ const isArrayParamName_js_1 = __importDefault(require("../helpers/isArrayParamName.js"));
35
36
  class OpenapiBodySegmentRenderer {
36
37
  controllerClass;
37
38
  bodySegment;
@@ -304,7 +305,7 @@ class OpenapiBodySegmentRenderer {
304
305
  }
305
306
  }
306
307
  typeIsOpenapiArrayPrimitive(openapiType) {
307
- return /\[\]$/.test((0, primitiveOpenapiStatementToOpenapi_js_1.maybeNullPrimitiveToPrimitive)(openapiType));
308
+ return (0, isArrayParamName_js_1.default)((0, primitiveOpenapiStatementToOpenapi_js_1.maybeNullPrimitiveToPrimitive)(openapiType));
308
309
  }
309
310
  applyConfigurationOptions(obj) {
310
311
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
@@ -6,6 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.MissingControllerActionPairingInRoutes = void 0;
7
7
  const dream_1 = require("@rvoh/dream");
8
8
  const lodash_es_1 = require("lodash-es");
9
+ const OpenApiDecoratorModelMissingSerializer_js_1 = __importDefault(require("../error/openapi/OpenApiDecoratorModelMissingSerializer.js"));
10
+ const OpenApiDecoratorModelMissingSerializerGetter_js_1 = __importDefault(require("../error/openapi/OpenApiDecoratorModelMissingSerializerGetter.js"));
9
11
  const index_js_1 = __importDefault(require("../psychic-app/index.js"));
10
12
  const body_segment_js_1 = __importDefault(require("./body-segment.js"));
11
13
  const defaults_js_1 = require("./defaults.js");
@@ -690,12 +692,11 @@ class OpenapiEndpointRenderer {
690
692
  */
691
693
  parseMultiEntitySerializerResponseShape() {
692
694
  const anyOf = { anyOf: [] };
693
- this.getSerializerClasses().forEach(serializerClass => {
695
+ const sortedSerializerClasses = (0, dream_1.sortBy)(this.getSerializerClasses() || [], serializerClass => serializerClass.openapiName);
696
+ sortedSerializerClasses.forEach(serializerClass => {
694
697
  const serializerKey = serializerClass.openapiName;
695
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
696
698
  const serializerObject = {
697
699
  $ref: `#/components/schemas/${serializerKey}`,
698
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
699
700
  };
700
701
  anyOf.anyOf.push(serializerObject);
701
702
  });
@@ -758,12 +759,20 @@ class OpenapiEndpointRenderer {
758
759
  getSerializerClasses() {
759
760
  if (!this.dreamsOrSerializers)
760
761
  return null;
761
- if (Array.isArray(this.dreamsOrSerializers)) {
762
- return (0, dream_1.compact)(this.dreamsOrSerializers.map(s => this.getSerializerClass(s)));
763
- }
764
- else {
765
- return (0, dream_1.compact)([this.getSerializerClass(this.dreamsOrSerializers)]);
766
- }
762
+ const dreamsOrSerializers = this.expandStiSerializersInDreamsOrSerializers(this.dreamsOrSerializers);
763
+ return (0, dream_1.compact)(dreamsOrSerializers.map(s => this.getSerializerClass(s)));
764
+ }
765
+ expandStiSerializersInDreamsOrSerializers(dreamsOrSerializers) {
766
+ if (Array.isArray(dreamsOrSerializers))
767
+ return dreamsOrSerializers.flatMap(dreamOrSerializer => this.expandStiSerializersInDreamsOrSerializers(dreamOrSerializer));
768
+ if (dreamsOrSerializers.prototype instanceof dream_1.DreamSerializer)
769
+ return [dreamsOrSerializers];
770
+ if (dreamsOrSerializers.prototype instanceof dream_1.Dream)
771
+ return this.expandDreamStiClasses(dreamsOrSerializers);
772
+ return [dreamsOrSerializers];
773
+ }
774
+ expandDreamStiClasses(dreamClass) {
775
+ return dreamClass['extendedBy'] ? [...dreamClass['extendedBy']] : [dreamClass];
767
776
  }
768
777
  /**
769
778
  * @internal
@@ -795,10 +804,19 @@ class OpenapiEndpointRenderer {
795
804
  else {
796
805
  const modelClass = dreamOrSerializerOrViewModel;
797
806
  const modelPrototype = modelClass.prototype;
798
- const serializerKey = modelPrototype.serializers[this.serializerKey || 'default'];
799
- if (serializerKey === undefined)
800
- throw new Error(`no serializerKey for ${this.serializerKey || 'default'}`);
801
- return dreamApp.serializers[serializerKey] ?? null;
807
+ const serializerKey = this.serializerKey || 'default';
808
+ let serializerGlobalName;
809
+ try {
810
+ serializerGlobalName =
811
+ modelPrototype.serializers[serializerKey];
812
+ }
813
+ catch {
814
+ throw new OpenApiDecoratorModelMissingSerializerGetter_js_1.default(modelClass);
815
+ }
816
+ if (serializerGlobalName === undefined) {
817
+ throw new OpenApiDecoratorModelMissingSerializer_js_1.default(modelClass, serializerKey);
818
+ }
819
+ return dreamApp.serializers[serializerGlobalName] ?? null;
802
820
  }
803
821
  }
804
822
  }
@@ -7,14 +7,14 @@ exports.PsychicNestedRouter = void 0;
7
7
  const dream_1 = require("@rvoh/dream");
8
8
  const express_1 = require("express");
9
9
  const pluralize_esm_1 = __importDefault(require("pluralize-esm"));
10
+ const ParamValidationError_js_1 = __importDefault(require("../error/controller/ParamValidationError.js"));
11
+ const ParamValidationErrors_js_1 = __importDefault(require("../error/controller/ParamValidationErrors.js"));
10
12
  const EnvInternal_js_1 = __importDefault(require("../helpers/EnvInternal.js"));
11
13
  const errorIsRescuableHttpError_js_1 = __importDefault(require("../helpers/error/errorIsRescuableHttpError.js"));
12
14
  const index_js_1 = __importDefault(require("../psychic-app/index.js"));
13
15
  const helpers_js_1 = require("../router/helpers.js");
14
16
  const route_manager_js_1 = __importDefault(require("./route-manager.js"));
15
17
  const types_js_1 = require("./types.js");
16
- const ParamValidationErrors_js_1 = __importDefault(require("../error/controller/ParamValidationErrors.js"));
17
- const ParamValidationError_js_1 = __importDefault(require("../error/controller/ParamValidationError.js"));
18
18
  class PsychicRouter {
19
19
  app;
20
20
  config;
@@ -101,7 +101,7 @@ class PsychicRouter {
101
101
  });
102
102
  const currentNamespace = replacedNamespaces[replacedNamespaces.length - 1];
103
103
  if (!currentNamespace)
104
- throw new Error('Must be within a resource to call the collection method');
104
+ throw new Error('Must be within a `resources` declaration to call the collection method');
105
105
  cb(nestedRouter);
106
106
  }
107
107
  makeResource(path, optionsOrCb, cb, plural) {
@@ -194,9 +194,8 @@ class PsychicRouter {
194
194
  }
195
195
  catch (error) {
196
196
  const err = error;
197
- const psychicApp = index_js_1.default.getOrFail();
198
197
  if (!EnvInternal_js_1.default.isTest)
199
- psychicApp.logger.error(err.message);
198
+ index_js_1.default.logWithLevel('error', err.message);
200
199
  if ((0, errorIsRescuableHttpError_js_1.default)(err)) {
201
200
  const httpErr = err;
202
201
  if (httpErr.data) {
@@ -8,6 +8,8 @@ const ParamValidationError_js_1 = __importDefault(require("../error/controller/P
8
8
  const ParamValidationErrors_js_1 = __importDefault(require("../error/controller/ParamValidationErrors.js"));
9
9
  const isUuid_js_1 = __importDefault(require("../helpers/isUuid.js"));
10
10
  const typechecks_js_1 = require("../helpers/typechecks.js");
11
+ const isArrayParamName_js_1 = __importDefault(require("../helpers/isArrayParamName.js"));
12
+ const alternateParamName_js_1 = __importDefault(require("../helpers/alternateParamName.js"));
11
13
  class Params {
12
14
  $params;
13
15
  /**
@@ -173,8 +175,12 @@ class Params {
173
175
  * Params.cast(this.params.stuff, 'string', { enum: ['chalupas', 'other'] })
174
176
  * ```
175
177
  */
176
- static cast(params, paramName, expectedType, opts) {
177
- const param = params[paramName];
178
+ static cast(_params, paramName, expectedType, opts) {
179
+ const params = _params;
180
+ let param = params[paramName];
181
+ if (param === undefined && (0, isArrayParamName_js_1.default)(expectedType)) {
182
+ param = params[(0, alternateParamName_js_1.default)(paramName)];
183
+ }
178
184
  return new this(params).cast(paramName, typeof param === 'string' ? param.trim() : param, expectedType, opts);
179
185
  }
180
186
  static casing(params, casing) {
@@ -34,7 +34,7 @@ export function BeforeAction(opts = {}) {
34
34
  * @param tags - Optional. string array
35
35
  * @param uri - Optional. A list of uri segments that this endpoint uses
36
36
  */
37
- export function OpenAPI(modelOrSerializer, opts) {
37
+ export function OpenAPI(modelOrSerializer, _opts) {
38
38
  return function (_, context) {
39
39
  const methodName = context.name;
40
40
  context.addInitializer(function () {
@@ -47,19 +47,25 @@ export function OpenAPI(modelOrSerializer, opts) {
47
47
  psychicControllerClass.openapi = {};
48
48
  if (!Object.getOwnPropertyDescriptor(psychicControllerClass, 'controllerActionMetadata'))
49
49
  psychicControllerClass['controllerActionMetadata'] = {};
50
- if (opts) {
51
- psychicControllerClass.openapi[methodNameString] = new OpenapiEndpointRenderer(modelOrSerializer, psychicControllerClass, methodNameString, opts);
50
+ if (_opts) {
51
+ const opts = _opts;
52
+ psychicControllerClass.openapi[methodNameString] = new OpenapiEndpointRenderer(
53
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
+ modelOrSerializer, psychicControllerClass, methodNameString, opts);
52
55
  psychicControllerClass['controllerActionMetadata'][methodNameString] ||= {};
53
- psychicControllerClass['controllerActionMetadata'][methodNameString]['serializerKey'] =
54
- opts.serializerKey;
56
+ psychicControllerClass['controllerActionMetadata'][methodNameString]['serializerKey'] = opts.serializerKey;
55
57
  //
56
58
  }
57
59
  else {
58
60
  if (isSerializable(modelOrSerializer)) {
59
- psychicControllerClass.openapi[methodNameString] = new OpenapiEndpointRenderer(modelOrSerializer, psychicControllerClass, methodNameString, undefined);
61
+ psychicControllerClass.openapi[methodNameString] = new OpenapiEndpointRenderer(
62
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
+ modelOrSerializer, psychicControllerClass, methodNameString, undefined);
60
64
  }
61
65
  else {
62
- psychicControllerClass.openapi[methodNameString] = new OpenapiEndpointRenderer(null, psychicControllerClass, methodNameString, modelOrSerializer);
66
+ psychicControllerClass.openapi[methodNameString] = new OpenapiEndpointRenderer(null, psychicControllerClass, methodNameString,
67
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
+ modelOrSerializer);
63
69
  }
64
70
  }
65
71
  });
@@ -0,0 +1,17 @@
1
+ export default class OpenApiDecoratorModelMissingSerializer extends Error {
2
+ modelClass;
3
+ serializerKey;
4
+ constructor(modelClass, serializerKey) {
5
+ super();
6
+ this.modelClass = modelClass;
7
+ this.serializerKey = serializerKey;
8
+ }
9
+ get message() {
10
+ return `
11
+ The specified class does not have a serializer for the specified serializer key:
12
+
13
+ class: ${this.modelClass.name}
14
+ serializer key: ${this.serializerKey.toString()}
15
+ `;
16
+ }
17
+ }
@@ -0,0 +1,14 @@
1
+ export default class OpenApiDecoratorModelMissingSerializerGetter extends Error {
2
+ modelClass;
3
+ constructor(modelClass) {
4
+ super();
5
+ this.modelClass = modelClass;
6
+ }
7
+ get message() {
8
+ return `
9
+ Model passed to @OpenAPI decorator is missing a serializers getter:
10
+
11
+ class: ${this.modelClass.name}
12
+ `;
13
+ }
14
+ }
@@ -0,0 +1,7 @@
1
+ import isArrayParamName from './isArrayParamName.js';
2
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3
+ export default function alternateParamName(paramName) {
4
+ if (typeof paramName !== 'string')
5
+ throw new Error(`paramName is not a string. received: ${paramName}`);
6
+ return isArrayParamName(paramName) ? paramName.replace(/\[\]$/, '') : `${paramName}[]`;
7
+ }
@@ -0,0 +1,6 @@
1
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2
+ export default function isArrayParamName(paramName) {
3
+ if (typeof paramName !== 'string')
4
+ return false;
5
+ return /\[\]$/.test(paramName);
6
+ }
@@ -4,6 +4,7 @@ import isBlankDescription from './helpers/isBlankDescription.js';
4
4
  import primitiveOpenapiStatementToOpenapi, { maybeNullPrimitiveToPrimitive, } from './helpers/primitiveOpenapiStatementToOpenapi.js';
5
5
  import schemaToRef from './helpers/schemaToRef.js';
6
6
  import OpenapiSerializerRenderer from './serializer.js';
7
+ import isArrayParamName from '../helpers/isArrayParamName.js';
7
8
  export default class OpenapiBodySegmentRenderer {
8
9
  controllerClass;
9
10
  bodySegment;
@@ -276,7 +277,7 @@ export default class OpenapiBodySegmentRenderer {
276
277
  }
277
278
  }
278
279
  typeIsOpenapiArrayPrimitive(openapiType) {
279
- return /\[\]$/.test(maybeNullPrimitiveToPrimitive(openapiType));
280
+ return isArrayParamName(maybeNullPrimitiveToPrimitive(openapiType));
280
281
  }
281
282
  applyConfigurationOptions(obj) {
282
283
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
@@ -1,5 +1,7 @@
1
- import { DreamApp, compact, } from '@rvoh/dream';
1
+ import { Dream, DreamApp, DreamSerializer, compact, sortBy, } from '@rvoh/dream';
2
2
  import { cloneDeep } from 'lodash-es';
3
+ import OpenApiDecoratorModelMissingSerializer from '../error/openapi/OpenApiDecoratorModelMissingSerializer.js';
4
+ import OpenApiDecoratorModelMissingSerializerGetter from '../error/openapi/OpenApiDecoratorModelMissingSerializerGetter.js';
3
5
  import PsychicApp from '../psychic-app/index.js';
4
6
  import OpenapiBodySegmentRenderer from './body-segment.js';
5
7
  import { DEFAULT_OPENAPI_RESPONSES } from './defaults.js';
@@ -684,12 +686,11 @@ export default class OpenapiEndpointRenderer {
684
686
  */
685
687
  parseMultiEntitySerializerResponseShape() {
686
688
  const anyOf = { anyOf: [] };
687
- this.getSerializerClasses().forEach(serializerClass => {
689
+ const sortedSerializerClasses = sortBy(this.getSerializerClasses() || [], serializerClass => serializerClass.openapiName);
690
+ sortedSerializerClasses.forEach(serializerClass => {
688
691
  const serializerKey = serializerClass.openapiName;
689
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
690
692
  const serializerObject = {
691
693
  $ref: `#/components/schemas/${serializerKey}`,
692
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
693
694
  };
694
695
  anyOf.anyOf.push(serializerObject);
695
696
  });
@@ -752,12 +753,20 @@ export default class OpenapiEndpointRenderer {
752
753
  getSerializerClasses() {
753
754
  if (!this.dreamsOrSerializers)
754
755
  return null;
755
- if (Array.isArray(this.dreamsOrSerializers)) {
756
- return compact(this.dreamsOrSerializers.map(s => this.getSerializerClass(s)));
757
- }
758
- else {
759
- return compact([this.getSerializerClass(this.dreamsOrSerializers)]);
760
- }
756
+ const dreamsOrSerializers = this.expandStiSerializersInDreamsOrSerializers(this.dreamsOrSerializers);
757
+ return compact(dreamsOrSerializers.map(s => this.getSerializerClass(s)));
758
+ }
759
+ expandStiSerializersInDreamsOrSerializers(dreamsOrSerializers) {
760
+ if (Array.isArray(dreamsOrSerializers))
761
+ return dreamsOrSerializers.flatMap(dreamOrSerializer => this.expandStiSerializersInDreamsOrSerializers(dreamOrSerializer));
762
+ if (dreamsOrSerializers.prototype instanceof DreamSerializer)
763
+ return [dreamsOrSerializers];
764
+ if (dreamsOrSerializers.prototype instanceof Dream)
765
+ return this.expandDreamStiClasses(dreamsOrSerializers);
766
+ return [dreamsOrSerializers];
767
+ }
768
+ expandDreamStiClasses(dreamClass) {
769
+ return dreamClass['extendedBy'] ? [...dreamClass['extendedBy']] : [dreamClass];
761
770
  }
762
771
  /**
763
772
  * @internal
@@ -789,10 +798,19 @@ export default class OpenapiEndpointRenderer {
789
798
  else {
790
799
  const modelClass = dreamOrSerializerOrViewModel;
791
800
  const modelPrototype = modelClass.prototype;
792
- const serializerKey = modelPrototype.serializers[this.serializerKey || 'default'];
793
- if (serializerKey === undefined)
794
- throw new Error(`no serializerKey for ${this.serializerKey || 'default'}`);
795
- return dreamApp.serializers[serializerKey] ?? null;
801
+ const serializerKey = this.serializerKey || 'default';
802
+ let serializerGlobalName;
803
+ try {
804
+ serializerGlobalName =
805
+ modelPrototype.serializers[serializerKey];
806
+ }
807
+ catch {
808
+ throw new OpenApiDecoratorModelMissingSerializerGetter(modelClass);
809
+ }
810
+ if (serializerGlobalName === undefined) {
811
+ throw new OpenApiDecoratorModelMissingSerializer(modelClass, serializerKey);
812
+ }
813
+ return dreamApp.serializers[serializerGlobalName] ?? null;
796
814
  }
797
815
  }
798
816
  }
@@ -1,14 +1,14 @@
1
1
  import { RecordNotFound, ValidationError, camelize } from '@rvoh/dream';
2
2
  import { Router } from 'express';
3
3
  import pluralize from 'pluralize-esm';
4
+ import ParamValidationError from '../error/controller/ParamValidationError.js';
5
+ import ParamValidationErrors from '../error/controller/ParamValidationErrors.js';
4
6
  import EnvInternal from '../helpers/EnvInternal.js';
5
7
  import errorIsRescuableHttpError from '../helpers/error/errorIsRescuableHttpError.js';
6
8
  import PsychicApp from '../psychic-app/index.js';
7
9
  import { applyResourceAction, applyResourcesAction, lookupControllerOrFail, routePath, } from '../router/helpers.js';
8
10
  import RouteManager from './route-manager.js';
9
11
  import { ResourceMethods, ResourcesMethods, } from './types.js';
10
- import ParamValidationErrors from '../error/controller/ParamValidationErrors.js';
11
- import ParamValidationError from '../error/controller/ParamValidationError.js';
12
12
  export default class PsychicRouter {
13
13
  app;
14
14
  config;
@@ -95,7 +95,7 @@ export default class PsychicRouter {
95
95
  });
96
96
  const currentNamespace = replacedNamespaces[replacedNamespaces.length - 1];
97
97
  if (!currentNamespace)
98
- throw new Error('Must be within a resource to call the collection method');
98
+ throw new Error('Must be within a `resources` declaration to call the collection method');
99
99
  cb(nestedRouter);
100
100
  }
101
101
  makeResource(path, optionsOrCb, cb, plural) {
@@ -188,9 +188,8 @@ export default class PsychicRouter {
188
188
  }
189
189
  catch (error) {
190
190
  const err = error;
191
- const psychicApp = PsychicApp.getOrFail();
192
191
  if (!EnvInternal.isTest)
193
- psychicApp.logger.error(err.message);
192
+ PsychicApp.logWithLevel('error', err.message);
194
193
  if (errorIsRescuableHttpError(err)) {
195
194
  const httpErr = err;
196
195
  if (httpErr.data) {
@@ -3,6 +3,8 @@ import ParamValidationError from '../error/controller/ParamValidationError.js';
3
3
  import ParamValidationErrors from '../error/controller/ParamValidationErrors.js';
4
4
  import isUuid from '../helpers/isUuid.js';
5
5
  import { isObject } from '../helpers/typechecks.js';
6
+ import isArrayParamName from '../helpers/isArrayParamName.js';
7
+ import alternateParamName from '../helpers/alternateParamName.js';
6
8
  export default class Params {
7
9
  $params;
8
10
  /**
@@ -168,8 +170,12 @@ export default class Params {
168
170
  * Params.cast(this.params.stuff, 'string', { enum: ['chalupas', 'other'] })
169
171
  * ```
170
172
  */
171
- static cast(params, paramName, expectedType, opts) {
172
- const param = params[paramName];
173
+ static cast(_params, paramName, expectedType, opts) {
174
+ const params = _params;
175
+ let param = params[paramName];
176
+ if (param === undefined && isArrayParamName(expectedType)) {
177
+ param = params[alternateParamName(paramName)];
178
+ }
173
179
  return new this(params).cast(paramName, typeof param === 'string' ? param.trim() : param, expectedType, opts);
174
180
  }
175
181
  static casing(params, casing) {
@@ -1,28 +1,9 @@
1
- import { DreamSerializer, SerializableDreamClassOrViewModelClass } from '@rvoh/dream';
1
+ import { DreamSerializable, DreamSerializableArray } from '@rvoh/dream';
2
2
  import { OpenapiEndpointRendererOpts } from '../openapi-renderer/endpoint.js';
3
3
  export declare function BeforeAction(opts?: {
4
4
  isStatic?: boolean;
5
5
  only?: string[];
6
6
  except?: string[];
7
7
  }): any;
8
- /**
9
- * Used to annotate your controller method in a way that enables
10
- * Psychic to automatically generate an openapi spec for you. Using
11
- * this feature, you can easily document your api in typescript, taking
12
- * advantage of powerful type completion and validation, as well as useful
13
- * shorthand notation to keep annotations simple when possible.
14
- *
15
- * @param modelOrSerializer - a function which immediately returns either a serializer class, a dream model class, or else something that has a serializers getter on it.
16
- * @param body - Optional. The shape of the request body
17
- * @param headers - Optional. The list of request headers to provide for this endpoint
18
- * @param many - Optional. whether or not to render a top level array for this serializer
19
- * @param method - The HTTP method to use when hitting this endpoint
20
- * @param path - Optional. If passed, this path will be used as the request path. If not, it will be looked up in the conf/routes.ts file.
21
- * @param query - Optional. A list of query params to provide for this endpoint
22
- * @param responses - Optional. A list of additional responses that your app may return
23
- * @param serializerKey - Optional. Use this to override the serializer key to use when looking up a serializer by the provided model or view model.
24
- * @param status - Optional. The status code this endpoint uses when responding successfully. If not passed, 200 is assummed.
25
- * @param tags - Optional. string array
26
- * @param uri - Optional. A list of uri segments that this endpoint uses
27
- */
28
- export declare function OpenAPI<I extends SerializableDreamClassOrViewModelClass | SerializableDreamClassOrViewModelClass[] | typeof DreamSerializer>(modelOrSerializer?: I | OpenapiEndpointRendererOpts<I>, opts?: OpenapiEndpointRendererOpts<I>): any;
8
+ export declare function OpenAPI<const I extends DreamSerializable | DreamSerializableArray>(modelOrSerializer: I, opts?: OpenapiEndpointRendererOpts<I>): any;
9
+ export declare function OpenAPI(modelOrSerializer?: OpenapiEndpointRendererOpts): any;
@@ -0,0 +1,7 @@
1
+ import { ViewModelClass } from '@rvoh/dream';
2
+ export default class OpenApiDecoratorModelMissingSerializer extends Error {
3
+ private modelClass;
4
+ private serializerKey;
5
+ constructor(modelClass: ViewModelClass, serializerKey: string);
6
+ get message(): string;
7
+ }
@@ -0,0 +1,6 @@
1
+ import { ViewModelClass } from '@rvoh/dream';
2
+ export default class OpenApiDecoratorModelMissingSerializerGetter extends Error {
3
+ private modelClass;
4
+ constructor(modelClass: ViewModelClass);
5
+ get message(): string;
6
+ }
@@ -0,0 +1 @@
1
+ export default function alternateParamName(paramName: any): string;
@@ -0,0 +1 @@
1
+ export default function isArrayParamName(paramName: any): boolean;
@@ -1,10 +1,10 @@
1
- import { Dream, DreamSerializer, OpenapiAllTypes, OpenapiFormats, OpenapiSchemaArray, OpenapiSchemaBody, OpenapiSchemaBodyShorthand, OpenapiSchemaExpressionAllOf, OpenapiSchemaExpressionAnyOf, OpenapiSchemaExpressionOneOf, OpenapiSchemaExpressionRef, OpenapiSchemaObject, OpenapiSchemaProperties, SerializableDreamClassOrViewModelClass } from '@rvoh/dream';
1
+ import { Dream, DreamOrViewModelClassSerializerArrayKeys, DreamOrViewModelClassSerializerKey, DreamSerializable, DreamSerializableArray, OpenapiAllTypes, OpenapiFormats, OpenapiSchemaArray, OpenapiSchemaBody, OpenapiSchemaBodyShorthand, OpenapiSchemaExpressionAllOf, OpenapiSchemaExpressionAnyOf, OpenapiSchemaExpressionOneOf, OpenapiSchemaExpressionRef, OpenapiSchemaObject, OpenapiSchemaProperties, ViewModelClass } from '@rvoh/dream';
2
2
  import PsychicController from '../controller/index.js';
3
3
  import { HttpStatusCode, HttpStatusCodeNumber } from '../error/http/status-codes.js';
4
4
  import { RouteConfig } from '../router/route-manager.js';
5
5
  import { HttpMethod } from '../router/types.js';
6
6
  import { OpenapiBodySegment } from './body-segment.js';
7
- export default class OpenapiEndpointRenderer<DreamsOrSerializersOrViewModels extends SerializableDreamClassOrViewModelClass | SerializableDreamClassOrViewModelClass[] | typeof DreamSerializer | (typeof DreamSerializer)[]> {
7
+ export default class OpenapiEndpointRenderer<DreamsOrSerializersOrViewModels extends DreamSerializable | DreamSerializableArray> {
8
8
  private dreamsOrSerializers;
9
9
  private controllerClass;
10
10
  private action;
@@ -224,6 +224,8 @@ export default class OpenapiEndpointRenderer<DreamsOrSerializersOrViewModels ext
224
224
  * match.
225
225
  */
226
226
  private getSerializerClasses;
227
+ private expandStiSerializersInDreamsOrSerializers;
228
+ private expandDreamStiClasses;
227
229
  /**
228
230
  * @internal
229
231
  *
@@ -245,11 +247,7 @@ export declare class MissingControllerActionPairingInRoutes extends Error {
245
247
  constructor(controllerClass: typeof PsychicController, action: string);
246
248
  get message(): string;
247
249
  }
248
- export interface OpenapiEndpointRendererOpts<T extends SerializableDreamClassOrViewModelClass | SerializableDreamClassOrViewModelClass[] | typeof DreamSerializer | (typeof DreamSerializer)[], NonArrayT extends T extends (infer R extends abstract new (...args: any) => any)[] ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
249
- R & (abstract new (...args: any) => any) : // eslint-disable-next-line @typescript-eslint/no-explicit-any
250
- T & (abstract new (...args: any) => any) = T extends (infer R extends abstract new (...args: any) => any)[] ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
251
- R & (abstract new (...args: any) => any) : // eslint-disable-next-line @typescript-eslint/no-explicit-any
252
- T & (abstract new (...args: any) => any)> {
250
+ export interface OpenapiEndpointRendererOpts<I extends DreamSerializable | DreamSerializableArray | undefined = undefined> {
253
251
  many?: boolean;
254
252
  pathParams?: OpenapiPathParams;
255
253
  headers?: OpenapiHeaders;
@@ -265,11 +263,7 @@ T & (abstract new (...args: any) => any)> {
265
263
  description: string;
266
264
  }>>;
267
265
  defaultResponse?: OpenapiEndpointRendererDefaultResponseOption;
268
- serializerKey?: InstanceType<NonArrayT> extends {
269
- serializers: {
270
- [key: string]: typeof DreamSerializer;
271
- };
272
- } ? string : undefined;
266
+ serializerKey?: I extends undefined ? never : I extends DreamSerializableArray ? DreamOrViewModelClassSerializerArrayKeys<I> : I extends typeof Dream | ViewModelClass ? DreamOrViewModelClassSerializerKey<I> : never;
273
267
  status?: HttpStatusCodeNumber;
274
268
  omitDefaultHeaders?: boolean;
275
269
  omitDefaultResponses?: boolean;
@@ -46,7 +46,7 @@ export default class Params {
46
46
  * Params.cast(this.params.stuff, 'string', { enum: ['chalupas', 'other'] })
47
47
  * ```
48
48
  */
49
- static cast<const EnumType extends readonly string[], OptsType extends ParamsCastOptions<EnumType>, ExpectedType extends (typeof PsychicParamsPrimitiveLiterals)[number] | RegExp, ValidatedType extends ValidatedReturnType<ExpectedType, OptsType>, AllowNullOrUndefined extends ValidatedAllowsNull<ExpectedType, OptsType>, FinalReturnType extends AllowNullOrUndefined extends true ? ValidatedType | null | undefined : ValidatedType>(params: object, paramName: string, expectedType: ExpectedType, opts?: OptsType): FinalReturnType;
49
+ static cast<const EnumType extends readonly string[], OptsType extends ParamsCastOptions<EnumType>, ExpectedType extends (typeof PsychicParamsPrimitiveLiterals)[number] | RegExp, ValidatedType extends ValidatedReturnType<ExpectedType, OptsType>, AllowNullOrUndefined extends ValidatedAllowsNull<ExpectedType, OptsType>, FinalReturnType extends AllowNullOrUndefined extends true ? ValidatedType | null | undefined : ValidatedType>(_params: object, paramName: string, expectedType: ExpectedType, opts?: OptsType): FinalReturnType;
50
50
  static casing<T extends typeof Params>(this: T, params: object, casing: 'snake' | 'camel'): Params;
51
51
  private _casing;
52
52
  constructor($params: object);
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": "0.32.0",
5
+ "version": "0.33.0",
6
6
  "author": "RVOHealth",
7
7
  "repository": {
8
8
  "type": "git",
@@ -60,7 +60,7 @@
60
60
  "devDependencies": {
61
61
  "@eslint/js": "^9.19.0",
62
62
  "@jest-mock/express": "^3.0.0",
63
- "@rvoh/dream": "^0.39.0",
63
+ "@rvoh/dream": "^0.42.0",
64
64
  "@rvoh/dream-spec-helpers": "^0.2.4",
65
65
  "@rvoh/psychic-spec-helpers": "^0.6.0",
66
66
  "@types/express": "^5.0.1",