@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.
- package/dist/cjs/src/controller/decorators.js +13 -7
- package/dist/cjs/src/error/openapi/OpenApiDecoratorModelMissingSerializer.js +20 -0
- package/dist/cjs/src/error/openapi/OpenApiDecoratorModelMissingSerializerGetter.js +17 -0
- package/dist/cjs/src/helpers/alternateParamName.js +13 -0
- package/dist/cjs/src/helpers/isArrayParamName.js +9 -0
- package/dist/cjs/src/openapi-renderer/body-segment.js +2 -1
- package/dist/cjs/src/openapi-renderer/endpoint.js +31 -13
- package/dist/cjs/src/router/index.js +4 -5
- package/dist/cjs/src/server/params.js +8 -2
- package/dist/esm/src/controller/decorators.js +13 -7
- package/dist/esm/src/error/openapi/OpenApiDecoratorModelMissingSerializer.js +17 -0
- package/dist/esm/src/error/openapi/OpenApiDecoratorModelMissingSerializerGetter.js +14 -0
- package/dist/esm/src/helpers/alternateParamName.js +7 -0
- package/dist/esm/src/helpers/isArrayParamName.js +6 -0
- package/dist/esm/src/openapi-renderer/body-segment.js +2 -1
- package/dist/esm/src/openapi-renderer/endpoint.js +32 -14
- package/dist/esm/src/router/index.js +4 -5
- package/dist/esm/src/server/params.js +8 -2
- package/dist/types/src/controller/decorators.d.ts +3 -22
- package/dist/types/src/error/openapi/OpenApiDecoratorModelMissingSerializer.d.ts +7 -0
- package/dist/types/src/error/openapi/OpenApiDecoratorModelMissingSerializerGetter.d.ts +6 -0
- package/dist/types/src/helpers/alternateParamName.d.ts +1 -0
- package/dist/types/src/helpers/isArrayParamName.d.ts +1 -0
- package/dist/types/src/openapi-renderer/endpoint.d.ts +6 -12
- package/dist/types/src/server/params.d.ts +1 -1
- 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,
|
|
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 (
|
|
58
|
-
|
|
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(
|
|
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,
|
|
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
|
|
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()
|
|
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
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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 =
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
177
|
-
const
|
|
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,
|
|
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 (
|
|
51
|
-
|
|
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(
|
|
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,
|
|
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
|
+
}
|
|
@@ -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
|
|
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()
|
|
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
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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 =
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
172
|
-
const
|
|
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 {
|
|
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
|
-
|
|
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 @@
|
|
|
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,
|
|
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
|
|
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<
|
|
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?:
|
|
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>(
|
|
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.
|
|
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.
|
|
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",
|