anbaric-cloud-hosting 1.4.0 → 1.6.0

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.
@@ -1,335 +1,38 @@
1
- import {readFile} from "node:fs/promises";
2
- import {IncomingMessage, ServerResponse} from "node:http";
3
- import {AuditChange, JobPersistence, JsonStore, SecretStore, deserializeJob, serializeJob} from "anbaric-tsapi";
4
- import {BuildLayer} from "../app-management/BuildLayer";
5
- import {AuditRecordStore} from "../auditing/AuditRecordStore";
6
-
7
- const AUDIT_CHANGES = new Set(["CREATE", "UPDATE_PROPERTIES", "CHANGE_STATE", "DELETE"]);
8
- import {CliAuthorizer} from "../auth/CliAuthorizer";
9
- import {Tenant} from "../auth/Tenant";
10
- import {User} from "../auth/User";
11
- import {ConfirmableQueue} from "../queuing/ConfirmableQueue";
12
- import {ConsumerRegistry} from "../queuing/ConsumerRegistry";
13
-
14
- const readRawBody = (request : IncomingMessage) : Promise<Buffer> =>
15
- new Promise((resolve, reject) => {
16
- const chunks : Array<Buffer> = [];
17
- request.on("data", chunk => chunks.push(chunk));
18
- request.on("error", reject);
19
- request.on("end", () => resolve(Buffer.concat(chunks)));
20
- });
21
-
22
- const readBody = async (request : IncomingMessage) : Promise<any> => {
23
- const raw = (await readRawBody(request)).toString();
24
- return raw.length === 0 ? undefined : JSON.parse(raw);
25
- };
1
+ import {Request} from "./Request";
2
+ import {RequestHandler} from "./RequestHandler";
26
3
 
