@occultist/occultist 0.0.1
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/LICENSE +21 -0
- package/README.md +144 -0
- package/dist/accept.d.ts +41 -0
- package/dist/accept.js +110 -0
- package/dist/accept.test.d.ts +1 -0
- package/dist/accept.test.js +44 -0
- package/dist/action.test.d.ts +1 -0
- package/dist/action.test.js +1 -0
- package/dist/actions/actionSets.d.ts +23 -0
- package/dist/actions/actionSets.js +49 -0
- package/dist/actions/actions.d.ts +163 -0
- package/dist/actions/actions.js +436 -0
- package/dist/actions/context.d.ts +78 -0
- package/dist/actions/context.js +112 -0
- package/dist/actions/meta.d.ts +49 -0
- package/dist/actions/meta.js +177 -0
- package/dist/actions/path.d.ts +21 -0
- package/dist/actions/path.js +83 -0
- package/dist/actions/path.test.d.ts +1 -0
- package/dist/actions/path.test.js +9 -0
- package/dist/actions/spec.d.ts +214 -0
- package/dist/actions/spec.js +1 -0
- package/dist/actions/types.d.ts +112 -0
- package/dist/actions/types.js +2 -0
- package/dist/actions/writer.d.ts +27 -0
- package/dist/actions/writer.js +140 -0
- package/dist/actions/writer.test.d.ts +1 -0
- package/dist/actions/writer.test.js +42 -0
- package/dist/auth/types.d.ts +14 -0
- package/dist/auth/types.js +1 -0
- package/dist/cache/cache.d.ts +30 -0
- package/dist/cache/cache.js +220 -0
- package/dist/cache/etag.d.ts +17 -0
- package/dist/cache/etag.js +83 -0
- package/dist/cache/etag.test.d.ts +1 -0
- package/dist/cache/etag.test.js +91 -0
- package/dist/cache/memory.d.ts +12 -0
- package/dist/cache/memory.js +36 -0
- package/dist/cache/types.d.ts +175 -0
- package/dist/cache/types.js +4 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +54 -0
- package/dist/jsonld.d.ts +43 -0
- package/dist/jsonld.js +1 -0
- package/dist/makeTypeDefs.d.ts +27 -0
- package/dist/makeTypeDefs.js +70 -0
- package/dist/merge.d.ts +61 -0
- package/dist/merge.js +1 -0
- package/dist/mod.d.ts +14 -0
- package/dist/mod.js +14 -0
- package/dist/processAction.d.ts +15 -0
- package/dist/processAction.js +512 -0
- package/dist/registry.d.ts +88 -0
- package/dist/registry.js +314 -0
- package/dist/registry.test.d.ts +1 -0
- package/dist/registry.test.js +133 -0
- package/dist/request.d.ts +29 -0
- package/dist/request.js +118 -0
- package/dist/scopes.d.ts +35 -0
- package/dist/scopes.js +121 -0
- package/dist/scopes.test.d.ts +1 -0
- package/dist/scopes.test.js +55 -0
- package/dist/transformers/fileTransformer.d.ts +1 -0
- package/dist/transformers/fileTransformer.js +8 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.js +1 -0
- package/dist/utils/alwaysArray.d.ts +1 -0
- package/dist/utils/alwaysArray.js +9 -0
- package/dist/utils/contextBuilder.d.ts +9 -0
- package/dist/utils/contextBuilder.js +82 -0
- package/dist/utils/getActionContext.d.ts +7 -0
- package/dist/utils/getActionContext.js +48 -0
- package/dist/utils/getInternalName.d.ts +6 -0
- package/dist/utils/getInternalName.js +7 -0
- package/dist/utils/getParamLocation.d.ts +2 -0
- package/dist/utils/getParamLocation.js +6 -0
- package/dist/utils/getPropertyValueSpecifications.d.ts +2 -0
- package/dist/utils/getPropertyValueSpecifications.js +49 -0
- package/dist/utils/getRequestBodyValues.d.ts +11 -0
- package/dist/utils/getRequestBodyValues.js +122 -0
- package/dist/utils/getRequestIRIValues.d.ts +14 -0
- package/dist/utils/getRequestIRIValues.js +133 -0
- package/dist/utils/isBodyInit.d.ts +1 -0
- package/dist/utils/isBodyInit.js +21 -0
- package/dist/utils/isNil.d.ts +1 -0
- package/dist/utils/isNil.js +4 -0
- package/dist/utils/isObject.d.ts +6 -0
- package/dist/utils/isObject.js +6 -0
- package/dist/utils/isPopulatedObject.d.ts +5 -0
- package/dist/utils/isPopulatedObject.js +8 -0
- package/dist/utils/isPopulatedString.d.ts +1 -0
- package/dist/utils/isPopulatedString.js +4 -0
- package/dist/utils/joinPaths.d.ts +1 -0
- package/dist/utils/joinPaths.js +31 -0
- package/dist/utils/makeAppendProblemDetails.d.ts +14 -0
- package/dist/utils/makeAppendProblemDetails.js +26 -0
- package/dist/utils/makeURLPattern.d.ts +5 -0
- package/dist/utils/makeURLPattern.js +12 -0
- package/dist/utils/normalizeURL.d.ts +4 -0
- package/dist/utils/normalizeURL.js +11 -0
- package/dist/utils/parseSearchParams.d.ts +3 -0
- package/dist/utils/parseSearchParams.js +24 -0
- package/dist/utils/preferredMediaTypes.d.ts +42 -0
- package/dist/utils/preferredMediaTypes.js +149 -0
- package/dist/utils/urlToIRI.d.ts +1 -0
- package/dist/utils/urlToIRI.js +8 -0
- package/dist/utils/validateSpecValue.d.ts +1 -0
- package/dist/utils/validateSpecValue.js +1 -0
- package/dist/validators.d.ts +16 -0
- package/dist/validators.js +134 -0
- package/lib/accept.test.ts +55 -0
- package/lib/accept.ts +147 -0
- package/lib/action.test.ts +2 -0
- package/lib/actions/actionSets.ts +88 -0
- package/lib/actions/actions.ts +795 -0
- package/lib/actions/context.ts +170 -0
- package/lib/actions/meta.ts +251 -0
- package/lib/actions/path.test.ts +15 -0
- package/lib/actions/path.ts +99 -0
- package/lib/actions/spec.ts +545 -0
- package/lib/actions/types.ts +146 -0
- package/lib/actions/writer.test.ts +57 -0
- package/lib/actions/writer.ts +176 -0
- package/lib/auth/types.ts +22 -0
- package/lib/cache/cache.ts +291 -0
- package/lib/cache/etag.test.ts +122 -0
- package/lib/cache/etag.ts +106 -0
- package/lib/cache/memory.ts +52 -0
- package/lib/cache/types.ts +240 -0
- package/lib/errors.ts +66 -0
- package/lib/jsonld.ts +67 -0
- package/lib/makeTypeDefs.ts +138 -0
- package/lib/merge.ts +86 -0
- package/lib/mod.ts +14 -0
- package/lib/processAction.ts +690 -0
- package/lib/registry.test.ts +174 -0
- package/lib/registry.ts +455 -0
- package/lib/request.ts +153 -0
- package/lib/scopes.test.ts +70 -0
- package/lib/scopes.ts +178 -0
- package/lib/transformers/fileTransformer.ts +10 -0
- package/lib/types.ts +13 -0
- package/lib/utils/alwaysArray.ts +10 -0
- package/lib/utils/contextBuilder.ts +111 -0
- package/lib/utils/getActionContext.ts +76 -0
- package/lib/utils/getInternalName.ts +15 -0
- package/lib/utils/getParamLocation.ts +14 -0
- package/lib/utils/getPropertyValueSpecifications.ts +76 -0
- package/lib/utils/getRequestBodyValues.ts +155 -0
- package/lib/utils/getRequestIRIValues.ts +201 -0
- package/lib/utils/isBodyInit.ts +22 -0
- package/lib/utils/isNil.ts +4 -0
- package/lib/utils/isObject.ts +8 -0
- package/lib/utils/isPopulatedObject.ts +9 -0
- package/lib/utils/isPopulatedString.ts +4 -0
- package/lib/utils/joinPaths.ts +36 -0
- package/lib/utils/makeAppendProblemDetails.ts +57 -0
- package/lib/utils/makeURLPattern.ts +18 -0
- package/lib/utils/normalizeURL.ts +15 -0
- package/lib/utils/parseSearchParams.ts +36 -0
- package/lib/utils/preferredMediaTypes.ts +220 -0
- package/lib/utils/urlToIRI.ts +11 -0
- package/lib/utils/validateSpecValue.ts +0 -0
- package/lib/validators.ts +186 -0
- package/package.json +41 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { Registry } from './registry.js';
|
|
4
|
+
import { createServer } from 'node:http';
|
|
5
|
+
import type { AddressInfo } from 'node:net';
|
|
6
|
+
|
|
7
|
+
const registry = new Registry({
|
|
8
|
+
rootIRI: 'https://example.com',
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
registry.http.get('get-index', '/')
|
|
12
|
+
.public()
|
|
13
|
+
.hint({
|
|
14
|
+
link: {
|
|
15
|
+
href: 'http://example.com/messages.json',
|
|
16
|
+
type: 'application/json',
|
|
17
|
+
preload: true,
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
.handle('text/plain', (ctx) => {
|
|
21
|
+
ctx.body = `Hello, world!`;
|
|
22
|
+
})
|
|
23
|
+
.handle('application/json', (ctx) => {
|
|
24
|
+
ctx.body = JSON.stringify({
|
|
25
|
+
foo: 'bar',
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
registry.http.post('post-things', '/posty/post')
|
|
30
|
+
.public()
|
|
31
|
+
.handle('text/html', async (ctx) => {
|
|
32
|
+
ctx.body = await Promise.resolve(`
|
|
33
|
+
<!doctype html>
|
|
34
|
+
<html>
|
|
35
|
+
<body>Got it</body>
|
|
36
|
+
</html>
|
|
37
|
+
`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function checkPayloadTypesMap(args: {
|
|
41
|
+
foo: string;
|
|
42
|
+
fee: number;
|
|
43
|
+
foe: Array<{ bar: boolean }>;
|
|
44
|
+
baz: string[],
|
|
45
|
+
}) {
|
|
46
|
+
return args;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
registry.http.post('post-me', '/foo/{fee}/bar{?baz}')
|
|
50
|
+
.public()
|
|
51
|
+
.define({
|
|
52
|
+
spec: {
|
|
53
|
+
foo: {
|
|
54
|
+
dataType: 'string',
|
|
55
|
+
},
|
|
56
|
+
fee: {
|
|
57
|
+
dataType: 'number',
|
|
58
|
+
valueName: 'fee',
|
|
59
|
+
},
|
|
60
|
+
foe: {
|
|
61
|
+
multipleValues: true,
|
|
62
|
+
properties: {
|
|
63
|
+
bar: {
|
|
64
|
+
dataType: 'boolean',
|
|
65
|
+
valueRequired: true,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
baz: {
|
|
70
|
+
dataType: 'string',
|
|
71
|
+
multipleValues: true,
|
|
72
|
+
valueName: 'baz',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
.handle('application/json', (ctx) => {
|
|
77
|
+
ctx.body = JSON.stringify(checkPayloadTypesMap(ctx.payload))
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
registry.finalize();
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
test('It responds to request objects', async () => {
|
|
84
|
+
const res = await registry.handleRequest(
|
|
85
|
+
new Request('https://example.com'),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
assert(await res.text() === 'Hello, world!');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('It responds to node incoming messages', async () => {
|
|
92
|
+
const res = await new Promise<Response>((resolve, reject) => {
|
|
93
|
+
const server = createServer();
|
|
94
|
+
|
|
95
|
+
server.on('request', async (req, res) => {
|
|
96
|
+
await registry.handleRequest(req, res);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
server.on('error', (err) => {
|
|
100
|
+
reject(err);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
server.listen(0, '127.0.0.1', async () => {
|
|
104
|
+
const { port, address } = server.address() as AddressInfo;
|
|
105
|
+
const res = await fetch(`http://${address}:${port}`);
|
|
106
|
+
|
|
107
|
+
server.close();
|
|
108
|
+
|
|
109
|
+
resolve(res);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
assert(await res.text() === 'Hello, world!');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('It uses the correct handler for the accepted content type', async () => {
|
|
117
|
+
const res = await registry.handleRequest(
|
|
118
|
+
new Request('https://example.com', {
|
|
119
|
+
headers: { 'Accept': 'application/*' }
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
assert((await res.json()).foo === 'bar');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('It responds to other HTTP methods', async () => {
|
|
127
|
+
const res = await registry.handleRequest(
|
|
128
|
+
new Request('https://example.com/posty/post', {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: { 'Accept': 'text/html' },
|
|
131
|
+
})
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
assert((await res.text()).includes('<body>Got it</body>'))
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('It handles request payloads for application/json requests', async () => {
|
|
138
|
+
const res = await registry.handleRequest(
|
|
139
|
+
new Request('https://example.com/foo/1234/bar?baz=foo&baz=bar&baz=baz', {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
body: JSON.stringify({
|
|
142
|
+
foo: 'bar',
|
|
143
|
+
foe: [
|
|
144
|
+
{ bar: true }, { bar: false },
|
|
145
|
+
],
|
|
146
|
+
}),
|
|
147
|
+
headers: {
|
|
148
|
+
'Content-Type': 'application/json',
|
|
149
|
+
'Accept': 'application/json',
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const body = await res.json();
|
|
155
|
+
|
|
156
|
+
assert(body.fee === 1234);
|
|
157
|
+
assert(body.foo === 'bar');
|
|
158
|
+
assert(body.foe[0].bar);
|
|
159
|
+
assert(!body.foe[1].bar);
|
|
160
|
+
assert(body.baz[0] === 'foo');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('It fires beforefinalize and after finalize events', { only: true }, () => {
|
|
164
|
+
const called: string[] = [];
|
|
165
|
+
const registry = new Registry({ rootIRI: 'https://example.com' });
|
|
166
|
+
|
|
167
|
+
registry.addEventListener('beforefinalize', () => called.push('beforefinalize'));
|
|
168
|
+
registry.addEventListener('afterfinalize', () => called.push('afterfinalize'))
|
|
169
|
+
registry.finalize();
|
|
170
|
+
|
|
171
|
+
assert(called[0] === 'beforefinalize');
|
|
172
|
+
assert(called[1] === 'afterfinalize');
|
|
173
|
+
});
|
|
174
|
+
|
package/lib/registry.ts
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import { Accept } from "./accept.js";
|
|
2
|
+
import { ActionAuth, HandlerDefinition } from "./actions/actions.js";
|
|
3
|
+
import { type ActionMatchResult, ActionSet } from "./actions/actionSets.js";
|
|
4
|
+
import { ActionMeta } from "./actions/meta.js";
|
|
5
|
+
import type { ImplementedAction } from "./actions/types.js";
|
|
6
|
+
import { ResponseWriter } from "./actions/writer.js";
|
|
7
|
+
import { Scope } from './scopes.js';
|
|
8
|
+
import { IncomingMessage, type ServerResponse } from "node:http";
|
|
9
|
+
import type { Merge } from "./actions/spec.js";
|
|
10
|
+
import type { ContextState, Middleware } from "./actions/spec.js";
|
|
11
|
+
import {ProblemDetailsError} from "./errors.js"
|
|
12
|
+
import {WrappedRequest} from "./request.js";
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
export interface Callable<
|
|
16
|
+
State extends ContextState = ContextState,
|
|
17
|
+
> {
|
|
18
|
+
method(method: string, name: string, path: string): ActionAuth<State>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class HTTP<
|
|
22
|
+
State extends ContextState = ContextState,
|
|
23
|
+
> {
|
|
24
|
+
|
|
25
|
+
#callable: Callable<State>;
|
|
26
|
+
|
|
27
|
+
constructor(callable: Callable<State>) {
|
|
28
|
+
this.#callable = callable;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
trace(name: string, path: string): ActionAuth<State> {
|
|
32
|
+
return this.#callable.method('trace', name, path);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
options(name: string, path: string): ActionAuth<State> {
|
|
36
|
+
return this.#callable.method('options', name, path);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
head(name: string, path: string): ActionAuth<State> {
|
|
40
|
+
return this.#callable.method('head', name, path);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get(name: string, path: string): ActionAuth<State> {
|
|
44
|
+
return this.#callable.method('get', name, path);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
put(name: string, path: string): ActionAuth<State> {
|
|
48
|
+
return this.#callable.method('put', name, path);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
patch(name: string, path: string): ActionAuth<State> {
|
|
52
|
+
return this.#callable.method('patch', name, path);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
post(name: string, path: string): ActionAuth<State> {
|
|
56
|
+
return this.#callable.method('post', name, path);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
delete(name: string, path: string): ActionAuth<State> {
|
|
60
|
+
return this.#callable.method('delete', name, path);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
export type IndexMatchArgs = {
|
|
67
|
+
debug?: boolean;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export class IndexEntry {
|
|
71
|
+
#actionSets: ActionSet[];
|
|
72
|
+
|
|
73
|
+
constructor(actionSets: ActionSet[]) {
|
|
74
|
+
this.#actionSets = actionSets;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
match(method: string, path: string, accept: Accept): null | ActionMatchResult {
|
|
78
|
+
for (let index = 0; index < this.#actionSets.length; index++) {
|
|
79
|
+
const actionSet = this.#actionSets[index];
|
|
80
|
+
const match = actionSet.matches(method, path, accept);
|
|
81
|
+
|
|
82
|
+
if (match != null) {
|
|
83
|
+
return match;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
export type RegistryEvents =
|
|
93
|
+
| 'beforefinalize'
|
|
94
|
+
| 'afterfinalize'
|
|
95
|
+
;
|
|
96
|
+
|
|
97
|
+
export type RegistryArgs = {
|
|
98
|
+
rootIRI: string;
|
|
99
|
+
serverTiming?: boolean;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export class Registry<
|
|
103
|
+
State extends ContextState = ContextState,
|
|
104
|
+
> implements Callable<State> {
|
|
105
|
+
|
|
106
|
+
#finalized: boolean = false;
|
|
107
|
+
#path: string;
|
|
108
|
+
#rootIRI: string;
|
|
109
|
+
#serverTiming: boolean;
|
|
110
|
+
#http: HTTP<State>;
|
|
111
|
+
#scopes: Scope[] = [];
|
|
112
|
+
#children: ActionMeta[] = [];
|
|
113
|
+
#index?: IndexEntry;
|
|
114
|
+
#writer = new ResponseWriter();
|
|
115
|
+
#eventTarget = new EventTarget();
|
|
116
|
+
#middleware: Middleware[] = [];
|
|
117
|
+
#actions: ImplementedAction[] | null = null;
|
|
118
|
+
#handlers: HandlerDefinition[] | null = null;
|
|
119
|
+
|
|
120
|
+
constructor(args: RegistryArgs) {
|
|
121
|
+
const url = new URL(args.rootIRI);
|
|
122
|
+
|
|
123
|
+
this.#rootIRI = args.rootIRI;
|
|
124
|
+
this.#path = url.pathname;
|
|
125
|
+
this.#serverTiming = args.serverTiming ?? false;
|
|
126
|
+
this.#http = new HTTP<State>(this);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
scope(path: string): Scope<State> {
|
|
130
|
+
const scope = new Scope<State>({
|
|
131
|
+
path,
|
|
132
|
+
serverTiming: this.#serverTiming,
|
|
133
|
+
registry: this,
|
|
134
|
+
writer: this.#writer,
|
|
135
|
+
propergateMeta: (meta) => this.#children.push(meta),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
this.#scopes.push(scope);
|
|
139
|
+
|
|
140
|
+
return scope;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
get rootIRI(): string {
|
|
144
|
+
return this.#rootIRI;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
get path(): string {
|
|
148
|
+
return this.#path;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
get http(): HTTP<State> {
|
|
152
|
+
return this.#http;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
get actions(): Array<ImplementedAction> {
|
|
156
|
+
if (this.#finalized && this.#actions != null) {
|
|
157
|
+
return this.#actions;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const actions: ImplementedAction[] = [];
|
|
161
|
+
|
|
162
|
+
for (let i = 0; i < this.#children.length; i++) {
|
|
163
|
+
if (this.#children[i].action == null) continue;
|
|
164
|
+
|
|
165
|
+
actions.push(this.#children[i].action);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for (let i = 0; i < this.#scopes.length; i++) {
|
|
169
|
+
for (let j = 0; j < this.#scopes[i].actions.length; j++) {
|
|
170
|
+
actions.push(this.#scopes[i].actions[j]);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (this.#finalized) this.#actions = actions;
|
|
175
|
+
|
|
176
|
+
return actions;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Returns the first action using the given action name. A content type
|
|
181
|
+
* can be provided to select another action going by the same name
|
|
182
|
+
* and returning a different content type.
|
|
183
|
+
*
|
|
184
|
+
* @param name - The name of the action.
|
|
185
|
+
* @param contentType - The action's content type.
|
|
186
|
+
*/
|
|
187
|
+
get(name: string, contentType?: string): ImplementedAction | undefined {
|
|
188
|
+
const actions = this.actions;
|
|
189
|
+
|
|
190
|
+
for (let i = 0; i < actions.length; i++) {
|
|
191
|
+
if (actions[i].name !== name) {
|
|
192
|
+
continue;
|
|
193
|
+
} else if (contentType == null && !this.actions[i].contentTypes.includes(contentType)) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return actions[i];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Returns a list of all action handler definitions.
|
|
203
|
+
*/
|
|
204
|
+
get handlers(): HandlerDefinition[] {
|
|
205
|
+
if (this.#finalized && this.#handlers != null) {
|
|
206
|
+
return this.#handlers;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const actions = this.actions;
|
|
210
|
+
const handlers: HandlerDefinition[] = [];
|
|
211
|
+
|
|
212
|
+
for (let i = 0; i < actions.length; i++) {
|
|
213
|
+
for (let j = 0; j < actions[i].handlers.length; j++) {
|
|
214
|
+
handlers.push(actions[i].handlers[j]);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (this.#finalized) this.#handlers = handlers;
|
|
219
|
+
|
|
220
|
+
return handlers;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Queries all handler definitions.
|
|
225
|
+
*
|
|
226
|
+
* @param args.method The HTTP method the action should handle.
|
|
227
|
+
* @param args.contentType A content type, or list of content types the action
|
|
228
|
+
* should handle. If a list is given the action
|
|
229
|
+
* will be included if it matches one content type
|
|
230
|
+
* in the list.
|
|
231
|
+
* @param args.meta A meta value, such as a unique symbol, which the action
|
|
232
|
+
* should have in its meta object.
|
|
233
|
+
*/
|
|
234
|
+
query({
|
|
235
|
+
method,
|
|
236
|
+
contentType,
|
|
237
|
+
meta,
|
|
238
|
+
}: {
|
|
239
|
+
method?: string | string[];
|
|
240
|
+
contentType?: string | string[]
|
|
241
|
+
meta?: string | symbol;
|
|
242
|
+
} = {}): HandlerDefinition[] {
|
|
243
|
+
const source = this.handlers;
|
|
244
|
+
const handlers: HandlerDefinition[] = [];
|
|
245
|
+
let handler: HandlerDefinition;
|
|
246
|
+
|
|
247
|
+
if (method == null &&
|
|
248
|
+
contentType == null &&
|
|
249
|
+
meta == null) {
|
|
250
|
+
return source;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
for (let i = 0; i < source.length; i++) {
|
|
254
|
+
handler = source[i];
|
|
255
|
+
|
|
256
|
+
if (Array.isArray(contentType)) {
|
|
257
|
+
if (!contentType.includes(handler.contentType)) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
} else if (contentType != null && contentType !== handler.contentType) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (Array.isArray(method)) {
|
|
265
|
+
if (!method.includes(handler.action.method)) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
} else if (method != null && method !== handler.action.method) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (meta != null) {
|
|
273
|
+
if (!Reflect.has(handler.meta, meta)) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
handlers.push(handler);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return handlers;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Creates an action for any HTTP method.
|
|
286
|
+
*
|
|
287
|
+
* @param method The HTTP method name.
|
|
288
|
+
* @param name Name for the action being produced.
|
|
289
|
+
* @param path Path the action responds to.
|
|
290
|
+
*/
|
|
291
|
+
public method(method: string, name: string, path: string): ActionAuth<State> {
|
|
292
|
+
const meta = new ActionMeta<State>(
|
|
293
|
+
this.#rootIRI,
|
|
294
|
+
method.toUpperCase(),
|
|
295
|
+
name,
|
|
296
|
+
path,
|
|
297
|
+
this,
|
|
298
|
+
this.#writer,
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
meta.serverTiming = this.#serverTiming;
|
|
302
|
+
|
|
303
|
+
this.#children.push(meta);
|
|
304
|
+
|
|
305
|
+
return new ActionAuth<State>(meta);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
public use<
|
|
309
|
+
const MiddlewareState extends ContextState = ContextState,
|
|
310
|
+
>(
|
|
311
|
+
middleware: Middleware<MiddlewareState>,
|
|
312
|
+
): Registry<Merge<State, MiddlewareState>> {
|
|
313
|
+
this.#middleware.push(middleware);
|
|
314
|
+
|
|
315
|
+
return this as unknown as Registry<Merge<State, MiddlewareState>>;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
finalize() {
|
|
319
|
+
if (this.#finalized)
|
|
320
|
+
throw new Error('Registry has already been finalized');
|
|
321
|
+
|
|
322
|
+
const actionSets: ActionSet[] = [];
|
|
323
|
+
const groupedMeta = new Map<string, Map<string, ActionMeta[]>>();
|
|
324
|
+
|
|
325
|
+
this.#eventTarget.dispatchEvent(
|
|
326
|
+
new Event('beforefinalize', { bubbles: true, cancelable: false })
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
for (let index = 0; index < this.#scopes.length; index++) {
|
|
330
|
+
const scope = this.#scopes[index];
|
|
331
|
+
|
|
332
|
+
scope.finalize();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
for (let index = 0; index < this.#children.length; index++) {
|
|
336
|
+
const meta = this.#children[index];
|
|
337
|
+
const method = meta.method;
|
|
338
|
+
const normalized = meta.path.normalized;
|
|
339
|
+
|
|
340
|
+
meta.finalize();
|
|
341
|
+
|
|
342
|
+
const group = groupedMeta.get(normalized);
|
|
343
|
+
const methodSet = group?.get(method);
|
|
344
|
+
|
|
345
|
+
if (methodSet != null) {
|
|
346
|
+
methodSet.push(meta);
|
|
347
|
+
} else if (group != null) {
|
|
348
|
+
group.set(method, [meta]);
|
|
349
|
+
} else {
|
|
350
|
+
groupedMeta.set(normalized, new Map([[method, [meta]]]));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
for (const [normalized, methodSet] of groupedMeta.entries()) {
|
|
355
|
+
for (const [method, meta] of methodSet.entries()) {
|
|
356
|
+
const actionSet = new ActionSet(
|
|
357
|
+
this.#rootIRI,
|
|
358
|
+
method,
|
|
359
|
+
normalized,
|
|
360
|
+
meta,
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
actionSets.push(actionSet);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
this.#finalized = true;
|
|
368
|
+
this.#index = new IndexEntry(actionSets);
|
|
369
|
+
this.#eventTarget.dispatchEvent(
|
|
370
|
+
new Event('afterfinalize', { bubbles: true, cancelable: false })
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
// force actions and handlers to cache.
|
|
374
|
+
this.handlers;
|
|
375
|
+
|
|
376
|
+
// freeze all scopes.
|
|
377
|
+
for (let i = 0; i < this.#scopes.length; i++) {
|
|
378
|
+
Object.freeze(this.#scopes[i]);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// freeze the registry.
|
|
382
|
+
Object.freeze(this);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
handleRequest(
|
|
386
|
+
req: Request,
|
|
387
|
+
): Promise<Response>;
|
|
388
|
+
|
|
389
|
+
handleRequest(
|
|
390
|
+
req: IncomingMessage,
|
|
391
|
+
res: ServerResponse,
|
|
392
|
+
): Promise<ServerResponse>;
|
|
393
|
+
|
|
394
|
+
async handleRequest(
|
|
395
|
+
req: Request | IncomingMessage,
|
|
396
|
+
res?: ServerResponse,
|
|
397
|
+
): Promise<Response | ServerResponse> {
|
|
398
|
+
const startTime = performance.now();
|
|
399
|
+
const wrapped = new WrappedRequest(this.#rootIRI, req);
|
|
400
|
+
const writer = new ResponseWriter(res);
|
|
401
|
+
const accept = Accept.from(wrapped);
|
|
402
|
+
const match = this.#index?.match(
|
|
403
|
+
req.method ?? 'GET',
|
|
404
|
+
wrapped.url.toString(),
|
|
405
|
+
accept,
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
let err: ProblemDetailsError;
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
if (match?.type === 'match') {
|
|
412
|
+
return await match.action.handleRequest({
|
|
413
|
+
url: wrapped.url,
|
|
414
|
+
contentType: match.contentType,
|
|
415
|
+
req: wrapped,
|
|
416
|
+
writer,
|
|
417
|
+
startTime,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
} catch (err2) {
|
|
421
|
+
if (err2 instanceof ProblemDetailsError) {
|
|
422
|
+
err = err2;
|
|
423
|
+
} else {
|
|
424
|
+
err = new ProblemDetailsError(500, 'Internal server error');
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (err == null) {
|
|
429
|
+
err = new ProblemDetailsError(404, 'Not found');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (err instanceof ProblemDetailsError && req instanceof Request) {
|
|
433
|
+
return new Response(err.toContent('application/problem+json'), {
|
|
434
|
+
status: err.status,
|
|
435
|
+
headers: {
|
|
436
|
+
'Content-Type': 'application/problem+json',
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
} else if (err instanceof ProblemDetailsError && res != null) {
|
|
440
|
+
res.writeHead(err.status, {
|
|
441
|
+
'Content-Type': 'application/problem+json',
|
|
442
|
+
});
|
|
443
|
+
res.end(err.toContent('application/problem+json'));
|
|
444
|
+
return res;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
addEventListener(type: RegistryEvents, callback: EventListener) {
|
|
449
|
+
this.#eventTarget.addEventListener(type, callback);
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
removeEventListener(type: RegistryEvents, callback: EventListener) {
|
|
453
|
+
this.#eventTarget.removeEventListener(type, callback)
|
|
454
|
+
}
|
|
455
|
+
}
|