@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 +20 -18
- package/lib/application.js +45 -23
- package/lib/endpoint.js +13 -8
- package/package.json +1 -1
- package/test/index.js +41 -0
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
|
|
236
|
-
| ---------------- |
|
|
237
|
-
| `application` | The application instance that has received the request.
|
|
238
|
-
| `request` | The request object from the HTTP server.
|
|
239
|
-
| `response` | The response object from the HTTP server.
|
|
240
|
-
| `parameters` | An empty object that will contain the parameters picked up when processing the parameters (if any) of the requested path.
|
|
241
|
-
| `path` | An object that has properties representing different paths.
|
|
242
|
-
| `path.full` | An array of strings that joined represent the path of the fully requested path.
|
|
243
|
-
| `path.current` | An array of strings that joined represents the path currently being processed.
|
|
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)).
|
|
246
|
-
| `state` | A string indicating the current state of the request – possible values are `'routing'`, `'rendering'`, `'completed'` or `'aborted'`.
|
|
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).
|
|
249
|
-
| `result` | Whatever has been returned by the method handlers (should be written to the response in the [`renderer`](#renderer)).
|
|
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
|
-
|
|
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
|
|
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
|
|
package/lib/application.js
CHANGED
|
@@ -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
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
324
|
+
if (context.response.statusCode === 200) {
|
|
325
|
+
context.response.statusCode = context.result ? 200 : 204;
|
|
326
|
+
}
|
|
299
327
|
|
|
300
328
|
} catch (error) {
|
|
301
329
|
|
|
302
|
-
|
|
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 (!
|
|
203
|
+
if (!component) return path.popped(next);
|
|
199
204
|
|
|
200
|
-
if (matchPath(
|
|
205
|
+
if (matchPath(component, layer.path, context)) {
|
|
201
206
|
return await layer.endpoint._route(path, context, next);
|
|
202
207
|
}
|
|
203
208
|
|
|
204
|
-
return
|
|
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 (!
|
|
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,
|
|
239
|
+
[ layer.name, component ]
|
|
235
240
|
])
|
|
236
|
-
) ||
|
|
241
|
+
) || component;
|
|
237
242
|
|
|
238
243
|
return await layer.endpoint._route(path, context, next);
|
|
239
244
|
|
package/package.json
CHANGED
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(
|