1 | const debug = require('debug')('upward-js:Context');
|
2 | const { pick } = require('lodash');
|
3 | const { URL } = require('url');
|
4 |
|
5 | const ContextPath = require('./ContextPath');
|
6 |
|
7 | const statusCodes = Array.from({ length: 600 }, (_, i) => i + 100);
|
8 | const constants = new Set([
|
9 | true,
|
10 | false,
|
11 | 'GET',
|
12 | 'POST',
|
13 | 'mustache',
|
14 | 'text/html',
|
15 | 'text/plain',
|
16 | 'application/json',
|
17 | 'utf-8',
|
18 | 'utf8',
|
19 | 'latin-1',
|
20 | 'base64',
|
21 | 'binary',
|
22 | 'hex',
|
23 | ...statusCodes,
|
24 | ...statusCodes.map(code => code.toString())
|
25 | ]);
|
26 |
|
27 | class Context {
|
28 | static fromRequest(env, request) {
|
29 | debug('generating from request: %s', request.url);
|
30 | const hostedUrl = new URL(request.url, `http://${request.get('host')}`);
|
31 | debug('url derived from host is %O', hostedUrl);
|
32 | const url = pick(hostedUrl, [
|
33 | 'host',
|
34 | 'hostname',
|
35 | 'port',
|
36 | 'pathname',
|
37 | 'path',
|
38 | 'search',
|
39 | 'searchParams'
|
40 | ]);
|
41 | url.query = request.query;
|
42 | return new Context({
|
43 | env,
|
44 | request: {
|
45 | url,
|
46 | headers: request.headers,
|
47 | headerEntries: Object.entries(request.headers).map(
|
48 | ([name, value]) => ({ name, value })
|
49 | ),
|
50 | queryEntries: Object.entries(url.query).map(
|
51 | ([name, value]) => ({ name, value })
|
52 | )
|
53 | }
|
54 | });
|
55 | }
|
56 |
|
57 | constructor(data) {
|
58 | this._data = data;
|
59 | this._promises = new Map();
|
60 | }
|
61 |
|
62 | setVisitor(visitor) {
|
63 | this.visitor = visitor;
|
64 | }
|
65 |
|
66 | async get(lookup) {
|
67 | if (constants.has(lookup) || constants.has(lookup.toString())) {
|
68 | debug('%s is a constant', lookup);
|
69 | return lookup;
|
70 | }
|
71 | const path = ContextPath.from(lookup);
|
72 | debug('lookup %s at path %s', lookup, path);
|
73 | const base = path.base();
|
74 | debug('%s is from context base %s', lookup, base);
|
75 | if (!this._data.hasOwnProperty(base)) {
|
76 | debug('%s not yet assigned, acquiring promise handle', base);
|
77 | let promise = this._promises.get(base);
|
78 | if (!promise) {
|
79 | debug('%s has never been requested, visiting from root', base);
|
80 | promise = this.visitor.downward([base]).then(value => {
|
81 | if (typeof value === 'function') {
|
82 | debug(
|
83 | '%s assigned to context as function: %o',
|
84 | base,
|
85 | value
|
86 | );
|
87 | this.set(base, value);
|
88 | } else {
|
89 | debug('%s assigned: %o', base, value[base]);
|
90 | this.set(base, value[base]);
|
91 | }
|
92 | });
|
93 | this._promises.set(base, promise);
|
94 | }
|
95 | await promise;
|
96 | }
|
97 | return path.getFrom(this._data);
|
98 | }
|
99 |
|
100 | set(base, value, override) {
|
101 | const isSet = constants.has(base) || this._data.hasOwnProperty(base);
|
102 | if (isSet && !override) {
|
103 | throw new Error(
|
104 | `Attempted to reassign context property '${base}' to '${value}'. Context properties cannot be reassigned.`
|
105 | );
|
106 | }
|
107 | this._data[base] = value;
|
108 | }
|
109 |
|
110 | forget(base) {
|
111 | delete this._data[base];
|
112 | }
|
113 | }
|
114 |
|
115 | module.exports = Context;
|