4
+ /* Deliberately hollow: other systems register a handler per top-level
5
+ resource, one for the root, and one fallback for anything unregistered
6
+ (the app proxy). All business logic lives in the handlers. */
27
7
  class Router {
28
8
 
29
- constructor(private persistence : JobPersistence, private queue : ConfirmableQueue,
30
- private registry : ConsumerRegistry,
31
- private buildLayer? : BuildLayer,
32
- private documentStoreFor? : (collection : string) => JsonStore,
33
- private secretStore? : SecretStore,
34
- private cliAuthorizer? : CliAuthorizer,
35
- private tenant? : string,
36
- private auditRecords? : AuditRecordStore) {}
9
+ private handlers = new Map<string, RequestHandler>();
10
+ private rootHandler? : RequestHandler;
11
+ private fallbackHandler? : RequestHandler;
37
12
 
38
- async route(request : IncomingMessage, response : ServerResponse, user? : User, sessionTenant? : Tenant) : Promise<void> {
39
- const url = new URL(request.url ?? "/", "http://localhost");
40
- const [resource, id, subresource] = url.pathname.split("/").filter(Boolean);
41
- const method = request.method ?? "GET";
42
-
43
- if (!resource && method === "GET") {
44
- return this.servePage(response);
45
- }
46
- if (resource === "authorize-cli" && id && this.cliAuthorizer) {
47
- return this.handleAuthorizeCli(method, id, subresource, request, response, user, sessionTenant);
48
- }
49
- if (resource === "manage-keys" && method === "GET" && !id && this.cliAuthorizer) {
50
- return this.servePage(response);
51
- }
52
- if (resource === "keys" && this.cliAuthorizer) {
53
- return this.handleKeys(method, id, response, user);
54
- }
55
- if (resource === "whoami" && method === "GET" && !id) {
56
- if (!user) return this.reply(response, 404, { error: "Not found" });
57
- return this.reply(response, 200, { id: user.id, roles: user.roles.map(role => role.id) });
58
- }
59
- if (resource === "audits" && this.auditRecords && !id) {
60
- if (method === "POST") {
61
- const record = await readBody(request);
62
- if (typeof record?.jobId !== "string" || typeof record?.description !== "string"
63
- || typeof record?.actorId !== "string" || typeof record?.actorType !== "string"
64
- || !AUDIT_CHANGES.has(record?.change)) {
65
- return this.reply(response, 400, { error: "Expected a body of { jobId, actorId, actorType, change, description, ... }" });
66
- }
67
- await this.auditRecords.save(record);
68
- return this.reply(response, 204);
69
- }
70
- if (method === "GET") {
71
- const change = url.searchParams.get("change");
72
- const records = await this.auditRecords.list({
73
- jobId: url.searchParams.get("jobId") ?? undefined,
74
- actorId: url.searchParams.get("actorId") ?? undefined,
75
- change: change && AUDIT_CHANGES.has(change) ? change as AuditChange : undefined,
76
- search: url.searchParams.get("search") ?? undefined,
77
- pageSize: url.searchParams.has("pageSize") ? Number(url.searchParams.get("pageSize")) : undefined,
78
- page: url.searchParams.has("page") ? Number(url.searchParams.get("page")) : undefined,
79
- });
80
- return this.reply(response, 200, records);
81
- }
82
- }
83
- if (resource === "audit" && method === "GET" && !id) {
84
- return this.servePage(response);
85
- }
86
- if (resource === "jobs") return this.handleJobs(method, id, subresource, url, request, response);
87
- if (resource === "queue" && method === "POST" && !subresource) return this.handleQueue(id, request, response);
88
- if (resource === "consumers" && method === "POST" && !id) {
89
- const { workflowId, url: consumerUrl } = await readBody(request);
90
- this.registry.register(workflowId, consumerUrl);
91
- return this.reply(response, 204);
92
- }
93
- if (resource === "apps" && this.buildLayer) {
94
- return this.handleApps(method, id, subresource, url, request, response);
95
- }
96
- if (resource === "state-machines" && method === "GET" && !id) {
97
- return this.reply(response, 200, this.registry.list());
98
- }
99
- if (resource === "documents" && id && this.documentStoreFor) {
100
- return this.handleDocuments(method, this.documentStoreFor(id), subresource, url, request, response);
101
- }
102
- if (resource === "secrets" && this.secretStore && !subresource) {
103
- return this.handleSecrets(method, id, request, response);
104
- }
105
- if (resource && this.buildLayer) {
106
- const app = this.buildLayer.status(resource);
107
- if (app && app.status === "running") {
108
- return this.forwardToApp(app.appHost, app.appPort, resource, method, url, request, response);
109
- }
110
- }
111
-
112
- this.reply(response, 404, { error: "Not found" });
13
+ register(resource : string, handler : RequestHandler) : void {
14
+ this.handlers.set(resource, handler);
113
15
  }
114
16
 
115
- private async handleAuthorizeCli(method : string, requestId : string, subresource : string | undefined,
116
- request : IncomingMessage, response : ServerResponse, user? : User,
117
- sessionTenant? : Tenant) : Promise<void> {
118
- if (method === "GET" && subresource === "poll") {
119
- const keyPair = this.cliAuthorizer!.collect(requestId);
120
- if (!keyPair) return this.reply(response, 202, { status: "pending" });
121
- return this.reply(response, 200, keyPair);
122
- }
123
-
124
- if (method === "GET" && !subresource) {
125
- return this.servePage(response);
126
- }
127
-
128
- if (method === "POST" && !subresource) {
129
- const { clientName } = await readBody(request);
130
- if (typeof clientName !== "string" || clientName.trim().length === 0) {
131
- return this.reply(response, 400, { error: "Expected a body of { clientName : string }" });
132
- }
133
- await this.cliAuthorizer!.approve(requestId, clientName.trim(), user ?? new User("local"),
134
- sessionTenant?.id ?? this.tenant);
135
- return this.reply(response, 204);
136
- }
137
-
138
- this.reply(response, 404, { error: "Not found" });
139
- }
140
-
141
- private async handleKeys(method : string, id : string | undefined,
142
- response : ServerResponse, user? : User) : Promise<void> {
143
- const owner = user ?? new User("local");
144
-
145
- if (method === "GET" && !id) {
146
- const keys = await this.cliAuthorizer!.keysFor(owner.id);
147
- return this.reply(response, 200, keys.map(key => ({
148
- id: key.id,
149
- clientName: key.clientName,
150
- createdAt: key.createdAt.toISOString(),
151
- })));
152
- }
153
-
154
- if (method === "DELETE" && id) {
155
- await this.cliAuthorizer!.revoke(id, owner.id);
156
- return this.reply(response, 204);
157
- }
158
-
159
- this.reply(response, 404, { error: "Not found" });
17
+ registerRoot(handler : RequestHandler) : void {
18
+ this.rootHandler = handler;
160
19
  }
161
20
 
162
- private async servePage(response : ServerResponse) : Promise<void> {
163
- try {
164
- const page = await readFile(new URL("./pages/platform-ui.html", import.meta.url));
165
- response.writeHead(200, { "content-type": "text/html" });
166
- response.end(page);
167
- } catch {
168
- this.reply(response, 501, { error: "The platform UI has not been built - run npm run build in anbaric-cloud-hosting/ui" });
169
- }
21
+ registerFallback(handler : RequestHandler) : void {
22
+ this.fallbackHandler = handler;
170
23
  }
171
24
 
172
- private async forwardToApp(appHost : string, appPort : number, appName : string, method : string, url : URL,
173
- request : IncomingMessage, response : ServerResponse) : Promise<void> {
174
- const appPath = url.pathname.slice(`/${appName}`.length) || "/";
175
- const body = method === "GET" || method === "HEAD" ? undefined : await readRawBody(request);
176
-
177
- const upstream = await fetch(`http://${appHost}:${appPort}${appPath}${url.search}`, {
178
- method,
179
- headers: { "content-type": String(request.headers["content-type"] ?? "application/json") },
180
- body: body && body.length > 0 ? new Uint8Array(body) : undefined,
181
- });
182
-
183
- const payload = Buffer.from(await upstream.arrayBuffer());
184
- response.writeHead(upstream.status, { "content-type": upstream.headers.get("content-type") ?? "application/octet-stream" });
185
- response.end(payload);
186
- }
187
-
188
- private async handleSecrets(method : string, name : string | undefined,
189
- request : IncomingMessage, response : ServerResponse) : Promise<void> {
190
- if (!name && method === "GET") {
191
- return this.reply(response, 200, await this.secretStore!.list());
25
+ async route(request : Request) : Promise<void> {
26
+ if (request.resource === undefined) {
27
+ if (this.rootHandler) return this.rootHandler.handle(request);
28
+ return request.notFound();
192
29
  }
193
30
 
194
- if (name) {
195
- if (method === "PUT") {
196
- const { value } = await readBody(request);
197
- if (typeof value !== "string") return this.reply(response, 400, { error: "Expected a body of { value : string }" });
198
- await this.secretStore!.save(name, value);
199
- return this.reply(response, 204);
200
- }
201
- if (method === "GET") {
202
- return this.reply(response, 200, { value: await this.secretStore!.retrieve(name) });
203
- }
204
- if (method === "DELETE") {
205
- await this.secretStore!.delete(name);
206
- return this.reply(response, 204);
207
- }
208
- }
31
+ const handler = this.handlers.get(request.resource);
32
+ if (handler) return handler.handle(request);
33
+ if (this.fallbackHandler) return this.fallbackHandler.handle(request);
209
34
 
210
- this.reply(response, 404, { error: "Not found" });
211
- }
212
-
213
- private async handleDocuments(method : string, store : JsonStore, documentId : string | undefined,
214
- url : URL, request : IncomingMessage, response : ServerResponse) : Promise<void> {
215
- if (!documentId && method === "GET") {
216
- const pageSize = Number(url.searchParams.get("pageSize") ?? 100);
217
- const page = Number(url.searchParams.get("page") ?? 0);
218
- return this.reply(response, 200, await store.list(pageSize, page));
219
- }
220
-
221
- if (documentId) {
222
- if (method === "PUT") {
223
- await store.save(documentId, await readBody(request));
224
- return this.reply(response, 204);
225
- }
226
- if (method === "GET") {
227
- return this.reply(response, 200, await store.retrieve(documentId));
228
- }
229
- if (method === "DELETE") {
230
- await store.delete(documentId);
231
- return this.reply(response, 204);
232
- }
233
- }
234
-
235
- this.reply(response, 404, { error: "Not found" });
236
- }
237
-
238
- private async handleApps(method : string, appName : string | undefined, subresource : string | undefined,
239
- url : URL, request : IncomingMessage, response : ServerResponse) : Promise<void> {
240
- if (method === "GET" && !appName) {
241
- return this.reply(response, 200, this.buildLayer!.list());
242
- }
243
-
244
- if (!appName) return this.reply(response, 404, { error: "Not found" });
245
-
246
- if (method === "POST" && subresource === "deploy") {
247
- const appPort = Number(url.searchParams.get("port"));
248
- if (!Number.isInteger(appPort) || appPort <= 0) {
249
- return this.reply(response, 400, { error: "Expected a numeric port query parameter" });
250
- }
251
- const tarball = await readRawBody(request);
252
- if (tarball.length === 0) return this.reply(response, 400, { error: "Expected a gzipped tarball body" });
253
- return this.reply(response, 202, this.buildLayer!.deploy(appName, appPort, tarball));
254
- }
255
-
256
- if (method === "GET" && !subresource) {
257
- const status = this.buildLayer!.status(appName);
258
- if (!status) return this.reply(response, 404, { error: `No app named "${appName}"` });
259
- return this.reply(response, 200, status);
260
- }
261
-
262
- this.reply(response, 404, { error: "Not found" });
263
- }
264
-
265
- private async handleJobs(method : string, id : string | undefined, subresource : string | undefined,
266
- url : URL, request : IncomingMessage, response : ServerResponse) : Promise<void> {
267
- if (!id && method === "GET") {
268
- const pageSize = Number(url.searchParams.get("pageSize") ?? 100);
269
- const page = Number(url.searchParams.get("page") ?? 0);
270
- const jobs = await this.persistence.list(pageSize, page);
271
- return this.reply(response, 200, jobs.map(serializeJob));
272
- }
273
-
274
- if (id && !subresource) {
275
- if (method === "PUT") {
276
- await this.persistence.save(deserializeJob(await readBody(request)));
277
- return this.reply(response, 204);
278
- }
279
- if (method === "GET") {
280
- const job = await this.persistence.retrieve(id);
281
- return this.reply(response, 200, serializeJob(job));
282
- }
283
- if (method === "DELETE") {
284
- await this.persistence.delete(id);
285
- return this.reply(response, 204);
286
- }
287
- }
288
-
289
- if (id && subresource === "properties" && method === "PATCH") {
290
- const properties = new Map<string, any>(Object.entries(await readBody(request)));
291
- await this.persistence.updateProperties(id, properties);
292
- return this.reply(response, 204);
293
- }
294
-
295
- this.reply(response, 404, { error: "Not found" });
296
- }
297
-
298
- private async handleQueue(operation : string | undefined, request : IncomingMessage,
299
- response : ServerResponse) : Promise<void> {
300
- if (operation === "enqueue") {
301
- const { jobId, workflowId } = await readBody(request);
302
- await this.queue.enqueue(jobId, workflowId);
303
- return this.reply(response, 204);
304
- }
305
-
306
- if (operation === "schedule") {
307
- const { jobId, workflowId, due } = await readBody(request);
308
- await this.queue.schedule(jobId, workflowId, new Date(due));
309
- return this.reply(response, 204);
310
- }
311
-
312
- if (operation === "dequeue") {
313
- return this.reply(response, 200, { messages: await this.queue.dequeueSome() });
314
- }
315
-
316
- if (operation === "confirm") {
317
- const { jobId, workflowId } = await readBody(request);
318
- await this.queue.confirm({ jobId, workflowId });
319
- return this.reply(response, 204);
320
- }
321
-
322
- this.reply(response, 404, { error: "Not found" });
323
- }
324
-
325
- private reply(response : ServerResponse, status : number, body? : unknown) : void {
326
- if (body === undefined) {
327
- response.statusCode = status;
328
- response.end();
329
- return;
330
- }
331
- response.writeHead(status, { "content-type": "application/json" });
332
- response.end(JSON.stringify(body));
35
+ request.notFound();
333
36
  }
334
37
 
335
38
  }
@@ -0,0 +1,54 @@
1
+ import {createServer, IncomingMessage, Server as HttpServer, ServerResponse} from "node:http";
2
+ import {AddressInfo} from "node:net";
3
+ import {Middleware} from "./Middleware";
4
+ import {Request} from "./Request";
5
+ import {Router} from "./Router";
6
+
7
+ /* One listener: builds the Request, stamps the ray trace id, runs the
8
+ middleware chain to decorate (or answer) it, then hands it to the
9
+ router. */
10
+ class Server {
11
+
12
+ private server : HttpServer;
13
+
14
+ constructor(private router : Router, private middlewares : Array<Middleware> = []) {
15
+ this.server = createServer((incoming, response) => {
16
+ this.handle(incoming, response).catch(error => {
17
+ const message = error instanceof Error ? error.message : "Internal error";
18
+ const status = /^No .+ found/.test(message) ? 404 : 500;
19
+ if (!response.writableEnded) {
20
+ response.writeHead(status, { "content-type": "application/json" });
21
+ response.end(JSON.stringify({ error: message }));
22
+ }
23
+ });
24
+ });
25
+ }
26
+
27
+ get listening() : boolean {
28
+ return this.server.listening;
29
+ }
30
+
31
+ listen(port : number) : Promise<number> {
32
+ return new Promise(resolve =>
33
+ this.server.listen(port, () => resolve((this.server.address() as AddressInfo).port)));
34
+ }
35
+
36
+ close() : Promise<void> {
37
+ return new Promise((resolve, reject) =>
38
+ this.server.close(error => error ? reject(error) : resolve()));
39
+ }
40
+
41
+ private async handle(incoming : IncomingMessage, response : ServerResponse) : Promise<void> {
42
+ const request = new Request(incoming, response);
43
+ response.setHeader("x-anbaric-ray", request.rayId);
44
+
45
+ for (const middleware of this.middlewares) {
46
+ if (!await middleware.apply(request)) return;
47
+ }
48
+
49
+ await this.router.route(request);
50
+ }
51
+
52
+ }
53
+
54
+ export { Server }
@@ -0,0 +1,34 @@
1
+ import {BuildLayer} from "../../app-management/BuildLayer";
2
+ import {Request} from "../Request";
3
+ import {RequestHandler} from "../RequestHandler";
4
+
5
+ /* The router's fallback: any unregistered top-level path naming a running
6
+ app is forwarded to it. */
7
+ class AppProxyHandler implements RequestHandler {
8
+
9
+ constructor(private buildLayer : BuildLayer) {}
10
+
11
+ async handle(request : Request) : Promise<void> {
12
+ const appName = request.resource!;
13
+ const app = this.buildLayer.status(appName);
14
+ if (!app || app.status !== "running") return request.notFound();
15
+
16
+ const appPath = request.url.pathname.slice(`/${appName}`.length) || "/";
17
+ const body = request.method === "GET" || request.method === "HEAD" ? undefined : await request.rawBody();
18
+
19
+ const upstream = await fetch(`http://${app.appHost}:${app.appPort}${appPath}${request.url.search}`, {
20
+ method: request.method,
21
+ headers: { "content-type": request.header("content-type") ?? "application/json" },
22
+ body: body && body.length > 0 ? new Uint8Array(body) : undefined,
23
+ });
24
+
25
+ const payload = Buffer.from(await upstream.arrayBuffer());
26
+ request.rawResponse.writeHead(upstream.status, {
27
+ "content-type": upstream.headers.get("content-type") ?? "application/octet-stream",
28
+ });
29
+ request.rawResponse.end(payload);
30
+ }
31
+
32
+ }
33
+
34
+ export { AppProxyHandler }
@@ -0,0 +1,57 @@
1
+ import {BuildLayer} from "../../app-management/BuildLayer";
2
+ import {Request} from "../Request";
3
+ import {RequestHandler} from "../RequestHandler";
4
+
5
+ class AppsHandler implements RequestHandler {
6
+
7
+ constructor(private buildLayer : BuildLayer) {}
8
+
9
+ async handle(request : Request) : Promise<void> {
10
+ switch (request.subresource) {
11
+ case "deploy":
12
+ if (request.id) return this.handleDeploy(request, request.id);
13
+ break;
14
+ case undefined:
15
+ if (request.id) return this.handleApp(request, request.id);
16
+ return this.handleCollection(request);
17
+ }
18
+ request.notFound();
19
+ }
20
+
21
+ private async handleDeploy(request : Request, appName : string) : Promise<void> {
22
+ switch (request.method) {
23
+ case "POST": {
24
+ const appPort = Number(request.query("port"));
25
+ if (!Number.isInteger(appPort) || appPort <= 0) {
26
+ return request.reply(400, { error: "Expected a numeric port query parameter" });
27
+ }
28
+ const tarball = await request.rawBody();
29
+ if (tarball.length === 0) return request.reply(400, { error: "Expected a gzipped tarball body" });
30
+ return request.reply(202, this.buildLayer.deploy(appName, appPort, tarball));
31
+ }
32
+ }
33
+ request.notFound();
34
+ }
35
+
36
+ private async handleApp(request : Request, appName : string) : Promise<void> {
37
+ switch (request.method) {
38
+ case "GET": {
39
+ const status = this.buildLayer.status(appName);
40
+ if (!status) return request.reply(404, { error: `No app named "${appName}"` });
41
+ return request.reply(200, status);
42
+ }
43
+ }
44
+ request.notFound();
45
+ }
46
+
47
+ private async handleCollection(request : Request) : Promise<void> {
48
+ switch (request.method) {
49
+ case "GET":
50
+ return request.reply(200, this.buildLayer.list());
51
+ }
52
+ request.notFound();
53
+ }
54
+
55
+ }
56
+
57
+ export { AppsHandler }
@@ -0,0 +1,50 @@
1
+ import {AuditChange} from "anbaric-tsapi";
2
+ import {AuditRecordStore} from "../../auditing/AuditRecordStore";
3
+ import {Request} from "../Request";
4
+ import {RequestHandler} from "../RequestHandler";
5
+
6
+ const AUDIT_CHANGES = new Set(["CREATE", "UPDATE_PROPERTIES", "CHANGE_STATE", "DELETE"]);
7
+
8
+ class AuditsHandler implements RequestHandler {
9
+
10
+ constructor(private auditRecords : AuditRecordStore) {}
11
+
12
+ async handle(request : Request) : Promise<void> {
13
+ if (request.id) return request.notFound();
14
+
15
+ switch (request.method) {
16
+ case "POST":
17
+ return this.handleRecord(request);
18
+ case "GET":
19
+ return this.handleQuery(request);
20
+ }
21
+ request.notFound();
22
+ }
23
+
24
+ private async handleRecord(request : Request) : Promise<void> {
25
+ const record = await request.body();
26
+ if (typeof record?.jobId !== "string" || typeof record?.description !== "string"
27
+ || typeof record?.actorId !== "string" || typeof record?.actorType !== "string"
28
+ || !AUDIT_CHANGES.has(record?.change)) {
29
+ return request.reply(400, { error: "Expected a body of { jobId, actorId, actorType, change, description, ... }" });
30
+ }
31
+ await this.auditRecords.save(record);
32
+ request.reply(204);
33
+ }
34
+
35
+ private async handleQuery(request : Request) : Promise<void> {
36
+ const change = request.query("change");
37
+ const records = await this.auditRecords.list({
38
+ jobId: request.query("jobId"),
39
+ actorId: request.query("actorId"),
40
+ change: change && AUDIT_CHANGES.has(change) ? change as AuditChange : undefined,
41
+ search: request.query("search"),
42
+ pageSize: request.query("pageSize") === undefined ? undefined : Number(request.query("pageSize")),
43
+ page: request.query("page") === undefined ? undefined : Number(request.query("page")),
44
+ });
45
+ request.reply(200, records);
46
+ }
47
+
48
+ }
49
+
50
+ export { AuditsHandler }
@@ -0,0 +1,24 @@
1
+ import {ConsumerRegistry} from "../../queuing/ConsumerRegistry";
2
+ import {Request} from "../Request";
3
+ import {RequestHandler} from "../RequestHandler";
4
+
5
+ class ConsumersHandler implements RequestHandler {
6
+
7
+ constructor(private registry : ConsumerRegistry) {}
8
+
9
+ async handle(request : Request) : Promise<void> {
10
+ if (request.id) return request.notFound();
11
+
12
+ switch (request.method) {
13
+ case "POST": {
14
+ const { workflowId, url } = await request.body();
15
+ this.registry.register(workflowId, url);
16
+ return request.reply(204);
17
+ }
18
+ }
19
+ request.notFound();
20
+ }
21
+
22
+ }
23
+
24
+ export { ConsumersHandler }
@@ -0,0 +1,44 @@
1
+ import {JsonStore} from "anbaric-tsapi";
2
+ import {Request} from "../Request";
3
+ import {RequestHandler} from "../RequestHandler";
4
+
5
+ class DocumentsHandler implements RequestHandler {
6
+
7
+ constructor(private storeFor : (collection : string) => JsonStore) {}
8
+
9
+ async handle(request : Request) : Promise<void> {
10
+ if (!request.id) return request.notFound();
11
+
12
+ const store = this.storeFor(request.id);
13
+ if (request.subresource) return this.handleDocument(request, store, request.subresource);
14
+ return this.handleCollection(request, store);
15
+ }
16
+
17
+ private async handleDocument(request : Request, store : JsonStore, documentId : string) : Promise<void> {
18
+ switch (request.method) {
19
+ case "PUT":
20
+ await store.save(documentId, await request.body());
21
+ return request.reply(204);
22
+ case "GET":
23
+ return request.reply(200, await store.retrieve(documentId));
24
+ case "DELETE":
25
+ await store.delete(documentId);
26
+ return request.reply(204);
27
+ }
28
+ request.notFound();
29
+ }
30
+
31
+ private async handleCollection(request : Request, store : JsonStore) : Promise<void> {
32
+ switch (request.method) {
33
+ case "GET": {
34
+ const pageSize = Number(request.query("pageSize") ?? 100);
35
+ const page = Number(request.query("page") ?? 0);
36
+ return request.reply(200, await store.list(pageSize, page));
37
+ }
38
+ }
39
+ request.notFound();
40
+ }
41
+
42
+ }
43
+
44
+ export { DocumentsHandler }