@flink-app/flink 0.14.0 → 0.14.2

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
@@ -1,5 +1,17 @@
1
1
  # @flink-app/flink
2
2
 
3
+ ## 0.14.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 6d39832: Add optional onError callback to FlinkApp for custom error handling. The callback receives a properly typed FlinkResponse<FlinkError> and rich request context (including request object, HTTP method, path, and request ID) when handler execution throws an error. Supports both synchronous and asynchronous callbacks, with automatic error handling to prevent callback failures from affecting the error response flow. Enables custom logging or monitoring without modifying the response flow.
8
+
9
+ ## 0.14.1
10
+
11
+ ### Patch Changes
12
+
13
+ - fix: restore compatibility between core framework and plugins after 0.14.0 release
14
+
3
15
  ## 0.14.0
4
16
 
5
17
  ### Minor Changes
@@ -5,10 +5,12 @@ import { Db, MongoClient } from "mongodb";
5
5
  import { ToadScheduler } from "toad-scheduler";
6
6
  import { FlinkAuthPlugin } from "./auth/FlinkAuthPlugin";
7
7
  import { FlinkContext } from "./FlinkContext";
8
- import { HandlerFile, HttpMethod, QueryParamMetadata, RouteProps } from "./FlinkHttpHandler";
8
+ import { FlinkError } from "./FlinkErrors";
9
+ import { FlinkRequest, HandlerFile, HttpMethod, QueryParamMetadata, RouteProps } from "./FlinkHttpHandler";
9
10
  import { FlinkJobFile } from "./FlinkJob";
10
11
  import { FlinkPlugin } from "./FlinkPlugin";
11
12
  import { FlinkRepo } from "./FlinkRepo";
13
+ import { FlinkResponse } from "./FlinkResponse";
12
14
  export type JSONSchema = JSONSchema7;
13
15
  /**
14
16
  * Re-export express factory function so sub packages
@@ -136,6 +138,50 @@ export interface FlinkOptions {
136
138
  */
137
139
  format?: string;
138
140
  };
141
+ /**
142
+ * Optional callback invoked when an error occurs in a handler.
143
+ * The error response and request context are passed for custom
144
+ * error logging or monitoring. This is a side-effect only and
145
+ * will not modify the response flow.
146
+ *
147
+ * Supports both synchronous and asynchronous callbacks. Any errors
148
+ * thrown or rejected by the callback will be caught and logged
149
+ * without affecting the error response to the client.
150
+ *
151
+ * @param error - The error response with status and error details
152
+ * @param context - Request context including method, path, and request ID
153
+ *
154
+ * @example
155
+ * ```ts
156
+ * // Synchronous callback
157
+ * onError: (error, context) => {
158
+ * if (error.status >= 500) {
159
+ * logger.error('Server error occurred', {
160
+ * status: error.status,
161
+ * code: error.error?.code,
162
+ * route: `${context.method} ${context.path}`,
163
+ * reqId: context.reqId
164
+ * });
165
+ * }
166
+ * }
167
+ *
168
+ * // Asynchronous callback
169
+ * onError: async (error, context) => {
170
+ * if (error.status >= 500) {
171
+ * await monitoringService.reportError({
172
+ * error,
173
+ * context
174
+ * });
175
+ * }
176
+ * }
177
+ * ```
178
+ */
179
+ onError?: (error: FlinkResponse<FlinkError>, context: {
180
+ req: FlinkRequest;
181
+ method: HttpMethod;
182
+ path: string;
183
+ reqId: string;
184
+ }) => void | Promise<void>;
139
185
  }
