@flink-app/flink 0.3.8 → 0.3.12

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.
@@ -1,3 +1,4 @@
1
+ import { OptionsJson } from "body-parser";
1
2
  import { Express } from "express";
2
3
  import { JSONSchema7 } from "json-schema";
3
4
  import { Db } from "mongodb";
@@ -88,6 +89,10 @@ export interface FlinkOptions {
88
89
  * Optional root folder of app. Defaults to `./`
89
90
  */
90
91
  appRoot?: string;
92
+ /**
93
+ * Options for json body parser
94
+ */
95
+ jsonOptions?: OptionsJson;
91
96
  }
92
97
  export interface HandlerConfig {
93
98
  schema?: {
@@ -124,6 +129,7 @@ export declare class FlinkApp<C extends FlinkContext> {
124
129
  private auth?;
125
130
  private corsOpts;
126
131
  private routingConfigured;
132
+ private jsonOptions?;
127
133
  private repos;
128
134
  /**
129
135
  * Internal cache used to track registered handlers and potentially any overlapping routes
@@ -99,6 +99,7 @@ var FlinkApp = /** @class */ (function () {
99
99
  this.plugins = opts.plugins || [];
100
100
  this.corsOpts = __assign(__assign({}, defaultCorsOptions), opts.cors);
101
101
  this.auth = opts.auth;
102
+ this.jsonOptions = opts.jsonOptions || { limit: "1mb" };
102
103
  }
103
104
  Object.defineProperty(FlinkApp.prototype, "ctx", {
104
105
  get: function () {
@@ -140,7 +141,7 @@ var FlinkApp = /** @class */ (function () {
140
141
  }
141
142
  this.expressApp = express_1.default();
142
143
  this.expressApp.use(cors_1.default(this.corsOpts));
143
- this.expressApp.use(body_parser_1.default.json());
144
+ this.expressApp.use(body_parser_1.default.json(this.jsonOptions));
144
145
  this.expressApp.use(function (req, res, next) {
145
146
  req.reqId = uuid_1.v4();
146
147
  next();
@@ -212,10 +213,7 @@ var FlinkApp = /** @class */ (function () {
212
213
  node_color_log_1.default.error("Failed to register handler '" + handler.__file + "': Missing 'path' in route props");
213
214
  return;
214
215
  }
215
- var dup = this.handlers.find(function (h) {
216
- return h.routeProps.path === routeProps.path &&
217
- h.routeProps.method === routeProps.method;
218
- });
216
+ var dup = this.handlers.find(function (h) { return h.routeProps.path === routeProps.path && h.routeProps.method === routeProps.method; });
219
217
  var methodAndPath = routeProps.method.toUpperCase() + " " + routeProps.path;
220
218
  if (dup) {
221
219
  // TODO: Not sure if there is a case where you'd want to overwrite a route?
@@ -302,6 +300,7 @@ var FlinkApp = /** @class */ (function () {
302
300
  case 5:
303
301
  err_1 = _a.sent();
304
302
  node_color_log_1.default.warn("Handler '" + methodAndRoute_1 + "' threw unhandled exception " + err_1);
303
+ console.error(err_1);
305
304
  return [2 /*return*/, res.status(500).json(FlinkErrors_1.internalServerError(err_1))];
306
305
  case 6:
307
306
  if (schema.resSchema && !utils_1.isError(handlerRes)) {
@@ -374,6 +373,8 @@ var FlinkApp = /** @class */ (function () {
374
373
  };
375
374
  FlinkApp.prototype.addRepo = function (instanceName, repoInstance) {
376
375
  this.repos[instanceName] = repoInstance;
376
+ // TODO: Find out if we need to set ctx here or wanted not to if plugin has its own context
377
+ // repoInstance.ctx = this.ctx;
377
378
  };
378
379
  /**
379
380
  * Constructs the app context. Will inject context in all components
@@ -381,8 +382,8 @@ var FlinkApp = /** @class */ (function () {
381
382
  */
382
383
  FlinkApp.prototype.buildContext = function () {
383
384
  return __awaiter(this, void 0, void 0, function () {
384
- var _i, autoRegisteredRepos_1, _a, collectionName, repoInstanceName, Repo, repoInstance, pluginCtx;
385
- return __generator(this, function (_b) {
385
+ var _i, autoRegisteredRepos_1, _a, collectionName, repoInstanceName, Repo, repoInstance, pluginCtx, _b, _c, repo;
386
+ return __generator(this, function (_d) {
386
387
  if (this.dbOpts) {
387
388
  for (_i = 0, autoRegisteredRepos_1 = exports.autoRegisteredRepos; _i < autoRegisteredRepos_1.length; _i++) {
388
389
  _a = autoRegisteredRepos_1[_i], collectionName = _a.collectionName, repoInstanceName = _a.repoInstanceName, Repo = _a.Repo;
@@ -406,6 +407,10 @@ var FlinkApp = /** @class */ (function () {
406
407
  plugins: pluginCtx,
407
408
  auth: this.auth,
408
409
  };
410
+ for (_b = 0, _c = Object.values(this.repos); _b < _c.length; _b++) {
411
+ repo = _c[_b];
412
+ repo.ctx = this.ctx;
413
+ }
409
414
  return [2 /*return*/];
410
415
  });
411
416
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flink-app/flink",
3
- "version": "0.3.8",
3
+ "version": "0.3.12",
4
4
  "description": "Typescript only framework for creating REST-like APIs on top of Express and mongodb",
5
5
  "types": "dist/src/index.d.ts",
6
6
  "main": "dist/src/index.js",
@@ -62,5 +62,5 @@
62
62
  "rimraf": "^3.0.2",
63
63
  "ts-node": "^9.1.1"
64
64
  },
65
- "gitHead": "9c45e69ffff300482f9945074c5ecc3e1d661467"
65
+ "gitHead": "14e613ca807ffc6f2eac3bdd0e30aebee2778932"
66
66
  }
package/readme.md CHANGED
@@ -107,12 +107,12 @@ export default () => {};
107
107
 
108
108
  The following building blocks exists in Flink:
109
109
 
110
- - Handler - a handler is responsible for handling API requests and return a response. Normally a handler has some type of logic and invokes a _repo_ to CRUD data from database.
111
- - Repo - a repository is used to abstract data access to database. A repo is used to access a mongo db and a repo is used per collection.
112
- - App Context - the app context is the glue that ties parts of the app together. By defining and creating an app context you make sure that i.e. handlers can get access to repositories.
113
- - Schemas - models that defines API requests and responses. These are typescript interfaces which will during compile time be converted into JSON schemas used to validate requests and responses and also used to generate API documentation.
114
- - Flink app - is the entry
115
- - Plugins - plugability is built into core of Flink. These can be external npm modules, or plugins inside your project. Plugins can for example extend the `request` object and add additional information such as auth user which can be used in handlers. Similar to how middleware works in express although a bit more constrained.
110
+ - Handler - a handler is responsible for handling API requests and return a response. Normally a handler has some type of logic and invokes a _repo_ to CRUD data from database.
111
+ - Repo - a repository is used to abstract data access to database. A repo is used to access a mongo db and a repo is used per collection.
112
+ - App Context - the app context is the glue that ties parts of the app together. By defining and creating an app context you make sure that i.e. handlers can get access to repositories.
113
+ - Schemas - models that defines API requests and responses. These are typescript interfaces which will during compile time be converted into JSON schemas used to validate requests and responses and also used to generate API documentation.
114
+ - Flink app - is the entry
115
+ - Plugins - plugability is built into core of Flink. These can be external npm modules, or plugins inside your project. Plugins can for example extend the `request` object and add additional information such as auth user which can be used in handlers. Similar to how middleware works in express although a bit more constrained.
116
116
 
117
117
  ### Handlers
118
118
 
@@ -164,10 +164,10 @@ Then handler method must be of type `Handler` or `GetHandler`.
164
164
 
165
165
  The handler function has generic type arguments which defines:
166
166
 
167
- - Application context
168
- - Request schema (optional)
169
- - Response schema (optional)
170
- - Params (optional)
167
+ - Application context
168
+ - Request schema (optional)
169
+ - Response schema (optional)
170
+ - Params (optional)
171
171
 
172
172
  > Note: `GetHandler<Ctx, ResSchema>` is just syntactic sugar since get handlers does not have request schemas so that type argument does not exist. Otherwise it is the same as `Handler<Ctx, ReqSchema, ResSchema>`
173
173
 
@@ -217,3 +217,7 @@ export const Props: RouteProps {
217
217
  method: HttpMethod.get
218
218
  }
219
219
  ```
220
+
221
+ ## 🤕 Known issues
222
+
223
+ - Current TypeScript compiler is slow when project gets larger
package/src/FlinkApp.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import Ajv from "ajv";
2
2
  import addFormats from "ajv-formats";
3
- import bodyParser from "body-parser";
3
+ import bodyParser, { OptionsJson } from "body-parser";
4
4
  import cors from "cors";
5
5
  import express, { Express, Request } from "express";
6
6
  import { JSONSchema7 } from "json-schema";
@@ -10,13 +10,7 @@ import { v4 } from "uuid";
10
10
  import { FlinkAuthPlugin } from "./auth/FlinkAuthPlugin";
11
11
  import { FlinkContext } from "./FlinkContext";
12
12
  import { internalServerError, notFound, unauthorized } from "./FlinkErrors";
13
- import {
14
- Handler,
15
- HandlerFile,
16
- HttpMethod,
17
- QueryParamMetadata,
18
- RouteProps,
19
- } from "./FlinkHttpHandler";
13
+ import { Handler, HandlerFile, HttpMethod, QueryParamMetadata, RouteProps } from "./FlinkHttpHandler";
20
14
  import { FlinkPlugin } from "./FlinkPlugin";
21
15
  import { FlinkRepo } from "./FlinkRepo";
22
16
  import { FlinkResponse } from "./FlinkResponse";
@@ -27,9 +21,9 @@ const ajv = new Ajv();
27
21
  addFormats(ajv);
28
22
 
29
23
  const defaultCorsOptions: FlinkOptions["cors"] = {
30
- allowedHeaders: "",
31
- credentials: true,
32
- origin: [/.*/],
24
+ allowedHeaders: "",
25
+ credentials: true,
26
+ origin: [/.*/],
33
27
  };
34
28
 
35
29
  export type JSONSchema = JSONSchema7;
@@ -39,8 +33,8 @@ export type JSONSchema = JSONSchema7;
39
33
  * are picked up by typescript compiler
40
34
  */
41
35
  export const autoRegisteredHandlers: {
42
- handler: HandlerFile;
43
- assumedHttpMethod: HttpMethod;
36
+ handler: HandlerFile;
37
+ assumedHttpMethod: HttpMethod;
44
38
  }[] = [];
45
39
 
46
40
  /**
@@ -48,586 +42,538 @@ export const autoRegisteredHandlers: {
48
42
  * are picked up by typescript compiler
49
43
  */
50
44
  export const autoRegisteredRepos: {
51
- collectionName: string;
52
- repoInstanceName: string;
53
- Repo: any;
45
+ collectionName: string;
46
+ repoInstanceName: string;
47
+ Repo: any;
54
48
  }[] = [];
55
49
 
56
50
  export interface FlinkOptions {
57
- /**
58
- * Name of application, will only show in logs and in HTTP header.
59
- */
60
- name: string;
61
-
62
- /**
63
- * HTTP port
64
- * @default 3333
65
- */
66
- port?: number;
67
-
68
- /**
69
- * Configuration related to database.
70
- * Leave empty if no database is needed.
71
- */
72
- db?: {
73
51
  /**
74
- * Uri to mongodb including any username and password.
75
- * @example mongodb://localhost:27017/my-db
52
+ * Name of application, will only show in logs and in HTTP header.
76
53
  */
77
- uri: string;
78
- };
79
-
80
- /**
81
- * Optional debug options, used to log and debug Flink internals.
82
- */
83
- debug?: boolean;
84
-
85
- /**
86
- * Callback invoked after database was connected
87
- * end before application starts.
88
- *
89
- * A good place to for example ensure database indexes.
90
- */
91
- onDbConnection?: (db: Db) => Promise<void>;
92
-
93
- /**
94
- * Callback invoked so Flink can load files from host project.
95
- * @deprecated not needed anymore since new `flink run`
96
- */
97
- loader?: (file: string) => Promise<any>;
98
-
99
- /**
100
- * Optional list of plugins that should be configured and used.
101
- */
102
- plugins?: FlinkPlugin[];
103
-
104
- /**
105
- * Plugin used for authentication.
106
- */
107
- auth?: FlinkAuthPlugin;
108
-
109
- /**
110
- * Optional cors options.
111
- */
112
- cors?: {
54
+ name: string;
55
+
56
+ /**
57
+ * HTTP port
58
+ * @default 3333
59
+ */
60
+ port?: number;
61
+
62
+ /**
63
+ * Configuration related to database.
64
+ * Leave empty if no database is needed.
65
+ */
66
+ db?: {
67
+ /**
68
+ * Uri to mongodb including any username and password.
69
+ * @example mongodb://localhost:27017/my-db
70
+ */
71
+ uri: string;
72
+ };
73
+
74
+ /**
75
+ * Optional debug options, used to log and debug Flink internals.
76
+ */
77
+ debug?: boolean;
78
+
113
79
  /**
114
- * Origin(s) to allow
80
+ * Callback invoked after database was connected
81
+ * end before application starts.
82
+ *
83
+ * A good place to for example ensure database indexes.
115
84
  */
116
- origin?: RegExp[];
85
+ onDbConnection?: (db: Db) => Promise<void>;
117
86
 
118
- credentials?: boolean;
87
+ /**
88
+ * Callback invoked so Flink can load files from host project.
89
+ * @deprecated not needed anymore since new `flink run`
90
+ */
91
+ loader?: (file: string) => Promise<any>;
92
+
93
+ /**
94
+ * Optional list of plugins that should be configured and used.
95
+ */
96
+ plugins?: FlinkPlugin[];
97
+
98
+ /**
99
+ * Plugin used for authentication.
100
+ */
101
+ auth?: FlinkAuthPlugin;
102
+
103
+ /**
104
+ * Optional cors options.
105
+ */
106
+ cors?: {
107
+ /**
108
+ * Origin(s) to allow
109
+ */
110
+ origin?: RegExp[];
111
+
112
+ credentials?: boolean;
113
+
114
+ /**
115
+ * Specify allowed headers for CORS, can be a comma separated string if multiple
116
+ * Defaults to none.
117
+ */
118
+ allowedHeaders?: string;
119
+ };
119
120
 
120
121
  /**
121
- * Specify allowed headers for CORS, can be a comma separated string if multiple
122
- * Defaults to none.
122
+ * Optional root folder of app. Defaults to `./`
123
123
  */
124
- allowedHeaders?: string;
125
- };
124
+ appRoot?: string;
126
125
 
127
- /**
128
- * Optional root folder of app. Defaults to `./`
129
- */
130
- appRoot?: string;
126
+
127
+ /**
128
+ * Options for json body parser
129
+ */
130
+ jsonOptions?: OptionsJson;
131
+
132
+
131
133
  }
132
134
 
133
135
  export interface HandlerConfig {
134
- schema?: {
135
- reqSchema?: JSONSchema;
136
- resSchema?: JSONSchema;
137
- };
138
- routeProps: RouteProps;
139
- queryMetadata: QueryParamMetadata[];
140
- paramsMetadata: QueryParamMetadata[];
136
+ schema?: {
137
+ reqSchema?: JSONSchema;
138
+ resSchema?: JSONSchema;
139
+ };
140
+ routeProps: RouteProps;
141
+ queryMetadata: QueryParamMetadata[];
142
+ paramsMetadata: QueryParamMetadata[];
141
143
  }
142
144
 
143
145
  export interface HandlerConfigWithMethod extends HandlerConfig {
144
- routeProps: RouteProps & { method: HttpMethod };
146
+ routeProps: RouteProps & { method: HttpMethod };
145
147
  }
146
- export interface HandlerConfigWithSchemaRefs
147
- extends Omit<HandlerConfig, "schema" | "origin"> {
148
- schema?: {
149
- reqSchema?: string;
150
- resSchema?: string;
151
- };
148
+ export interface HandlerConfigWithSchemaRefs extends Omit<HandlerConfig, "schema" | "origin"> {
149
+ schema?: {
150
+ reqSchema?: string;
151
+ resSchema?: string;
152
+ };
152
153
  }
153
154
 
154
155
  export class FlinkApp<C extends FlinkContext> {
155
- public name: string;
156
- public expressApp?: Express;
157
- public db?: Db;
158
- public handlers: HandlerConfig[] = [];
159
- public port?: number;
160
- public started = false;
161
-
162
- private _ctx?: C;
163
- private dbOpts?: FlinkOptions["db"];
164
- private debug = false;
165
- private onDbConnection?: FlinkOptions["onDbConnection"];
166
-
167
- private plugins: FlinkPlugin[] = [];
168
- private auth?: FlinkAuthPlugin;
169
- private corsOpts: FlinkOptions["cors"];
170
- private routingConfigured = false;
171
-
172
- private repos: { [x: string]: FlinkRepo<C> } = {};
173
-
174
- /**
175
- * Internal cache used to track registered handlers and potentially any overlapping routes
176
- */
177
- private handlerRouteCache = new Map<string, string>();
178
-
179
- constructor(opts: FlinkOptions) {
180
- this.name = opts.name;
181
- this.port = opts.port || 3333;
182
- this.dbOpts = opts.db;
183
- this.debug = !!opts.debug;
184
- this.onDbConnection = opts.onDbConnection;
185
- this.plugins = opts.plugins || [];
186
- this.corsOpts = { ...defaultCorsOptions, ...opts.cors };
187
- this.auth = opts.auth;
188
- }
189
-
190
- get ctx() {
191
- if (!this._ctx) {
192
- throw new Error("Context is not yet initialized");
193
- }
194
- return this._ctx;
195
- }
156
+ public name: string;
157
+ public expressApp?: Express;
158
+ public db?: Db;
159
+ public handlers: HandlerConfig[] = [];
160
+ public port?: number;
161
+ public started = false;
162
+
163
+ private _ctx?: C;
164
+ private dbOpts?: FlinkOptions["db"];
165
+ private debug = false;
166
+ private onDbConnection?: FlinkOptions["onDbConnection"];
167
+
168
+ private plugins: FlinkPlugin[] = [];
169
+ private auth?: FlinkAuthPlugin;
170
+ private corsOpts: FlinkOptions["cors"];
171
+ private routingConfigured = false;
172
+ private jsonOptions? : OptionsJson;
173
+
174
+ private repos: { [x: string]: FlinkRepo<C> } = {};
196
175
 
197
- async start() {
198
- const startTime = Date.now();
199
- let offsetTime = 0;
200
-
201
- await this.initDb();
202
-
203
- if (this.debug) {
204
- offsetTime = Date.now();
205
- log.bgColorLog("cyan", `Init db took ${offsetTime - startTime} ms`);
176
+ /**
177
+ * Internal cache used to track registered handlers and potentially any overlapping routes
178
+ */
179
+ private handlerRouteCache = new Map<string, string>();
180
+
181
+ constructor(opts: FlinkOptions) {
182
+ this.name = opts.name;
183
+ this.port = opts.port || 3333;
184
+ this.dbOpts = opts.db;
185
+ this.debug = !!opts.debug;
186
+ this.onDbConnection = opts.onDbConnection;
187
+ this.plugins = opts.plugins || [];
188
+ this.corsOpts = { ...defaultCorsOptions, ...opts.cors };
189
+ this.auth = opts.auth;
190
+ this.jsonOptions = opts.jsonOptions || { limit : "1mb"}
206
191
  }
207
192
 
208
- await this.buildContext();
209
-
210
- if (this.debug) {
211
- log.bgColorLog(
212
- "cyan",
213
- `Build context took ${Date.now() - offsetTime} ms`
214
- );
215
- offsetTime = Date.now();
193
+ get ctx() {
194
+ if (!this._ctx) {
195
+ throw new Error("Context is not yet initialized");
196
+ }
197
+ return this._ctx;
216
198
  }
217
199
 
218
- if (this.debug) {
219
- log.bgColorLog(
220
- "cyan",
221
- `Registered JSON schemas took ${Date.now() - offsetTime} ms`
222
- );
223
- offsetTime = Date.now();
224
- }
200
+ async start() {
201
+ const startTime = Date.now();
202
+ let offsetTime = 0;
225
203
 
226
- this.expressApp = express();
204
+ await this.initDb();
227
205
 
228
- this.expressApp.use(cors(this.corsOpts));
206
+ if (this.debug) {
207
+ offsetTime = Date.now();
208
+ log.bgColorLog("cyan", `Init db took ${offsetTime - startTime} ms`);
209
+ }
229
210
 
230
- this.expressApp.use(bodyParser.json());
211
+ await this.buildContext();
231
212
 
232
- this.expressApp.use((req, res, next) => {
233
- req.reqId = v4();
234
- next();
235
- });
213
+ if (this.debug) {
214
+ log.bgColorLog("cyan", `Build context took ${Date.now() - offsetTime} ms`);
215
+ offsetTime = Date.now();
216
+ }
236
217
 
237
- // TODO: Add better more fine grained control when plugins are initialized, i.e. in what order
218
+ if (this.debug) {
219
+ log.bgColorLog("cyan", `Registered JSON schemas took ${Date.now() - offsetTime} ms`);
220
+ offsetTime = Date.now();
221
+ }
238
222
 
239
- for (const plugin of this.plugins) {
240
- let db;
223
+ this.expressApp = express();
241
224
 
242
- if (plugin.db) {
243
- db = await this.initPluginDb(plugin);
244
- }
225
+ this.expressApp.use(cors(this.corsOpts));
245
226
 
246
- if (plugin.init) {
247
- await plugin.init(this, db);
248
- }
227
+ this.expressApp.use(bodyParser.json(this.jsonOptions));
249
228
 
250
- log.info(`Initialized plugin '${plugin.id}'`);
251
- }
229
+ this.expressApp.use((req, res, next) => {
230
+ req.reqId = v4();
231
+ next();
232
+ });
252
233
 
253
- await this.registerAutoRegisterableHandlers();
234
+ // TODO: Add better more fine grained control when plugins are initialized, i.e. in what order
254
235
 
255
- if (this.debug) {
256
- log.bgColorLog(
257
- "cyan",
258
- `Register handlers took ${Date.now() - offsetTime} ms`
259
- );
260
- offsetTime = Date.now();
261
- }
236
+ for (const plugin of this.plugins) {
237
+ let db;
262
238
 
263
- // Register 404 with slight delay to allow all manually added routes to be added
264
- // TODO: Is there a better solution to force this handler to always run last?
265
- setTimeout(() => {
266
- this.expressApp!.use((req, res, next) => {
267
- res.status(404).json(notFound());
268
- });
269
-
270
- this.routingConfigured = true;
271
- });
272
-
273
- this.expressApp?.listen(this.port, () => {
274
- log.fontColorLog(
275
- "magenta",
276
- `⚡️ HTTP server '${this.name}' is running and waiting for connections on ${this.port}`
277
- );
278
-
279
- this.started = true;
280
- });
281
-
282
- return this;
283
- }
284
-
285
- /**
286
- * Manually registers a handler.
287
- *
288
- * Typescript compiler will scan handler function and set schemas
289
- * which are derived from handler function type arguments.
290
- */
291
- public addHandler(
292
- handler: HandlerFile,
293
- routePropsOverride?: Partial<HandlerConfig["routeProps"]>
294
- ) {
295
- if (this.routingConfigured) {
296
- throw new Error(
297
- "Cannot add handler after routes has been registered, make sure to invoke earlier"
298
- );
299
- }
239
+ if (plugin.db) {
240
+ db = await this.initPluginDb(plugin);
241
+ }
300
242
 
301
- const routeProps = { ...(handler.Route || {}), ...routePropsOverride };
243
+ if (plugin.init) {
244
+ await plugin.init(this, db);
245
+ }
302
246
 
303
- if (!routeProps.method) {
304
- log.error(
305
- `Failed to register handler '${handler.__file}': Missing 'method' in route props, either set it or name handler file with HTTP method as prefix`
306
- );
307
- return;
308
- }
247
+ log.info(`Initialized plugin '${plugin.id}'`);
248
+ }
309
249
 
310
- if (!routeProps.path) {
311
- log.error(
312
- `Failed to register handler '${handler.__file}': Missing 'path' in route props`
313
- );
314
- return;
315
- }
250
+ await this.registerAutoRegisterableHandlers();
316
251
 
317
- const dup = this.handlers.find(
318
- (h) =>
319
- h.routeProps.path === routeProps.path &&
320
- h.routeProps.method === routeProps.method
321
- );
252
+ if (this.debug) {
253
+ log.bgColorLog("cyan", `Register handlers took ${Date.now() - offsetTime} ms`);
254
+ offsetTime = Date.now();
255
+ }
322
256
 
323
- const methodAndPath = `${routeProps.method.toUpperCase()} ${
324
- routeProps.path
325
- }`;
257
+ // Register 404 with slight delay to allow all manually added routes to be added
258
+ // TODO: Is there a better solution to force this handler to always run last?
259
+ setTimeout(() => {
260
+ this.expressApp!.use((req, res, next) => {
261
+ res.status(404).json(notFound());
262
+ });
326
263
 
327
- if (dup) {
328
- // TODO: Not sure if there is a case where you'd want to overwrite a route?
329
- log.warn(`${methodAndPath} overlaps existing route`);
330
- }
264
+ this.routingConfigured = true;
265
+ });
331
266
 
332
- const handlerConfig: HandlerConfigWithMethod = {
333
- routeProps: {
334
- ...routeProps,
335
- method: routeProps.method!,
336
- path: routeProps.path!,
337
- },
338
- schema: {
339
- reqSchema: handler.__schemas?.reqSchema,
340
- resSchema: handler.__schemas?.resSchema,
341
- },
342
- queryMetadata: handler.__query || [],
343
- paramsMetadata: handler.__params || [],
344
- };
267
+ this.expressApp?.listen(this.port, () => {
268
+ log.fontColorLog("magenta", `⚡️ HTTP server '${this.name}' is running and waiting for connections on ${this.port}`);
345
269
 
346
- if (handler.__schemas?.reqSchema && !handlerConfig.schema?.reqSchema) {
347
- log.warn(
348
- `Expected request schema ${handler.__schemas.reqSchema} for handler ${methodAndPath} but no such schema was found`
349
- );
350
- }
270
+ this.started = true;
271
+ });
351
272
 
352
- if (handler.__schemas?.resSchema && !handlerConfig.schema?.resSchema) {
353
- log.warn(
354
- `Expected response schema ${handler.__schemas.resSchema} for handler ${methodAndPath} but no such schema was found`
355
- );
273
+ return this;
356
274
  }
357
275
 
358
- this.registerHandler(handlerConfig, handler.default);
359
- }
276
+ /**
277
+ * Manually registers a handler.
278
+ *
279
+ * Typescript compiler will scan handler function and set schemas
280
+ * which are derived from handler function type arguments.
281
+ */
282
+ public addHandler(handler: HandlerFile, routePropsOverride?: Partial<HandlerConfig["routeProps"]>) {
283
+ if (this.routingConfigured) {
284
+ throw new Error("Cannot add handler after routes has been registered, make sure to invoke earlier");
285
+ }
360
286
 
361
- private registerHandler(handlerConfig: HandlerConfig, handler: Handler<any>) {
362
- this.handlers.push(handlerConfig);
287
+ const routeProps = { ...(handler.Route || {}), ...routePropsOverride };
363
288
 
364
- const { routeProps, schema = {} } = handlerConfig;
365
- const { method } = routeProps;
366
- const app = this.expressApp!;
289
+ if (!routeProps.method) {
290
+ log.error(
291
+ `Failed to register handler '${handler.__file}': Missing 'method' in route props, either set it or name handler file with HTTP method as prefix`
292
+ );
293
+ return;
294
+ }
367
295
 
368
- if (!method) {
369
- log.error(`Route ${routeProps.path} is missing http method`);
370
- }
296
+ if (!routeProps.path) {
297
+ log.error(`Failed to register handler '${handler.__file}': Missing 'path' in route props`);
298
+ return;
299
+ }
300
+
301
+ const dup = this.handlers.find((h) => h.routeProps.path === routeProps.path && h.routeProps.method === routeProps.method);
371
302
 
372
- if (method) {
373
- const methodAndRoute = `${method.toUpperCase()} ${routeProps.path}`;
303
+ const methodAndPath = `${routeProps.method.toUpperCase()} ${routeProps.path}`;
374
304
 
375
- app[method](routeProps.path, async (req, res) => {
376
- if (routeProps.permissions) {
377
- if (!(await this.authenticate(req, routeProps.permissions))) {
378
- return res.status(401).json(unauthorized());
379
- }
305
+ if (dup) {
306
+ // TODO: Not sure if there is a case where you'd want to overwrite a route?
307
+ log.warn(`${methodAndPath} overlaps existing route`);
380
308
  }
381
309
 
382
- if (schema.reqSchema) {
383
- const validate = ajv.compile(schema.reqSchema);
384
- const valid = validate(req.body);
385
-
386
- if (!valid) {
387
- log.warn(
388
- `${methodAndRoute}: Bad request ${JSON.stringify(
389
- validate.errors,
390
- null,
391
- 2
392
- )}`
393
- );
310
+ const handlerConfig: HandlerConfigWithMethod = {
311
+ routeProps: {
312
+ ...routeProps,
313
+ method: routeProps.method!,
314
+ path: routeProps.path!,
315
+ },
316
+ schema: {
317
+ reqSchema: handler.__schemas?.reqSchema,
318
+ resSchema: handler.__schemas?.resSchema,
319
+ },
320
+ queryMetadata: handler.__query || [],
321
+ paramsMetadata: handler.__params || [],
322
+ };
323
+
324
+ if (handler.__schemas?.reqSchema && !handlerConfig.schema?.reqSchema) {
325
+ log.warn(`Expected request schema ${handler.__schemas.reqSchema} for handler ${methodAndPath} but no such schema was found`);
326
+ }
394
327
 
395
- log.debug(`Invalid json: ${JSON.stringify(req.body)}`);
396
-
397
- return res.status(400).json({
398
- status: 400,
399
- error: {
400
- id: v4(),
401
- title: "Bad request",
402
- detail: `Schema did not validate ${JSON.stringify(
403
- validate.errors
404
- )}`,
405
- },
406
- });
407
- }
328
+ if (handler.__schemas?.resSchema && !handlerConfig.schema?.resSchema) {
329
+ log.warn(`Expected response schema ${handler.__schemas.resSchema} for handler ${methodAndPath} but no such schema was found`);
408
330
  }
409
331
 
410
- if (routeProps.mockApi && schema.resSchema) {
411
- log.warn(`Mock response for ${req.method.toUpperCase()} ${req.path}`);
332
+ this.registerHandler(handlerConfig, handler.default);
333
+ }
334
+
335
+ private registerHandler(handlerConfig: HandlerConfig, handler: Handler<any>) {
336
+ this.handlers.push(handlerConfig);
412
337
 
413
- const data = generateMockData(schema.resSchema);
338
+ const { routeProps, schema = {} } = handlerConfig;
339
+ const { method } = routeProps;
340
+ const app = this.expressApp!;
414
341
 
415
- res.status(200).json({
416
- status: 200,
417
- data,
418
- });
419
- return;
342
+ if (!method) {
343
+ log.error(`Route ${routeProps.path} is missing http method`);
420
344
  }
421
345
 
422
- let handlerRes: FlinkResponse<any>;
423
-
424
- try {
425
- // 👇 This is where the actual handler gets invoked
426
- handlerRes = await handler({
427
- req,
428
- ctx: this.ctx,
429
- origin: routeProps.origin,
430
- });
431
- } catch (err) {
432
- log.warn(
433
- `Handler '${methodAndRoute}' threw unhandled exception ${err}`
434
- );
435
- return res.status(500).json(internalServerError(err as any));
346
+ if (method) {
347
+ const methodAndRoute = `${method.toUpperCase()} ${routeProps.path}`;
348
+
349
+ app[method](routeProps.path, async (req, res) => {
350
+ if (routeProps.permissions) {
351
+ if (!(await this.authenticate(req, routeProps.permissions))) {
352
+ return res.status(401).json(unauthorized());
353
+ }
354
+ }
355
+
356
+ if (schema.reqSchema) {
357
+ const validate = ajv.compile(schema.reqSchema);
358
+ const valid = validate(req.body);
359
+
360
+ if (!valid) {
361
+ log.warn(`${methodAndRoute}: Bad request ${JSON.stringify(validate.errors, null, 2)}`);
362
+
363
+ log.debug(`Invalid json: ${JSON.stringify(req.body)}`);
364
+
365
+ return res.status(400).json({
366
+ status: 400,
367
+ error: {
368
+ id: v4(),
369
+ title: "Bad request",
370
+ detail: `Schema did not validate ${JSON.stringify(validate.errors)}`,
371
+ },
372
+ });
373
+ }
374
+ }
375
+
376
+ if (routeProps.mockApi && schema.resSchema) {
377
+ log.warn(`Mock response for ${req.method.toUpperCase()} ${req.path}`);
378
+
379
+ const data = generateMockData(schema.resSchema);
380
+
381
+ res.status(200).json({
382
+ status: 200,
383
+ data,
384
+ });
385
+ return;
386
+ }
387
+
388
+ let handlerRes: FlinkResponse<any>;
389
+
390
+ try {
391
+ // 👇 This is where the actual handler gets invoked
392
+ handlerRes = await handler({
393
+ req,
394
+ ctx: this.ctx,
395
+ origin: routeProps.origin,
396
+ });
397
+ } catch (err) {
398
+ log.warn(`Handler '${methodAndRoute}' threw unhandled exception ${err}`);
399
+ console.error(err);
400
+ return res.status(500).json(internalServerError(err as any));
401
+ }
402
+
403
+ if (schema.resSchema && !isError(handlerRes)) {
404
+ const validate = ajv.compile(schema.resSchema);
405
+ const valid = validate(JSON.parse(JSON.stringify(handlerRes.data)));
406
+
407
+ if (!valid) {
408
+ log.warn(`[${req.reqId}] ${methodAndRoute}: Bad response ${JSON.stringify(validate.errors, null, 2)}`);
409
+ log.debug(`Invalid json: ${JSON.stringify(handlerRes.data)}`);
410
+ // log.debug(JSON.stringify(schema, null, 2));
411
+
412
+ return res.status(500).json({
413
+ status: 500,
414
+ error: {
415
+ id: v4(),
416
+ title: "Bad response",
417
+ detail: `Schema did not validate ${JSON.stringify(validate.errors)}`,
418
+ },
419
+ });
420
+ }
421
+ }
422
+
423
+ res.set(handlerRes.headers);
424
+
425
+ res.status(handlerRes.status || 200).json(handlerRes);
426
+ });
427
+
428
+ if (this.handlerRouteCache.has(methodAndRoute)) {
429
+ log.error(`Cannot register handler ${methodAndRoute} - route already registered`);
430
+ return process.exit(1); // TODO: Do we need to exit?
431
+ } else {
432
+ this.handlerRouteCache.set(methodAndRoute, JSON.stringify(routeProps));
433
+ log.info(`Registered route ${methodAndRoute}`);
434
+ }
436
435
  }
436
+ }
437
437
 
438
- if (schema.resSchema && !isError(handlerRes)) {
439
- const validate = ajv.compile(schema.resSchema);
440
- const valid = validate(JSON.parse(JSON.stringify(handlerRes.data)));
441
-
442
- if (!valid) {
443
- log.warn(
444
- `[${req.reqId}] ${methodAndRoute}: Bad response ${JSON.stringify(
445
- validate.errors,
446
- null,
447
- 2
448
- )}`
438
+ /**
439
+ * Register handlers found within the `/src/handlers`
440
+ * directory in Flink App.
441
+ *
442
+ * Will not register any handlers added programmatically.
443
+ */
444
+ private async registerAutoRegisterableHandlers() {
445
+ for (const { handler, assumedHttpMethod } of autoRegisteredHandlers) {
446
+ if (!handler.Route) {
447
+ log.error(`Missing Props in handler ${handler.__file}`);
448
+ continue;
449
+ }
450
+
451
+ if (!handler.default) {
452
+ log.error(`Missing exported handler function in handler ${handler.__file}`);
453
+ continue;
454
+ }
455
+
456
+ this.registerHandler(
457
+ {
458
+ routeProps: {
459
+ ...handler.Route,
460
+ method: handler.Route.method || assumedHttpMethod,
461
+ origin: this.name,
462
+ },
463
+ schema: {
464
+ reqSchema: handler.__schemas?.reqSchema,
465
+ resSchema: handler.__schemas?.resSchema,
466
+ },
467
+ queryMetadata: handler.__query || [],
468
+ paramsMetadata: handler.__params || [],
469
+ },
470
+ handler.default
449
471
  );
450
- log.debug(`Invalid json: ${JSON.stringify(handlerRes.data)}`);
451
- // log.debug(JSON.stringify(schema, null, 2));
452
-
453
- return res.status(500).json({
454
- status: 500,
455
- error: {
456
- id: v4(),
457
- title: "Bad response",
458
- detail: `Schema did not validate ${JSON.stringify(
459
- validate.errors
460
- )}`,
461
- },
462
- });
463
- }
464
472
  }
473
+ }
465
474
 
466
- res.set(handlerRes.headers);
475
+ public addRepo(instanceName: string, repoInstance: FlinkRepo<C>) {
476
+ this.repos[instanceName] = repoInstance;
477
+ // TODO: Find out if we need to set ctx here or wanted not to if plugin has its own context
478
+ // repoInstance.ctx = this.ctx;
479
+ }
467
480
 
468
- res.status(handlerRes.status || 200).json(handlerRes);
469
- });
481
+ /**
482
+ * Constructs the app context. Will inject context in all components
483
+ * except for handlers which are handled in later stage.
484
+ */
485
+ private async buildContext() {
486
+ if (this.dbOpts) {
487
+ for (const { collectionName, repoInstanceName, Repo } of autoRegisteredRepos) {
488
+ const repoInstance: FlinkRepo<C> = new Repo(collectionName, this.db);
470
489
 
471
- if (this.handlerRouteCache.has(methodAndRoute)) {
472
- log.error(
473
- `Cannot register handler ${methodAndRoute} - route already registered`
474
- );
475
- return process.exit(1); // TODO: Do we need to exit?
476
- } else {
477
- this.handlerRouteCache.set(methodAndRoute, JSON.stringify(routeProps));
478
- log.info(`Registered route ${methodAndRoute}`);
479
- }
480
- }
481
- }
482
-
483
- /**
484
- * Register handlers found within the `/src/handlers`
485
- * directory in Flink App.
486
- *
487
- * Will not register any handlers added programmatically.
488
- */
489
- private async registerAutoRegisterableHandlers() {
490
- for (const { handler, assumedHttpMethod } of autoRegisteredHandlers) {
491
- if (!handler.Route) {
492
- log.error(`Missing Props in handler ${handler.__file}`);
493
- continue;
494
- }
495
-
496
- if (!handler.default) {
497
- log.error(
498
- `Missing exported handler function in handler ${handler.__file}`
499
- );
500
- continue;
501
- }
502
-
503
- this.registerHandler(
504
- {
505
- routeProps: {
506
- ...handler.Route,
507
- method: handler.Route.method || assumedHttpMethod,
508
- origin: this.name,
509
- },
510
- schema: {
511
- reqSchema: handler.__schemas?.reqSchema,
512
- resSchema: handler.__schemas?.resSchema,
513
- },
514
- queryMetadata: handler.__query || [],
515
- paramsMetadata: handler.__params || [],
516
- },
517
- handler.default
518
- );
519
- }
520
- }
521
-
522
- public addRepo(instanceName: string, repoInstance: FlinkRepo<C>) {
523
- this.repos[instanceName] = repoInstance;
524
- }
525
-
526
- /**
527
- * Constructs the app context. Will inject context in all components
528
- * except for handlers which are handled in later stage.
529
- */
530
- private async buildContext() {
531
- if (this.dbOpts) {
532
- for (const {
533
- collectionName,
534
- repoInstanceName,
535
- Repo,
536
- } of autoRegisteredRepos) {
537
- const repoInstance: FlinkRepo<C> = new Repo(collectionName, this.db);
538
-
539
- this.repos[repoInstanceName] = repoInstance;
540
-
541
- log.info(`Registered repo ${repoInstanceName}`);
542
- }
543
- } else if (autoRegisteredRepos.length > 0) {
544
- log.warn(`No db configured but found repo(s)`);
545
- }
490
+ this.repos[repoInstanceName] = repoInstance;
546
491
 
547
- const pluginCtx = this.plugins.reduce<{ [x: string]: any }>(
548
- (out, plugin) => {
549
- if (out[plugin.id]) {
550
- throw new Error(`Plugin ${plugin.id} is already registered`);
492
+ log.info(`Registered repo ${repoInstanceName}`);
493
+ }
494
+ } else if (autoRegisteredRepos.length > 0) {
495
+ log.warn(`No db configured but found repo(s)`);
496
+ }
497
+
498
+ const pluginCtx = this.plugins.reduce<{ [x: string]: any }>((out, plugin) => {
499
+ if (out[plugin.id]) {
500
+ throw new Error(`Plugin ${plugin.id} is already registered`);
501
+ }
502
+ out[plugin.id] = plugin.ctx;
503
+ return out;
504
+ }, {});
505
+
506
+ this._ctx = {
507
+ repos: this.repos,
508
+ plugins: pluginCtx,
509
+ auth: this.auth,
510
+ } as C;
511
+
512
+ for (const repo of Object.values(this.repos)) {
513
+ repo.ctx = this.ctx;
551
514
  }
552
- out[plugin.id] = plugin.ctx;
553
- return out;
554
- },
555
- {}
556
- );
557
-
558
- this._ctx = {
559
- repos: this.repos,
560
- plugins: pluginCtx,
561
- auth: this.auth,
562
- } as C;
563
- }
564
-
565
- /**
566
- * Connects to database.
567
- */
568
- private async initDb() {
569
- if (this.dbOpts) {
570
- try {
571
- log.debug("Connecting to db");
572
- const client = await mongodb.connect(this.dbOpts.uri, {
573
- useUnifiedTopology: true,
574
- connectTimeoutMS: 4000,
575
- });
576
- this.db = client.db();
577
- } catch (err) {
578
- log.error("Failed to connect to db: " + err);
579
- process.exit(1);
580
- }
581
-
582
- if (this.onDbConnection) {
583
- await this.onDbConnection(this.db);
584
- }
585
515
  }
586
- }
587
-
588
- /**
589
- * Connects plugin to database.
590
- */
591
- private async initPluginDb(plugin: FlinkPlugin) {
592
- if (!plugin.db) {
593
- return;
516
+
517
+ /**
518
+ * Connects to database.
519
+ */
520
+ private async initDb() {
521
+ if (this.dbOpts) {
522
+ try {
523
+ log.debug("Connecting to db");
524
+ const client = await mongodb.connect(this.dbOpts.uri, {
525
+ useUnifiedTopology: true,
526
+ connectTimeoutMS: 4000,
527
+ });
528
+ this.db = client.db();
529
+ } catch (err) {
530
+ log.error("Failed to connect to db: " + err);
531
+ process.exit(1);
532
+ }
533
+
534
+ if (this.onDbConnection) {
535
+ await this.onDbConnection(this.db);
536
+ }
537
+ }
594
538
  }
595
539
 
596
- if (plugin.db) {
597
- if (plugin.db.useHostDb) {
598
- if (!this.db) {
599
- log.error(
600
- `Plugin '${this.name} configured to use host app db, but no db exists in FlinkApp'`
601
- );
602
- } else {
603
- return this.db;
540
+ /**
541
+ * Connects plugin to database.
542
+ */
543
+ private async initPluginDb(plugin: FlinkPlugin) {
544
+ if (!plugin.db) {
545
+ return;
604
546
  }
605
- } else if (plugin.db.uri) {
606
- try {
607
- log.debug(`Connecting to '${plugin.id}' db`);
608
- const client = await mongodb.connect(plugin.db.uri, {
609
- useUnifiedTopology: true,
610
- });
611
- return client.db();
612
- } catch (err) {
613
- log.error(
614
- `Failed to connect to db defined in plugin '${plugin.id}': ` + err
615
- );
547
+
548
+ if (plugin.db) {
549
+ if (plugin.db.useHostDb) {
550
+ if (!this.db) {
551
+ log.error(`Plugin '${this.name} configured to use host app db, but no db exists in FlinkApp'`);
552
+ } else {
553
+ return this.db;
554
+ }
555
+ } else if (plugin.db.uri) {
556
+ try {
557
+ log.debug(`Connecting to '${plugin.id}' db`);
558
+ const client = await mongodb.connect(plugin.db.uri, {
559
+ useUnifiedTopology: true,
560
+ });
561
+ return client.db();
562
+ } catch (err) {
563
+ log.error(`Failed to connect to db defined in plugin '${plugin.id}': ` + err);
564
+ }
565
+ }
616
566
  }
617
- }
618
567
  }
619
- }
620
568
 
621
- private async authenticate(req: Request, permissions: string | string[]) {
622
- if (!this.auth) {
623
- throw new Error(
624
- `Attempting to authenticate request (${req.method} ${req.path}) but no authPlugin is set`
625
- );
569
+ private async authenticate(req: Request, permissions: string | string[]) {
570
+ if (!this.auth) {
571
+ throw new Error(`Attempting to authenticate request (${req.method} ${req.path}) but no authPlugin is set`);
572
+ }
573
+ return await this.auth.authenticateRequest(req, permissions);
626
574
  }
627
- return await this.auth.authenticateRequest(req, permissions);
628
- }
629
575
 
630
- public getRegisteredRoutes() {
631
- return Array.from(this.handlerRouteCache.values());
632
- }
576
+ public getRegisteredRoutes() {
577
+ return Array.from(this.handlerRouteCache.values());
578
+ }
633
579
  }