@trenskow/app 0.9.30 → 0.9.32
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 +3 -10
- package/lib/application.js +1 -17
- package/lib/request.js +0 -12
- package/package.json +2 -2
- package/test/index.js +0 -77
- package/lib/cookies.js +0 -179
package/README.md
CHANGED
|
@@ -248,7 +248,6 @@ When a request is incoming, the `context` object looks like this.
|
|
|
248
248
|
| `path.current` | An array of strings that joined represents the path currently being processed. | Array of String |
|
|
249
249
|
| `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 (useful when serving single page applications to a browser). | Array of String |
|
|
250
250
|
| `query` | An object holding the URL query parameters as an object ([keys has been converted to camel case](#query-parameters)). | Object |
|
|
251
|
-
| `cookies` | An mutable object with cookie values. | Object |
|
|
252
251
|
| `state` | A string indicating the current state of the request – possible values are `'routing'`, `'rendering'`, `'completed'` or `'aborted'`. | String |
|
|
253
252
|
| `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 |
|
|
254
253
|
| `render` | A function that tells the application to stop processing the request and jump directly to the [renderer](#renderer). | Function |
|
|
@@ -277,11 +276,7 @@ The same goes for request headers like `Accept-Language: en` which is accessible
|
|
|
277
276
|
|
|
278
277
|
##### Query parameters
|
|
279
278
|
|
|
280
|
-
Request with
|
|
281
|
-
|
|
282
|
-
##### Cookies
|
|
283
|
-
|
|
284
|
-
Cookies are available through `context.cookies.myCookieKey` and are passed to the client using `camel` (can be changed using the [`Application` options](#constructor)).
|
|
279
|
+
Request with quuries like `?my-parameter=value` is accessible through `context.query.myParameter` .
|
|
285
280
|
|
|
286
281
|
##### Mount paths
|
|
287
282
|
|
|
@@ -361,13 +356,11 @@ The `Application` class takes an "options" object as it's parameter.
|
|
|
361
356
|
| `options.port` | The port at which to listen for incoming connections. | Number | | `0` (automatically assigned) |
|
|
362
357
|
| `options.RequestType` | An object that inherits from the [`Request`](#request-2) class (an `http.IncomingMessage` subclass) that is used as the request object in routes. | class | | [`Request`](#Request) |
|
|
363
358
|
| `options.ResponseType` | An object that inherits from the [`Response`](#response-2) class (`http.ServerResponse` subclass) that is used as the response object in routes. | class | | [`Response`](#Response) |
|
|
364
|
-
| `
|
|
365
|
-
| `
|
|
359
|
+
| `path` | An object that represents path related options. | Object | | `{}` |
|
|
360
|
+
| `path.matchMode` | Indicates [how to match requests to mounted paths](#mount-paths) (eg. should the path be converted to camel case). | `'loosely'` or `'strict'` | | `'loosely'` |
|
|
366
361
|
| `options.server` | An object that represents how to instantiate the HTTP server. | Object | | `{}` |
|
|
367
362
|
| `options.server.create` | A function that is able to create a server. | Function | | `http.createServer` |
|
|
368
363
|
| `options.server.options` | An object to be passed as options when creating a server. | Object | | `{}` |
|
|
369
|
-
| `options.casing` | An object that represents casing options. | Object | | {} |
|
|
370
|
-
| `options.casing.cookies` | Client side cookie key value casing (available as in [@trenskow/caseit](https://github.com/trenskow/caseit)). | String | | `camel` |
|
|
371
364
|
|
|
372
365
|
#### Events
|
|
373
366
|
|
package/lib/application.js
CHANGED
|
@@ -16,7 +16,6 @@ import caseit from '@trenskow/caseit';
|
|
|
16
16
|
import Request from './request.js';
|
|
17
17
|
import Response from './response.js';
|
|
18
18
|
import Endpoint from './endpoint.js';
|
|
19
|
-
import Cookies from './cookies.js';
|
|
20
19
|
|
|
21
20
|
import { isObject } from './util/index.js';
|
|
22
21
|
|
|
@@ -26,7 +25,6 @@ export default class Application extends EventEmitter {
|
|
|
26
25
|
#_path;
|
|
27
26
|
#_state;
|
|
28
27
|
#_server;
|
|
29
|
-
#_casing;
|
|
30
28
|
#_rootEndpoint;
|
|
31
29
|
#_renderer;
|
|
32
30
|
|
|
@@ -43,10 +41,7 @@ export default class Application extends EventEmitter {
|
|
|
43
41
|
server = Object.assign({
|
|
44
42
|
create: http.createServer,
|
|
45
43
|
options: {}
|
|
46
|
-
}, options.server || {})
|
|
47
|
-
casing = Object.assign({
|
|
48
|
-
cookies: 'camel'
|
|
49
|
-
}, options.casing || {})
|
|
44
|
+
}, options.server || {})
|
|
50
45
|
} = options;
|
|
51
46
|
|
|
52
47
|
this.#_port = port;
|
|
@@ -67,8 +62,6 @@ export default class Application extends EventEmitter {
|
|
|
67
62
|
this.#_onIncomingRequest(req, res);
|
|
68
63
|
});
|
|
69
64
|
|
|
70
|
-
this.#_casing = casing;
|
|
71
|
-
|
|
72
65
|
this.#_rootEndpoint = new Endpoint()
|
|
73
66
|
.use(() => { throw new ApiError.NotFound(); });
|
|
74
67
|
|
|
@@ -321,13 +314,6 @@ export default class Application extends EventEmitter {
|
|
|
321
314
|
enumerable: true
|
|
322
315
|
});
|
|
323
316
|
|
|
324
|
-
const cookies = new Cookies(context, this.#_casing.cookies);
|
|
325
|
-
|
|
326
|
-
Object.defineProperty(context, 'cookies', {
|
|
327
|
-
get: () => cookies._proxy,
|
|
328
|
-
enumerable: true
|
|
329
|
-
});
|
|
330
|
-
|
|
331
317
|
const listeners = {
|
|
332
318
|
eventEmitters: [
|
|
333
319
|
[request.socket, ['end', 'error']],
|
|
@@ -384,8 +370,6 @@ export default class Application extends EventEmitter {
|
|
|
384
370
|
|
|
385
371
|
}
|
|
386
372
|
|
|
387
|
-
cookies._render();
|
|
388
|
-
|
|
389
373
|
if (!['routing', 'rendering'].includes(state)) return response.end();
|
|
390
374
|
|
|
391
375
|
await render();
|
package/lib/request.js
CHANGED
|
@@ -28,18 +28,6 @@ export default class Request extends IncomingMessage {
|
|
|
28
28
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
get host() {
|
|
32
|
-
return this.headers.xForwardedHost || super.host || this.socket.localAddress;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
get protocol() {
|
|
36
|
-
return this.headers.xForwardedProto || super.protocol || (this.socket.encrypted ? 'https' : 'http');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
get port() {
|
|
40
|
-
return this.headers.xForwardedPort || this.socket.localPort || (super.protocol === 'https' ? 443 : 80);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
31
|
get origin() {
|
|
44
32
|
return (this.headers.xForwardedFor || this.socket.remoteAddress || '').split(/, ?/);
|
|
45
33
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trenskow/app",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.32",
|
|
4
4
|
"description": "A small HTTP router.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"supertest": "^7.1.1"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@trenskow/api-error": "^2.5.
|
|
37
|
+
"@trenskow/api-error": "^2.5.32",
|
|
38
38
|
"@trenskow/caseit": "^1.4.13",
|
|
39
39
|
"@trenskow/units": "^0.2.29"
|
|
40
40
|
}
|
package/test/index.js
CHANGED
|
@@ -439,83 +439,6 @@ describe('Application', () => {
|
|
|
439
439
|
|
|
440
440
|
});
|
|
441
441
|
|
|
442
|
-
it ('should come back with header to set cookie.', async () => {
|
|
443
|
-
|
|
444
|
-
app.root(
|
|
445
|
-
new Endpoint()
|
|
446
|
-
.get(({ cookies }) => {
|
|
447
|
-
cookies['testValue'] = 'Hello, World!';
|
|
448
|
-
return 'Hello, World!';
|
|
449
|
-
})
|
|
450
|
-
);
|
|
451
|
-
|
|
452
|
-
await request
|
|
453
|
-
.get('/')
|
|
454
|
-
.expect('Set-Cookie', 'testValue=Hello%2C%20World!; Path=/; Domain=::1; Secure')
|
|
455
|
-
.expect(200, 'Hello, World!');
|
|
456
|
-
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
it ('should come back with an updated header to set cookie.', async () => {
|
|
460
|
-
|
|
461
|
-
app.root(
|
|
462
|
-
new Endpoint()
|
|
463
|
-
.get(({ cookies }) => {
|
|
464
|
-
cookies['testValue'] = {
|
|
465
|
-
value: 'Hello, World!',
|
|
466
|
-
path: '/test',
|
|
467
|
-
expires: '30d'
|
|
468
|
-
};
|
|
469
|
-
return 'Hello, World!';
|
|
470
|
-
})
|
|
471
|
-
);
|
|
472
|
-
|
|
473
|
-
await request
|
|
474
|
-
.get('/')
|
|
475
|
-
.set('Cookie', 'testValue=Hello')
|
|
476
|
-
.expect('Set-Cookie', 'testValue=Hello%2C%20World!; Max-Age=2592000; Path=/test; Domain=::1; Secure')
|
|
477
|
-
.expect(200, 'Hello, World!');
|
|
478
|
-
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
it ('should come back with an updated header to a deleted cookie.', async () => {
|
|
482
|
-
|
|
483
|
-
app.root(
|
|
484
|
-
new Endpoint()
|
|
485
|
-
.get(({ cookies }) => {
|
|
486
|
-
delete cookies['testValue'];
|
|
487
|
-
return 'Hello, World!';
|
|
488
|
-
})
|
|
489
|
-
);
|
|
490
|
-
|
|
491
|
-
await request
|
|
492
|
-
.get('/')
|
|
493
|
-
.set('Cookie', 'testValue=Hello')
|
|
494
|
-
.expect('Set-Cookie', 'testValue=; Max-Age=0')
|
|
495
|
-
.expect(200, 'Hello, World!');
|
|
496
|
-
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
it ('should come back with no new cookies if no cookies are set.', (done) => {
|
|
500
|
-
|
|
501
|
-
app.root(
|
|
502
|
-
new Endpoint()
|
|
503
|
-
.get(() => 'Hello, World!')
|
|
504
|
-
);
|
|
505
|
-
|
|
506
|
-
request
|
|
507
|
-
.get('/')
|
|
508
|
-
.expect(200, 'Hello, World!')
|
|
509
|
-
.end((error, response) => {
|
|
510
|
-
if (error) return done(error);
|
|
511
|
-
if (response.headers['set-cookie']) {
|
|
512
|
-
return done(new Error('Response should not have set-cookie header.'));
|
|
513
|
-
}
|
|
514
|
-
done();
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
});
|
|
518
|
-
|
|
519
442
|
after(async () => {
|
|
520
443
|
await app.close({ awaitAllConnections: true });
|
|
521
444
|
});
|
package/lib/cookies.js
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// application.js
|
|
3
|
-
// @trenskow/app
|
|
4
|
-
//
|
|
5
|
-
// Created by Kristian Trenskow on 2021/11/07
|
|
6
|
-
// For license see LICENSE.
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
import caseit from '@trenskow/caseit';
|
|
10
|
-
import { duration } from '@trenskow/units';
|
|
11
|
-
|
|
12
|
-
export default class Cookies {
|
|
13
|
-
|
|
14
|
-
#_cookies = {
|
|
15
|
-
current: {},
|
|
16
|
-
requested: {},
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
#_request;
|
|
20
|
-
#_response;
|
|
21
|
-
#_path;
|
|
22
|
-
#_casing;
|
|
23
|
-
#_proxy;
|
|
24
|
-
|
|
25
|
-
constructor({ request, response, path }, casing = this.#_casing) {
|
|
26
|
-
|
|
27
|
-
if (!request || !response) {
|
|
28
|
-
throw new Error('Cookies must be initialized with a request and response.');
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
this.#_request = request;
|
|
32
|
-
this.#_response = response;
|
|
33
|
-
this.#_path = path;
|
|
34
|
-
this.#_casing = casing;
|
|
35
|
-
|
|
36
|
-
(this.#_request.headers?.cookie || '').split(/; ?/)
|
|
37
|
-
.filter(cookie => cookie)
|
|
38
|
-
.forEach(cookie => {
|
|
39
|
-
|
|
40
|
-
const [name, value] = cookie.split('=')
|
|
41
|
-
.map(part => decodeURIComponent(part.trim()));
|
|
42
|
-
|
|
43
|
-
this.#_cookies.requested[caseit(name)] = (this.#_cookies.current[caseit(name)] = { value }).value;
|
|
44
|
-
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
get _proxy() {
|
|
50
|
-
return this.#_proxy || (this.#_proxy = new Proxy(this, {
|
|
51
|
-
get: (target, prop) => {
|
|
52
|
-
|
|
53
|
-
prop = caseit(prop);
|
|
54
|
-
|
|
55
|
-
if (Object.hasOwn(target, prop) || Object.hasOwn(Object.getPrototypeOf(target), prop)) {
|
|
56
|
-
return target[prop];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return target.#_cookies.current[prop]?.value;
|
|
60
|
-
|
|
61
|
-
},
|
|
62
|
-
set: (target, prop, value) => {
|
|
63
|
-
|
|
64
|
-
prop = caseit(prop);
|
|
65
|
-
|
|
66
|
-
if (typeof value === 'undefined') {
|
|
67
|
-
delete target.#_cookies.current[prop];
|
|
68
|
-
} else if (typeof value === 'undefined' || typeof value === 'string') {
|
|
69
|
-
this.#_proxy[prop] = { value };
|
|
70
|
-
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
71
|
-
|
|
72
|
-
let {
|
|
73
|
-
value: cookieValue,
|
|
74
|
-
expires,
|
|
75
|
-
path = 'current',
|
|
76
|
-
domain = 'current',
|
|
77
|
-
secure = true,
|
|
78
|
-
httpOnly = false
|
|
79
|
-
} = value;
|
|
80
|
-
|
|
81
|
-
if (typeof cookieValue === 'undefined') {
|
|
82
|
-
this.#_proxy[prop] = undefined;
|
|
83
|
-
return true;
|
|
84
|
-
} else if (typeof cookieValue !== 'string') {
|
|
85
|
-
throw new TypeError('Cookie values must be a string.');
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (path === 'current') {
|
|
89
|
-
path = `${this.#_path.current.join('/')}/`;
|
|
90
|
-
} else if (path === 'root') {
|
|
91
|
-
path = '/';
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (domain === 'current') {
|
|
95
|
-
domain = target.#_request.host;
|
|
96
|
-
} else if (domain === 'root') {
|
|
97
|
-
domain = target.#_request.host.split('.').slice(-2).join('.');
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (typeof expires !== 'undefined' && !(['number', 'string'].includes(typeof expires) || expires instanceof Date)) {
|
|
101
|
-
throw new TypeError('Expires must be a number, string or Date.');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
target.#_cookies.current[prop] = {
|
|
105
|
-
value: cookieValue,
|
|
106
|
-
expires,
|
|
107
|
-
path,
|
|
108
|
-
domain,
|
|
109
|
-
secure,
|
|
110
|
-
httpOnly
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
} else {
|
|
114
|
-
throw new TypeError('Cookie values must be a strings, object or undefined.');
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return true;
|
|
118
|
-
|
|
119
|
-
},
|
|
120
|
-
deleteProperty: (_, prop) => {
|
|
121
|
-
this.#_proxy[prop] = undefined;
|
|
122
|
-
return true;
|
|
123
|
-
},
|
|
124
|
-
}));
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
#_format(name, { value, expires, path, domain, secure, httpOnly }) {
|
|
128
|
-
|
|
129
|
-
const parts = [`${encodeURIComponent(caseit(name, this.#_casing))}=${encodeURIComponent(value || '')}`];
|
|
130
|
-
|
|
131
|
-
if (!value) {
|
|
132
|
-
parts.push('Max-Age=0');
|
|
133
|
-
} else if (expires) {
|
|
134
|
-
if (typeof expires === 'number' || typeof expires === 'string') {
|
|
135
|
-
parts.push(`Max-Age=${duration.s(expires)}`);
|
|
136
|
-
} else if (expires instanceof Date) {
|
|
137
|
-
parts.push(`Expires=${expires.toUTCString()}`);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (path) {
|
|
142
|
-
parts.push(`Path=${path}`);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (domain) {
|
|
146
|
-
parts.push(`Domain=${domain}`);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (secure) {
|
|
150
|
-
parts.push('Secure');
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (httpOnly) {
|
|
154
|
-
parts.push('HttpOnly');
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return parts.join('; ');
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
_render() {
|
|
162
|
-
|
|
163
|
-
const deletedCookies = Object.keys(this.#_cookies.requested)
|
|
164
|
-
.filter(name => !Object.hasOwn(this.#_cookies.current, name))
|
|
165
|
-
.map(name => this.#_format(name, { value: undefined }));
|
|
166
|
-
|
|
167
|
-
const allCookies = Object.entries(this.#_cookies.current)
|
|
168
|
-
.map(([name, cookie]) => this.#_format(name, cookie))
|
|
169
|
-
.concat(deletedCookies);
|
|
170
|
-
|
|
171
|
-
if (allCookies.length === 0) {
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
this.#_response.headers.setCookie = allCookies;
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
};
|