140
186
  export interface HandlerConfig {
141
187
  schema?: {
@@ -178,6 +224,7 @@ export declare class FlinkApp<C extends FlinkContext> {
178
224
  private schedulingOptions?;
179
225
  private disableHttpServer;
180
226
  private expressServer;
227
+ private onError?;
181
228
  private repos;
182
229
  /**
183
230
  * Internal cache used to track registered handlers and potentially any overlapping routes
@@ -122,6 +122,7 @@ var FlinkApp = /** @class */ (function () {
122
122
  this.schedulingOptions = opts.scheduling;
123
123
  this.disableHttpServer = !!opts.disableHttpServer;
124
124
  this.accessLog = __assign({ enabled: true, format: "dev" }, opts.accessLog);
125
+ this.onError = opts.onError;
125
126
  }
126
127
  Object.defineProperty(FlinkApp.prototype, "ctx", {
127
128
  get: function () {
@@ -341,7 +342,7 @@ var FlinkApp = /** @class */ (function () {
341
342
  validateRes_1 = ajv.compile(schema.resSchema);
342
343
  }
343
344
  this.expressApp[method](routeProps.path, function (req, res) { return __awaiter(_this, void 0, void 0, function () {
344
- var valid, formattedErrors, data, handlerRes, err_1, valid, formattedErrors;
345
+ var valid, formattedErrors, data, handlerRes, err_1, errorResponse, result, valid, formattedErrors;
345
346
  return __generator(this, function (_a) {
346
347
  switch (_a.label) {
347
348
  case 0:
@@ -391,21 +392,45 @@ var FlinkApp = /** @class */ (function () {
391
392
  return [3 /*break*/, 6];
392
393
  case 5:
393
394
  err_1 = _a.sent();
395
+ errorResponse = void 0;
394
396
  // duck typing to check if it is a FlinkError
395
397
  if (typeof err_1.status === "number" && err_1.status >= 400 && err_1.status < 600 && err_1.error) {
396
- return [2 /*return*/, res.status(err_1.status).json({
397
- status: err_1.status,
398
- error: {
399
- id: err_1.error.id || (0, uuid_1.v4)(),
400
- title: err_1.error.title || "Unhandled error: ".concat(err_1.error.code || err_1.status),
401
- detail: err_1.error.detail,
402
- code: err_1.error.code,
403
- },
404
- })];
398
+ errorResponse = {
399
+ status: err_1.status,
400
+ error: {
401
+ id: err_1.error.id || (0, uuid_1.v4)(),
402
+ title: err_1.error.title || "Unhandled error: ".concat(err_1.error.code || err_1.status),
403
+ detail: err_1.error.detail,
404
+ code: err_1.error.code,
405
+ },
406
+ };
405
407
  }
406
- FlinkLog_1.log.warn("Handler '".concat(methodAndRoute_1, "' threw unhandled exception ").concat(err_1));
407
- console.error(err_1);
408
- return [2 /*return*/, res.status(500).json((0, FlinkErrors_1.internalServerError)(err_1))];
408
+ else {
409
+ FlinkLog_1.log.warn("Handler '".concat(methodAndRoute_1, "' threw unhandled exception ").concat(err_1));
410
+ console.error(err_1);
411
+ errorResponse = (0, FlinkErrors_1.internalServerError)(err_1);
412
+ }
413
+ // Invoke onError callback if provided
414
+ if (this.onError) {
415
+ try {
416
+ result = this.onError(errorResponse, {
417
+ req: req,
418
+ method: method,
419
+ path: routeProps.path,
420
+ reqId: req.reqId,
421
+ });
422
+ // Handle async callbacks - don't wait for them
423
+ if (result instanceof Promise) {
424
+ result.catch(function (callbackErr) {
425
+ FlinkLog_1.log.error("onError callback rejected with: ".concat(callbackErr));
426
+ });
427
+ }
428
+ }
429
+ catch (callbackErr) {
430
+ FlinkLog_1.log.error("onError callback threw an exception: ".concat(callbackErr));
431
+ }
432
+ }
433
+ return [2 /*return*/, res.status(errorResponse.status || 500).json(errorResponse)];
409
434
  case 6:
410
435
  if (validateRes_1 && !(0, utils_1.isError)(handlerRes)) {
411
436
  valid = validateRes_1(JSON.parse(JSON.stringify(handlerRes.data)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flink-app/flink",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
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",
@@ -0,0 +1,80 @@
1
+ import { FlinkApp } from "../src/FlinkApp";
2
+ import { FlinkContext } from "../src/FlinkContext";
3
+ import { FlinkResponse } from "../src/FlinkResponse";
4
+ import { FlinkError } from "../src/FlinkErrors";
5
+ import { badRequest, internalServerError, notFound } from "../src/FlinkErrors";
6
+ import { HttpMethod } from "../src/FlinkHttpHandler";
7
+
8
+ interface TestContext extends FlinkContext {}
9
+
10
+ describe("FlinkApp onError callback", () => {
11
+ let app: FlinkApp<TestContext>;
12
+ let capturedError: FlinkResponse<FlinkError> | null;
13
+ let capturedContext: any;
14
+
15
+ beforeEach(() => {
16
+ capturedError = null;
17
+ capturedContext = null;
18
+ });
19
+
20
+ afterEach(async () => {
21
+ if (app && app.started) {
22
+ await app.stop();
23
+ }
24
+ });
25
+
26
+ it("should be configurable in FlinkOptions", () => {
27
+ const callback = (error: FlinkResponse<FlinkError>, context: any) => {
28
+ capturedError = error;
29
+ capturedContext = context;
30
+ };
31
+
32
+ app = new FlinkApp<TestContext>({
33
+ name: "test-app",
34
+ disableHttpServer: true,
35
+ onError: callback,
36
+ });
37
+
38
+ expect(app).toBeDefined();
39
+ });
40
+
41
+ it("should have proper type signature for onError callback", () => {
42
+ // This test verifies TypeScript compilation
43
+ // The callback should accept FlinkResponse<FlinkError> and context object
44
+
45
+ const syncCallback = (error: FlinkResponse<FlinkError>, context: { req: any; method: HttpMethod; path: string; reqId: string }) => {
46
+ console.log(error.status, context.reqId);
47
+ };
48
+
49
+ const asyncCallback = async (error: FlinkResponse<FlinkError>, context: { req: any; method: HttpMethod; path: string; reqId: string }) => {
50
+ await Promise.resolve();
51
+ console.log(error.status, context.reqId);
52
+ };
53
+
54
+ const app1 = new FlinkApp<TestContext>({
55
+ name: "test-app-1",
56
+ disableHttpServer: true,
57
+ onError: syncCallback,
58
+ });
59
+
60
+ const app2 = new FlinkApp<TestContext>({
61
+ name: "test-app-2",
62
+ disableHttpServer: true,
63
+ onError: asyncCallback,
64
+ });
65
+
66
+ expect(app1).toBeDefined();
67
+ expect(app2).toBeDefined();
68
+ });
69
+
70
+ it("should be optional in FlinkOptions", async () => {
71
+ app = new FlinkApp<TestContext>({
72
+ name: "test-app",
73
+ disableHttpServer: true,
74
+ // No onError callback
75
+ });
76
+
77
+ await app.start();
78
+ expect(app.started).toBe(true);
79
+ });
80
+ });
package/src/FlinkApp.ts CHANGED
@@ -11,7 +11,7 @@ import { AsyncTask, CronJob, SimpleIntervalJob, ToadScheduler } from "toad-sched
11
11
  import { v4 } from "uuid";
12
12
  import { FlinkAuthPlugin } from "./auth/FlinkAuthPlugin";
13
13
  import { FlinkContext } from "./FlinkContext";
14
- import { internalServerError, notFound, unauthorized } from "./FlinkErrors";
14
+ import { FlinkError, internalServerError, notFound, unauthorized } from "./FlinkErrors";
15
15
  import { FlinkRequest, Handler, HandlerFile, HttpMethod, QueryParamMetadata, RouteProps } from "./FlinkHttpHandler";
16
16
  import { FlinkJobFile } from "./FlinkJob";
17
17
  import { log } from "./FlinkLog";
@@ -197,6 +197,54 @@ export interface FlinkOptions {
197
197
  */
198
198
  format?: string;
199
199
  };
200
+
201
+ /**
202
+ * Optional callback invoked when an error occurs in a handler.
203
+ * The error response and request context are passed for custom
204
+ * error logging or monitoring. This is a side-effect only and
205
+ * will not modify the response flow.
206
+ *
207
+ * Supports both synchronous and asynchronous callbacks. Any errors
208
+ * thrown or rejected by the callback will be caught and logged
209
+ * without affecting the error response to the client.
210
+ *
211
+ * @param error - The error response with status and error details
212
+ * @param context - Request context including method, path, and request ID
213
+ *
214
+ * @example
215
+ * ```ts
216
+ * // Synchronous callback
217
+ * onError: (error, context) => {
218
+ * if (error.status >= 500) {
219
+ * logger.error('Server error occurred', {
220
+ * status: error.status,
221
+ * code: error.error?.code,
222
+ * route: `${context.method} ${context.path}`,
223
+ * reqId: context.reqId
224
+ * });
225
+ * }
226
+ * }
227
+ *
228
+ * // Asynchronous callback
229
+ * onError: async (error, context) => {
230
+ * if (error.status >= 500) {
231
+ * await monitoringService.reportError({
232
+ * error,
233
+ * context
234
+ * });
235
+ * }
236
+ * }
237
+ * ```
238
+ */
239
+ onError?: (
240
+ error: FlinkResponse<FlinkError>,
241
+ context: {
242
+ req: FlinkRequest;
243
+ method: HttpMethod;
244
+ path: string;
245
+ reqId: string;
246
+ }
247
+ ) => void | Promise<void>;
200
248
  }
201
249
 
202
250
  export interface HandlerConfig {
@@ -242,6 +290,7 @@ export class FlinkApp<C extends FlinkContext> {
242
290
  private schedulingOptions?: FlinkOptions["scheduling"];
243
291
  private disableHttpServer = false;
244
292
  private expressServer: any; // for simplicity, we don't want to import types from express/node here
293
+ private onError?: FlinkOptions["onError"];
245
294
 
246
295
  private repos: { [x: string]: FlinkRepo<C, any> } = {};
247
296
 
@@ -272,6 +321,7 @@ export class FlinkApp<C extends FlinkContext> {
272
321
  this.schedulingOptions = opts.scheduling;
273
322
  this.disableHttpServer = !!opts.disableHttpServer;
274
323
  this.accessLog = { enabled: true, format: "dev", ...opts.accessLog };
324
+ this.onError = opts.onError;
275
325
  }
276
326
 
277
327
  get ctx() {
@@ -540,9 +590,11 @@ export class FlinkApp<C extends FlinkContext> {
540
590
  origin: routeProps.origin,
541
591
  });
542
592
  } catch (err: any) {
593
+ let errorResponse: FlinkResponse<FlinkError>;
594
+
543
595
  // duck typing to check if it is a FlinkError
544
596
  if (typeof err.status === "number" && err.status >= 400 && err.status < 600 && err.error) {
545
- return res.status(err.status).json({
597
+ errorResponse = {
546
598
  status: err.status,
547
599
  error: {
548
600
  id: err.error.id || v4(),
@@ -550,12 +602,35 @@ export class FlinkApp<C extends FlinkContext> {
550
602
  detail: err.error.detail,
551
603
  code: err.error.code,
552
604
  },
553
- });
605
+ };
606
+ } else {
607
+ log.warn(`Handler '${methodAndRoute}' threw unhandled exception ${err}`);
608
+ console.error(err);
609
+ errorResponse = internalServerError(err as any);
554
610
  }
555
611
 
556
- log.warn(`Handler '${methodAndRoute}' threw unhandled exception ${err}`);
557
- console.error(err);
558
- return res.status(500).json(internalServerError(err as any));
612
+ // Invoke onError callback if provided
613
+ if (this.onError) {
614
+ try {
615
+ const result = this.onError(errorResponse, {
616
+ req: req as FlinkRequest,
617
+ method: method!,
618
+ path: routeProps.path,
619
+ reqId: req.reqId,
620
+ });
621
+
622
+ // Handle async callbacks - don't wait for them
623
+ if (result instanceof Promise) {
624
+ result.catch((callbackErr) => {
625
+ log.error(`onError callback rejected with: ${callbackErr}`);
626
+ });
627
+ }
628
+ } catch (callbackErr) {
629
+ log.error(`onError callback threw an exception: ${callbackErr}`);
630
+ }
631
+ }
632
+
633
+ return res.status(errorResponse.status || 500).json(errorResponse);
559
634
  }
560
635
 
561
636
  if (validateRes && !isError(handlerRes)) {