@rvoh/psychic 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -0,0 +1,41 @@
1
+ ## v1.1.0
2
+
3
+ Provides easier access to express middleware by exposing `PsychicApp#use`, which enables a developer to provide express middleware directly through the psychcic application, without tapping into any hooks.
4
+
5
+ ```ts
6
+ psy.use((_, res) => {
7
+ res.send(
8
+ 'this will be run after psychic middleware (i.e. cors and bodyParser) are processed, but before routes are processed',
9
+ )
10
+ })
11
+ ```
12
+
13
+ Some middleware needs to be run before other middleware, so we expose an optional first argument which can be provided so explicitly send your middleware into express at various stages of the psychic configuration process. For example, to inject your middleware before cors and bodyParser are configured, provide `before-middleware` as the first argument. To initialize your middleware after the psychic default middleware, but before your routes have been processed, provide `after-middleware` as the first argument (or simply provide a callback function directly, since this is the default). To run after routes have been processed, provide `after-routes` as the first argument.
14
+
15
+ ```ts
16
+ psy.use('before-middleware', (_, res) => {
17
+ res.send('this will be run before psychic has configured any default middleware')
18
+ })
19
+
20
+ psy.use('after-middleware', (_, res) => {
21
+ res.send('this will be run after psychic has configured default middleware')
22
+ })
23
+
24
+ psy.use('after-routes', (_, res) => {
25
+ res.send('this will be run after psychic has processed all the routes in your conf/routes.ts file')
26
+ })
27
+ ```
28
+
29
+ Additionally, a new overload has been added to all CRUD methods on the PsychicRouter class, enabling you to provide RequestHandler middleware directly to psychic, like so:
30
+
31
+ ```ts
32
+ // conf/routes.ts
33
+
34
+ r.get('helloworld', (req, res, next) => {
35
+ res.json({ hello: 'world' })
36
+ })
37
+ ```
38
+
39
+ ## 1.1.1
40
+
41
+ Fix route printing regression causing route printouts to show the path instead of the action
@@ -25,10 +25,14 @@ async function printRoutes() {
25
25
  }
26
26
  function buildExpressions(routes) {
27
27
  return routes.map(route => {
28
+ const formattedPath = '/' + route.path.replace(/^\//, '');
28
29
  const method = route.httpMethod.toUpperCase();
29
30
  const numMethodSpaces = 8 - method.length;
30
- const beginningOfExpression = `${route.httpMethod.toUpperCase()}${' '.repeat(numMethodSpaces)}${route.path}`;
31
- const endOfExpression = route.controller.disaplayName + '#' + route.action;
31
+ const beginningOfExpression = `${route.httpMethod.toUpperCase()}${' '.repeat(numMethodSpaces)}${formattedPath}`;
32
+ const controllerRouteConf = route;
33
+ const endOfExpression = controllerRouteConf.controller
34
+ ? controllerRouteConf.controller.controllerActionPath(controllerRouteConf.action)
35
+ : 'middleware';
32
36
  return [beginningOfExpression, endOfExpression];
33
37
  });
34
38
  }
@@ -53,5 +57,5 @@ function calculateNumSpacesInLastGap(expressions) {
53
57
  if (expression.length > desiredSpaceCount)
54
58
  desiredSpaceCount = expression.length;
55
59
  });
56
- return desiredSpaceCount + 3;
60
+ return desiredSpaceCount;
57
61
  }
