@trenskow/app 0.5.3 → 0.5.7

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/README.md CHANGED
@@ -232,21 +232,21 @@ Middleware can also use the context to provide data and services, which is then
232
232
 
233
233
  When a request is incoming, the `context`object looks like this.
234
234
 
235
- | Name | Description | Type |
236
- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------: |
237
- | `application` | The application instance that has received the request. | [Application](#Application) |
238
- | `request` | The request object from the HTTP server. | [Request](#Request) |
239
- | `response` | The response object from the HTTP server. | [Response](#Response) |
240
- | `parameters` | An empty object that will contain the parameters picked up when processing the parameters (if any) of the requested path. | Object |
241
- | `path` | An object that has properties representing different paths. | Object |
242
- | `path.full` | An array of strings that joined represent the path of the fully requested path. | Array of String |
243
- | `path.current` | An array of strings that joined represents the path currently being processed. | Array of String |
244
- | `path.remaining` | An array of strings that joined represents the path that is above the currently processed path. | |
245
- | `query` | An object holding the URL query parameters as an object ([keys has been converted to camel case](#query-parameters)). | Object |
246
- | `state` | A string indicating the current state of the request – possible values are `'routing'`, `'rendering'`, `'completed'` or `'aborted'`. | String |
235
+ | Name | Description | Type |
236
+ | ---------------- | ------------------------------------------------------------ | :-------------------------: |
237
+ | `application` | The application instance that has received the request. | [Application](#Application) |
238
+ | `request` | The request object from the HTTP server. | [Request](#Request) |
239
+ | `response` | The response object from the HTTP server. | [Response](#Response) |
240
+ | `parameters` | An empty object that will contain the parameters picked up when processing the parameters (if any) of the requested path. | Object |
241
+ | `path` | An object that has properties representing different paths. | Object |
242
+ | `path.full` | An array of strings that joined represent the path of the fully requested path. | Array of String |
243
+ | `path.current` | An array of strings that joined represents the path currently being processed. | Array of String |
244
+ | `path.remaining` | An array of strings that joined represents the path that is above the currently processed path. Setting this will rewrite the remaining path. | Array of String |
245
+ | `query` | An object holding the URL query parameters as an object ([keys has been converted to camel case](#query-parameters)). | Object |
246
+ | `state` | A string indicating the current state of the request – possible values are `'routing'`, `'rendering'`, `'completed'` or `'aborted'`. | String |
247
247
  | `abort` | A function that aborts the request. It takes the parameters `(error, brutally)`, where `error` is the error that needs to be handled by the [renderer](#renderer) – and `brutally` which indicates if the connection should also be closed. | AsyncFunction |
248
- | `render` | A function that tells the application to stop processing the request and jump directly to the [renderer](#renderer). | Function |
249
- | `result` | Whatever has been returned by the method handlers (should be written to the response in the [`renderer`](#renderer)). | Any |
248
+ | `render` | A function that tells the application to stop processing the request and jump directly to the [renderer](#renderer). | Function |
249
+ | `result` | Whatever has been returned by the method handlers (should be written to the response in the [`renderer`](#renderer)). | Any |
250
250
 
251
251
  ##### Example
252
252
 
@@ -495,15 +495,17 @@ This method takes care of handing a specified HTTP method.
495
495
 
496
496
  Supported HTTP methods are the same as those returned by [`http.METHODS`](https://nodejs.org/dist/latest/docs/api/http.html#httpmethods).
497
497
 
498
- No more routes will be processed, when one of these handlers return. The returned value is send to the [renderer](#renderer).
498
+ You can only call these methods once per method per endpoint calling it multiple times will result in only the last one getting used.
499
499
 
500
500
  > Returns the endpoint.
501
501
 
502
502
  ###### Parameters
503
503
 
504
- | Name | Description | Type | Required | Default value |
505
- | ---------- | ---------------------------- | :------------------------------------------------------: | :----------------: | :-----------: |
506
- | `handlers` | A (or an array of) handlers. | Function, AsyncFunction or Array ([see also](#handlers)) | :white_check_mark: | |
504
+ | Name | Description | Type | Required | Default value |
505
+ | ---------- | ------------------------------ | :------------------------------------------------------: | :----------------: | :-----------: |
506
+ | `handlers` | A (or an array of) handlers. * | Function, AsyncFunction or Array ([see also](#handlers)) | :white_check_mark: | |
507
+
508
+ > \* When more than one handler are provided all but the last are treated as `.use` handlers (but specific to the HTTP method). Only the return value of the last handler is sent to the renderer.
507
509
 
508
510
  ###### Example
509
511
 
@@ -173,10 +173,13 @@ export default class Application extends EventEmitter {
173
173
 
174
174
  if (path.substr(-1) === '/') path = path.slice(0, -1);
175
175
 
176
- query = Object.fromEntries(query.split(/&/).map((entry) => {
177
- const [key, value] = entry.split(/=/).map(decodeURIComponent);
178
- return [caseit(key), value];
179
- }));
176
+ query = Object.fromEntries(query
177
+ .split(/&/)
178
+ .filter((entry) => entry)
179
+ .map((entry) => {
180
+ const [key, value] = entry.split(/=/).map(decodeURIComponent);
181
+ return [caseit(key), value];
182
+ }));
180
183
 
181
184
  let state = 'routing';
182
185
 
@@ -186,14 +189,20 @@ export default class Application extends EventEmitter {
186
189
  .map(decodeURIComponent)
187
190
  .slice(1),
188
191
  position: -1,
189
- async pushed(todo) {
190
- this.position++;
192
+ async _walk(direction, todo) {
193
+ this.position += direction;
191
194
  try {
192
- await todo();
195
+ return await todo(path.component);
193
196
  } finally {
194
- this.position--;
197
+ this.position -= direction;
195
198
  }
196
199
  },
200
+ async pushed(todo) {
201
+ return this._walk(1, todo);
202
+ },
203
+ async popped(todo) {
204
+ return this._walk(-1, todo);
205
+ },
197
206
  get isLast() {
198
207
  return this.position === this.components.length;
199
208
  },
@@ -216,12 +225,28 @@ export default class Application extends EventEmitter {
216
225
  },
217
226
  get remaining() {
218
227
  return path.components.slice(path.position);
228
+ },
229
+ set remaining(remaining) {
230
+
231
+ if (typeof remaining === 'string') {
232
+ remaining = remaining
233
+ .split('/')
234
+ .filter((component) => component);
235
+ }
236
+
237
+ if (!Array.isArray(remaining)) {
238
+ remaining = [remaining];
239
+ }
240
+
241
+ remaining.forEach((component) => {
242
+ if (typeof component !== 'string') throw new Error('Path component must be a string.');
243
+ });
244
+
245
+ path.components = context.path.current.concat(remaining);
246
+
219
247
  }
220
248
  },
221
- query: new Proxy(query, {
222
- get: (target, property) => target[caseit(property)],
223
- set: (target, property, value) => target[caseit(property)] = value
224
- }),
249
+ query,
225
250
  render: () => {
226
251
  state = 'rendering';
227
252
  },
@@ -231,9 +256,10 @@ export default class Application extends EventEmitter {
231
256
  ({ error, brutally } = error);
232
257
  }
233
258
 
234
- state = 'aborted';
235
-
236
- if (error) context.result = error;
259
+ if (!['completed', 'aborted'].includes(state)) {
260
+ state = 'aborted';
261
+ if (error) context.result = error;
262
+ }
237
263
 
238
264
  if (brutally) {
239
265
  return new Promise((resolve, reject) => {
@@ -295,17 +321,13 @@ export default class Application extends EventEmitter {
295
321
  });
296
322
  });
297
323
 
298
- context.response.statusCode = context.result ? 200 : 204;
324
+ if (context.response.statusCode === 200) {
325
+ context.response.statusCode = context.result ? 200 : 204;
326
+ }
299
327
 
300
328
  } catch (error) {
301
329
 
302
- if (typeof error.toJSON !== 'function') {
303
- console.error(error.stack);
304
- context.result = new ApiError.InternalError({ underlying: error });
305
- } else {
306
- context.result = error;
307
- }
308
-
330
+ context.result = error;
309
331
  context.response.statusCode = error.statusCode ?? 500;
310
332
 
311
333
  }
package/lib/endpoint.js CHANGED
@@ -59,6 +59,11 @@ export default class Endpoint extends Router {
59
59
  throw new Error('Handler must be a function.');
60
60
  }
61
61
 
62
+ const existing = this._layers.findIndex((layer) => layer.method === method);
63
+ if (existing !== -1) {
64
+ this._layers.splice(existing, 1);
65
+ }
66
+
62
67
  this._layers.push({
63
68
  type: 'method',
64
69
  method,
@@ -193,15 +198,15 @@ export default class Endpoint extends Router {
193
198
 
194
199
  case 'mount': {
195
200
 
196
- return await path.pushed(async () => {
201
+ return await path.pushed(async (component) => {
197
202
 
198
- if (!path.component) return await next();
203
+ if (!component) return path.popped(next);
199
204
 
200
- if (matchPath(path.component, layer.path, context)) {
205
+ if (matchPath(component, layer.path, context)) {
201
206
  return await layer.endpoint._route(path, context, next);
202
207
  }
203
208
 
204
- return await next();
209
+ return path.popped(next);
205
210
 
206
211
  });
207
212
 
@@ -224,16 +229,16 @@ export default class Endpoint extends Router {
224
229
 
225
230
  case 'parameter': {
226
231
 
227
- return await path.pushed(async () => {
232
+ return await path.pushed(async (component) => {
228
233
 
229
- if (!path.component) return await next();
234
+ if (!component) return await path.popped(next);
230
235
 
231
236
  context.parameters[layer.name] = await layer.transform?.(
232
237
  Object.fromEntries([
233
238
  [ 'context', context ],
234
- [ layer.name, path.component ]
239
+ [ layer.name, component ]
235
240
  ])
236
- ) || path.component;
241
+ ) || component;
237
242
 
238
243
  return await layer.endpoint._route(path, context, next);
239
244
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trenskow/app",
3
- "version": "0.5.3",
3
+ "version": "0.5.7",
4
4
  "description": "A small HTTP router.",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/test/index.js CHANGED
@@ -74,6 +74,47 @@ describe('Application', () => {
74
74
 
75
75
  });
76
76
 
77
+ it ('should ignore multiple GET method handlers and respond with 200 and `Hello, World!`.', async () => {
78
+
79
+ app.root(
80
+ new Endpoint()
81
+ .get(() => 'Ignore this!')
82
+ .post(() => 'Not used')
83
+ .get(() => 'Hello, World!')
84
+ );
85
+
86
+ await request
87
+ .get('/')
88
+ .expect('Content-Type', 'text/plain; charset=utf-8')
89
+ .expect(200, 'Hello, World!');
90
+
91
+ });
92
+
93
+ it ('should ignore GET method handler when path has been rewritten and respond with 200 and `Hello, World!`.', async () => {
94
+
95
+ app.root(
96
+ new Endpoint()
97
+ .use(({ path }) => {
98
+ if (path.remaining[0] === 'ignore') {
99
+ path.remaining = ['hello'];
100
+ }
101
+ })
102
+ .mounts.ignore(
103
+ new Endpoint()
104
+ .get(() => 'Ignore this!'))
105
+ .mounts.hello(
106
+ new Endpoint()
107
+ .get(() => 'Hello!')
108
+ )
109
+ );
110
+
111
+ await request
112
+ .get('/ignore')
113
+ .expect('Content-Type', 'text/plain; charset=utf-8')
114
+ .expect(200, 'Hello!');
115
+
116
+ });
117
+
77
118
  it ('should respond with 404 when a mount is configured but another is requested.', async () => {
78
119
 
79
120
  app.root(