anbaric-cloud-hosting 1.4.0 → 1.5.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anbaric-cloud-hosting",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Anbaric Cloud hosting service: Postgres-backed job persistence and queuing exposed over an HTTP API",
5
5
  "license": "MIT",
6
6
  "author": "chris@anbaric.ai",
@@ -17,15 +17,15 @@
17
17
  "@aws-sdk/client-s3": "^3.1110.0",
18
18
  "@aws-sdk/client-secrets-manager": "^3.700.0",
19
19
  "@aws-sdk/client-servicediscovery": "^3.1110.0",
20
- "anbaric-data-store": "^1.4.0",
21
- "anbaric-tsapi": "^1.4.0",
20
+ "anbaric-data-store": "^1.5.0",
21
+ "anbaric-tsapi": "^1.5.0",
22
22
  "pg": "^8.16.0"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/node": "^26.2.0",
26
26
  "@types/pg": "^8.15.0",
27
- "anbaric-cloud": "^1.4.0",
28
- "anbaric-state-machine": "^1.4.0",
27
+ "anbaric-impl-cloud": "^1.5.0",
28
+ "anbaric-state-machine": "^1.5.0",
29
29
  "tsx": "^4.20.0",
30
30
  "typescript": "^7.0.2"
31
31
  },
@@ -30,6 +30,7 @@ class HostingServer {
30
30
  private tokenAuthenticator? : TokenAuthenticator,
31
31
  private tenant? : string,
32
32
  auditRecords? : AuditRecordStore) {
33
+
33
34
  this.router = new Router(persistence, queue, registry, buildLayer, documentStoreFor, secretStore, cliAuthorizer, tenant, auditRecords);
34
35
  this.server = this.serverFor((request, response) => this.handle(request, response));
35
36
  this.internalServer = this.serverFor((request, response) => this.handleInternal(request, response));
@@ -3,14 +3,14 @@ import {IncomingMessage, ServerResponse} from "node:http";
3
3
  import {AuditChange, JobPersistence, JsonStore, SecretStore, deserializeJob, serializeJob} from "anbaric-tsapi";
4
4
  import {BuildLayer} from "../app-management/BuildLayer";
5
5
  import {AuditRecordStore} from "../auditing/AuditRecordStore";
6
-
7
- const AUDIT_CHANGES = new Set(["CREATE", "UPDATE_PROPERTIES", "CHANGE_STATE", "DELETE"]);
8
6
  import {CliAuthorizer} from "../auth/CliAuthorizer";
9
7
  import {Tenant} from "../auth/Tenant";
10
8
  import {User} from "../auth/User";
11
9
  import {ConfirmableQueue} from "../queuing/ConfirmableQueue";
12
10
  import {ConsumerRegistry} from "../queuing/ConsumerRegistry";
13
11
 
12
+ const AUDIT_CHANGES = new Set(["CREATE", "UPDATE_PROPERTIES", "CHANGE_STATE", "DELETE"]);
13
+
14
14
  const readRawBody = (request : IncomingMessage) : Promise<Buffer> =>
15
15
  new Promise((resolve, reject) => {
16
16
  const chunks : Array<Buffer> = [];
@@ -24,6 +24,10 @@ const readBody = async (request : IncomingMessage) : Promise<any> => {
24
24
  return raw.length === 0 ? undefined : JSON.parse(raw);
25
25
  };
26
26
 
27
+ /* Routing is a tree: route() dispatches on the resource family (or the few
28
+ specific resources that fit no family), each family handler switches on
29
+ the specific resource shape, and the method switch sits underneath.
30
+ Unmatched requests fall through to the app proxy, then 404. */
27
31
  class Router {
28
32
 
29
33
  constructor(private persistence : JobPersistence, private queue : ConfirmableQueue,
@@ -40,36 +44,141 @@ class Router {
40
44
  const [resource, id, subresource] = url.pathname.split("/").filter(Boolean);
41
45
  const method = request.method ?? "GET";
42
46
 
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);
47
+ switch (resource) {
48
+ case undefined:
49
+ if (method === "GET") return this.servePage(response);
50
+ break;
51
+ case "audit":
52
+ if (method === "GET" && !id) return this.servePage(response);
53
+ break;
54
+ case "manage-keys":
55
+ if (method === "GET" && !id && this.cliAuthorizer) return this.servePage(response);
56
+ break;
57
+ case "whoami":
58
+ if (method === "GET" && !id && user) {
59
+ return this.reply(response, 200, { id: user.id, roles: user.roles.map(role => role.id) });
60
+ }
61
+ break;
62
+ case "state-machines":
63
+ if (method === "GET" && !id) return this.reply(response, 200, this.registry.list());
64
+ break;
65
+ case "authorize-cli":
66
+ if (this.cliAuthorizer && id) {
67
+ return this.handleAuthorizeCli(method, id, subresource, request, response, user, sessionTenant);
68
+ }
69
+ break;
70
+ case "keys":
71
+ if (this.cliAuthorizer) return this.handleKeys(method, id, response, user);
72
+ break;
73
+ case "audits":
74
+ if (this.auditRecords && !id) return this.handleAudits(method, url, request, response);
75
+ break;
76
+ case "jobs":
77
+ return this.handleJobs(method, id, subresource, url, request, response);
78
+ case "queue":
79
+ if (id && !subresource) return this.handleQueue(method, id, request, response);
80
+ break;
81
+ case "consumers":
82
+ if (!id) return this.handleConsumers(method, request, response);
83
+ break;
84
+ case "apps":
85
+ if (this.buildLayer) return this.handleApps(method, id, subresource, url, request, response);
86
+ break;
87
+ case "documents":
88
+ if (id && this.documentStoreFor) {
89
+ return this.handleDocuments(method, this.documentStoreFor(id), subresource, url, request, response);
90
+ }
91
+ break;
92
+ case "secrets":
93
+ if (!subresource && this.secretStore) return this.handleSecrets(method, id, request, response);
94
+ break;
48
95
  }
49
- if (resource === "manage-keys" && method === "GET" && !id && this.cliAuthorizer) {
50
- return this.servePage(response);
96
+
97
+ if (resource && this.buildLayer) {
98
+ const app = this.buildLayer.status(resource);
99
+ if (app && app.status === "running") {
100
+ return this.forwardToApp(app.appHost, app.appPort, resource, method, url, request, response);
101
+ }
51
102
  }
52
- if (resource === "keys" && this.cliAuthorizer) {
53
- return this.handleKeys(method, id, response, user);
103
+
104
+ this.notFound(response);
105
+ }
106
+
107
+ private async handleAuthorizeCli(method : string, requestId : string, subresource : string | undefined,
108
+ request : IncomingMessage, response : ServerResponse, user? : User,
109
+ sessionTenant? : Tenant) : Promise<void> {
110
+ switch (subresource) {
111
+ case "poll":
112
+ switch (method) {
113
+ case "GET": {
114
+ const keyPair = this.cliAuthorizer!.collect(requestId);
115
+ if (!keyPair) return this.reply(response, 202, { status: "pending" });
116
+ return this.reply(response, 200, keyPair);
117
+ }
118
+ }
119
+ break;
120
+ case undefined:
121
+ switch (method) {
122
+ case "GET":
123
+ return this.servePage(response);
124
+ case "POST": {
125
+ const { clientName } = await readBody(request);
126
+ if (typeof clientName !== "string" || clientName.trim().length === 0) {
127
+ return this.reply(response, 400, { error: "Expected a body of { clientName : string }" });
128
+ }
129
+ await this.cliAuthorizer!.approve(requestId, clientName.trim(), user ?? new User("local"),
130
+ sessionTenant?.id ?? this.tenant);
131
+ return this.reply(response, 204);
132
+ }
133
+ }
134
+ break;
54
135
  }
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) });
136
+
137
+ this.notFound(response);
138
+ }
139
+
140
+ private async handleKeys(method : string, id : string | undefined,
141
+ response : ServerResponse, user? : User) : Promise<void> {
142
+ const owner = user ?? new User("local");
143
+
144
+ if (id) {
145
+ switch (method) {
146
+ case "DELETE":
147
+ await this.cliAuthorizer!.revoke(id, owner.id);
148
+ return this.reply(response, 204);
149
+ }
150
+ } else {
151
+ switch (method) {
152
+ case "GET": {
153
+ const keys = await this.cliAuthorizer!.keysFor(owner.id);
154
+ return this.reply(response, 200, keys.map(key => ({
155
+ id: key.id,
156
+ clientName: key.clientName,
157
+ createdAt: key.createdAt.toISOString(),
158
+ })));
159
+ }
160
+ }
58
161
  }
59
- if (resource === "audits" && this.auditRecords && !id) {
60
- if (method === "POST") {
162
+
163
+ this.notFound(response);
164
+ }
165
+
166
+ private async handleAudits(method : string, url : URL,
167
+ request : IncomingMessage, response : ServerResponse) : Promise<void> {
168
+ switch (method) {
169
+ case "POST": {
61
170
  const record = await readBody(request);
62
171
  if (typeof record?.jobId !== "string" || typeof record?.description !== "string"
63
172
  || typeof record?.actorId !== "string" || typeof record?.actorType !== "string"
64
173
  || !AUDIT_CHANGES.has(record?.change)) {
65
174
  return this.reply(response, 400, { error: "Expected a body of { jobId, actorId, actorType, change, description, ... }" });
66
175
  }
67
- await this.auditRecords.save(record);
176
+ await this.auditRecords!.save(record);
68
177
  return this.reply(response, 204);
69
178
  }
70
- if (method === "GET") {
179
+ case "GET": {
71
180
  const change = url.searchParams.get("change");
72
- const records = await this.auditRecords.list({
181
+ const records = await this.auditRecords!.list({
73
182
  jobId: url.searchParams.get("jobId") ?? undefined,
74
183
  actorId: url.searchParams.get("actorId") ?? undefined,
75
184
  change: change && AUDIT_CHANGES.has(change) ? change as AuditChange : undefined,
@@ -80,83 +189,195 @@ class Router {
80
189
  return this.reply(response, 200, records);
81
190
  }
82
191
  }
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
192
 
112
- this.reply(response, 404, { error: "Not found" });
193
+ this.notFound(response);
113
194
  }
114
195
 
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);
196
+ private async handleJobs(method : string, id : string | undefined, subresource : string | undefined,
197
+ url : URL, request : IncomingMessage, response : ServerResponse) : Promise<void> {
198
+ switch (subresource) {
199
+ case "properties":
200
+ if (id) {
201
+ switch (method) {
202
+ case "PATCH": {
203
+ const properties = new Map<string, any>(Object.entries(await readBody(request)));
204
+ await this.persistence.updateProperties(id, properties);
205
+ return this.reply(response, 204);
206
+ }
207
+ }
208
+ }
209
+ break;
210
+ case undefined:
211
+ if (id) {
212
+ switch (method) {
213
+ case "PUT":
214
+ await this.persistence.save(deserializeJob(await readBody(request)));
215
+ return this.reply(response, 204);
216
+ case "GET":
217
+ return this.reply(response, 200, serializeJob(await this.persistence.retrieve(id)));
218
+ case "DELETE":
219
+ await this.persistence.delete(id);
220
+ return this.reply(response, 204);
221
+ }
222
+ } else {
223
+ switch (method) {
224
+ case "GET": {
225
+ const pageSize = Number(url.searchParams.get("pageSize") ?? 100);
226
+ const page = Number(url.searchParams.get("page") ?? 0);
227
+ const jobs = await this.persistence.list(pageSize, page);
228
+ return this.reply(response, 200, jobs.map(serializeJob));
229
+ }
230
+ }
231
+ }
232
+ break;
122
233
  }
123
234
 
124
- if (method === "GET" && !subresource) {
125
- return this.servePage(response);
235
+ this.notFound(response);
236
+ }
237
+
238
+ private async handleQueue(method : string, operation : string,
239
+ request : IncomingMessage, response : ServerResponse) : Promise<void> {
240
+ switch (operation) {
241
+ case "enqueue":
242
+ switch (method) {
243
+ case "POST": {
244
+ const { jobId, workflowId } = await readBody(request);
245
+ await this.queue.enqueue(jobId, workflowId);
246
+ return this.reply(response, 204);
247
+ }
248
+ }
249
+ break;
250
+ case "schedule":
251
+ switch (method) {
252
+ case "POST": {
253
+ const { jobId, workflowId, due } = await readBody(request);
254
+ await this.queue.schedule(jobId, workflowId, new Date(due));
255
+ return this.reply(response, 204);
256
+ }
257
+ }
258
+ break;
259
+ case "dequeue":
260
+ switch (method) {
261
+ case "POST":
262
+ return this.reply(response, 200, { messages: await this.queue.dequeueSome() });
263
+ }
264
+ break;
265
+ case "confirm":
266
+ switch (method) {
267
+ case "POST": {
268
+ const { jobId, workflowId } = await readBody(request);
269
+ await this.queue.confirm({ jobId, workflowId });
270
+ return this.reply(response, 204);
271
+ }
272
+ }
273
+ break;
126
274
  }
127
275
 
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 }" });
276
+ this.notFound(response);
277
+ }
278
+
279
+ private async handleConsumers(method : string, request : IncomingMessage,
280
+ response : ServerResponse) : Promise<void> {
281
+ switch (method) {
282
+ case "POST": {
283
+ const { workflowId, url: consumerUrl } = await readBody(request);
284
+ this.registry.register(workflowId, consumerUrl);
285
+ return this.reply(response, 204);
132
286
  }
133
- await this.cliAuthorizer!.approve(requestId, clientName.trim(), user ?? new User("local"),
134
- sessionTenant?.id ?? this.tenant);
135
- return this.reply(response, 204);
136
287
  }
137
288
 
138
- this.reply(response, 404, { error: "Not found" });
289
+ this.notFound(response);
139
290
  }
140
291
 
141
- private async handleKeys(method : string, id : string | undefined,
142
- response : ServerResponse, user? : User) : Promise<void> {
143
- const owner = user ?? new User("local");
292
+ private async handleApps(method : string, appName : string | undefined, subresource : string | undefined,
293
+ url : URL, request : IncomingMessage, response : ServerResponse) : Promise<void> {
294
+ switch (subresource) {
295
+ case "deploy":
296
+ if (appName) {
297
+ switch (method) {
298
+ case "POST": {
299
+ const appPort = Number(url.searchParams.get("port"));
300
+ if (!Number.isInteger(appPort) || appPort <= 0) {
301
+ return this.reply(response, 400, { error: "Expected a numeric port query parameter" });
302
+ }
303
+ const tarball = await readRawBody(request);
304
+ if (tarball.length === 0) return this.reply(response, 400, { error: "Expected a gzipped tarball body" });
305
+ return this.reply(response, 202, this.buildLayer!.deploy(appName, appPort, tarball));
306
+ }
307
+ }
308
+ }
309
+ break;
310
+ case undefined:
311
+ if (appName) {
312
+ switch (method) {
313
+ case "GET": {
314
+ const status = this.buildLayer!.status(appName);
315
+ if (!status) return this.reply(response, 404, { error: `No app named "${appName}"` });
316
+ return this.reply(response, 200, status);
317
+ }
318
+ }
319
+ } else {
320
+ switch (method) {
321
+ case "GET":
322
+ return this.reply(response, 200, this.buildLayer!.list());
323
+ }
324
+ }
325
+ break;
326
+ }
144
327
 
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
- })));
328
+ this.notFound(response);
329
+ }
330
+
331
+ private async handleDocuments(method : string, store : JsonStore, documentId : string | undefined,
332
+ url : URL, request : IncomingMessage, response : ServerResponse) : Promise<void> {
333
+ if (documentId) {
334
+ switch (method) {
335
+ case "PUT":
336
+ await store.save(documentId, await readBody(request));
337
+ return this.reply(response, 204);
338
+ case "GET":
339
+ return this.reply(response, 200, await store.retrieve(documentId));
340
+ case "DELETE":
341
+ await store.delete(documentId);
342
+ return this.reply(response, 204);
343
+ }
344
+ } else {
345
+ switch (method) {
346
+ case "GET": {
347
+ const pageSize = Number(url.searchParams.get("pageSize") ?? 100);
348
+ const page = Number(url.searchParams.get("page") ?? 0);
349
+ return this.reply(response, 200, await store.list(pageSize, page));
350
+ }
351
+ }
152
352
  }
153
353
 
154
- if (method === "DELETE" && id) {
155
- await this.cliAuthorizer!.revoke(id, owner.id);
156
- return this.reply(response, 204);
354
+ this.notFound(response);
355
+ }
356
+
357
+ private async handleSecrets(method : string, name : string | undefined,
358
+ request : IncomingMessage, response : ServerResponse) : Promise<void> {
359
+ if (name) {
360
+ switch (method) {
361
+ case "PUT": {
362
+ const { value } = await readBody(request);
363
+ if (typeof value !== "string") return this.reply(response, 400, { error: "Expected a body of { value : string }" });
364
+ await this.secretStore!.save(name, value);
365
+ return this.reply(response, 204);
366
+ }
367
+ case "GET":
368
+ return this.reply(response, 200, { value: await this.secretStore!.retrieve(name) });
369
+ case "DELETE":
370
+ await this.secretStore!.delete(name);
371
+ return this.reply(response, 204);
372
+ }
373
+ } else {
374
+ switch (method) {
375
+ case "GET":
376
+ return this.reply(response, 200, await this.secretStore!.list());
377
+ }
157
378
  }
158
379
 
159
- this.reply(response, 404, { error: "Not found" });
380
+ this.notFound(response);
160
381
  }
161
382
 
162
383
  private async servePage(response : ServerResponse) : Promise<void> {
@@ -185,140 +406,7 @@ class Router {
185
406
  response.end(payload);
186
407
  }
187
408
 
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());
192
- }
193
-
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
- }
209
-
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
-
409
+ private notFound(response : ServerResponse) : void {
322
410
  this.reply(response, 404, { error: "Not found" });
323
411
  }
324
412