@@ -242,7 +242,10 @@ class OpenapiEndpointRenderer {
242
242
  getCurrentRouteConfig(routes) {
243
243
  // if the action is update, we want to specifically find the 'patch' route,
244
244
  // otherwise we find any route that matches
245
- const filteredRoutes = routes.filter(routeConfig => routeConfig.controller === this.controllerClass && routeConfig.action === this.action);
245
+ const filteredRoutes = routes.filter(routeConfig => {
246
+ const controllerRouteConf = routeConfig;
247
+ return (controllerRouteConf.controller === this.controllerClass && controllerRouteConf.action === this.action);
248
+ });
246
249
  const route = this.action === 'update'
247
250
  ? filteredRoutes.find(routeConfig => routeConfig.httpMethod === 'patch')
248
251
  : filteredRoutes.at(0);
@@ -290,6 +290,35 @@ Try setting it to something valid, like:
290
290
  await this.inflections?.();
291
291
  this.booted = true;
292
292
  }
293
+ use(pathOrOnOrHandler, maybeHandler) {
294
+ if (maybeHandler) {
295
+ const eventType = pathOrOnOrHandler;
296
+ const handler = maybeHandler;
297
+ const wrappedHandler = (server) => {
298
+ server.expressApp.use(handler);
299
+ };
300
+ switch (eventType) {
301
+ case 'before-middleware':
302
+ this.on('server:init:before-middleware', wrappedHandler);
303
+ break;
304
+ case 'after-middleware':
305
+ this.on('server:init:after-middleware', wrappedHandler);
306
+ break;
307
+ case 'after-routes':
308
+ this.on('server:init:after-routes', wrappedHandler);
309
+ break;
310
+ default:
311
+ throw new Error(`missing required case handler for PsychicApp#use: "${eventType}"`);
312
+ }
313
+ return;
314
+ }
315
+ else {
316
+ const wrappedHandler = (server) => {
317
+ server.expressApp.use(pathOrOnOrHandler);
318
+ };
319
+ this.on('server:init:after-middleware', wrappedHandler);
320
+ }
321
+ }
293
322
  plugin(cb) {
294
323
  this._plugins.push(cb);
295
324
  }
@@ -1,2 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsychicUseEventTypeValues = void 0;
4
+ exports.PsychicUseEventTypeValues = ['before-middleware', 'after-middleware', 'after-routes'];
@@ -36,9 +36,18 @@ class PsychicRouter {
36
36
  // this is called after all routes have been processed.
37
37
  commit() {
38
38
  this.routes.forEach(route => {
39
- this.app[route.httpMethod]((0, helpers_js_1.routePath)(route.path), (req, res) => {
40
- this.handle(route.controller, route.action, { req, res }).catch(() => { });
41
- });
39
+ if (route.middleware) {
40
+ const routeConf = route;
41
+ this.app[routeConf.httpMethod]((0, helpers_js_1.routePath)(routeConf.path), (req, res, next) => {
42
+ this.handleMiddleware(routeConf.middleware, { req, res, next }).catch(() => { });
43
+ });
44
+ }
45
+ else {
46
+ const routeConf = route;
47
+ this.app[routeConf.httpMethod]((0, helpers_js_1.routePath)(routeConf.path), (req, res) => {
48
+ this.handle(routeConf.controller, routeConf.action, { req, res }).catch(() => { });
49
+ });
50
+ }
42
51
  });
43
52
  }
44
53
  get(path, controller, action) {
@@ -64,17 +73,30 @@ class PsychicRouter {
64
73
  return str;
65
74
  return '/' + this.currentNamespacePaths.join('/') + '/' + str;
66
75
  }
67
- crud(httpMethod, path, controller, action) {
68
- controller ||= (0, helpers_js_1.lookupControllerOrFail)(this, { path, httpMethod });
69
- action ||= path.replace(/^\//, '');
70
- if (action.match(/\//))
71
- throw new Error('action cant have a slash in it - action was inferred from path');
72
- this.routeManager.addRoute({
73
- httpMethod,
74
- path: this.prefixPathWithNamespaces(path),
75
- controller,
76
- action,
77
- });
76
+ crud(httpMethod, path, controllerOrMiddleware, action) {
77
+ const isMiddleware = typeof controllerOrMiddleware === 'function' &&
78
+ !controllerOrMiddleware?.isPsychicController;
79
+ // devs can provide custom express middleware which bypasses
80
+ // the normal Controller#action paradigm.
81
+ if (isMiddleware) {
82
+ this.routeManager.addMiddleware({
83
+ httpMethod,
84
+ path: this.prefixPathWithNamespaces(path),
85
+ middleware: controllerOrMiddleware,
86
+ });
87
+ }
88
+ else {
89
+ controllerOrMiddleware ||= (0, helpers_js_1.lookupControllerOrFail)(this, { path, httpMethod });
90
+ action ||= path.replace(/^\//, '');
91
+ if (action.match(/\//))
92
+ throw new Error('action cant have a slash in it - action was inferred from path');
93
+ this.routeManager.addRoute({
94
+ httpMethod,
95
+ path: this.prefixPathWithNamespaces(path),
96
+ controller: controllerOrMiddleware,
97
+ action,
98
+ });
99
+ }
78
100
  }
79
101
  namespace(namespace, cb) {
80
102
  const nestedRouter = new PsychicNestedRouter(this.app, this.config, this.routeManager, {
@@ -187,6 +209,9 @@ class PsychicRouter {
187
209
  if (nestedRouter)
188
210
  nestedRouter.currentNamespaces = this.currentNamespaces;
189
211
  }
212
+ async handleMiddleware(middleware, { req, res, next, }) {
213
+ await middleware(req, res, next);
214
+ }
190
215
  async handle(controller, action, { req, res, }) {
191
216
  const controllerInstance = this._initializeController(controller, req, res, action);
192
217
  try {
@@ -10,5 +10,12 @@ class RouteManager {
10
10
  action,
11
11
  });
12
12
  }
13
+ addMiddleware({ httpMethod, path, middleware, }) {
14
+ this.routes.push({
15
+ httpMethod,
16
+ path,
17
+ middleware,
18
+ });
19
+ }
13
20
  }
14
21
  exports.default = RouteManager;
@@ -19,10 +19,14 @@ export default async function printRoutes() {
19
19
  }
20
20
  function buildExpressions(routes) {
21
21
  return routes.map(route => {
22
+ const formattedPath = '/' + route.path.replace(/^\//, '');
22
23
  const method = route.httpMethod.toUpperCase();
23
24
  const numMethodSpaces = 8 - method.length;
24
- const beginningOfExpression = `${route.httpMethod.toUpperCase()}${' '.repeat(numMethodSpaces)}${route.path}`;
25
- const endOfExpression = route.controller.disaplayName + '#' + route.action;
25
+ const beginningOfExpression = `${route.httpMethod.toUpperCase()}${' '.repeat(numMethodSpaces)}${formattedPath}`;
26
+ const controllerRouteConf = route;
27
+ const endOfExpression = controllerRouteConf.controller
28
+ ? controllerRouteConf.controller.controllerActionPath(controllerRouteConf.action)
29
+ : 'middleware';
26
30
  return [beginningOfExpression, endOfExpression];
27
31
  });
28
32
  }
@@ -47,5 +51,5 @@ function calculateNumSpacesInLastGap(expressions) {
47
51
  if (expression.length > desiredSpaceCount)
48
52
  desiredSpaceCount = expression.length;
49
53
  });
50
- return desiredSpaceCount + 3;
54
+ return desiredSpaceCount;
51
55
  }
@@ -236,7 +236,10 @@ export default class OpenapiEndpointRenderer {
236
236
  getCurrentRouteConfig(routes) {
237
237
  // if the action is update, we want to specifically find the 'patch' route,
238
238
  // otherwise we find any route that matches
239
- const filteredRoutes = routes.filter(routeConfig => routeConfig.controller === this.controllerClass && routeConfig.action === this.action);
239
+ const filteredRoutes = routes.filter(routeConfig => {
240
+ const controllerRouteConf = routeConfig;
241
+ return (controllerRouteConf.controller === this.controllerClass && controllerRouteConf.action === this.action);
242
+ });
240
243
  const route = this.action === 'update'
241
244
  ? filteredRoutes.find(routeConfig => routeConfig.httpMethod === 'patch')
242
245
  : filteredRoutes.at(0);
@@ -261,6 +261,35 @@ Try setting it to something valid, like:
261
261
  await this.inflections?.();
262
262
  this.booted = true;
263
263
  }
264
+ use(pathOrOnOrHandler, maybeHandler) {
265
+ if (maybeHandler) {
266
+ const eventType = pathOrOnOrHandler;
267
+ const handler = maybeHandler;
268
+ const wrappedHandler = (server) => {
269
+ server.expressApp.use(handler);
270
+ };
271
+ switch (eventType) {
272
+ case 'before-middleware':
273
+ this.on('server:init:before-middleware', wrappedHandler);
274
+ break;
275
+ case 'after-middleware':
276
+ this.on('server:init:after-middleware', wrappedHandler);
277
+ break;
278
+ case 'after-routes':
279
+ this.on('server:init:after-routes', wrappedHandler);
280
+ break;
281
+ default:
282
+ throw new Error(`missing required case handler for PsychicApp#use: "${eventType}"`);
283
+ }
284
+ return;
285
+ }
286
+ else {
287
+ const wrappedHandler = (server) => {
288
+ server.expressApp.use(pathOrOnOrHandler);
289
+ };
290
+ this.on('server:init:after-middleware', wrappedHandler);
291
+ }
292
+ }
264
293
  plugin(cb) {
265
294
  this._plugins.push(cb);
266
295
  }
@@ -1 +1 @@
1
- export {};
1
+ export const PsychicUseEventTypeValues = ['before-middleware', 'after-middleware', 'after-routes'];
@@ -30,9 +30,18 @@ export default class PsychicRouter {
30
30
  // this is called after all routes have been processed.
31
31
  commit() {
32
32
  this.routes.forEach(route => {
33
- this.app[route.httpMethod](routePath(route.path), (req, res) => {
34
- this.handle(route.controller, route.action, { req, res }).catch(() => { });
35
- });
33
+ if (route.middleware) {
34
+ const routeConf = route;
35
+ this.app[routeConf.httpMethod](routePath(routeConf.path), (req, res, next) => {
36
+ this.handleMiddleware(routeConf.middleware, { req, res, next }).catch(() => { });
37
+ });
38
+ }
39
+ else {
40
+ const routeConf = route;
41
+ this.app[routeConf.httpMethod](routePath(routeConf.path), (req, res) => {
42
+ this.handle(routeConf.controller, routeConf.action, { req, res }).catch(() => { });
43
+ });
44
+ }
36
45
  });
37
46
  }
38
47
  get(path, controller, action) {
@@ -58,17 +67,30 @@ export default class PsychicRouter {
58
67
  return str;
59
68
  return '/' + this.currentNamespacePaths.join('/') + '/' + str;
60
69
  }
61
- crud(httpMethod, path, controller, action) {
62
- controller ||= lookupControllerOrFail(this, { path, httpMethod });
63
- action ||= path.replace(/^\//, '');
64
- if (action.match(/\//))
65
- throw new Error('action cant have a slash in it - action was inferred from path');
66
- this.routeManager.addRoute({
67
- httpMethod,
68
- path: this.prefixPathWithNamespaces(path),
69
- controller,
70
- action,
71
- });
70
+ crud(httpMethod, path, controllerOrMiddleware, action) {
71
+ const isMiddleware = typeof controllerOrMiddleware === 'function' &&
72
+ !controllerOrMiddleware?.isPsychicController;
73
+ // devs can provide custom express middleware which bypasses
74
+ // the normal Controller#action paradigm.
75
+ if (isMiddleware) {
76
+ this.routeManager.addMiddleware({
77
+ httpMethod,
78
+ path: this.prefixPathWithNamespaces(path),
79
+ middleware: controllerOrMiddleware,
80
+ });
81
+ }
82
+ else {
83
+ controllerOrMiddleware ||= lookupControllerOrFail(this, { path, httpMethod });
84
+ action ||= path.replace(/^\//, '');
85
+ if (action.match(/\//))
86
+ throw new Error('action cant have a slash in it - action was inferred from path');
87
+ this.routeManager.addRoute({
88
+ httpMethod,
89
+ path: this.prefixPathWithNamespaces(path),
90
+ controller: controllerOrMiddleware,
91
+ action,
92
+ });
93
+ }
72
94
  }
73
95
  namespace(namespace, cb) {
74
96
  const nestedRouter = new PsychicNestedRouter(this.app, this.config, this.routeManager, {
@@ -181,6 +203,9 @@ export default class PsychicRouter {
181
203
  if (nestedRouter)
182
204
  nestedRouter.currentNamespaces = this.currentNamespaces;
183
205
  }
206
+ async handleMiddleware(middleware, { req, res, next, }) {
207
+ await middleware(req, res, next);
208
+ }
184
209
  async handle(controller, action, { req, res, }) {
185
210
  const controllerInstance = this._initializeController(controller, req, res, action);
186
211
  try {
@@ -8,4 +8,11 @@ export default class RouteManager {
8
8
  action,
9
9
  });
10
10
  }
11
+ addMiddleware({ httpMethod, path, middleware, }) {
12
+ this.routes.push({
13
+ httpMethod,
14
+ path,
15
+ middleware,
16
+ });
17
+ }
11
18
  }
@@ -2,12 +2,12 @@ import { DreamApp, DreamAppInitOptions, DreamLogLevel, DreamLogger, EncryptOptio
2
2
  import * as bodyParser from 'body-parser';
3
3
  import { Command } from 'commander';
4
4
  import { CorsOptions } from 'cors';
5
- import { Request, Response } from 'express';
5
+ import { Request, RequestHandler, Response } from 'express';
6
6
  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 } from './types.js';
10
+ import { PsychicHookEventType, PsychicUseEventType } from './types.js';
11
11
  export default class PsychicApp {
12
12
  static init(cb: (app: PsychicApp) => void | Promise<void>, dreamCb: (app: DreamApp) => void | Promise<void>, opts?: PsychicAppInitOptions): Promise<PsychicApp>;
13
13
  static lookupClassByGlobalName(name: string): any;
@@ -89,6 +89,9 @@ export default class PsychicApp {
89
89
  load<RT extends 'controllers' | 'services' | 'initializers'>(resourceType: RT, resourcePath: string, importCb: (path: string) => Promise<any>): Promise<void>;
90
90
  private booted;
91
91
  boot(force?: boolean): Promise<void>;
92
+ use(on: PsychicUseEventType, handler: RequestHandler): void;
93
+ use(handler: RequestHandler): void;
94
+ use(handler: () => void): void;
92
95
  plugin(cb: (app: PsychicApp) => void | Promise<void>): void;
93
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: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;
94
97
  set(option: 'openapi', name: string, value: NamedPsychicOpenapiOptions): void;
@@ -2,6 +2,8 @@ import PsychicApp from './index.js';
2
2
  export type UUID = string;
3
3
  export type PsychicHookEventType = 'cli:sync' | 'server:init:before-middleware' | 'server:init:after-middleware' | 'server:init:after-routes' | 'server:start' | 'server:error' | 'server:shutdown';
4
4
  export type PsychicAppInitializerCb = (psychicApp: PsychicApp) => void | Promise<void>;
5
+ export declare const PsychicUseEventTypeValues: readonly ["before-middleware", "after-middleware", "after-routes"];
6
+ export type PsychicUseEventType = (typeof PsychicUseEventTypeValues)[number];
5
7
  type Only<T, U> = T & Partial<Record<Exclude<keyof U, keyof T>, never>>;
6
8
  export type Either<T, U> = Only<T, U> | Only<U, T>;
7
9
  export {};
@@ -1,4 +1,4 @@
1
- import { Express, Request, Response, Router } from 'express';
1
+ import { Express, NextFunction, Request, RequestHandler, Response, Router } from 'express';
2
2
  import PsychicController from '../controller/index.js';
3
3
  import PsychicApp from '../psychic-app/index.js';
4
4
  import { NamespaceConfig, PsychicControllerActions } from '../router/helpers.js';
@@ -15,19 +15,26 @@ export default class PsychicRouter {
15
15
  private get currentNamespacePaths();
16
16
  commit(): void;
17
17
  get(path: string): void;
18
+ get(path: string, middleware: RequestHandler): void;
18
19
  get<T extends typeof PsychicController>(path: string, controller: T, action: PsychicControllerActions<T>): void;
19
20
  post(path: string): void;
21
+ post(path: string, middleware: RequestHandler): void;
20
22
  post<T extends typeof PsychicController>(path: string, controller: T, action: PsychicControllerActions<T>): void;
21
23
  put(path: string): void;
24
+ put(path: string, middleware: RequestHandler): void;
22
25
  put<T extends typeof PsychicController>(path: string, controller: T, action: PsychicControllerActions<T>): void;
23
26
  patch(path: string): void;
27
+ patch(path: string, middleware: RequestHandler): void;
24
28
  patch<T extends typeof PsychicController>(path: string, controller: T, action: PsychicControllerActions<T>): void;
25
29
  delete(path: string): void;
30
+ delete(path: string, middleware: RequestHandler): void;
26
31
  delete<T extends typeof PsychicController>(path: string, controller: T, action: PsychicControllerActions<T>): void;
27
32
  options(path: string): void;
33
+ options(path: string, middleware: RequestHandler): void;
28
34
  options<T extends typeof PsychicController>(path: string, controller: T, action: PsychicControllerActions<T>): void;
29
35
  private prefixPathWithNamespaces;
30
36
  crud(httpMethod: HttpMethod, path: string): void;
37
+ crud(httpMethod: HttpMethod, path: string, middleware: RequestHandler): void;
31
38
  crud(httpMethod: HttpMethod, path: string, controller: typeof PsychicController, action: string): void;
32
39
  namespace(namespace: string, cb: (router: PsychicNestedRouter) => void): void;
33
40
  scope(scope: string, cb: (router: PsychicNestedRouter) => void): void;
@@ -40,6 +47,11 @@ export default class PsychicRouter {
40
47
  private addNamespace;
41
48
  private removeLastNamespace;
42
49
  private makeRoomForNewIdParam;
50
+ handleMiddleware(middleware: RequestHandler, { req, res, next, }: {
51
+ req: Request;
52
+ res: Response;
53
+ next: NextFunction;
54
+ }): Promise<void>;
43
55
  handle(controller: typeof PsychicController, action: string, { req, res, }: {
44
56
  req: Request;
45
57
  res: Response;
@@ -1,3 +1,4 @@
1
+ import { RequestHandler } from 'express';
1
2
  import PsychicController from '../controller/index.js';
2
3
  import { HttpMethod } from './types.js';
3
4
  export default class RouteManager {
@@ -8,10 +9,22 @@ export default class RouteManager {
8
9
  controller: typeof PsychicController;
9
10
  action: string;
10
11
  }): void;
12
+ addMiddleware({ httpMethod, path, middleware, }: {
13
+ httpMethod: HttpMethod;
14
+ path: string;
15
+ middleware: RequestHandler;
16
+ }): void;
11
17
  }
12
- export interface RouteConfig {
13
- controller: typeof PsychicController;
14
- action: string;
15
- path: string;
18
+ export type RouteConfig = ControllerActionRouteConfig | MiddlewareRouteConfig;
19
+ interface BaseRouteConfig {
16
20
  httpMethod: HttpMethod;
21
+ path: string;
17
22
  }
23
+ export type ControllerActionRouteConfig = BaseRouteConfig & {
24
+ controller: typeof PsychicController;
25
+ action: string;
26
+ };
27
+ export type MiddlewareRouteConfig = BaseRouteConfig & {
28
+ middleware: RequestHandler;
29
+ };
30
+ export {};
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": "1.0.0",
5
+ "version": "1.1.1",
6
6
  "author": "RVOHealth",
7
7
  "repository": {
8
8
  "type": "git",