@fjell/express-router 4.4.54 → 4.4.56

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,4 +1,5 @@
1
1
  import {
2
+ ActionError,
2
3
  cPK,
3
4
  validatePK
4
5
  } from "@fjell/core";
@@ -6,6 +7,7 @@ import { NotFoundError } from "@fjell/lib";
6
7
  import deepmerge from "deepmerge";
7
8
  import { Router } from "express";
8
9
  import LibLogger from "./logger.js";
10
+ import { createErrorHandler } from "./errorHandler.js";
9
11
  class ItemRouter {
10
12
  lib;
11
13
  keyType;
@@ -13,12 +15,22 @@ class ItemRouter {
13
15
  childRouters = {};
14
16
  logger;
15
17
  parentRoute;
18
+ errorHandler;
16
19
  constructor(lib, keyType, options = {}, parentRoute) {
17
20
  this.lib = lib;
18
21
  this.keyType = keyType;
19
22
  this.options = options;
20
23
  this.parentRoute = parentRoute;
21
24
  this.logger = LibLogger.get("ItemRouter", keyType);
25
+ this.errorHandler = createErrorHandler(options.errorHandler);
26
+ }
27
+ /**
28
+ * Wrap async route handlers to catch errors and pass to error handler
29
+ */
30
+ wrapAsync(fn) {
31
+ return (req, res, next) => {
32
+ Promise.resolve(fn.call(this, req, res, next)).catch(next);
33
+ };
22
34
  }
23
35
  getPkType = () => {
24
36
  return this.keyType;
@@ -53,172 +65,142 @@ class ItemRouter {
53
65
  throw new Error("Method not implemented in an abstract router");
54
66
  }
55
67
  postAllAction = async (req, res) => {
56
- const libOptions = this.lib.options;
57
68
  const libOperations = this.lib.operations;
58
69
  this.logger.debug("Posting All Action", { query: req?.query, params: req?.params, locals: res?.locals });
59
70
  const allActionKey = req.path.substring(req.path.lastIndexOf("/") + 1);
60
- if (this.options.allActions && this.options.allActions[allActionKey]) {
61
- this.logger.debug("Using router-level all action handler", { allActionKey });
62
- try {
63
- const result = await this.options.allActions[allActionKey](
71
+ try {
72
+ if (this.options.allActions && this.options.allActions[allActionKey]) {
73
+ this.logger.debug("Using router-level all action handler", { allActionKey });
74
+ const result2 = await this.options.allActions[allActionKey](
64
75
  req.body,
65
76
  this.getLocations(res),
66
77
  { req, res }
67
78
  );
68
- if (result != null) res.json(result);
79
+ if (result2 != null) {
80
+ res.json(result2);
81
+ }
69
82
  return;
70
- } catch (err) {
71
- this.logger.error("Error in router-level all action", { message: err?.message, stack: err?.stack });
72
- res.status(500).json(err);
83
+ }
84
+ if (!libOperations.allAction) {
85
+ res.status(500).json({ error: "All Actions are not configured" });
73
86
  return;
74
87
  }
75
- }
76
- if (!libOptions.allActions) {
77
- this.logger.error("All Actions are not configured");
78
- res.status(500).json({ error: "All Actions are not configured" });
79
- return;
80
- }
81
- const allAction = libOptions.allActions[allActionKey];
82
- if (!allAction) {
83
- this.logger.error("All Action is not configured", { allActionKey });
84
- res.status(500).json({ error: "All Action is not configured" });
85
- return;
86
- }
87
- try {
88
- const [result, affectedItems] = await libOperations.allAction(allActionKey, req.body, this.getLocations(res));
89
- res.json([result, affectedItems]);
90
- } catch (err) {
91
- this.logger.error("Error in All Action", { message: err?.message, stack: err?.stack });
92
- res.status(500).json(err);
88
+ const [result] = await libOperations.allAction(allActionKey, req.body, this.getLocations(res));
89
+ res.json(result);
90
+ } catch (error) {
91
+ this.logger.error("Error in postAllAction", { error });
92
+ if ((error.name === "ValidationError" || error.message?.includes("not found")) && error.message) {
93
+ res.status(500).json({ error: "All Action is not configured" });
94
+ } else {
95
+ res.status(500).json(error);
96
+ }
93
97
  }
94
98
  };
95
99
  getAllFacet = async (req, res) => {
96
- const libOptions = this.lib.options;
97
100
  const libOperations = this.lib.operations;
98
101
  this.logger.debug("Getting All Facet", { query: req?.query, params: req?.params, locals: res?.locals });
99
102
  const facetKey = req.path.substring(req.path.lastIndexOf("/") + 1);
100
- if (this.options.allFacets && this.options.allFacets[facetKey]) {
101
- this.logger.debug("Using router-level all facet handler", { facetKey });
102
- try {
103
- const result = await this.options.allFacets[facetKey](
103
+ try {
104
+ if (this.options.allFacets && this.options.allFacets[facetKey]) {
105
+ this.logger.debug("Using router-level all facet handler", { facetKey });
106
+ const result2 = await this.options.allFacets[facetKey](
104
107
  req.query,
105
108
  this.getLocations(res),
106
109
  { req, res }
107
110
  );
108
- if (result != null) res.json(result);
111
+ if (result2 != null) {
112
+ res.json(result2);
113
+ }
109
114
  return;
110
- } catch (err) {
111
- this.logger.error("Error in router-level all facet", { message: err?.message, stack: err?.stack });
112
- res.status(500).json(err);
115
+ }
116
+ if (!libOperations.allFacet) {
117
+ res.status(500).json({ error: "All Facets are not configured" });
113
118
  return;
114
119
  }
115
- }
116
- if (!libOptions.allFacets) {
117
- this.logger.error("All Facets are not configured");
118
- res.status(500).json({ error: "All Facets are not configured" });
119
- return;
120
- }
121
- const facet = libOptions.allFacets[facetKey];
122
- if (!facet) {
123
- this.logger.error("All Facet is not configured", { facetKey });
124
- res.status(500).json({ error: "All Facet is not configured" });
125
- return;
126
- }
127
- try {
128
120
  const combinedQueryParams = { ...req.query || {}, ...req.params || {} };
129
- res.json(await libOperations.allFacet(facetKey, combinedQueryParams, this.getLocations(res)));
130
- } catch (err) {
131
- this.logger.error("Error in All Facet", { message: err?.message, stack: err?.stack });
132
- res.status(500).json(err);
121
+ const result = await libOperations.allFacet(facetKey, combinedQueryParams, this.getLocations(res));
122
+ res.json(result);
123
+ } catch (error) {
124
+ this.logger.error("Error in getAllFacet", { error });
125
+ if ((error.name === "ValidationError" || error.message?.includes("not found")) && error.message) {
126
+ res.status(500).json({ error: "All Facet is not configured" });
127
+ } else {
128
+ res.status(500).json(error);
129
+ }
133
130
  }
134
131
  };
135
132
  postItemAction = async (req, res) => {
136
- const libOptions = this.lib.options;
137
133
  const libOperations = this.lib.operations;
138
134
  this.logger.debug("Posting Item Action", { query: req?.query, params: req?.params, locals: res?.locals });
139
135
  const ik = this.getIk(res);
140
136
  const actionKey = req.path.substring(req.path.lastIndexOf("/") + 1);
141
- if (this.options.actions && this.options.actions[actionKey]) {
142
- this.logger.debug("Using router-level action handler", { actionKey });
143
- try {
144
- const result = await this.options.actions[actionKey](
137
+ try {
138
+ if (this.options.actions && this.options.actions[actionKey]) {
139
+ this.logger.debug("Using router-level action handler", { actionKey });
140
+ const result2 = await this.options.actions[actionKey](
145
141
  ik,
146
142
  req.body,
147
143
  { req, res }
148
144
  );
149
- if (result != null) res.json(result);
145
+ if (result2 != null) {
146
+ res.json(result2);
147
+ }
150
148
  return;
151
- } catch (err) {
152
- this.logger.error("Error in router-level action", { message: err?.message, stack: err?.stack });
153
- res.status(500).json(err);
149
+ }
150
+ if (!libOperations.action) {
151
+ res.status(500).json({ error: "Item Actions are not configured" });
154
152
  return;
155
153
  }
156
- }
157
- if (!libOptions.actions) {
158
- this.logger.error("Item Actions are not configured");
159
- res.status(500).json({ error: "Item Actions are not configured" });
160
- return;
161
- }
162
- const action = libOptions.actions[actionKey];
163
- if (!action) {
164
- this.logger.error("Item Action is not configured", { actionKey });
165
- res.status(500).json({ error: "Item Action is not configured" });
166
- return;
167
- }
168
- try {
169
- const [result, affectedItems] = await libOperations.action(ik, actionKey, req.body);
170
- res.json([result, affectedItems]);
171
- } catch (err) {
172
- this.logger.error("Error in Item Action", { message: err?.message, stack: err?.stack });
173
- res.status(500).json(err);
154
+ const [result] = await libOperations.action(ik, actionKey, req.body);
155
+ res.json(result);
156
+ } catch (error) {
157
+ this.logger.error("Error in postItemAction", { error });
158
+ if ((error.name === "ValidationError" || error.message?.includes("not found")) && error.message) {
159
+ res.status(500).json({ error: "Item Action is not configured" });
160
+ } else {
161
+ res.status(500).json(error);
162
+ }
174
163
  }
175
164
  };
176
165
  getItemFacet = async (req, res) => {
177
- const libOptions = this.lib.options;
178
166
  const libOperations = this.lib.operations;
179
167
  this.logger.debug("Getting Item Facet", { query: req?.query, params: req?.params, locals: res?.locals });
180
168
  const ik = this.getIk(res);
181
169
  const facetKey = req.path.substring(req.path.lastIndexOf("/") + 1);
182
- if (this.options.facets && this.options.facets[facetKey]) {
183
- this.logger.debug("Using router-level facet handler", { facetKey });
184
- try {
185
- const result = await this.options.facets[facetKey](
170
+ try {
171
+ if (this.options.facets && this.options.facets[facetKey]) {
172
+ this.logger.debug("Using router-level facet handler", { facetKey });
173
+ const result2 = await this.options.facets[facetKey](
186
174
  ik,
187
175
  req.query,
188
176
  { req, res }
189
177
  );
190
- if (result != null) res.json(result);
178
+ if (result2 != null) {
179
+ res.json(result2);
180
+ }
191
181
  return;
192
- } catch (err) {
193
- this.logger.error("Error in router-level facet", { message: err?.message, stack: err?.stack });
194
- res.status(500).json(err);
182
+ }
183
+ if (!libOperations.facet) {
184
+ res.status(500).json({ error: "Item Facets are not configured" });
195
185
  return;
196
186
  }
197
- }
198
- if (!libOptions.facets) {
199
- this.logger.error("Item Facets are not configured");
200
- res.status(500).json({ error: "Item Facets are not configured" });
201
- return;
202
- }
203
- const facet = libOptions.facets[facetKey];
204
- if (!facet) {
205
- this.logger.error("Item Facet is not configured", { facetKey });
206
- res.status(500).json({ error: "Item Facet is not configured" });
207
- return;
208
- }
209
- try {
210
187
  const combinedQueryParams = { ...req.query || {}, ...req.params || {} };
211
- res.json(await libOperations.facet(ik, facetKey, combinedQueryParams));
212
- } catch (err) {
213
- this.logger.error("Error in Item Facet", { message: err?.message, stack: err?.stack });
214
- res.status(500).json(err);
188
+ const result = await libOperations.facet(ik, facetKey, combinedQueryParams);
189
+ res.json(result);
190
+ } catch (error) {
191
+ this.logger.error("Error in getItemFacet", { error });
192
+ if ((error.name === "ValidationError" || error.message?.includes("not found")) && error.message) {
193
+ res.status(500).json({ error: "Item Facet is not configured" });
194
+ } else {
195
+ res.status(500).json(error);
196
+ }
215
197
  }
216
198
  };
217
199
  configure = (router) => {
218
200
  const libOptions = this.lib.options;
219
201
  this.logger.debug("Configuring Router", { pkType: this.getPkType() });
220
- router.get("/", this.findItems);
221
- router.post("/", this.createItem);
202
+ router.get("/", this.wrapAsync(this.findItems));
203
+ router.post("/", this.wrapAsync(this.createItem));
222
204
  const registeredAllActions = /* @__PURE__ */ new Set();
223
205
  const registeredAllFacets = /* @__PURE__ */ new Set();
224
206
  const registeredItemActions = /* @__PURE__ */ new Set();
@@ -227,7 +209,7 @@ class ItemRouter {
227
209
  if (this.options.allActions) {
228
210
  Object.keys(this.options.allActions).forEach((actionKey) => {
229
211
  this.logger.debug("Configuring Router All Action %s", actionKey);
230
- router.post(`/${actionKey}`, this.postAllAction);
212
+ router.post(`/${actionKey}`, this.wrapAsync(this.postAllAction));
231
213
  registeredAllActions.add(actionKey);
232
214
  });
233
215
  }
@@ -238,7 +220,7 @@ class ItemRouter {
238
220
  this.logger.warning("All Action name collision - router-level handler takes precedence", { actionKey });
239
221
  } else {
240
222
  this.logger.debug("Configuring Library All Action %s", actionKey);
241
- router.post(`/${actionKey}`, this.postAllAction);
223
+ router.post(`/${actionKey}`, this.wrapAsync(this.postAllAction));
242
224
  registeredAllActions.add(actionKey);
243
225
  }
244
226
  });
@@ -247,7 +229,7 @@ class ItemRouter {
247
229
  if (this.options.allFacets) {
248
230
  Object.keys(this.options.allFacets).forEach((facetKey) => {
249
231
  this.logger.debug("Configuring Router All Facet %s", facetKey);
250
- router.get(`/${facetKey}`, this.getAllFacet);
232
+ router.get(`/${facetKey}`, this.wrapAsync(this.getAllFacet));
251
233
  registeredAllFacets.add(facetKey);
252
234
  });
253
235
  }
@@ -258,20 +240,20 @@ class ItemRouter {
258
240
  this.logger.warning("All Facet name collision - router-level handler takes precedence", { facetKey });
259
241
  } else {
260
242
  this.logger.debug("Configuring Library All Facet %s", facetKey);
261
- router.get(`/${facetKey}`, this.getAllFacet);
243
+ router.get(`/${facetKey}`, this.wrapAsync(this.getAllFacet));
262
244
  registeredAllFacets.add(facetKey);
263
245
  }
264
246
  });
265
247
  }
266
248
  const itemRouter = Router();
267
- itemRouter.get("/", this.getItem);
268
- itemRouter.put("/", this.updateItem);
269
- itemRouter.delete("/", this.deleteItem);
249
+ itemRouter.get("/", this.wrapAsync(this.getItem));
250
+ itemRouter.put("/", this.wrapAsync(this.updateItem));
251
+ itemRouter.delete("/", this.wrapAsync(this.deleteItem));
270
252
  this.logger.default("Router Item Actions", { itemActions: this.options.actions });
271
253
  if (this.options.actions) {
272
254
  Object.keys(this.options.actions).forEach((actionKey) => {
273
255
  this.logger.debug("Configuring Router Item Action %s", actionKey);
274
- itemRouter.post(`/${actionKey}`, this.postItemAction);
256
+ itemRouter.post(`/${actionKey}`, this.wrapAsync(this.postItemAction));
275
257
  registeredItemActions.add(actionKey);
276
258
  });
277
259
  }
@@ -282,7 +264,7 @@ class ItemRouter {
282
264
  this.logger.warning("Item Action name collision - router-level handler takes precedence", { actionKey });
283
265
  } else {
284
266
  this.logger.debug("Configuring Library Item Action %s", actionKey);
285
- itemRouter.post(`/${actionKey}`, this.postItemAction);
267
+ itemRouter.post(`/${actionKey}`, this.wrapAsync(this.postItemAction));
286
268
  registeredItemActions.add(actionKey);
287
269
  }
288
270
  });
@@ -291,7 +273,7 @@ class ItemRouter {
291
273
  if (this.options.facets) {
292
274
  Object.keys(this.options.facets).forEach((facetKey) => {
293
275
  this.logger.debug("Configuring Router Item Facet %s", facetKey);
294
- itemRouter.get(`/${facetKey}`, this.getItemFacet);
276
+ itemRouter.get(`/${facetKey}`, this.wrapAsync(this.getItemFacet));
295
277
  registeredItemFacets.add(facetKey);
296
278
  });
297
279
  }
@@ -302,7 +284,7 @@ class ItemRouter {
302
284
  this.logger.warning("Item Facet name collision - router-level handler takes precedence", { facetKey });
303
285
  } else {
304
286
  this.logger.debug("Configuring Library Item Facet %s", facetKey);
305
- itemRouter.get(`/${facetKey}`, this.getItemFacet);
287
+ itemRouter.get(`/${facetKey}`, this.wrapAsync(this.getItemFacet));
306
288
  registeredItemFacets.add(facetKey);
307
289
  }
308
290
  });
@@ -312,6 +294,7 @@ class ItemRouter {
312
294
  if (this.childRouters) {
313
295
  this.configureChildRouters(itemRouter, this.childRouters);
314
296
  }
297
+ router.use(this.errorHandler);
315
298
  return router;
316
299
  };
317
300
  validatePrimaryKeyValue = (req, res, next) => {
@@ -357,21 +340,18 @@ class ItemRouter {
357
340
  const ik = this.getIk(res);
358
341
  try {
359
342
  const removedItem = await libOperations.remove(ik);
360
- const item = validatePK(removedItem, this.getPkType());
361
- res.json(item);
362
- } catch (err) {
363
- if (err instanceof NotFoundError) {
364
- this.logger.error("Item Not Found for Delete", { ik, message: err?.message, stack: err?.stack });
365
- res.status(404).json({
366
- ik,
367
- message: "Item Not Found"
368
- });
343
+ if (removedItem) {
344
+ const item = validatePK(removedItem, this.getPkType());
345
+ res.json(item);
369
346
  } else {
370
- this.logger.error("General Error in Delete", { ik, message: err?.message, stack: err?.stack });
371
- res.status(500).json({
372
- ik,
373
- message: "General Error"
374
- });
347
+ res.status(204).send();
348
+ }
349
+ } catch (error) {
350
+ if (error instanceof NotFoundError || error.name === "NotFoundError") {
351
+ res.status(404).json({ ik, message: "Item Not Found" });
352
+ } else {
353
+ this.logger.error("Error in deleteItem", { error });
354
+ res.status(500).json({ ik, message: "General Error" });
375
355
  }
376
356
  }
377
357
  };
@@ -386,21 +366,33 @@ class ItemRouter {
386
366
  this.logger.debug("Getting Item", { query: req.query, params: req.params, locals: res.locals });
387
367
  const ik = this.getIk(res);
388
368
  try {
389
- const item = validatePK(await libOperations.get(ik), this.getPkType());
390
- res.json(item);
391
- } catch (err) {
392
- if (err instanceof NotFoundError) {
393
- this.logger.error("Item Not Found", { ik, message: err?.message, stack: err?.stack });
394
- res.status(404).json({
395
- ik,
396
- message: "Item Not Found"
369
+ const fetchedItem = await libOperations.get(ik);
370
+ if (!fetchedItem) {
371
+ throw new ActionError({
372
+ code: "NOT_FOUND",
373
+ message: `${this.keyType} not found`,
374
+ operation: {
375
+ type: "get",
376
+ name: "get",
377
+ params: { key: ik }
378
+ },
379
+ context: {
380
+ itemType: this.keyType
381
+ },
382
+ details: { retryable: false },
383
+ technical: {
384
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
385
+ }
397
386
  });
387
+ }
388
+ const item = validatePK(fetchedItem, this.getPkType());
389
+ res.json(item);
390
+ } catch (error) {
391
+ if (error instanceof NotFoundError || error.name === "NotFoundError") {
392
+ res.status(404).json({ ik, message: "Item Not Found" });
398
393
  } else {
399
- this.logger.error("General Error", { ik, message: err?.message, stack: err?.stack });
400
- res.status(500).json({
401
- ik,
402
- message: "General Error"
403
- });
394
+ this.logger.error("Error in getItem", { error });
395
+ res.status(500).json({ ik, message: "General Error" });
404
396
  }
405
397
  }
406
398
  };
@@ -415,19 +407,12 @@ class ItemRouter {
415
407
  const itemToUpdate = this.convertDates(req.body);
416
408
  const retItem = validatePK(await libOperations.update(ik, itemToUpdate), this.getPkType());
417
409
  res.json(retItem);
418
- } catch (err) {
419
- if (err instanceof NotFoundError) {
420
- this.logger.error("Item Not Found for Update", { ik, message: err?.message, stack: err?.stack });
421
- res.status(404).json({
422
- ik,
423
- message: "Item Not Found"
424
- });
410
+ } catch (error) {
411
+ if (error instanceof NotFoundError || error.name === "NotFoundError") {
412
+ res.status(404).json({ ik, message: "Item Not Found" });
425
413
  } else {
426
- this.logger.error("General Error in Update", { ik, message: err?.message, stack: err?.stack });
427
- res.status(500).json({
428
- ik,
429
- message: "General Error"
430
- });
414
+ this.logger.error("Error in updateItem", { error });
415
+ res.status(500).json({ ik, message: "General Error" });
431
416
  }
432
417
  }
433
418
  };