@trenskow/app 0.9.5 → 0.9.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.
@@ -17,7 +17,7 @@ import Request from './request.js';
17
17
  import Response from './response.js';
18
18
  import Endpoint from './endpoint.js';
19
19
 
20
- import { isObject } from './util.js';
20
+ import { isObject } from './util/index.js';
21
21
 
22
22
  export default class Application extends EventEmitter {
23
23
 
package/lib/endpoint.js CHANGED
@@ -10,36 +10,47 @@ import ApiError from '@trenskow/api-error';
10
10
 
11
11
  import Router from './router.js';
12
12
 
13
- import { isObject, matchPath, resolveInlineImport, methods } from './util.js';
13
+ import { isObject, matchPath, resolveInlineImport, Methods } from './util/index.js';
14
14
 
15
15
  export default class Endpoint extends Router {
16
16
 
17
17
  constructor() {
18
18
  super();
19
19
 
20
- methods.concat(['all']).forEach((method) => {
20
+ Methods.all.forEach((method) => {
21
21
 
22
- this[method] = (...handlers) => {
23
- handlers = [].concat(...handlers);
24
- return this._on(method, handlers);
25
- };
22
+ Object.defineProperty(this, method, {
23
+ get: () => new Methods({
24
+ existing: [method],
25
+ additional: ['catchAll'],
26
+ todo: (methods, ...handlers) => {
26
27
 
27
- this[method].catchAll = (...handlers) => {
28
- handlers = [].concat(...handlers);
29
- return this._on(method, handlers, 'indirect');
30
- };
28
+ handlers = [].concat(...handlers);
29
+
30
+ let match = 'direct';
31
+
32
+ if (methods.includes('catchAll')) {
33
+ match = 'indirect';
34
+ methods = methods.filter((method) => method !== 'catchAll');
35
+ }
36
+
37
+ return this._on(methods, handlers, match);
38
+
39
+ }
40
+ })
41
+ });
31
42
 
32
43
  });
33
44
 
34
45
  }
35
46
 
36
- _on(method, handlers, match) {
37
-
38
- method = method.toLowerCase();
47
+ _on(methods, handlers, match) {
39
48
 
40
- if (!methods.concat(['all']).includes(method)) {
41
- throw new Error(`Method ${method} is unknown.`);
42
- }
49
+ methods.forEach((method) => {
50
+ if (!Methods.all.includes(method)) {
51
+ throw new Error(`Method ${method} is unknown.`);
52
+ }
53
+ });
43
54
 
44
55
  match = match || 'direct';
45
56
 
@@ -56,16 +67,16 @@ export default class Endpoint extends Router {
56
67
  }
57
68
 
58
69
  const existing = this._layers.findIndex((layer) => {
59
- return layer.method === method && layer.handler === this._handleMethod;
70
+ return (layer.handler === this._handleMethod && methods.some((method) => layer.methods.includes(method)));
60
71
  });
61
72
 
62
73
  if (existing !== -1) {
63
- throw new Error(`Endpoint already has a \`${method}\` handler.`);
74
+ throw new Error('Endpoint already has a handler.');
64
75
  }
65
76
 
66
77
  this._layers.push({
67
78
  handler: this._handleMethod,
68
- method,
79
+ methods,
69
80
  handlers,
70
81
  match
71
82
  });
@@ -74,6 +85,10 @@ export default class Endpoint extends Router {
74
85
 
75
86
  }
76
87
 
88
+ all(...handlers) {
89
+ return this._on(Methods.all, handlers);
90
+ }
91
+
77
92
  mount(path, endpoint) {
78
93
 
79
94
  if (isObject(path)) {
@@ -100,10 +115,10 @@ export default class Endpoint extends Router {
100
115
  }
101
116
 
102
117
  get mounts() {
103
- return new Proxy({}, {
104
- get: (_, path) => {
118
+ return new Proxy(this, {
119
+ get: (target, path) => {
105
120
  return (endpoint) => {
106
- return this.mount(path, endpoint);
121
+ return target.mount(path, endpoint);
107
122
  };
108
123
  }
109
124
  });
@@ -183,12 +198,14 @@ export default class Endpoint extends Router {
183
198
 
184
199
  const methods = this._layers
185
200
  .filter((layer) => layer.handler === this._handleMethod)
186
- .map((layer) => layer.method.toUpperCase())
201
+ .reduce((methods, layer) => methods.concat(layer.methods), [])
187
202
  .filter((value, index, array) => array.indexOf(value) === index);
188
203
 
189
204
  if (methods.length) {
190
- context.response.headers.allow = methods.join(', ');
205
+ context.response.headers.allow = methods.map((method) => method.toUpperCase()).join(', ');
191
206
  throw new ApiError.MethodNotAllowed();
207
+ } else {
208
+ throw new ApiError.NotFound();
192
209
  }
193
210
 
194
211
  } else {
@@ -221,12 +238,12 @@ export default class Endpoint extends Router {
221
238
 
222
239
  let underlyingMethod = context.request.method.toLowerCase();
223
240
 
224
- if (underlyingMethod === 'head' && !this._layers.some((layer) => layer.method === 'head')) {
241
+ if (underlyingMethod === 'head' && !this._layers.some((layer) => layer.methods?.includes('head'))) {
225
242
  underlyingMethod = 'get';
226
243
  }
227
244
 
228
245
  if (layer.match === 'direct' && !path.isLast) return await next();
229
- if (layer.method !== 'all' && layer.method !== underlyingMethod) return await next();
246
+ if (!layer.methods?.includes(underlyingMethod)) return await next();
230
247
 
231
248
  for (let handler of layer.handlers) {
232
249
  const result = await handler(context);
package/lib/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  //
2
2
  // index.js
3
3
  // @trenskow/app
4
- //
4
+ //
5
5
  // Created by Kristian Trenskow on 2021/11/07
6
6
  // For license see LICENSE.
7
- //
7
+ //
8
8
 
9
9
  import Router from './router.js';
10
10
  import Endpoint from './endpoint.js';
@@ -13,7 +13,7 @@ import Request from './request.js';
13
13
  import Response from './response.js';
14
14
  import ApiError from '@trenskow/api-error';
15
15
 
16
- import { isObject, matchPath, resolveInlineImport } from './util.js';
16
+ import { isObject, matchPath, resolveInlineImport } from './util/index.js';
17
17
 
18
18
  export default Application;
19
19
 
package/lib/router.js CHANGED
@@ -9,10 +9,8 @@
9
9
  import {
10
10
  isObject,
11
11
  resolveInlineImport,
12
- methods,
13
- methodsRead,
14
- methodsWrite
15
- } from './util.js';
12
+ Methods
13
+ } from './util/index.js';
16
14
 
17
15
  export default class Router {
18
16
 
@@ -20,27 +18,9 @@ export default class Router {
20
18
 
21
19
  this._layers = [];
22
20
 
23
- methods.forEach((method) => {
24
- return this.use[method] = (...handlers) => {
25
- return this.#_use(handlers, method);
26
- };
27
- });
28
-
29
- this.use.read = (...handlers) => {
30
- methodsRead.forEach((method) => {
31
- return this.#_use(handlers, method);
32
- });
33
- };
34
-
35
- this.use.write = (...handlers) => {
36
- methodsWrite.forEach((method) => {
37
- return this.#_use(handlers, method);
38
- });
39
- };
40
-
41
21
  }
42
22
 
43
- #_use(handlers, method) {
23
+ #_use(handlers, methods) {
44
24
 
45
25
  handlers = [].concat(...handlers);
46
26
 
@@ -55,15 +35,19 @@ export default class Router {
55
35
  this._layers.push({
56
36
  handler: this._handleUse,
57
37
  handlers,
58
- method
38
+ methods
59
39
  });
60
40
 
61
41
  return this;
62
42
 
63
43
  }
64
44
 
65
- use(...handlers) {
66
- return this.#_use(handlers, 'all');
45
+ get use() {
46
+ return new Methods({
47
+ todo: (methods, ...handlers) => {
48
+ return this.#_use(handlers, methods);
49
+ }
50
+ });
67
51
  }
68
52
 
69
53
  mixin(router) {
@@ -127,7 +111,7 @@ export default class Router {
127
111
 
128
112
  const { request } = context;
129
113
 
130
- if (layer.method === 'all' || request.method.toLowerCase() === layer.method) {
114
+ if (layer.methods.length === 0 || layer.methods.includes(request.method.toLowerCase())) {
131
115
 
132
116
  for (let handler of layer.handlers) {
133
117
  await handler(context);
@@ -6,13 +6,9 @@
6
6
  // For license see LICENSE.
7
7
  //
8
8
 
9
- import { METHODS } from 'http';
10
-
11
9
  import caseit from '@trenskow/caseit';
12
10
 
13
- const methods = METHODS.map((method) => method.toLowerCase());
14
- const methodsWrite = ['post', 'put', 'patch', 'delete'];
15
- const methodsRead = methods.filter((method) => !methodsWrite.includes(method));
11
+ import Methods from './methods.js';
16
12
 
17
13
  const isObject = (value) => value?.constructor === Object;
18
14
 
@@ -27,9 +23,7 @@ const resolveInlineImport = (value) => {
27
23
  };
28
24
 
29
25
  export {
30
- methods,
31
- methodsWrite,
32
- methodsRead,
26
+ Methods,
33
27
  isObject,
34
28
  matchPath,
35
29
  resolveInlineImport
@@ -0,0 +1,49 @@
1
+ //
2
+ // is-object.js
3
+ // @trenskow/app
4
+ //
5
+ // Created by Kristian Trenskow on 2025/01/02
6
+ // For license see LICENSE.
7
+ //
8
+
9
+ import { METHODS } from 'http';
10
+
11
+ const methods = METHODS.map((method) => method.toLowerCase());
12
+
13
+ export default class Methods extends Function {
14
+
15
+ static get all() {
16
+ return methods;
17
+ }
18
+
19
+ constructor({ existing = [], additional = [], todo }) {
20
+ super();
21
+
22
+ if (!Array.isArray(existing)) {
23
+ throw new Error('Existing must be an array.');
24
+ }
25
+
26
+ let methods = existing;
27
+
28
+ return new Proxy(this, {
29
+ get: (_ /* target */, property, receiver) => {
30
+
31
+ if (property === 'all') {
32
+ methods = Methods.all;
33
+ } else if (Methods.all.concat(additional).includes(property)) {
34
+ if (!methods.includes(property)) methods.push(property);
35
+ } else {
36
+ throw new Error(`Method ${property} is unknown.`);
37
+ }
38
+
39
+ return receiver;
40
+
41
+ },
42
+ apply: (_ /* target */, __ /* this */, args) => {
43
+ return todo(methods, ...args);
44
+ }
45
+ });
46
+
47
+ }
48
+
49
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trenskow/app",
3
- "version": "0.9.5",
3
+ "version": "0.9.7",
4
4
  "description": "A small HTTP router.",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/test/index.js CHANGED
@@ -50,9 +50,7 @@ describe('Application', () => {
50
50
  it ('should respond with 405 when a route is configured, but request method is not specified,', async () => {
51
51
  app.root(
52
52
  new Endpoint()
53
- .post(() => {})
54
- .put(() => {})
55
- .delete(() => {})
53
+ .post.put.delete(() => {})
56
54
  );
57
55
  await request
58
56
  .get('/')
@@ -109,7 +107,7 @@ describe('Application', () => {
109
107
  new Endpoint()
110
108
  .get(() => 'Hello, World!')
111
109
  .get(() => 'Hello, World! (2)');
112
- }).to.throw('Endpoint already has a `get` handler.');
110
+ }).to.throw('Endpoint already has a handler.');
113
111
  });
114
112
 
115
113
  it ('should ignore GET method handler when path has been rewritten and respond with 200 and `Hello, World!`.', async () => {
@@ -197,6 +195,25 @@ describe('Application', () => {
197
195
 
198
196
  });
199
197
 
198
+ it ('should respond with 404 when endpoint has no methods.', async () => {
199
+
200
+ app.root(
201
+ new Endpoint()
202
+ .mounts.hello(
203
+ new Endpoint()
204
+ .mounts.world(
205
+ new Endpoint()
206
+ .get(() => 'Hello, World!')
207
+ )
208
+ )
209
+ );
210
+
211
+ await request
212
+ .get('/hello')
213
+ .expect(404);
214
+
215
+ });
216
+
200
217
  it ('should respond with parameter value.', async () => {
201
218
 
202
219
  app.root(
@@ -283,7 +300,7 @@ describe('Application', () => {
283
300
 
284
301
  });
285
302
 
286
- it ('should respond with a value when using PUT on a catch-all method.', async () => {
303
+ it ('should respond with a value when using PUT on a all handler.', async () => {
287
304
 
288
305
  app.root(
289
306
  new Endpoint()