@rvoh/psychic 0.34.4 → 0.35.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/bin/index.js +1 -1
- package/dist/cjs/src/controller/helpers/isPaginatedResult.js +18 -0
- package/dist/cjs/src/controller/index.js +6 -0
- package/dist/cjs/src/generate/helpers/reduxBindings/writeApiFile.js +1 -1
- package/dist/cjs/src/openapi-renderer/endpoint.js +99 -20
- package/dist/cjs/src/openapi-renderer/helpers/pageParamOpenapiProperty.js +9 -0
- package/dist/cjs/src/openapi-renderer/helpers/safelyAttachPaginationParamsToBodySegment.js +35 -0
- package/dist/cjs/src/psychic-app/index.js +10 -35
- package/dist/cjs/src/server/index.js +5 -3
- package/dist/esm/src/bin/index.js +1 -1
- package/dist/esm/src/controller/helpers/isPaginatedResult.js +15 -0
- package/dist/esm/src/controller/index.js +6 -0
- package/dist/esm/src/generate/helpers/reduxBindings/writeApiFile.js +1 -1
- package/dist/esm/src/openapi-renderer/endpoint.js +99 -20
- package/dist/esm/src/openapi-renderer/helpers/pageParamOpenapiProperty.js +6 -0
- package/dist/esm/src/openapi-renderer/helpers/safelyAttachPaginationParamsToBodySegment.js +29 -0
- package/dist/esm/src/psychic-app/index.js +10 -35
- package/dist/esm/src/server/index.js +5 -3
- package/dist/types/src/controller/helpers/isPaginatedResult.d.ts +1 -0
- package/dist/types/src/openapi-renderer/endpoint.d.ts +270 -1
- package/dist/types/src/openapi-renderer/helpers/pageParamOpenapiProperty.d.ts +4 -0
- package/dist/types/src/openapi-renderer/helpers/safelyAttachPaginationParamsToBodySegment.d.ts +15 -0
- package/dist/types/src/psychic-app/index.d.ts +5 -7
- package/dist/types/src/psychic-app/types.d.ts +1 -2
- package/package.json +3 -3
|
@@ -71,7 +71,7 @@ class PsychicBin {
|
|
|
71
71
|
await PsychicBin.syncOpenapiJson();
|
|
72
72
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
73
73
|
let output = {};
|
|
74
|
-
for (const hook of psychicApp.specialHooks.
|
|
74
|
+
for (const hook of psychicApp.specialHooks.cliSync) {
|
|
75
75
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
76
76
|
const res = await hook();
|
|
77
77
|
if ((0, typechecks_js_1.isObject)(res)) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = isPaginatedResult;
|
|
4
|
+
const typechecks_js_1 = require("../../helpers/typechecks.js");
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
|
+
function isPaginatedResult(result) {
|
|
7
|
+
if (!(0, typechecks_js_1.isObject)(result))
|
|
8
|
+
return false;
|
|
9
|
+
const paginatedFields = ['currentPage', 'pageCount', 'recordCount', 'results'];
|
|
10
|
+
const keys = Object.keys(result);
|
|
11
|
+
if (keys.length !== paginatedFields.length)
|
|
12
|
+
return false;
|
|
13
|
+
for (const paginatedField of paginatedFields) {
|
|
14
|
+
if (!keys.includes(paginatedField))
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
@@ -39,6 +39,7 @@ const UnsupportedMediaType_js_1 = __importDefault(require("../error/http/Unsuppo
|
|
|
39
39
|
const params_js_1 = __importDefault(require("../server/params.js"));
|
|
40
40
|
const index_js_1 = __importDefault(require("../session/index.js"));
|
|
41
41
|
const ParamValidationError_js_1 = __importDefault(require("../error/controller/ParamValidationError.js"));
|
|
42
|
+
const isPaginatedResult_js_1 = __importDefault(require("./helpers/isPaginatedResult.js"));
|
|
42
43
|
exports.PsychicParamsPrimitiveLiterals = [
|
|
43
44
|
'bigint',
|
|
44
45
|
'bigint[]',
|
|
@@ -294,6 +295,11 @@ The key in question is: "${serializerKey}"`);
|
|
|
294
295
|
return this.res.json(data.map(d =>
|
|
295
296
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
296
297
|
this.singleObjectJson(d, opts)));
|
|
298
|
+
if ((0, isPaginatedResult_js_1.default)(data))
|
|
299
|
+
return this.res.json({
|
|
300
|
+
...data,
|
|
301
|
+
results: data.results.map(result => this.singleObjectJson(result, opts)),
|
|
302
|
+
});
|
|
297
303
|
return this.res.json(this.singleObjectJson(data, opts));
|
|
298
304
|
}
|
|
299
305
|
defaultSerializerPassthrough = {};
|
|
@@ -15,11 +15,14 @@ const openapiRoute_js_1 = __importDefault(require("./helpers/openapiRoute.js"));
|
|
|
15
15
|
const primitiveOpenapiStatementToOpenapi_js_1 = __importDefault(require("./helpers/primitiveOpenapiStatementToOpenapi.js"));
|
|
16
16
|
const serializer_js_1 = __importDefault(require("./serializer.js"));
|
|
17
17
|
const FailedToLookupSerializerForEndpoint_js_1 = __importDefault(require("../error/openapi/FailedToLookupSerializerForEndpoint.js"));
|
|
18
|
+
const pageParamOpenapiProperty_js_1 = __importDefault(require("./helpers/pageParamOpenapiProperty.js"));
|
|
19
|
+
const safelyAttachPaginationParamsToBodySegment_js_1 = __importDefault(require("./helpers/safelyAttachPaginationParamsToBodySegment.js"));
|
|
18
20
|
class OpenapiEndpointRenderer {
|
|
19
21
|
dreamsOrSerializers;
|
|
20
22
|
controllerClass;
|
|
21
23
|
action;
|
|
22
24
|
many;
|
|
25
|
+
paginate;
|
|
23
26
|
responses;
|
|
24
27
|
serializerKey;
|
|
25
28
|
pathParams;
|
|
@@ -50,13 +53,14 @@ class OpenapiEndpointRenderer {
|
|
|
50
53
|
* const json = JSON.encode(openapiJsonContents, null, 2)
|
|
51
54
|
* ```
|
|
52
55
|
*/
|
|
53
|
-
constructor(dreamsOrSerializers, controllerClass, action, { requestBody, headers, many, query, responses, serializerKey, status, tags, security, pathParams, description, summary, omitDefaultHeaders, omitDefaultResponses, defaultResponse, } = {}) {
|
|
56
|
+
constructor(dreamsOrSerializers, controllerClass, action, { requestBody, headers, many, paginate, query, responses, serializerKey, status, tags, security, pathParams, description, summary, omitDefaultHeaders, omitDefaultResponses, defaultResponse, } = {}) {
|
|
54
57
|
this.dreamsOrSerializers = dreamsOrSerializers;
|
|
55
58
|
this.controllerClass = controllerClass;
|
|
56
59
|
this.action = action;
|
|
57
60
|
this.requestBody = requestBody;
|
|
58
61
|
this.headers = headers;
|
|
59
62
|
this.many = many;
|
|
63
|
+
this.paginate = paginate;
|
|
60
64
|
this.query = query;
|
|
61
65
|
this.responses = responses;
|
|
62
66
|
this.serializerKey = serializerKey;
|
|
@@ -297,7 +301,7 @@ class OpenapiEndpointRenderer {
|
|
|
297
301
|
* "parameters" field for a single endpoint.
|
|
298
302
|
*/
|
|
299
303
|
queryArray(openapiName) {
|
|
300
|
-
|
|
304
|
+
const queryParams = Object.keys(this.query || {}).map((queryName) => {
|
|
301
305
|
const queryParam = this.query[queryName];
|
|
302
306
|
let output = {
|
|
303
307
|
in: 'query',
|
|
@@ -323,7 +327,21 @@ class OpenapiEndpointRenderer {
|
|
|
323
327
|
};
|
|
324
328
|
}
|
|
325
329
|
return output;
|
|
326
|
-
}) || []
|
|
330
|
+
}) || [];
|
|
331
|
+
const paginationName = this.paginate?.query;
|
|
332
|
+
if (paginationName) {
|
|
333
|
+
queryParams.push({
|
|
334
|
+
in: 'query',
|
|
335
|
+
required: false,
|
|
336
|
+
name: paginationName,
|
|
337
|
+
description: 'Page number',
|
|
338
|
+
allowReserved: true,
|
|
339
|
+
schema: {
|
|
340
|
+
type: 'string',
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
return queryParams;
|
|
327
345
|
}
|
|
328
346
|
/**
|
|
329
347
|
* @internal
|
|
@@ -333,17 +351,22 @@ class OpenapiEndpointRenderer {
|
|
|
333
351
|
computedRequestBody(openapiName, processedSchemas, routes) {
|
|
334
352
|
const method = this.computedMethod(routes);
|
|
335
353
|
if (this.requestBody === null)
|
|
336
|
-
return
|
|
354
|
+
return this.defaultRequestBody();
|
|
337
355
|
const httpMethodsThatAllowBody = ['post', 'patch', 'put'];
|
|
338
356
|
if (!httpMethodsThatAllowBody.includes(method))
|
|
339
|
-
return
|
|
357
|
+
return this.defaultRequestBody();
|
|
340
358
|
if (this.shouldAutogenerateBody()) {
|
|
341
359
|
return this.generateRequestBodyForModel(openapiName, processedSchemas);
|
|
342
360
|
}
|
|
343
361
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
344
|
-
|
|
362
|
+
let schema = this.recursivelyParseBody(openapiName, this.requestBody, processedSchemas, {
|
|
345
363
|
target: 'request',
|
|
346
364
|
});
|
|
365
|
+
const bodyPageParam = this.paginate?.body;
|
|
366
|
+
if (bodyPageParam) {
|
|
367
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
368
|
+
schema = (0, safelyAttachPaginationParamsToBodySegment_js_1.default)(bodyPageParam, schema);
|
|
369
|
+
}
|
|
347
370
|
if (!schema)
|
|
348
371
|
return undefined;
|
|
349
372
|
return {
|
|
@@ -355,6 +378,24 @@ class OpenapiEndpointRenderer {
|
|
|
355
378
|
},
|
|
356
379
|
};
|
|
357
380
|
}
|
|
381
|
+
defaultRequestBody() {
|
|
382
|
+
const bodyPageParam = this.paginate?.body;
|
|
383
|
+
if (bodyPageParam) {
|
|
384
|
+
return {
|
|
385
|
+
content: {
|
|
386
|
+
'application/json': {
|
|
387
|
+
schema: {
|
|
388
|
+
type: 'object',
|
|
389
|
+
properties: {
|
|
390
|
+
[bodyPageParam]: (0, pageParamOpenapiProperty_js_1.default)(),
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
358
399
|
/**
|
|
359
400
|
* @internal
|
|
360
401
|
*
|
|
@@ -388,7 +429,7 @@ class OpenapiEndpointRenderer {
|
|
|
388
429
|
const forDreamClass = this.requestBody?.for;
|
|
389
430
|
const dreamClass = forDreamClass || this.getSingleDreamModelClass();
|
|
390
431
|
if (!dreamClass)
|
|
391
|
-
return
|
|
432
|
+
return this.defaultRequestBody();
|
|
392
433
|
let paramSafeColumns = dreamClass.paramSafeColumnsOrFallback();
|
|
393
434
|
const only = this.requestBody?.only;
|
|
394
435
|
if (only) {
|
|
@@ -539,12 +580,17 @@ class OpenapiEndpointRenderer {
|
|
|
539
580
|
}
|
|
540
581
|
}
|
|
541
582
|
}
|
|
583
|
+
let processedSchema = this.recursivelyParseBody(openapiName, paramsShape, processedSchemas, {
|
|
584
|
+
target: 'request',
|
|
585
|
+
});
|
|
586
|
+
const bodyPageParam = this.paginate?.body;
|
|
587
|
+
if (bodyPageParam) {
|
|
588
|
+
processedSchema = (0, safelyAttachPaginationParamsToBodySegment_js_1.default)(bodyPageParam, processedSchema);
|
|
589
|
+
}
|
|
542
590
|
return {
|
|
543
591
|
content: {
|
|
544
592
|
'application/json': {
|
|
545
|
-
schema:
|
|
546
|
-
target: 'request',
|
|
547
|
-
}),
|
|
593
|
+
schema: processedSchema,
|
|
548
594
|
},
|
|
549
595
|
},
|
|
550
596
|
};
|
|
@@ -659,16 +705,9 @@ class OpenapiEndpointRenderer {
|
|
|
659
705
|
const serializerClass = this.getSerializerClasses()[0];
|
|
660
706
|
if (serializerClass === undefined)
|
|
661
707
|
throw new FailedToLookupSerializerForEndpoint_js_1.default(this.controllerClass, this.action);
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
? {
|
|
666
|
-
type: 'array',
|
|
667
|
-
items: serializerObject,
|
|
668
|
-
}
|
|
669
|
-
: this.defaultResponse?.maybeNull
|
|
670
|
-
? { anyOf: [serializerObject, { type: 'null' }] }
|
|
671
|
-
: serializerObject;
|
|
708
|
+
const baseSchema = this.baseSchemaForSerializerObject({
|
|
709
|
+
$ref: `#/components/schemas/${serializerClass.openapiName}`,
|
|
710
|
+
});
|
|
672
711
|
const finalOutput = {
|
|
673
712
|
content: {
|
|
674
713
|
'application/json': {
|
|
@@ -680,6 +719,46 @@ class OpenapiEndpointRenderer {
|
|
|
680
719
|
};
|
|
681
720
|
return finalOutput;
|
|
682
721
|
}
|
|
722
|
+
/**
|
|
723
|
+
* @internal
|
|
724
|
+
*
|
|
725
|
+
* takes a base serializer object (i.e. { $ref: '#/components/schemas/MySerializer' })
|
|
726
|
+
* and transforms it based on the openapi renderer options.
|
|
727
|
+
*
|
|
728
|
+
* if many, it will return an array of this serializer object.
|
|
729
|
+
* if paginate, it will return a pagination object with an array of this serializer object
|
|
730
|
+
* if defaultResponse.maybeNull is set, it will optionally return null as well (only for non-many and non-paginate cases)
|
|
731
|
+
*/
|
|
732
|
+
baseSchemaForSerializerObject(serializerObject) {
|
|
733
|
+
if (this.paginate)
|
|
734
|
+
return {
|
|
735
|
+
type: 'object',
|
|
736
|
+
required: ['recordCount', 'pageCount', 'currentPage', 'results'],
|
|
737
|
+
properties: {
|
|
738
|
+
recordCount: {
|
|
739
|
+
type: 'number',
|
|
740
|
+
},
|
|
741
|
+
pageCount: {
|
|
742
|
+
type: 'number',
|
|
743
|
+
},
|
|
744
|
+
currentPage: {
|
|
745
|
+
type: 'number',
|
|
746
|
+
},
|
|
747
|
+
results: {
|
|
748
|
+
type: 'array',
|
|
749
|
+
items: serializerObject,
|
|
750
|
+
},
|
|
751
|
+
},
|
|
752
|
+
};
|
|
753
|
+
if (this.many)
|
|
754
|
+
return {
|
|
755
|
+
type: 'array',
|
|
756
|
+
items: serializerObject,
|
|
757
|
+
};
|
|
758
|
+
if (this.defaultResponse?.maybeNull)
|
|
759
|
+
return { anyOf: [serializerObject, { type: 'null' }] };
|
|
760
|
+
return serializerObject;
|
|
761
|
+
}
|
|
683
762
|
/**
|
|
684
763
|
* @internal
|
|
685
764
|
*
|
|
@@ -0,0 +1,35 @@
|
|
|
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 = safelyAttachPaginationParamToRequestBodySegment;
|
|
7
|
+
const pageParamOpenapiProperty_js_1 = __importDefault(require("./pageParamOpenapiProperty.js"));
|
|
8
|
+
/**
|
|
9
|
+
* @internal
|
|
10
|
+
*
|
|
11
|
+
* Used to carefully bind implicit pagination params
|
|
12
|
+
* to the requestBody properties. It will not apply
|
|
13
|
+
* the pagination param unless the provided bodySegment
|
|
14
|
+
* is:
|
|
15
|
+
*
|
|
16
|
+
* a.) falsey (null, undefined, etc...)
|
|
17
|
+
* b.) an openapi object (must have { type: 'object'})
|
|
18
|
+
*
|
|
19
|
+
* If neither of these apply, it will simply return
|
|
20
|
+
* what was given to it, without any modifications
|
|
21
|
+
*/
|
|
22
|
+
function safelyAttachPaginationParamToRequestBodySegment(paramName, bodySegment) {
|
|
23
|
+
bodySegment ||= {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {},
|
|
26
|
+
};
|
|
27
|
+
if (bodySegment.type === 'object') {
|
|
28
|
+
;
|
|
29
|
+
bodySegment.properties = {
|
|
30
|
+
...bodySegment.properties,
|
|
31
|
+
[paramName]: (0, pageParamOpenapiProperty_js_1.default)(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return bodySegment;
|
|
35
|
+
}
|
|
@@ -220,19 +220,10 @@ Try setting it to something valid, like:
|
|
|
220
220
|
get inflections() {
|
|
221
221
|
return this._inflections;
|
|
222
222
|
}
|
|
223
|
-
_bootHooks = {
|
|
224
|
-
boot: [],
|
|
225
|
-
load: [],
|
|
226
|
-
'load:dev': [],
|
|
227
|
-
'load:test': [],
|
|
228
|
-
'load:prod': [],
|
|
229
|
-
};
|
|
230
|
-
get bootHooks() {
|
|
231
|
-
return this._bootHooks;
|
|
232
|
-
}
|
|
233
223
|
_specialHooks = {
|
|
234
|
-
|
|
235
|
-
|
|
224
|
+
cliSync: [],
|
|
225
|
+
serverInitBeforeMiddleware: [],
|
|
226
|
+
serverInitAfterMiddleware: [],
|
|
236
227
|
serverInitAfterRoutes: [],
|
|
237
228
|
serverStart: [],
|
|
238
229
|
serverError: [],
|
|
@@ -297,18 +288,6 @@ Try setting it to something valid, like:
|
|
|
297
288
|
if (this.booted && !force)
|
|
298
289
|
return;
|
|
299
290
|
// await new IntegrityChecker().check()
|
|
300
|
-
await this.runHooksFor('load');
|
|
301
|
-
switch (EnvInternal_js_1.default.nodeEnv) {
|
|
302
|
-
case 'development':
|
|
303
|
-
await this.runHooksFor('load:dev');
|
|
304
|
-
break;
|
|
305
|
-
case 'production':
|
|
306
|
-
await this.runHooksFor('load:prod');
|
|
307
|
-
break;
|
|
308
|
-
case 'test':
|
|
309
|
-
await this.runHooksFor('load:test');
|
|
310
|
-
break;
|
|
311
|
-
}
|
|
312
291
|
await this.inflections?.();
|
|
313
292
|
this.booted = true;
|
|
314
293
|
}
|
|
@@ -320,8 +299,11 @@ Try setting it to something valid, like:
|
|
|
320
299
|
case 'server:error':
|
|
321
300
|
this._specialHooks.serverError.push(cb);
|
|
322
301
|
break;
|
|
323
|
-
case 'server:init':
|
|
324
|
-
this._specialHooks.
|
|
302
|
+
case 'server:init:before-middleware':
|
|
303
|
+
this._specialHooks.serverInitBeforeMiddleware.push(cb);
|
|
304
|
+
break;
|
|
305
|
+
case 'server:init:after-middleware':
|
|
306
|
+
this._specialHooks.serverInitAfterMiddleware.push(cb);
|
|
325
307
|
break;
|
|
326
308
|
case 'server:start':
|
|
327
309
|
this._specialHooks.serverStart.push(cb);
|
|
@@ -332,12 +314,10 @@ Try setting it to something valid, like:
|
|
|
332
314
|
case 'server:init:after-routes':
|
|
333
315
|
this._specialHooks.serverInitAfterRoutes.push(cb);
|
|
334
316
|
break;
|
|
335
|
-
case 'sync':
|
|
317
|
+
case 'cli:sync':
|
|
336
318
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
337
|
-
this._specialHooks['
|
|
319
|
+
this._specialHooks['cliSync'].push(cb);
|
|
338
320
|
break;
|
|
339
|
-
default:
|
|
340
|
-
this.bootHooks[hookEventType].push(cb);
|
|
341
321
|
}
|
|
342
322
|
}
|
|
343
323
|
set(option, unknown1, unknown2) {
|
|
@@ -429,11 +409,6 @@ Try setting it to something valid, like:
|
|
|
429
409
|
this.overrides['server:start'] = value;
|
|
430
410
|
}
|
|
431
411
|
}
|
|
432
|
-
async runHooksFor(hookEventType) {
|
|
433
|
-
for (const hook of this.bootHooks[hookEventType]) {
|
|
434
|
-
await hook(this);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
412
|
}
|
|
438
413
|
exports.default = PsychicApp;
|
|
439
414
|
exports.PsychicAppAllowedPackageManagersEnumValues = ['yarn', 'npm', 'pnpm'];
|
|
@@ -73,7 +73,9 @@ class PsychicServer {
|
|
|
73
73
|
});
|
|
74
74
|
next();
|
|
75
75
|
});
|
|
76
|
-
|
|
76
|
+
for (const serverInitBeforeMiddlewareHook of this.config.specialHooks.serverInitBeforeMiddleware) {
|
|
77
|
+
await serverInitBeforeMiddlewareHook(this);
|
|
78
|
+
}
|
|
77
79
|
this.initializeCors();
|
|
78
80
|
this.initializeJSON();
|
|
79
81
|
try {
|
|
@@ -87,8 +89,8 @@ class PsychicServer {
|
|
|
87
89
|
${error.message}
|
|
88
90
|
`);
|
|
89
91
|
}
|
|
90
|
-
for (const
|
|
91
|
-
await
|
|
92
|
+
for (const serverInitAfterMiddlewareHook of this.config.specialHooks.serverInitAfterMiddleware) {
|
|
93
|
+
await serverInitAfterMiddlewareHook(this);
|
|
92
94
|
}
|
|
93
95
|
this.initializeOpenapiValidation();
|
|
94
96
|
await this.buildRoutes();
|
|
@@ -43,7 +43,7 @@ export default class PsychicBin {
|
|
|
43
43
|
await PsychicBin.syncOpenapiJson();
|
|
44
44
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
45
|
let output = {};
|
|
46
|
-
for (const hook of psychicApp.specialHooks.
|
|
46
|
+
for (const hook of psychicApp.specialHooks.cliSync) {
|
|
47
47
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
48
48
|
const res = await hook();
|
|
49
49
|
if (isObject(res)) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { isObject } from '../../helpers/typechecks.js';
|
|
2
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3
|
+
export default function isPaginatedResult(result) {
|
|
4
|
+
if (!isObject(result))
|
|
5
|
+
return false;
|
|
6
|
+
const paginatedFields = ['currentPage', 'pageCount', 'recordCount', 'results'];
|
|
7
|
+
const keys = Object.keys(result);
|
|
8
|
+
if (keys.length !== paginatedFields.length)
|
|
9
|
+
return false;
|
|
10
|
+
for (const paginatedField of paginatedFields) {
|
|
11
|
+
if (!keys.includes(paginatedField))
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
@@ -33,6 +33,7 @@ import HttpStatusUnsupportedMediaType from '../error/http/UnsupportedMediaType.j
|
|
|
33
33
|
import Params from '../server/params.js';
|
|
34
34
|
import Session from '../session/index.js';
|
|
35
35
|
import ParamValidationError from '../error/controller/ParamValidationError.js';
|
|
36
|
+
import isPaginatedResult from './helpers/isPaginatedResult.js';
|
|
36
37
|
export const PsychicParamsPrimitiveLiterals = [
|
|
37
38
|
'bigint',
|
|
38
39
|
'bigint[]',
|
|
@@ -288,6 +289,11 @@ The key in question is: "${serializerKey}"`);
|
|
|
288
289
|
return this.res.json(data.map(d =>
|
|
289
290
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
290
291
|
this.singleObjectJson(d, opts)));
|
|
292
|
+
if (isPaginatedResult(data))
|
|
293
|
+
return this.res.json({
|
|
294
|
+
...data,
|
|
295
|
+
results: data.results.map(result => this.singleObjectJson(result, opts)),
|
|
296
|
+
});
|
|
291
297
|
return this.res.json(this.singleObjectJson(data, opts));
|
|
292
298
|
}
|
|
293
299
|
defaultSerializerPassthrough = {};
|
|
@@ -9,11 +9,14 @@ import openapiRoute from './helpers/openapiRoute.js';
|
|
|
9
9
|
import primitiveOpenapiStatementToOpenapi from './helpers/primitiveOpenapiStatementToOpenapi.js';
|
|
10
10
|
import OpenapiSerializerRenderer from './serializer.js';
|
|
11
11
|
import OpenApiFailedToLookupSerializerForEndpoint from '../error/openapi/FailedToLookupSerializerForEndpoint.js';
|
|
12
|
+
import openapiPageParamProperty from './helpers/pageParamOpenapiProperty.js';
|
|
13
|
+
import safelyAttachPaginationParamToRequestBodySegment from './helpers/safelyAttachPaginationParamsToBodySegment.js';
|
|
12
14
|
export default class OpenapiEndpointRenderer {
|
|
13
15
|
dreamsOrSerializers;
|
|
14
16
|
controllerClass;
|
|
15
17
|
action;
|
|
16
18
|
many;
|
|
19
|
+
paginate;
|
|
17
20
|
responses;
|
|
18
21
|
serializerKey;
|
|
19
22
|
pathParams;
|
|
@@ -44,13 +47,14 @@ export default class OpenapiEndpointRenderer {
|
|
|
44
47
|
* const json = JSON.encode(openapiJsonContents, null, 2)
|
|
45
48
|
* ```
|
|
46
49
|
*/
|
|
47
|
-
constructor(dreamsOrSerializers, controllerClass, action, { requestBody, headers, many, query, responses, serializerKey, status, tags, security, pathParams, description, summary, omitDefaultHeaders, omitDefaultResponses, defaultResponse, } = {}) {
|
|
50
|
+
constructor(dreamsOrSerializers, controllerClass, action, { requestBody, headers, many, paginate, query, responses, serializerKey, status, tags, security, pathParams, description, summary, omitDefaultHeaders, omitDefaultResponses, defaultResponse, } = {}) {
|
|
48
51
|
this.dreamsOrSerializers = dreamsOrSerializers;
|
|
49
52
|
this.controllerClass = controllerClass;
|
|
50
53
|
this.action = action;
|
|
51
54
|
this.requestBody = requestBody;
|
|
52
55
|
this.headers = headers;
|
|
53
56
|
this.many = many;
|
|
57
|
+
this.paginate = paginate;
|
|
54
58
|
this.query = query;
|
|
55
59
|
this.responses = responses;
|
|
56
60
|
this.serializerKey = serializerKey;
|
|
@@ -291,7 +295,7 @@ export default class OpenapiEndpointRenderer {
|
|
|
291
295
|
* "parameters" field for a single endpoint.
|
|
292
296
|
*/
|
|
293
297
|
queryArray(openapiName) {
|
|
294
|
-
|
|
298
|
+
const queryParams = Object.keys(this.query || {}).map((queryName) => {
|
|
295
299
|
const queryParam = this.query[queryName];
|
|
296
300
|
let output = {
|
|
297
301
|
in: 'query',
|
|
@@ -317,7 +321,21 @@ export default class OpenapiEndpointRenderer {
|
|
|
317
321
|
};
|
|
318
322
|
}
|
|
319
323
|
return output;
|
|
320
|
-
}) || []
|
|
324
|
+
}) || [];
|
|
325
|
+
const paginationName = this.paginate?.query;
|
|
326
|
+
if (paginationName) {
|
|
327
|
+
queryParams.push({
|
|
328
|
+
in: 'query',
|
|
329
|
+
required: false,
|
|
330
|
+
name: paginationName,
|
|
331
|
+
description: 'Page number',
|
|
332
|
+
allowReserved: true,
|
|
333
|
+
schema: {
|
|
334
|
+
type: 'string',
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
return queryParams;
|
|
321
339
|
}
|
|
322
340
|
/**
|
|
323
341
|
* @internal
|
|
@@ -327,17 +345,22 @@ export default class OpenapiEndpointRenderer {
|
|
|
327
345
|
computedRequestBody(openapiName, processedSchemas, routes) {
|
|
328
346
|
const method = this.computedMethod(routes);
|
|
329
347
|
if (this.requestBody === null)
|
|
330
|
-
return
|
|
348
|
+
return this.defaultRequestBody();
|
|
331
349
|
const httpMethodsThatAllowBody = ['post', 'patch', 'put'];
|
|
332
350
|
if (!httpMethodsThatAllowBody.includes(method))
|
|
333
|
-
return
|
|
351
|
+
return this.defaultRequestBody();
|
|
334
352
|
if (this.shouldAutogenerateBody()) {
|
|
335
353
|
return this.generateRequestBodyForModel(openapiName, processedSchemas);
|
|
336
354
|
}
|
|
337
355
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
338
|
-
|
|
356
|
+
let schema = this.recursivelyParseBody(openapiName, this.requestBody, processedSchemas, {
|
|
339
357
|
target: 'request',
|
|
340
358
|
});
|
|
359
|
+
const bodyPageParam = this.paginate?.body;
|
|
360
|
+
if (bodyPageParam) {
|
|
361
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
362
|
+
schema = safelyAttachPaginationParamToRequestBodySegment(bodyPageParam, schema);
|
|
363
|
+
}
|
|
341
364
|
if (!schema)
|
|
342
365
|
return undefined;
|
|
343
366
|
return {
|
|
@@ -349,6 +372,24 @@ export default class OpenapiEndpointRenderer {
|
|
|
349
372
|
},
|
|
350
373
|
};
|
|
351
374
|
}
|
|
375
|
+
defaultRequestBody() {
|
|
376
|
+
const bodyPageParam = this.paginate?.body;
|
|
377
|
+
if (bodyPageParam) {
|
|
378
|
+
return {
|
|
379
|
+
content: {
|
|
380
|
+
'application/json': {
|
|
381
|
+
schema: {
|
|
382
|
+
type: 'object',
|
|
383
|
+
properties: {
|
|
384
|
+
[bodyPageParam]: openapiPageParamProperty(),
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
return undefined;
|
|
392
|
+
}
|
|
352
393
|
/**
|
|
353
394
|
* @internal
|
|
354
395
|
*
|
|
@@ -382,7 +423,7 @@ export default class OpenapiEndpointRenderer {
|
|
|
382
423
|
const forDreamClass = this.requestBody?.for;
|
|
383
424
|
const dreamClass = forDreamClass || this.getSingleDreamModelClass();
|
|
384
425
|
if (!dreamClass)
|
|
385
|
-
return
|
|
426
|
+
return this.defaultRequestBody();
|
|
386
427
|
let paramSafeColumns = dreamClass.paramSafeColumnsOrFallback();
|
|
387
428
|
const only = this.requestBody?.only;
|
|
388
429
|
if (only) {
|
|
@@ -533,12 +574,17 @@ export default class OpenapiEndpointRenderer {
|
|
|
533
574
|
}
|
|
534
575
|
}
|
|
535
576
|
}
|
|
577
|
+
let processedSchema = this.recursivelyParseBody(openapiName, paramsShape, processedSchemas, {
|
|
578
|
+
target: 'request',
|
|
579
|
+
});
|
|
580
|
+
const bodyPageParam = this.paginate?.body;
|
|
581
|
+
if (bodyPageParam) {
|
|
582
|
+
processedSchema = safelyAttachPaginationParamToRequestBodySegment(bodyPageParam, processedSchema);
|
|
583
|
+
}
|
|
536
584
|
return {
|
|
537
585
|
content: {
|
|
538
586
|
'application/json': {
|
|
539
|
-
schema:
|
|
540
|
-
target: 'request',
|
|
541
|
-
}),
|
|
587
|
+
schema: processedSchema,
|
|
542
588
|
},
|
|
543
589
|
},
|
|
544
590
|
};
|
|
@@ -653,16 +699,9 @@ export default class OpenapiEndpointRenderer {
|
|
|
653
699
|
const serializerClass = this.getSerializerClasses()[0];
|
|
654
700
|
if (serializerClass === undefined)
|
|
655
701
|
throw new OpenApiFailedToLookupSerializerForEndpoint(this.controllerClass, this.action);
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
? {
|
|
660
|
-
type: 'array',
|
|
661
|
-
items: serializerObject,
|
|
662
|
-
}
|
|
663
|
-
: this.defaultResponse?.maybeNull
|
|
664
|
-
? { anyOf: [serializerObject, { type: 'null' }] }
|
|
665
|
-
: serializerObject;
|
|
702
|
+
const baseSchema = this.baseSchemaForSerializerObject({
|
|
703
|
+
$ref: `#/components/schemas/${serializerClass.openapiName}`,
|
|
704
|
+
});
|
|
666
705
|
const finalOutput = {
|
|
667
706
|
content: {
|
|
668
707
|
'application/json': {
|
|
@@ -674,6 +713,46 @@ export default class OpenapiEndpointRenderer {
|
|
|
674
713
|
};
|
|
675
714
|
return finalOutput;
|
|
676
715
|
}
|
|
716
|
+
/**
|
|
717
|
+
* @internal
|
|
718
|
+
*
|
|
719
|
+
* takes a base serializer object (i.e. { $ref: '#/components/schemas/MySerializer' })
|
|
720
|
+
* and transforms it based on the openapi renderer options.
|
|
721
|
+
*
|
|
722
|
+
* if many, it will return an array of this serializer object.
|
|
723
|
+
* if paginate, it will return a pagination object with an array of this serializer object
|
|
724
|
+
* if defaultResponse.maybeNull is set, it will optionally return null as well (only for non-many and non-paginate cases)
|
|
725
|
+
*/
|
|
726
|
+
baseSchemaForSerializerObject(serializerObject) {
|
|
727
|
+
if (this.paginate)
|
|
728
|
+
return {
|
|
729
|
+
type: 'object',
|
|
730
|
+
required: ['recordCount', 'pageCount', 'currentPage', 'results'],
|
|
731
|
+
properties: {
|
|
732
|
+
recordCount: {
|
|
733
|
+
type: 'number',
|
|
734
|
+
},
|
|
735
|
+
pageCount: {
|
|
736
|
+
type: 'number',
|
|
737
|
+
},
|
|
738
|
+
currentPage: {
|
|
739
|
+
type: 'number',
|
|
740
|
+
},
|
|
741
|
+
results: {
|
|
742
|
+
type: 'array',
|
|
743
|
+
items: serializerObject,
|
|
744
|
+
},
|
|
745
|
+
},
|
|
746
|
+
};
|
|
747
|
+
if (this.many)
|
|
748
|
+
return {
|
|
749
|
+
type: 'array',
|
|
750
|
+
items: serializerObject,
|
|
751
|
+
};
|
|
752
|
+
if (this.defaultResponse?.maybeNull)
|
|
753
|
+
return { anyOf: [serializerObject, { type: 'null' }] };
|
|
754
|
+
return serializerObject;
|
|
755
|
+
}
|
|
677
756
|
/**
|
|
678
757
|
* @internal
|
|
679
758
|
*
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import openapiPageParamProperty from './pageParamOpenapiProperty.js';
|
|
2
|
+
/**
|
|
3
|
+
* @internal
|
|
4
|
+
*
|
|
5
|
+
* Used to carefully bind implicit pagination params
|
|
6
|
+
* to the requestBody properties. It will not apply
|
|
7
|
+
* the pagination param unless the provided bodySegment
|
|
8
|
+
* is:
|
|
9
|
+
*
|
|
10
|
+
* a.) falsey (null, undefined, etc...)
|
|
11
|
+
* b.) an openapi object (must have { type: 'object'})
|
|
12
|
+
*
|
|
13
|
+
* If neither of these apply, it will simply return
|
|
14
|
+
* what was given to it, without any modifications
|
|
15
|
+
*/
|
|
16
|
+
export default function safelyAttachPaginationParamToRequestBodySegment(paramName, bodySegment) {
|
|
17
|
+
bodySegment ||= {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {},
|
|
20
|
+
};
|
|
21
|
+
if (bodySegment.type === 'object') {
|
|
22
|
+
;
|
|
23
|
+
bodySegment.properties = {
|
|
24
|
+
...bodySegment.properties,
|
|
25
|
+
[paramName]: openapiPageParamProperty(),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return bodySegment;
|
|
29
|
+
}
|
|
@@ -191,19 +191,10 @@ Try setting it to something valid, like:
|
|
|
191
191
|
get inflections() {
|
|
192
192
|
return this._inflections;
|
|
193
193
|
}
|
|
194
|
-
_bootHooks = {
|
|
195
|
-
boot: [],
|
|
196
|
-
load: [],
|
|
197
|
-
'load:dev': [],
|
|
198
|
-
'load:test': [],
|
|
199
|
-
'load:prod': [],
|
|
200
|
-
};
|
|
201
|
-
get bootHooks() {
|
|
202
|
-
return this._bootHooks;
|
|
203
|
-
}
|
|
204
194
|
_specialHooks = {
|
|
205
|
-
|
|
206
|
-
|
|
195
|
+
cliSync: [],
|
|
196
|
+
serverInitBeforeMiddleware: [],
|
|
197
|
+
serverInitAfterMiddleware: [],
|
|
207
198
|
serverInitAfterRoutes: [],
|
|
208
199
|
serverStart: [],
|
|
209
200
|
serverError: [],
|
|
@@ -268,18 +259,6 @@ Try setting it to something valid, like:
|
|
|
268
259
|
if (this.booted && !force)
|
|
269
260
|
return;
|
|
270
261
|
// await new IntegrityChecker().check()
|
|
271
|
-
await this.runHooksFor('load');
|
|
272
|
-
switch (EnvInternal.nodeEnv) {
|
|
273
|
-
case 'development':
|
|
274
|
-
await this.runHooksFor('load:dev');
|
|
275
|
-
break;
|
|
276
|
-
case 'production':
|
|
277
|
-
await this.runHooksFor('load:prod');
|
|
278
|
-
break;
|
|
279
|
-
case 'test':
|
|
280
|
-
await this.runHooksFor('load:test');
|
|
281
|
-
break;
|
|
282
|
-
}
|
|
283
262
|
await this.inflections?.();
|
|
284
263
|
this.booted = true;
|
|
285
264
|
}
|
|
@@ -291,8 +270,11 @@ Try setting it to something valid, like:
|
|
|
291
270
|
case 'server:error':
|
|
292
271
|
this._specialHooks.serverError.push(cb);
|
|
293
272
|
break;
|
|
294
|
-
case 'server:init':
|
|
295
|
-
this._specialHooks.
|
|
273
|
+
case 'server:init:before-middleware':
|
|
274
|
+
this._specialHooks.serverInitBeforeMiddleware.push(cb);
|
|
275
|
+
break;
|
|
276
|
+
case 'server:init:after-middleware':
|
|
277
|
+
this._specialHooks.serverInitAfterMiddleware.push(cb);
|
|
296
278
|
break;
|
|
297
279
|
case 'server:start':
|
|
298
280
|
this._specialHooks.serverStart.push(cb);
|
|
@@ -303,12 +285,10 @@ Try setting it to something valid, like:
|
|
|
303
285
|
case 'server:init:after-routes':
|
|
304
286
|
this._specialHooks.serverInitAfterRoutes.push(cb);
|
|
305
287
|
break;
|
|
306
|
-
case 'sync':
|
|
288
|
+
case 'cli:sync':
|
|
307
289
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
308
|
-
this._specialHooks['
|
|
290
|
+
this._specialHooks['cliSync'].push(cb);
|
|
309
291
|
break;
|
|
310
|
-
default:
|
|
311
|
-
this.bootHooks[hookEventType].push(cb);
|
|
312
292
|
}
|
|
313
293
|
}
|
|
314
294
|
set(option, unknown1, unknown2) {
|
|
@@ -400,10 +380,5 @@ Try setting it to something valid, like:
|
|
|
400
380
|
this.overrides['server:start'] = value;
|
|
401
381
|
}
|
|
402
382
|
}
|
|
403
|
-
async runHooksFor(hookEventType) {
|
|
404
|
-
for (const hook of this.bootHooks[hookEventType]) {
|
|
405
|
-
await hook(this);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
383
|
}
|
|
409
384
|
export const PsychicAppAllowedPackageManagersEnumValues = ['yarn', 'npm', 'pnpm'];
|
|
@@ -45,7 +45,9 @@ export default class PsychicServer {
|
|
|
45
45
|
});
|
|
46
46
|
next();
|
|
47
47
|
});
|
|
48
|
-
|
|
48
|
+
for (const serverInitBeforeMiddlewareHook of this.config.specialHooks.serverInitBeforeMiddleware) {
|
|
49
|
+
await serverInitBeforeMiddlewareHook(this);
|
|
50
|
+
}
|
|
49
51
|
this.initializeCors();
|
|
50
52
|
this.initializeJSON();
|
|
51
53
|
try {
|
|
@@ -59,8 +61,8 @@ export default class PsychicServer {
|
|
|
59
61
|
${error.message}
|
|
60
62
|
`);
|
|
61
63
|
}
|
|
62
|
-
for (const
|
|
63
|
-
await
|
|
64
|
+
for (const serverInitAfterMiddlewareHook of this.config.specialHooks.serverInitAfterMiddleware) {
|
|
65
|
+
await serverInitAfterMiddlewareHook(this);
|
|
64
66
|
}
|
|
65
67
|
this.initializeOpenapiValidation();
|
|
66
68
|
await this.buildRoutes();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function isPaginatedResult(result: any): boolean;
|
|
@@ -9,6 +9,7 @@ export default class OpenapiEndpointRenderer<DreamsOrSerializersOrViewModels ext
|
|
|
9
9
|
private controllerClass;
|
|
10
10
|
private action;
|
|
11
11
|
private many;
|
|
12
|
+
private paginate;
|
|
12
13
|
private responses;
|
|
13
14
|
private serializerKey;
|
|
14
15
|
private pathParams;
|
|
@@ -39,7 +40,7 @@ export default class OpenapiEndpointRenderer<DreamsOrSerializersOrViewModels ext
|
|
|
39
40
|
* const json = JSON.encode(openapiJsonContents, null, 2)
|
|
40
41
|
* ```
|
|
41
42
|
*/
|
|
42
|
-
constructor(dreamsOrSerializers: DreamsOrSerializersOrViewModels | null, controllerClass: typeof PsychicController, action: string, { requestBody, headers, many, query, responses, serializerKey, status, tags, security, pathParams, description, summary, omitDefaultHeaders, omitDefaultResponses, defaultResponse, }?: OpenapiEndpointRendererOpts<DreamsOrSerializersOrViewModels>);
|
|
43
|
+
constructor(dreamsOrSerializers: DreamsOrSerializersOrViewModels | null, controllerClass: typeof PsychicController, action: string, { requestBody, headers, many, paginate, query, responses, serializerKey, status, tags, security, pathParams, description, summary, omitDefaultHeaders, omitDefaultResponses, defaultResponse, }?: OpenapiEndpointRendererOpts<DreamsOrSerializersOrViewModels>);
|
|
43
44
|
/**
|
|
44
45
|
* @internal
|
|
45
46
|
*
|
|
@@ -136,6 +137,7 @@ export default class OpenapiEndpointRenderer<DreamsOrSerializersOrViewModels ext
|
|
|
136
137
|
* Generates the requestBody portion of the endpoint
|
|
137
138
|
*/
|
|
138
139
|
private computedRequestBody;
|
|
140
|
+
private defaultRequestBody;
|
|
139
141
|
/**
|
|
140
142
|
* @internal
|
|
141
143
|
*
|
|
@@ -189,6 +191,17 @@ export default class OpenapiEndpointRenderer<DreamsOrSerializersOrViewModels ext
|
|
|
189
191
|
* ```
|
|
190
192
|
*/
|
|
191
193
|
private parseSingleEntitySerializerResponseShape;
|
|
194
|
+
/**
|
|
195
|
+
* @internal
|
|
196
|
+
*
|
|
197
|
+
* takes a base serializer object (i.e. { $ref: '#/components/schemas/MySerializer' })
|
|
198
|
+
* and transforms it based on the openapi renderer options.
|
|
199
|
+
*
|
|
200
|
+
* if many, it will return an array of this serializer object.
|
|
201
|
+
* if paginate, it will return a pagination object with an array of this serializer object
|
|
202
|
+
* if defaultResponse.maybeNull is set, it will optionally return null as well (only for non-many and non-paginate cases)
|
|
203
|
+
*/
|
|
204
|
+
private baseSchemaForSerializerObject;
|
|
192
205
|
/**
|
|
193
206
|
* @internal
|
|
194
207
|
*
|
|
@@ -248,26 +261,282 @@ export declare class MissingControllerActionPairingInRoutes extends Error {
|
|
|
248
261
|
get message(): string;
|
|
249
262
|
}
|
|
250
263
|
export interface OpenapiEndpointRendererOpts<I extends DreamSerializable | DreamSerializableArray | undefined = undefined> {
|
|
264
|
+
/**
|
|
265
|
+
* when true, it will render an openapi document which produces an array of serializables,
|
|
266
|
+
* rather than a single one.
|
|
267
|
+
*/
|
|
251
268
|
many?: boolean;
|
|
269
|
+
/**
|
|
270
|
+
* when true, it will render an openapi document which
|
|
271
|
+
* produces an object containing pagination data. This
|
|
272
|
+
* output is formatted to match the format returned
|
|
273
|
+
* when calling the `paginate` method
|
|
274
|
+
* within Dream, i.e.
|
|
275
|
+
*
|
|
276
|
+
* ```ts
|
|
277
|
+
* \@OpenAPI(Post, {
|
|
278
|
+
* paginate: true,
|
|
279
|
+
* status: 200,
|
|
280
|
+
* })
|
|
281
|
+
* public async index() {
|
|
282
|
+
* const page = this.castParam('page', 'integer')
|
|
283
|
+
* const posts = await Post.where(...).paginate({ pageSize: 100, page })
|
|
284
|
+
* this.ok(posts)
|
|
285
|
+
* }
|
|
286
|
+
* ```
|
|
287
|
+
*/
|
|
288
|
+
paginate?: boolean | CustomPaginationOpts;
|
|
289
|
+
/**
|
|
290
|
+
* specify path params. This is usually not necessary, since path params
|
|
291
|
+
* are automatically computed for your endpoint by matching against the
|
|
292
|
+
* corresponding routes.ts entry.
|
|
293
|
+
*
|
|
294
|
+
* ```ts
|
|
295
|
+
* \@OpenAPI(User, {
|
|
296
|
+
* status: 204,
|
|
297
|
+
* pathParams: { id: { description: 'The ID of the User' } },
|
|
298
|
+
* })
|
|
299
|
+
* ```
|
|
300
|
+
*/
|
|
252
301
|
pathParams?: OpenapiPathParams;
|
|
302
|
+
/**
|
|
303
|
+
* specify additional headers for this request
|
|
304
|
+
*
|
|
305
|
+
* ```ts
|
|
306
|
+
* \@OpenAPI(User, {
|
|
307
|
+
* status: 204,
|
|
308
|
+
* headers: {
|
|
309
|
+
* myDate: {
|
|
310
|
+
* required: true,
|
|
311
|
+
* description: 'my date',
|
|
312
|
+
* format: 'date'
|
|
313
|
+
* }
|
|
314
|
+
* }
|
|
315
|
+
* })
|
|
316
|
+
* ```
|
|
317
|
+
*/
|
|
253
318
|
headers?: OpenapiHeaders;
|
|
319
|
+
/**
|
|
320
|
+
* specify query parameters for the request
|
|
321
|
+
*
|
|
322
|
+
* ```ts
|
|
323
|
+
* \@OpenAPI(User, {
|
|
324
|
+
* status: 204,
|
|
325
|
+
* query: {
|
|
326
|
+
* search: {
|
|
327
|
+
* required: false,
|
|
328
|
+
* description: 'my query param',
|
|
329
|
+
* schema: {
|
|
330
|
+
* type: 'string',
|
|
331
|
+
* },
|
|
332
|
+
* allowReserved: true,
|
|
333
|
+
* allowEmptyValue: true,
|
|
334
|
+
* },
|
|
335
|
+
* },
|
|
336
|
+
* })
|
|
337
|
+
* ```
|
|
338
|
+
*/
|
|
254
339
|
query?: OpenapiQueries;
|
|
340
|
+
/**
|
|
341
|
+
* specify the shape of the request body. For POST, PATCH, and PUT,
|
|
342
|
+
* this will automatically be done for you if you provide a Dream
|
|
343
|
+
* class as the first argument. Otherwise, you can specify this
|
|
344
|
+
* manually, like so:
|
|
345
|
+
*
|
|
346
|
+
* ```ts
|
|
347
|
+
* \@OpenAPI(User, {
|
|
348
|
+
* status: 204,
|
|
349
|
+
* requestBody: {
|
|
350
|
+
* type: 'object',
|
|
351
|
+
* properties: {
|
|
352
|
+
* myFiend: 'string',
|
|
353
|
+
* }
|
|
354
|
+
* }
|
|
355
|
+
* })
|
|
356
|
+
* ```
|
|
357
|
+
*/
|
|
255
358
|
requestBody?: OpenapiSchemaBodyShorthand | OpenapiSchemaRequestBodyOnlyOption | null;
|
|
359
|
+
/**
|
|
360
|
+
* an array of tag names you wish to apply to this endpoint.
|
|
361
|
+
* tag names will determine placement of this request within
|
|
362
|
+
* the swagger editor, which is used for previewing openapi
|
|
363
|
+
* documents.
|
|
364
|
+
*
|
|
365
|
+
* ```ts
|
|
366
|
+
* \@OpenAPI(User, {
|
|
367
|
+
* status: 204,
|
|
368
|
+
* tags: ['users']
|
|
369
|
+
* })
|
|
370
|
+
* ```
|
|
371
|
+
*/
|
|
256
372
|
tags?: string[];
|
|
373
|
+
/**
|
|
374
|
+
* The top-level description for this endpoint.
|
|
375
|
+
*
|
|
376
|
+
* ```ts
|
|
377
|
+
* \@OpenAPI(User, {
|
|
378
|
+
* status: 204,
|
|
379
|
+
* description: 'my endpoint description'
|
|
380
|
+
* })
|
|
381
|
+
* ```
|
|
382
|
+
*/
|
|
257
383
|
description?: string;
|
|
384
|
+
/**
|
|
385
|
+
* The top-level summary for this endpoint.
|
|
386
|
+
*
|
|
387
|
+
* ```ts
|
|
388
|
+
* \@OpenAPI(User, {
|
|
389
|
+
* status: 204,
|
|
390
|
+
* summary: 'my endpoint summary'
|
|
391
|
+
* })
|
|
392
|
+
* ```
|
|
393
|
+
*/
|
|
258
394
|
summary?: string;
|
|
395
|
+
/**
|
|
396
|
+
* which security scheme to use for this endpoint.
|
|
397
|
+
*
|
|
398
|
+
* ```ts
|
|
399
|
+
* \@OpenAPI(User, {
|
|
400
|
+
* status: 204,
|
|
401
|
+
* security: { customAuth: [] },
|
|
402
|
+
* })
|
|
403
|
+
* ```
|
|
404
|
+
*/
|
|
259
405
|
security?: OpenapiSecurity;
|
|
406
|
+
/**
|
|
407
|
+
* a list of responses to provide for this endpoint. Only
|
|
408
|
+
* provide this if you intend to override the automatic
|
|
409
|
+
* response generation mechanisms built into Psychic.
|
|
410
|
+
*
|
|
411
|
+
* ```ts
|
|
412
|
+
* \@OpenAPI(User, {
|
|
413
|
+
* status: 201,
|
|
414
|
+
* responses: {
|
|
415
|
+
* 201: {
|
|
416
|
+
* type: 'string'
|
|
417
|
+
* }
|
|
418
|
+
* }
|
|
419
|
+
* })
|
|
420
|
+
* ```
|
|
421
|
+
*/
|
|
260
422
|
responses?: Partial<Record<HttpStatusCode, (OpenapiSchemaBodyShorthand & {
|
|
261
423
|
description?: string;
|
|
262
424
|
}) | {
|
|
263
425
|
description: string;
|
|
264
426
|
}>>;
|
|
427
|
+
/**
|
|
428
|
+
* enables you to augment the default response that Psychic
|
|
429
|
+
* renders. This enables you to allow Psychic to mix in your
|
|
430
|
+
* custom options with the default response it provides.
|
|
431
|
+
*
|
|
432
|
+
* ```ts
|
|
433
|
+
* \@OpenAPI(User, {
|
|
434
|
+
* status: 201,
|
|
435
|
+
* defaultResponse: {
|
|
436
|
+
* description: 'my custom description',
|
|
437
|
+
* }
|
|
438
|
+
* })
|
|
439
|
+
* ```
|
|
440
|
+
*/
|
|
265
441
|
defaultResponse?: OpenapiEndpointRendererDefaultResponseOption;
|
|
442
|
+
/**
|
|
443
|
+
* provide a custom serializerKey to specify which
|
|
444
|
+
* serializer should be used when rendering your
|
|
445
|
+
* serializable entity, i.e.
|
|
446
|
+
*
|
|
447
|
+
* ```ts
|
|
448
|
+
* \@OpenAPI(User, {
|
|
449
|
+
* status: 201,
|
|
450
|
+
* many: true,
|
|
451
|
+
* serializerKey: 'summary'
|
|
452
|
+
* })
|
|
453
|
+
* ```
|
|
454
|
+
*/
|
|
266
455
|
serializerKey?: I extends undefined ? never : I extends DreamSerializableArray ? DreamOrViewModelClassSerializerArrayKeys<I> : I extends typeof Dream | ViewModelClass ? DreamOrViewModelClassSerializerKey<I> : never;
|
|
456
|
+
/**
|
|
457
|
+
* the status code that your endpoint will render
|
|
458
|
+
* when it succeeds.
|
|
459
|
+
*
|
|
460
|
+
* ```ts
|
|
461
|
+
* \@OpenAPI(User, {
|
|
462
|
+
* status: 201,
|
|
463
|
+
* })
|
|
464
|
+
* ```
|
|
465
|
+
*/
|
|
267
466
|
status?: HttpStatusCodeNumber;
|
|
467
|
+
/**
|
|
468
|
+
* Whether or not to omit the default headers provided
|
|
469
|
+
* by your Psychic and your application. When set to true,
|
|
470
|
+
* you will have to manually specify all headers that
|
|
471
|
+
* are required for this endpoint.
|
|
472
|
+
*
|
|
473
|
+
* ```ts
|
|
474
|
+
* \@OpenAPI(User, {
|
|
475
|
+
* omitDefaultHeaders: true
|
|
476
|
+
* })
|
|
477
|
+
* ```
|
|
478
|
+
*/
|
|
268
479
|
omitDefaultHeaders?: boolean;
|
|
480
|
+
/**
|
|
481
|
+
* Whether or not to omit the default response provided
|
|
482
|
+
* by your Psychic and your application. When set to true,
|
|
483
|
+
* you will have to manually specify the response shape.
|
|
484
|
+
*
|
|
485
|
+
* ```ts
|
|
486
|
+
* \@OpenAPI(User, {
|
|
487
|
+
* omitDefaultResponses: true
|
|
488
|
+
* })
|
|
489
|
+
* ```
|
|
490
|
+
*/
|
|
269
491
|
omitDefaultResponses?: boolean;
|
|
270
492
|
}
|
|
493
|
+
export type CustomPaginationOpts = {
|
|
494
|
+
/**
|
|
495
|
+
* if provided, a query param will be added to the
|
|
496
|
+
* openapi spec with the name provided.
|
|
497
|
+
*
|
|
498
|
+
* ```ts
|
|
499
|
+
* @OpenAPI(Post, {
|
|
500
|
+
* paginate: {
|
|
501
|
+
* query: 'page'
|
|
502
|
+
* },
|
|
503
|
+
* })
|
|
504
|
+
* public async index() {
|
|
505
|
+
* this.ok(
|
|
506
|
+
* await this.currentUser
|
|
507
|
+
* .associationQuery('posts')
|
|
508
|
+
* .paginate({
|
|
509
|
+
* page: this.castParam('page', 'integer')
|
|
510
|
+
* })
|
|
511
|
+
* )
|
|
512
|
+
* }
|
|
513
|
+
* ```
|
|
514
|
+
*/
|
|
515
|
+
query: string;
|
|
516
|
+
} | {
|
|
517
|
+
/**
|
|
518
|
+
* if provided, a requestBody field will be added
|
|
519
|
+
* to the openapi spec with the name provided.
|
|
520
|
+
*
|
|
521
|
+
* ```ts
|
|
522
|
+
* @OpenAPI(Post, {
|
|
523
|
+
* paginate: {
|
|
524
|
+
* body: 'page'
|
|
525
|
+
* },
|
|
526
|
+
* })
|
|
527
|
+
* public async index() {
|
|
528
|
+
* this.ok(
|
|
529
|
+
* await this.currentUser
|
|
530
|
+
* .associationQuery('posts')
|
|
531
|
+
* .paginate({
|
|
532
|
+
* page: this.castParam('page', 'integer')
|
|
533
|
+
* })
|
|
534
|
+
* )
|
|
535
|
+
* }
|
|
536
|
+
* ```
|
|
537
|
+
*/
|
|
538
|
+
body: string;
|
|
539
|
+
};
|
|
271
540
|
export interface OpenapiEndpointRendererDefaultResponseOption {
|
|
272
541
|
description?: string;
|
|
273
542
|
maybeNull?: boolean;
|
package/dist/types/src/openapi-renderer/helpers/safelyAttachPaginationParamsToBodySegment.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @internal
|
|
3
|
+
*
|
|
4
|
+
* Used to carefully bind implicit pagination params
|
|
5
|
+
* to the requestBody properties. It will not apply
|
|
6
|
+
* the pagination param unless the provided bodySegment
|
|
7
|
+
* is:
|
|
8
|
+
*
|
|
9
|
+
* a.) falsey (null, undefined, etc...)
|
|
10
|
+
* b.) an openapi object (must have { type: 'object'})
|
|
11
|
+
*
|
|
12
|
+
* If neither of these apply, it will simply return
|
|
13
|
+
* what was given to it, without any modifications
|
|
14
|
+
*/
|
|
15
|
+
export default function safelyAttachPaginationParamToRequestBodySegment<T>(paramName: string, bodySegment: T): T;
|
|
@@ -7,7 +7,7 @@ import * as http from 'node:http';
|
|
|
7
7
|
import { OpenapiContent, OpenapiHeaders, OpenapiResponses, OpenapiSecurity, OpenapiSecuritySchemes, OpenapiServer } from '../openapi-renderer/endpoint.js';
|
|
8
8
|
import PsychicRouter from '../router/index.js';
|
|
9
9
|
import PsychicServer from '../server/index.js';
|
|
10
|
-
import { PsychicHookEventType
|
|
10
|
+
import { PsychicHookEventType } from './types.js';
|
|
11
11
|
import { Command } from 'commander';
|
|
12
12
|
export default class PsychicApp {
|
|
13
13
|
static init(cb: (app: PsychicApp) => void | Promise<void>, dreamCb: (app: DreamApp) => void | Promise<void>, opts?: PsychicAppInitOptions): Promise<PsychicApp>;
|
|
@@ -70,8 +70,6 @@ export default class PsychicApp {
|
|
|
70
70
|
get paths(): Required<PsychicPathOptions>;
|
|
71
71
|
private _inflections?;
|
|
72
72
|
get inflections(): (() => void | Promise<void>) | undefined;
|
|
73
|
-
private _bootHooks;
|
|
74
|
-
get bootHooks(): Record<PsychicHookLoadEventTypes, ((conf: PsychicApp) => void | Promise<void>)[]>;
|
|
75
73
|
private _specialHooks;
|
|
76
74
|
get specialHooks(): PsychicAppSpecialHooks;
|
|
77
75
|
private _overrides;
|
|
@@ -93,18 +91,18 @@ export default class PsychicApp {
|
|
|
93
91
|
private booted;
|
|
94
92
|
boot(force?: boolean): Promise<void>;
|
|
95
93
|
plugin(cb: (app: PsychicApp) => void | Promise<void>): void;
|
|
96
|
-
on<T extends PsychicHookEventType>(hookEventType: T, cb: T extends 'server:error' ? (err: Error, req: Request, res: Response) => void | Promise<void> : T extends 'server:init' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:start' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:shutdown' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:init:after-routes' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'cli:start' ? (program: Command) => void | Promise<void> : T extends 'sync' ? () => any : (conf: PsychicApp) => void | Promise<void>): void;
|
|
94
|
+
on<T extends PsychicHookEventType>(hookEventType: T, cb: T extends 'server:error' ? (err: Error, req: Request, res: Response) => void | Promise<void> : T extends 'server:init:before-middleware' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:init:after-middleware' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:start' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:shutdown' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:init:after-routes' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'cli:start' ? (program: Command) => void | Promise<void> : T extends 'cli:sync' ? () => any : (conf: PsychicApp) => void | Promise<void>): void;
|
|
97
95
|
set(option: 'openapi', name: string, value: NamedPsychicOpenapiOptions): void;
|
|
98
96
|
set<Opt extends PsychicAppOption>(option: Opt, value: Opt extends 'appName' ? string : Opt extends 'apiOnly' ? boolean : Opt extends 'defaultResponseHeaders' ? Record<string, string | null> : Opt extends 'encryption' ? PsychicAppEncryptionOptions : Opt extends 'cors' ? CorsOptions : Opt extends 'cookie' ? CustomCookieOptions : Opt extends 'apiRoot' ? string : Opt extends 'sessionCookieName' ? string : Opt extends 'clientRoot' ? string : Opt extends 'json' ? bodyParser.Options : Opt extends 'logger' ? PsychicLogger : Opt extends 'client' ? PsychicClientOptions : Opt extends 'ssl' ? PsychicSslCredentials : Opt extends 'openapi' ? DefaultPsychicOpenapiOptions : Opt extends 'paths' ? PsychicPathOptions : Opt extends 'port' ? number : Opt extends 'saltRounds' ? number : Opt extends 'packageManager' ? PsychicAppAllowedPackageManagersEnum : Opt extends 'inflections' ? () => void | Promise<void> : Opt extends 'routes' ? (r: PsychicRouter) => void | Promise<void> : never): void;
|
|
99
97
|
override<Override extends keyof PsychicAppOverrides>(override: Override, value: PsychicAppOverrides[Override]): void;
|
|
100
|
-
private runHooksFor;
|
|
101
98
|
}
|
|
102
99
|
export type PsychicAppOption = 'appName' | 'apiOnly' | 'apiRoot' | 'encryption' | 'sessionCookieName' | 'client' | 'clientRoot' | 'cookie' | 'cors' | 'defaultResponseHeaders' | 'inflections' | 'json' | 'logger' | 'openapi' | 'packageManager' | 'paths' | 'port' | 'routes' | 'saltRounds' | 'ssl';
|
|
103
100
|
export declare const PsychicAppAllowedPackageManagersEnumValues: readonly ["yarn", "npm", "pnpm"];
|
|
104
101
|
export type PsychicAppAllowedPackageManagersEnum = (typeof PsychicAppAllowedPackageManagersEnumValues)[number];
|
|
105
102
|
export interface PsychicAppSpecialHooks {
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
cliSync: (() => any)[];
|
|
104
|
+
serverInitBeforeMiddleware: ((server: PsychicServer) => void | Promise<void>)[];
|
|
105
|
+
serverInitAfterMiddleware: ((server: PsychicServer) => void | Promise<void>)[];
|
|
108
106
|
serverInitAfterRoutes: ((server: PsychicServer) => void | Promise<void>)[];
|
|
109
107
|
serverStart: ((server: PsychicServer) => void | Promise<void>)[];
|
|
110
108
|
serverShutdown: ((server: PsychicServer) => void | Promise<void>)[];
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import PsychicApp from './index.js';
|
|
2
2
|
export type UUID = string;
|
|
3
|
-
export type PsychicHookEventType = '
|
|
4
|
-
export type PsychicHookLoadEventTypes = Exclude<PsychicHookEventType, 'server:error' | 'server:init' | 'server:init:after-routes' | 'server:start' | 'server:shutdown' | 'sync'>;
|
|
3
|
+
export type PsychicHookEventType = 'cli:sync' | 'server:init:before-middleware' | 'server:init:after-middleware' | 'server:init:after-routes' | 'server:start' | 'server:error' | 'server:shutdown';
|
|
5
4
|
export type PsychicAppInitializerCb = (psychicApp: PsychicApp) => void | Promise<void>;
|
|
6
5
|
type Only<T, U> = T & Partial<Record<Exclude<keyof U, keyof T>, never>>;
|
|
7
6
|
export type Either<T, U> = Only<T, U> | Only<U, T>;
|
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.35.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.42.
|
|
63
|
+
"@rvoh/dream": "^0.42.3",
|
|
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",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"typedoc": "^0.26.6",
|
|
84
84
|
"typescript": "^5.5.4",
|
|
85
85
|
"typescript-eslint": "=7.18.0",
|
|
86
|
-
"vitest": "^3.1.
|
|
86
|
+
"vitest": "^3.1.3",
|
|
87
87
|
"winston": "^3.14.2"
|
|
88
88
|
},
|
|
89
89
|
"packageManager": "yarn@4.7.0"
|