@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.
Files changed (25) hide show
  1. package/dist/cjs/src/bin/index.js +1 -1
  2. package/dist/cjs/src/controller/helpers/isPaginatedResult.js +18 -0
  3. package/dist/cjs/src/controller/index.js +6 -0
  4. package/dist/cjs/src/generate/helpers/reduxBindings/writeApiFile.js +1 -1
  5. package/dist/cjs/src/openapi-renderer/endpoint.js +99 -20
  6. package/dist/cjs/src/openapi-renderer/helpers/pageParamOpenapiProperty.js +9 -0
  7. package/dist/cjs/src/openapi-renderer/helpers/safelyAttachPaginationParamsToBodySegment.js +35 -0
  8. package/dist/cjs/src/psychic-app/index.js +10 -35
  9. package/dist/cjs/src/server/index.js +5 -3
  10. package/dist/esm/src/bin/index.js +1 -1
  11. package/dist/esm/src/controller/helpers/isPaginatedResult.js +15 -0
  12. package/dist/esm/src/controller/index.js +6 -0
  13. package/dist/esm/src/generate/helpers/reduxBindings/writeApiFile.js +1 -1
  14. package/dist/esm/src/openapi-renderer/endpoint.js +99 -20
  15. package/dist/esm/src/openapi-renderer/helpers/pageParamOpenapiProperty.js +6 -0
  16. package/dist/esm/src/openapi-renderer/helpers/safelyAttachPaginationParamsToBodySegment.js +29 -0
  17. package/dist/esm/src/psychic-app/index.js +10 -35
  18. package/dist/esm/src/server/index.js +5 -3
  19. package/dist/types/src/controller/helpers/isPaginatedResult.d.ts +1 -0
  20. package/dist/types/src/openapi-renderer/endpoint.d.ts +270 -1
  21. package/dist/types/src/openapi-renderer/helpers/pageParamOpenapiProperty.d.ts +4 -0
  22. package/dist/types/src/openapi-renderer/helpers/safelyAttachPaginationParamsToBodySegment.d.ts +15 -0
  23. package/dist/types/src/psychic-app/index.d.ts +5 -7
  24. package/dist/types/src/psychic-app/types.d.ts +1 -2
  25. 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.sync) {
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 = {};
@@ -65,7 +65,7 @@ export const ${apiImport} = createApi({
65
65
  keepUnusedDataFor: 0,
66
66
 
67
67
  baseQuery: fetchBaseQuery({
68
- baseUrl: baseURL(),
68
+ baseUrl: baseUrl(),
69
69
  credentials: 'include',
70
70
 
71
71
  // we recommend that you use a function like this for preparing
@@ -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
- return (Object.keys(this.query || {}).map((queryName) => {
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 undefined;
354
+ return this.defaultRequestBody();
337
355
  const httpMethodsThatAllowBody = ['post', 'patch', 'put'];
338
356
  if (!httpMethodsThatAllowBody.includes(method))
339
- return undefined;
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
- const schema = this.recursivelyParseBody(openapiName, this.requestBody, processedSchemas, {
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 undefined;
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: this.recursivelyParseBody(openapiName, paramsShape, processedSchemas, {
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 serializerKey = serializerClass.openapiName;
663
- const serializerObject = { $ref: `#/components/schemas/${serializerKey}` };
664
- const baseSchema = this.many
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,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = openapiPageParamProperty;
4
+ function openapiPageParamProperty() {
5
+ return {
6
+ type: 'integer',
7
+ description: 'Page number',
8
+ };
9
+ }
@@ -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
- sync: [],
235
- serverInit: [],
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.serverInit.push(cb);
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['sync'].push(cb);
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
- await this.config['runHooksFor']('boot');
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 expressInitHook of this.config.specialHooks.serverInit) {
91
- await expressInitHook(this);
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.sync) {
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 = {};
@@ -39,7 +39,7 @@ export const ${apiImport} = createApi({
39
39
  keepUnusedDataFor: 0,
40
40
 
41
41
  baseQuery: fetchBaseQuery({
42
- baseUrl: baseURL(),
42
+ baseUrl: baseUrl(),
43
43
  credentials: 'include',
44
44
 
45
45
  // we recommend that you use a function like this for preparing
@@ -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
- return (Object.keys(this.query || {}).map((queryName) => {
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 undefined;
348
+ return this.defaultRequestBody();
331
349
  const httpMethodsThatAllowBody = ['post', 'patch', 'put'];
332
350
  if (!httpMethodsThatAllowBody.includes(method))
333
- return undefined;
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
- const schema = this.recursivelyParseBody(openapiName, this.requestBody, processedSchemas, {
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 undefined;
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: this.recursivelyParseBody(openapiName, paramsShape, processedSchemas, {
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 serializerKey = serializerClass.openapiName;
657
- const serializerObject = { $ref: `#/components/schemas/${serializerKey}` };
658
- const baseSchema = this.many
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,6 @@
1
+ export default function openapiPageParamProperty() {
2
+ return {
3
+ type: 'integer',
4
+ description: 'Page number',
5
+ };
6
+ }
@@ -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
- sync: [],
206
- serverInit: [],
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.serverInit.push(cb);
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['sync'].push(cb);
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
- await this.config['runHooksFor']('boot');
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 expressInitHook of this.config.specialHooks.serverInit) {
63
- await expressInitHook(this);
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;
@@ -0,0 +1,4 @@
1
+ export default function openapiPageParamProperty(): {
2
+ readonly type: "integer";
3
+ readonly description: "Page number";
4
+ };
@@ -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, PsychicHookLoadEventTypes } from './types.js';
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
- sync: (() => any)[];
107
- serverInit: ((server: PsychicServer) => void | Promise<void>)[];
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 = 'boot' | 'sync' | 'load' | 'load:dev' | 'load:prod' | 'load:test' | 'server:init' | 'server:init:after-routes' | 'server:start' | 'server:error' | 'server:shutdown';
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.34.4",
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.0",
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.1",
86
+ "vitest": "^3.1.3",
87
87
  "winston": "^3.14.2"
88
88
  },
89
89
  "packageManager": "yarn@4.7.0"