@rvoh/psychic 2.3.8 → 3.0.0-alpha.1
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/cli/index.js +4 -0
- package/dist/cjs/src/controller/helpers/logIfDevelopment.js +5 -5
- package/dist/cjs/src/controller/index.js +119 -40
- package/dist/cjs/src/devtools/helpers/launchDevServer.js +15 -1
- package/dist/cjs/src/error/openapi/UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute.js +44 -0
- package/dist/cjs/src/error/router/cannot-commit-routes-without-koa-app.js +12 -0
- package/dist/cjs/src/helpers/toJson.js +2 -8
- package/dist/cjs/src/helpers/validateOpenApiSchema.js +1 -1
- package/dist/cjs/src/openapi-renderer/SerializerOpenapiRenderer.js +2 -2
- package/dist/cjs/src/openapi-renderer/endpoint.js +2 -2
- package/dist/cjs/src/openapi-renderer/helpers/OpenapiPayloadValidator.js +75 -9
- package/dist/cjs/src/openapi-renderer/helpers/{dreamAttributeOpenapiShape.js → dreamColumnOpenapiShape.js} +19 -6
- package/dist/cjs/src/openapi-renderer/helpers/stringify-cache.js +55 -0
- package/dist/cjs/src/openapi-renderer/helpers/validator-cache.js +52 -0
- package/dist/cjs/src/psychic-app/helpers/import/importControllers.js +1 -1
- package/dist/cjs/src/psychic-app/index.js +3 -10
- package/dist/cjs/src/router/index.js +31 -25
- package/dist/cjs/src/server/helpers/startPsychicServer.js +6 -2
- package/dist/cjs/src/server/index.js +32 -35
- package/dist/cjs/src/server/params.js +56 -3
- package/dist/cjs/src/session/index.js +9 -12
- package/dist/esm/src/cli/index.js +4 -0
- package/dist/esm/src/controller/helpers/logIfDevelopment.js +5 -5
- package/dist/esm/src/controller/index.js +119 -40
- package/dist/esm/src/devtools/helpers/launchDevServer.js +15 -1
- package/dist/esm/src/error/openapi/UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute.js +44 -0
- package/dist/esm/src/error/router/cannot-commit-routes-without-koa-app.js +12 -0
- package/dist/esm/src/helpers/toJson.js +2 -8
- package/dist/esm/src/helpers/validateOpenApiSchema.js +1 -1
- package/dist/esm/src/openapi-renderer/SerializerOpenapiRenderer.js +2 -2
- package/dist/esm/src/openapi-renderer/endpoint.js +2 -2
- package/dist/esm/src/openapi-renderer/helpers/OpenapiPayloadValidator.js +75 -9
- package/dist/esm/src/openapi-renderer/helpers/{dreamAttributeOpenapiShape.js → dreamColumnOpenapiShape.js} +19 -6
- package/dist/esm/src/openapi-renderer/helpers/stringify-cache.js +55 -0
- package/dist/esm/src/openapi-renderer/helpers/validator-cache.js +52 -0
- package/dist/esm/src/psychic-app/helpers/import/importControllers.js +1 -1
- package/dist/esm/src/psychic-app/index.js +3 -10
- package/dist/esm/src/router/index.js +31 -25
- package/dist/esm/src/server/helpers/startPsychicServer.js +6 -2
- package/dist/esm/src/server/index.js +32 -35
- package/dist/esm/src/server/params.js +56 -3
- package/dist/esm/src/session/index.js +9 -12
- package/dist/types/src/controller/helpers/logIfDevelopment.d.ts +3 -4
- package/dist/types/src/controller/index.d.ts +19 -8
- package/dist/types/src/devtools/helpers/launchDevServer.d.ts +2 -1
- package/dist/types/src/error/openapi/UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute.d.ts +7 -0
- package/dist/types/src/error/router/cannot-commit-routes-without-koa-app.d.ts +3 -0
- package/dist/types/src/helpers/cookieMaxAgeFromCookieOpts.d.ts +1 -1
- package/dist/types/src/helpers/toJson.d.ts +1 -1
- package/dist/types/src/helpers/validateOpenApiSchema.d.ts +5 -1
- package/dist/types/src/openapi-renderer/helpers/OpenapiPayloadValidator.d.ts +41 -0
- package/dist/types/src/openapi-renderer/helpers/{dreamAttributeOpenapiShape.d.ts → dreamColumnOpenapiShape.d.ts} +1 -1
- package/dist/types/src/openapi-renderer/helpers/stringify-cache.d.ts +34 -0
- package/dist/types/src/openapi-renderer/helpers/validator-cache.d.ts +35 -0
- package/dist/types/src/psychic-app/index.d.ts +11 -14
- package/dist/types/src/router/index.d.ts +17 -17
- package/dist/types/src/router/route-manager.d.ts +4 -3
- package/dist/types/src/server/helpers/startPsychicServer.d.ts +3 -3
- package/dist/types/src/server/index.d.ts +3 -3
- package/dist/types/src/server/params.d.ts +2 -2
- package/dist/types/src/session/index.d.ts +13 -5
- package/package.json +30 -19
- package/dist/cjs/src/error/router/cannot-commit-routes-without-express-app.js +0 -12
- package/dist/esm/src/error/router/cannot-commit-routes-without-express-app.js +0 -12
- package/dist/types/src/error/router/cannot-commit-routes-without-express-app.d.ts +0 -3
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { SerializingPlainPropertyWithoutOpenapiShape } from '../../error/openapi/SerializingPlainPropertyWithoutOpenapiShape.js';
|
|
2
|
+
import UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute from '../../error/openapi/UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute.js';
|
|
2
3
|
import OpenapiSegmentExpander from '../body-segment.js';
|
|
3
4
|
import openapiShorthandToOpenapi from './openapiShorthandToOpenapi.js';
|
|
4
|
-
export function dreamColumnOpenapiShape(
|
|
5
|
+
export function dreamColumnOpenapiShape(
|
|
6
|
+
// this is the global name of the serializer or controller calling down
|
|
7
|
+
// to get this information. If an unrecognized db type is provided, the
|
|
8
|
+
// source will be rendered in the exception that is returned, enabling
|
|
9
|
+
// the dev to identify the source of the issue and fix it
|
|
10
|
+
source, dreamClass, column, openapi = undefined, { suppressResponseEnums = false, allowGenericJson = false, } = {}) {
|
|
5
11
|
if (dreamClass.isVirtualColumn(column)) {
|
|
6
12
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
|
|
7
13
|
const openapiObject = openapiShorthandToOpenapi((openapi ?? {}));
|
|
@@ -46,7 +52,7 @@ export function dreamColumnOpenapiShape(dreamClass, column, openapi = undefined,
|
|
|
46
52
|
}
|
|
47
53
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
|
|
48
54
|
const openapiObject = openapiShorthandToOpenapi((openapi ?? {}));
|
|
49
|
-
const singleType = singularAttributeOpenapiShape(dreamColumnInfo, suppressResponseEnums, openapiObject);
|
|
55
|
+
const singleType = singularAttributeOpenapiShape(source, column, dreamColumnInfo, suppressResponseEnums, openapiObject);
|
|
50
56
|
if (dreamColumnInfo.isArray) {
|
|
51
57
|
return {
|
|
52
58
|
type: dreamColumnInfo.allowNull ? ['array', 'null'] : 'array',
|
|
@@ -71,7 +77,12 @@ export function dreamColumnOpenapiShape(dreamClass, column, openapi = undefined,
|
|
|
71
77
|
function baseDbType(dreamColumnInfo) {
|
|
72
78
|
return dreamColumnInfo.dbType.replace('[]', '');
|
|
73
79
|
}
|
|
74
|
-
function singularAttributeOpenapiShape(
|
|
80
|
+
function singularAttributeOpenapiShape(
|
|
81
|
+
// this is the global name of the serializer or controller calling down
|
|
82
|
+
// to get this information. If an unrecognized db type is provided, the
|
|
83
|
+
// source will be rendered in the exception that is returned, enabling
|
|
84
|
+
// the dev to identify the source of the issue and fix it
|
|
85
|
+
source, column, dreamColumnInfo, suppressResponseEnums, openapiSchema) {
|
|
75
86
|
if (dreamColumnInfo.enumValues) {
|
|
76
87
|
const enumOverrides = openapiSchema.enum || dreamColumnInfo.enumValues;
|
|
77
88
|
if (suppressResponseEnums) {
|
|
@@ -102,6 +113,10 @@ function singularAttributeOpenapiShape(dreamColumnInfo, suppressResponseEnums, o
|
|
|
102
113
|
case 'money':
|
|
103
114
|
case 'path':
|
|
104
115
|
case 'text':
|
|
116
|
+
case 'time':
|
|
117
|
+
case 'time without time zone':
|
|
118
|
+
case 'timetz':
|
|
119
|
+
case 'time with time zone':
|
|
105
120
|
case 'uuid':
|
|
106
121
|
case 'varbit':
|
|
107
122
|
case 'varchar':
|
|
@@ -121,8 +136,6 @@ function singularAttributeOpenapiShape(dreamColumnInfo, suppressResponseEnums, o
|
|
|
121
136
|
case 'real':
|
|
122
137
|
return { type: 'number' };
|
|
123
138
|
case 'datetime':
|
|
124
|
-
case 'time':
|
|
125
|
-
case 'time with time zone':
|
|
126
139
|
case 'timestamp':
|
|
127
140
|
case 'timestamp with time zone':
|
|
128
141
|
case 'timestamp without time zone':
|
|
@@ -133,7 +146,7 @@ function singularAttributeOpenapiShape(dreamColumnInfo, suppressResponseEnums, o
|
|
|
133
146
|
case 'jsonb':
|
|
134
147
|
return { type: 'object' };
|
|
135
148
|
default:
|
|
136
|
-
throw new
|
|
149
|
+
throw new UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute(source, column, dreamColumnInfo.dbType);
|
|
137
150
|
}
|
|
138
151
|
}
|
|
139
152
|
export class UseCustomOpenapiForJson extends Error {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @internal
|
|
3
|
+
*
|
|
4
|
+
* Cache for compiled fast-json-stringify functions.
|
|
5
|
+
* Eliminates redundant schema compilation on every request by storing
|
|
6
|
+
* stringify functions keyed by controller, action, openapiName, and status code.
|
|
7
|
+
*/
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
|
+
const _stringifyCache = {};
|
|
10
|
+
/**
|
|
11
|
+
* @internal
|
|
12
|
+
*
|
|
13
|
+
* Retrieves a cached stringify function if it exists.
|
|
14
|
+
*
|
|
15
|
+
* @param cacheKey - The cache key identifying the stringify function
|
|
16
|
+
* @returns The cached stringify function, or undefined if not found
|
|
17
|
+
*/
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
|
+
export function getCachedStringify(cacheKey) {
|
|
20
|
+
return _stringifyCache[cacheKey];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* @internal
|
|
24
|
+
*
|
|
25
|
+
* Stores a compiled stringify function in the cache.
|
|
26
|
+
*
|
|
27
|
+
* @param cacheKey - The cache key identifying the stringify function
|
|
28
|
+
* @param stringifyFn - The compiled fast-json-stringify function to cache
|
|
29
|
+
*/
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
|
+
export function cacheStringify(cacheKey, stringifyFn) {
|
|
32
|
+
_stringifyCache[cacheKey] = stringifyFn;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* @internal
|
|
36
|
+
*
|
|
37
|
+
* Clears a specific stringify function from the cache.
|
|
38
|
+
* Used in test environments to ensure test isolation.
|
|
39
|
+
*
|
|
40
|
+
* @param cacheKey - The cache key identifying the stringify function to clear
|
|
41
|
+
*/
|
|
42
|
+
export function _testOnlyClearStringify(cacheKey) {
|
|
43
|
+
delete _stringifyCache[cacheKey];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* @internal
|
|
47
|
+
*
|
|
48
|
+
* Clears all stringify functions from the cache.
|
|
49
|
+
* Used in test environments to ensure test isolation.
|
|
50
|
+
*/
|
|
51
|
+
export function _testOnlyClearStringifyCache() {
|
|
52
|
+
Object.keys(_stringifyCache).forEach(key => {
|
|
53
|
+
delete _stringifyCache[key];
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @internal
|
|
3
|
+
*
|
|
4
|
+
* Cache for compiled AJV validator functions.
|
|
5
|
+
* Eliminates redundant schema compilation on every request by storing
|
|
6
|
+
* validators keyed by controller, action, openapiName, and validation target.
|
|
7
|
+
*/
|
|
8
|
+
const _validatorCache = {};
|
|
9
|
+
/**
|
|
10
|
+
* @internal
|
|
11
|
+
*
|
|
12
|
+
* Retrieves a cached validator function if it exists.
|
|
13
|
+
*
|
|
14
|
+
* @param cacheKey - The cache key identifying the validator
|
|
15
|
+
* @returns The cached validator function, or undefined if not found
|
|
16
|
+
*/
|
|
17
|
+
export function getCachedValidator(cacheKey) {
|
|
18
|
+
return _validatorCache[cacheKey];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* @internal
|
|
22
|
+
*
|
|
23
|
+
* Stores a compiled validator function in the cache.
|
|
24
|
+
*
|
|
25
|
+
* @param cacheKey - The cache key identifying the validator
|
|
26
|
+
* @param validator - The compiled AJV validator function to cache
|
|
27
|
+
*/
|
|
28
|
+
export function cacheValidator(cacheKey, validator) {
|
|
29
|
+
_validatorCache[cacheKey] = validator;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* @internal
|
|
33
|
+
*
|
|
34
|
+
* Clears a specific validator from the cache.
|
|
35
|
+
* Used in test environments to ensure test isolation.
|
|
36
|
+
*
|
|
37
|
+
* @param cacheKey - The cache key identifying the validator to clear
|
|
38
|
+
*/
|
|
39
|
+
export function _testOnlyClearValidator(cacheKey) {
|
|
40
|
+
delete _validatorCache[cacheKey];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* @internal
|
|
44
|
+
*
|
|
45
|
+
* Clears all validators from the cache.
|
|
46
|
+
* Used in test environments to ensure test isolation.
|
|
47
|
+
*/
|
|
48
|
+
export function _testOnlyClearValidatorCache() {
|
|
49
|
+
Object.keys(_validatorCache).forEach(key => {
|
|
50
|
+
delete _validatorCache[key];
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -32,7 +32,7 @@ importCb) {
|
|
|
32
32
|
* at decoration time such that the class of a property being decorated is only avilable during instance instantiation. In order
|
|
33
33
|
* to only apply static values once, on boot, `globallyInitializingDecorators` is set to true on Dream, and all Dream models are instantiated.
|
|
34
34
|
*/
|
|
35
|
-
new controllerClass({}, {
|
|
35
|
+
new controllerClass({}, { action: 'a' });
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
PsychicController['globallyInitializingDecorators'] = false;
|
|
@@ -250,7 +250,7 @@ Try setting it to something valid, like:
|
|
|
250
250
|
get httpServerOptions() {
|
|
251
251
|
return this._httpServerOptions;
|
|
252
252
|
}
|
|
253
|
-
_corsOptions
|
|
253
|
+
_corsOptions;
|
|
254
254
|
get corsOptions() {
|
|
255
255
|
return this._corsOptions;
|
|
256
256
|
}
|
|
@@ -274,10 +274,6 @@ Try setting it to something valid, like:
|
|
|
274
274
|
get saltRounds() {
|
|
275
275
|
return this._saltRounds;
|
|
276
276
|
}
|
|
277
|
-
_sanitizeResponseJson = false;
|
|
278
|
-
get sanitizeResponseJson() {
|
|
279
|
-
return this._sanitizeResponseJson;
|
|
280
|
-
}
|
|
281
277
|
_packageManager;
|
|
282
278
|
get packageManager() {
|
|
283
279
|
return this._packageManager;
|
|
@@ -427,7 +423,7 @@ Try setting it to something valid, like:
|
|
|
427
423
|
const eventType = pathOrOnOrHandler;
|
|
428
424
|
const handler = maybeHandler;
|
|
429
425
|
const wrappedHandler = (server) => {
|
|
430
|
-
server.
|
|
426
|
+
server.koaApp.use(handler);
|
|
431
427
|
};
|
|
432
428
|
switch (eventType) {
|
|
433
429
|
case 'before-middleware':
|
|
@@ -446,7 +442,7 @@ Try setting it to something valid, like:
|
|
|
446
442
|
}
|
|
447
443
|
else {
|
|
448
444
|
const wrappedHandler = (server) => {
|
|
449
|
-
server.
|
|
445
|
+
server.koaApp.use(pathOrOnOrHandler);
|
|
450
446
|
};
|
|
451
447
|
this.on('server:init:after-middleware', wrappedHandler);
|
|
452
448
|
}
|
|
@@ -541,9 +537,6 @@ Try setting it to something valid, like:
|
|
|
541
537
|
case 'saltRounds':
|
|
542
538
|
this._saltRounds = value;
|
|
543
539
|
break;
|
|
544
|
-
case 'sanitizeResponseJson':
|
|
545
|
-
this._sanitizeResponseJson = value;
|
|
546
|
-
break;
|
|
547
540
|
case 'openapi':
|
|
548
541
|
this._openapi = {
|
|
549
542
|
...this.openapi,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { DataIncompatibleWithDatabaseField, RecordNotFound, ValidationError } from '@rvoh/dream/errors';
|
|
2
2
|
import { camelize } from '@rvoh/dream/utils';
|
|
3
|
-
import
|
|
3
|
+
import KoaRouter from '@koa/router';
|
|
4
4
|
import util, { debuglog } from 'node:util';
|
|
5
5
|
import pluralize from 'pluralize-esm';
|
|
6
6
|
import ParamValidationError from '../error/controller/ParamValidationError.js';
|
|
7
7
|
import ParamValidationErrors from '../error/controller/ParamValidationErrors.js';
|
|
8
8
|
import OpenapiRequestValidationFailure from '../error/openapi/OpenapiRequestValidationFailure.js';
|
|
9
|
-
import
|
|
9
|
+
import CannotCommitRoutesWithoutKoaApp from '../error/router/cannot-commit-routes-without-koa-app.js';
|
|
10
10
|
import EnvInternal from '../helpers/EnvInternal.js';
|
|
11
11
|
import errorIsRescuableHttpError from '../helpers/error/errorIsRescuableHttpError.js';
|
|
12
12
|
import PsychicApp from '../psychic-app/index.js';
|
|
@@ -31,19 +31,25 @@ export default class PsychicRouter {
|
|
|
31
31
|
commit() {
|
|
32
32
|
const app = this.app;
|
|
33
33
|
if (!app)
|
|
34
|
-
throw new
|
|
34
|
+
throw new CannotCommitRoutesWithoutKoaApp();
|
|
35
|
+
const router = new KoaRouter();
|
|
35
36
|
this.routes.forEach(route => {
|
|
36
37
|
if (route.middleware) {
|
|
37
38
|
const routeConf = route;
|
|
38
|
-
|
|
39
|
+
const middlewares = Array.isArray(routeConf.middleware)
|
|
40
|
+
? routeConf.middleware
|
|
41
|
+
: [routeConf.middleware];
|
|
42
|
+
router[routeConf.httpMethod](routePath(routeConf.path), ...middlewares);
|
|
39
43
|
}
|
|
40
44
|
else {
|
|
41
45
|
const routeConf = route;
|
|
42
|
-
|
|
43
|
-
this.handle(routeConf.controller, routeConf.action, {
|
|
46
|
+
router[routeConf.httpMethod](routePath(routeConf.path), async (ctx) => {
|
|
47
|
+
await this.handle(routeConf.controller, routeConf.action, { ctx });
|
|
44
48
|
});
|
|
45
49
|
}
|
|
46
50
|
});
|
|
51
|
+
app.use(router.routes());
|
|
52
|
+
app.use(router.allowedMethods());
|
|
47
53
|
}
|
|
48
54
|
get(path, controller, action) {
|
|
49
55
|
this.crud('get', path, controller, action);
|
|
@@ -72,7 +78,7 @@ export default class PsychicRouter {
|
|
|
72
78
|
this.checkPathForInvalidChars(path);
|
|
73
79
|
const isMiddleware = (typeof controllerOrMiddleware === 'function' || Array.isArray(controllerOrMiddleware)) &&
|
|
74
80
|
!controllerOrMiddleware?.isPsychicController;
|
|
75
|
-
// devs can provide custom
|
|
81
|
+
// devs can provide custom Koa middleware which bypasses
|
|
76
82
|
// the normal Controller#action paradigm.
|
|
77
83
|
if (isMiddleware) {
|
|
78
84
|
this.routeManager.addMiddleware({
|
|
@@ -98,7 +104,7 @@ export default class PsychicRouter {
|
|
|
98
104
|
if (path.includes('{'))
|
|
99
105
|
throw new Error(`
|
|
100
106
|
The provided route "${path}" contains characters that are not supported.
|
|
101
|
-
If you are trying to write a uri param, you will need to use
|
|
107
|
+
If you are trying to write a uri param, you will need to use the
|
|
102
108
|
param syntax, which is a prefixing colon, rather than using brackets
|
|
103
109
|
to surround the param.
|
|
104
110
|
|
|
@@ -236,10 +242,10 @@ suggested fix: "${convertRouteParams(path)}"
|
|
|
236
242
|
* By default, do not provide an attacker with any visibility into which layer
|
|
237
243
|
* of the application rejected their request.
|
|
238
244
|
*/
|
|
239
|
-
async handle(controller, action, {
|
|
240
|
-
const controllerInstance = this._initializeController(controller,
|
|
245
|
+
async handle(controller, action, { ctx, }) {
|
|
246
|
+
const controllerInstance = this._initializeController(controller, ctx, action);
|
|
241
247
|
if (typeof controllerInstance[action] !== 'function') {
|
|
242
|
-
controllerInstance['
|
|
248
|
+
controllerInstance['koaSendStatus'](404);
|
|
243
249
|
return;
|
|
244
250
|
}
|
|
245
251
|
try {
|
|
@@ -250,20 +256,20 @@ suggested fix: "${convertRouteParams(path)}"
|
|
|
250
256
|
if (errorIsRescuableHttpError(err)) {
|
|
251
257
|
const httpErr = err;
|
|
252
258
|
if (httpErr.data) {
|
|
253
|
-
controllerInstance['
|
|
259
|
+
controllerInstance['koaSendJson'](httpErr.data, httpErr.status);
|
|
254
260
|
}
|
|
255
261
|
else {
|
|
256
|
-
controllerInstance['
|
|
262
|
+
controllerInstance['koaSendStatus'](httpErr.status);
|
|
257
263
|
}
|
|
258
264
|
}
|
|
259
265
|
else if (err instanceof RecordNotFound) {
|
|
260
|
-
controllerInstance['
|
|
266
|
+
controllerInstance['koaSendStatus'](404);
|
|
261
267
|
}
|
|
262
268
|
else if (err instanceof DataIncompatibleWithDatabaseField) {
|
|
263
269
|
/**
|
|
264
270
|
* See comment at top of this method for philosophy of 400
|
|
265
271
|
*/
|
|
266
|
-
controllerInstance['
|
|
272
|
+
controllerInstance['koaSendStatus'](400);
|
|
267
273
|
}
|
|
268
274
|
else if (err instanceof ValidationError) {
|
|
269
275
|
if (this.validationErrorLoggingEnabled) {
|
|
@@ -275,7 +281,7 @@ suggested fix: "${convertRouteParams(path)}"
|
|
|
275
281
|
/**
|
|
276
282
|
* See comment at top of this method for philosophy of 400
|
|
277
283
|
*/
|
|
278
|
-
controllerInstance['
|
|
284
|
+
controllerInstance['koaSendStatus'](400);
|
|
279
285
|
}
|
|
280
286
|
else if (err instanceof OpenapiRequestValidationFailure) {
|
|
281
287
|
if (this.validationErrorLoggingEnabled) {
|
|
@@ -288,7 +294,7 @@ suggested fix: "${convertRouteParams(path)}"
|
|
|
288
294
|
/**
|
|
289
295
|
* See comment at top of this method for philosophy of 400
|
|
290
296
|
*/
|
|
291
|
-
controllerInstance['
|
|
297
|
+
controllerInstance['koaSendStatus'](400);
|
|
292
298
|
}
|
|
293
299
|
else if (err instanceof ParamValidationError) {
|
|
294
300
|
if (this.validationErrorLoggingEnabled) {
|
|
@@ -302,7 +308,7 @@ suggested fix: "${convertRouteParams(path)}"
|
|
|
302
308
|
/**
|
|
303
309
|
* See comment at top of this method for philosophy of 400
|
|
304
310
|
*/
|
|
305
|
-
controllerInstance['
|
|
311
|
+
controllerInstance['koaSendStatus'](400);
|
|
306
312
|
}
|
|
307
313
|
else if (err instanceof ParamValidationErrors) {
|
|
308
314
|
if (this.validationErrorLoggingEnabled) {
|
|
@@ -314,14 +320,14 @@ suggested fix: "${convertRouteParams(path)}"
|
|
|
314
320
|
/**
|
|
315
321
|
* See comment at top of this method for philosophy of 400
|
|
316
322
|
*/
|
|
317
|
-
controllerInstance['
|
|
323
|
+
controllerInstance['koaSendStatus'](400);
|
|
318
324
|
}
|
|
319
325
|
else {
|
|
320
326
|
PsychicApp.logWithLevel('error', util.inspect(err, { depth: ERROR_LOGGING_DEPTH }));
|
|
321
327
|
if (PsychicApp.getOrFail().specialHooks.serverError.length) {
|
|
322
328
|
try {
|
|
323
329
|
for (const hook of PsychicApp.getOrFail().specialHooks.serverError) {
|
|
324
|
-
await hook(err,
|
|
330
|
+
await hook(err, ctx);
|
|
325
331
|
}
|
|
326
332
|
}
|
|
327
333
|
catch (error) {
|
|
@@ -348,17 +354,17 @@ suggested fix: "${convertRouteParams(path)}"
|
|
|
348
354
|
}
|
|
349
355
|
}
|
|
350
356
|
}
|
|
351
|
-
_initializeController(ControllerClass,
|
|
352
|
-
return new ControllerClass(
|
|
357
|
+
_initializeController(ControllerClass, ctx, action) {
|
|
358
|
+
return new ControllerClass(ctx, {
|
|
353
359
|
action,
|
|
354
360
|
});
|
|
355
361
|
}
|
|
356
362
|
}
|
|
357
363
|
export class PsychicNestedRouter extends PsychicRouter {
|
|
358
364
|
router;
|
|
359
|
-
constructor(
|
|
360
|
-
super(
|
|
361
|
-
this.router =
|
|
365
|
+
constructor(koaApp, routeManager, { namespaces = [], } = {}) {
|
|
366
|
+
super(koaApp);
|
|
367
|
+
this.router = new KoaRouter();
|
|
362
368
|
this.currentNamespaces = namespaces;
|
|
363
369
|
this.routeManager = routeManager;
|
|
364
370
|
}
|
|
@@ -17,6 +17,7 @@ export default async function startPsychicServer({ app, port, sslCredentials, })
|
|
|
17
17
|
}
|
|
18
18
|
export function createPsychicHttpInstance(app, sslCredentials) {
|
|
19
19
|
const psychicApp = PsychicApp.getOrFail();
|
|
20
|
+
const callback = app.callback();
|
|
20
21
|
if (sslCredentials?.key && sslCredentials?.cert) {
|
|
21
22
|
return https.createServer({
|
|
22
23
|
key: fs.readFileSync(sslCredentials.key),
|
|
@@ -24,10 +25,13 @@ export function createPsychicHttpInstance(app, sslCredentials) {
|
|
|
24
25
|
ca: sslCredentials.ca?.map(filePath => fs.readFileSync(filePath)),
|
|
25
26
|
rejectUnauthorized: sslCredentials?.rejectUnauthorized,
|
|
26
27
|
...psychicApp.httpServerOptions,
|
|
27
|
-
},
|
|
28
|
+
},
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
30
|
+
callback);
|
|
28
31
|
}
|
|
29
32
|
else {
|
|
30
|
-
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
34
|
+
return http.createServer(psychicApp.httpServerOptions, callback);
|
|
31
35
|
}
|
|
32
36
|
}
|
|
33
37
|
function welcomeMessage({ port }) {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { closeAllDbConnections } from '@rvoh/dream/db';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
2
|
+
import cors from '@koa/cors';
|
|
3
|
+
import Koa from 'koa';
|
|
4
|
+
import koaBodyparser from 'koa-bodyparser';
|
|
5
|
+
import conditional from 'koa-conditional-get';
|
|
6
|
+
import etag from 'koa-etag';
|
|
5
7
|
import logIfDevelopment from '../controller/helpers/logIfDevelopment.js';
|
|
6
8
|
import EnvInternal from '../helpers/EnvInternal.js';
|
|
7
9
|
import PsychicApp from '../psychic-app/index.js';
|
|
@@ -15,14 +17,14 @@ export default class PsychicServer {
|
|
|
15
17
|
static createPsychicHttpInstance(app, sslCredentials) {
|
|
16
18
|
return createPsychicHttpInstance(app, sslCredentials);
|
|
17
19
|
}
|
|
18
|
-
|
|
20
|
+
koaApp;
|
|
19
21
|
httpServer;
|
|
20
22
|
booted = false;
|
|
21
23
|
constructor() {
|
|
22
24
|
this.buildApp();
|
|
23
25
|
}
|
|
24
26
|
async routes() {
|
|
25
|
-
const r = new PsychicRouter(this.
|
|
27
|
+
const r = new PsychicRouter(this.koaApp);
|
|
26
28
|
await PsychicApp.getOrFail().routesCb(r);
|
|
27
29
|
return r.routes;
|
|
28
30
|
}
|
|
@@ -31,16 +33,19 @@ export default class PsychicServer {
|
|
|
31
33
|
return;
|
|
32
34
|
const psychicApp = PsychicApp.getOrFail();
|
|
33
35
|
this.setSecureDefaultHeaders();
|
|
34
|
-
this.
|
|
36
|
+
this.koaApp.use(async (ctx, next) => {
|
|
35
37
|
Object.keys(psychicApp.defaultResponseHeaders).forEach(key => {
|
|
36
|
-
|
|
38
|
+
ctx.set(key, psychicApp.defaultResponseHeaders[key]);
|
|
37
39
|
});
|
|
38
|
-
next();
|
|
40
|
+
await next();
|
|
39
41
|
});
|
|
40
42
|
for (const serverInitBeforeMiddlewareHook of PsychicApp.getOrFail().specialHooks
|
|
41
43
|
.serverInitBeforeMiddleware) {
|
|
42
44
|
await serverInitBeforeMiddlewareHook(this);
|
|
43
45
|
}
|
|
46
|
+
// ETag support (Express has this built-in, Koa needs middleware)
|
|
47
|
+
this.koaApp.use(conditional());
|
|
48
|
+
this.koaApp.use(etag());
|
|
44
49
|
this.initializeCors();
|
|
45
50
|
this.initializeJSON();
|
|
46
51
|
try {
|
|
@@ -69,29 +74,23 @@ export default class PsychicServer {
|
|
|
69
74
|
applyNotFoundMiddleware() {
|
|
70
75
|
if (!EnvInternal.isDevelopment)
|
|
71
76
|
return;
|
|
72
|
-
this.
|
|
73
|
-
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// pick up the correct status code.
|
|
80
|
-
if (res.statusCode === 200)
|
|
81
|
-
res.status(404);
|
|
82
|
-
logIfDevelopment({ req, res, startTime: Date.now(), fallbackStatusCode: 404 });
|
|
83
|
-
// call next to let express handle sending the 404
|
|
84
|
-
next();
|
|
77
|
+
this.koaApp.use(async (ctx, next) => {
|
|
78
|
+
await next();
|
|
79
|
+
// Koa defaults to 404 for unmatched routes. If nothing set the body,
|
|
80
|
+
// log the 404 in development.
|
|
81
|
+
if (ctx.status === 404 && !ctx.body) {
|
|
82
|
+
logIfDevelopment({ ctx, startTime: Date.now(), fallbackStatusCode: 404 });
|
|
83
|
+
}
|
|
85
84
|
});
|
|
86
85
|
}
|
|
87
86
|
setSecureDefaultHeaders() {
|
|
88
|
-
|
|
89
|
-
this.
|
|
90
|
-
|
|
87
|
+
// Koa doesn't send x-powered-by by default, no need to disable it.
|
|
88
|
+
this.koaApp.use(async (ctx, next) => {
|
|
89
|
+
ctx.set('X-Content-Type-Options', 'nosniff');
|
|
91
90
|
if (EnvInternal.isProduction) {
|
|
92
|
-
|
|
91
|
+
ctx.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
|
93
92
|
}
|
|
94
|
-
next();
|
|
93
|
+
await next();
|
|
95
94
|
});
|
|
96
95
|
}
|
|
97
96
|
// TODO: use config helper for fetching default port
|
|
@@ -104,7 +103,7 @@ export default class PsychicServer {
|
|
|
104
103
|
}
|
|
105
104
|
else {
|
|
106
105
|
const httpServer = await startPsychicServer({
|
|
107
|
-
app: this.
|
|
106
|
+
app: this.koaApp,
|
|
108
107
|
port: port || psychicApp.port,
|
|
109
108
|
sslCredentials: PsychicApp.getOrFail().sslCredentials,
|
|
110
109
|
});
|
|
@@ -146,26 +145,24 @@ export default class PsychicServer {
|
|
|
146
145
|
await this.boot();
|
|
147
146
|
let server;
|
|
148
147
|
await new Promise(accept => {
|
|
149
|
-
server = this.
|
|
148
|
+
server = this.koaApp.listen(port, () => accept({}));
|
|
150
149
|
});
|
|
151
150
|
await block();
|
|
152
151
|
server.close();
|
|
153
152
|
return true;
|
|
154
153
|
}
|
|
155
154
|
buildApp() {
|
|
156
|
-
this.
|
|
157
|
-
this.expressApp.use(cookieParser.default());
|
|
155
|
+
this.koaApp = new Koa();
|
|
158
156
|
}
|
|
159
157
|
initializeCors() {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
cors.default(PsychicApp.getOrFail().corsOptions));
|
|
158
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
159
|
+
this.koaApp.use(cors(PsychicApp.getOrFail().corsOptions));
|
|
163
160
|
}
|
|
164
161
|
initializeJSON() {
|
|
165
|
-
this.
|
|
162
|
+
this.koaApp.use(koaBodyparser(PsychicApp.getOrFail().jsonOptions));
|
|
166
163
|
}
|
|
167
164
|
async buildRoutes() {
|
|
168
|
-
const r = new PsychicRouter(this.
|
|
165
|
+
const r = new PsychicRouter(this.koaApp);
|
|
169
166
|
await PsychicApp.getOrFail().routesCb(r);
|
|
170
167
|
r.commit();
|
|
171
168
|
}
|