@highstate/backend-api 0.27.0 → 0.29.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/dist/highstate.manifest.json +1 -1
- package/dist/index.js +1202 -93
- package/package.json +12 -10
- package/src/handlers/instance-state.ts +96 -0
- package/src/handlers/library.ts +63 -0
- package/src/handlers/operation.ts +108 -0
- package/src/handlers/panel.ts +7 -6
- package/src/handlers/project-model.ts +131 -0
- package/src/handlers/project.ts +46 -0
- package/src/handlers/secret.ts +6 -5
- package/src/handlers/worker.ts +29 -32
- package/src/index.ts +125 -19
- package/src/shared/api-error.ts +42 -0
- package/src/shared/authentication.ts +172 -12
- package/src/shared/authorization-header.test.ts +36 -0
- package/src/shared/authorization-header.ts +24 -0
- package/src/shared/conversion.test.ts +122 -0
- package/src/shared/conversion.ts +593 -0
- package/src/shared/error-handling.test.ts +138 -0
- package/src/shared/error-handling.ts +127 -14
- package/src/shared/field-mask.test.ts +37 -0
- package/src/shared/field-mask.ts +81 -0
- package/src/shared/index.ts +5 -0
- package/src/shared/serialization.test.ts +17 -0
- package/src/shared/serialization.ts +35 -0
- package/src/shared/validation.ts +45 -5
- package/src/handlers/instance.ts +0 -44
package/dist/index.js
CHANGED
|
@@ -1,74 +1,1016 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// src/index.ts
|
|
3
3
|
import { rm } from "fs/promises";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
import { createServer } from "http";
|
|
5
|
+
import { connectNodeAdapter } from "@connectrpc/connect-node";
|
|
6
|
+
import {
|
|
7
|
+
InstanceStateService,
|
|
8
|
+
LibraryService,
|
|
9
|
+
OperationService,
|
|
10
|
+
PanelService,
|
|
11
|
+
ProjectModelService,
|
|
12
|
+
ProjectService,
|
|
13
|
+
SecretService
|
|
14
|
+
} from "@highstate/api/v1";
|
|
15
|
+
import { WorkerService } from "@highstate/api/worker.v1";
|
|
9
16
|
|
|
10
|
-
// src/handlers/instance.ts
|
|
11
|
-
import { instanceCustomStatusInputSchema } from "@highstate/backend/shared";
|
|
12
|
-
import { z } from "@highstate/contract";
|
|
17
|
+
// src/handlers/instance-state.ts
|
|
18
|
+
import { instanceCustomStatusInputSchema as instanceCustomStatusInputSchema2 } from "@highstate/backend/shared";
|
|
19
|
+
import { z as z2 } from "@highstate/contract";
|
|
13
20
|
|
|
21
|
+
// src/shared/api-error.ts
|
|
22
|
+
import { create } from "@bufbuild/protobuf";
|
|
23
|
+
import { ConnectError } from "@connectrpc/connect";
|
|
24
|
+
import { BadRequestSchema, ErrorInfoSchema } from "@highstate/api/v1";
|
|
25
|
+
function createApiError(options) {
|
|
26
|
+
const details = [
|
|
27
|
+
{
|
|
28
|
+
desc: ErrorInfoSchema,
|
|
29
|
+
value: create(ErrorInfoSchema, {
|
|
30
|
+
reason: options.reason,
|
|
31
|
+
domain: "highstate.io",
|
|
32
|
+
metadata: options.metadata
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
];
|
|
36
|
+
if (options.fieldViolations?.length) {
|
|
37
|
+
details.push({
|
|
38
|
+
desc: BadRequestSchema,
|
|
39
|
+
value: create(BadRequestSchema, {
|
|
40
|
+
fieldViolations: options.fieldViolations.map((violation) => ({ ...violation }))
|
|
41
|
+
})
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return new ConnectError(options.message, options.code, undefined, details);
|
|
45
|
+
}
|
|
14
46
|
// src/shared/authentication.ts
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
47
|
+
import { Code as Code2 } from "@connectrpc/connect";
|
|
48
|
+
import { ProjectLockedError, ProjectNotFoundError } from "@highstate/backend/shared";
|
|
49
|
+
|
|
50
|
+
// src/shared/authorization-header.ts
|
|
51
|
+
import { Code } from "@connectrpc/connect";
|
|
52
|
+
function parseBearerToken(authorization) {
|
|
53
|
+
const match = /^Bearer ([^\s]+)$/i.exec(authorization ?? "");
|
|
54
|
+
if (!match || [...match[1]].some((character) => character.charCodeAt(0) <= 31 || character === "\x7F")) {
|
|
55
|
+
throw createApiError({
|
|
56
|
+
message: "Invalid authorization header",
|
|
57
|
+
code: Code.Unauthenticated,
|
|
58
|
+
reason: "AUTHORIZATION_HEADER_INVALID"
|
|
59
|
+
});
|
|
20
60
|
}
|
|
21
|
-
|
|
61
|
+
return match[1];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/shared/authentication.ts
|
|
65
|
+
async function authenticateBackend(services, context) {
|
|
66
|
+
const token = getBearerToken(context);
|
|
67
|
+
const apiKey = await services.apiKeyService.getBackendApiKeyByToken(token);
|
|
68
|
+
const roleBindings = await services.database.backend.serviceAccountBackendRoleBinding.findMany({
|
|
69
|
+
where: { serviceAccountId: apiKey.serviceAccountId },
|
|
70
|
+
include: { role: { select: { rules: true } } }
|
|
71
|
+
});
|
|
72
|
+
return {
|
|
73
|
+
realm: "backend",
|
|
74
|
+
subject: {
|
|
75
|
+
type: "service-account",
|
|
76
|
+
serviceAccountId: apiKey.serviceAccountId,
|
|
77
|
+
apiKeyId: apiKey.id
|
|
78
|
+
},
|
|
79
|
+
permissions: resolveBackendPermissions(roleBindings.flatMap((binding) => binding.role.rules.map((rule) => ({
|
|
80
|
+
permissions: rule.permissions,
|
|
81
|
+
restrictions: rule.restrictions ?? []
|
|
82
|
+
}))), apiKey.restrictionRules)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async function authenticateProject(services, request, context) {
|
|
86
|
+
const projectId = request.projectId;
|
|
22
87
|
if (!projectId) {
|
|
23
|
-
throw
|
|
88
|
+
throw createApiError({
|
|
89
|
+
message: "No project ID provided",
|
|
90
|
+
code: Code2.InvalidArgument,
|
|
91
|
+
reason: "PROJECT_ID_REQUIRED",
|
|
92
|
+
fieldViolations: [
|
|
93
|
+
{
|
|
94
|
+
field: "project_id",
|
|
95
|
+
reason: "REQUIRED",
|
|
96
|
+
description: "The project ID is required"
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const project = await services.database.backend.project.findUnique({
|
|
102
|
+
where: { id: projectId },
|
|
103
|
+
select: { id: true }
|
|
104
|
+
});
|
|
105
|
+
if (!project) {
|
|
106
|
+
throw new ProjectNotFoundError(projectId);
|
|
107
|
+
}
|
|
108
|
+
if (!await services.projectUnlockService.checkProjectUnlocked(projectId)) {
|
|
109
|
+
throw new ProjectLockedError(projectId);
|
|
110
|
+
}
|
|
111
|
+
const token = getBearerToken(context);
|
|
112
|
+
const apiKey = await services.apiKeyService.getProjectCredentialByToken(projectId, token);
|
|
113
|
+
const projectDatabase = await services.database.forProject(projectId);
|
|
114
|
+
const roleBindings = await projectDatabase.serviceAccountRoleBinding.findMany({
|
|
115
|
+
where: { serviceAccountId: apiKey.serviceAccountId },
|
|
116
|
+
include: { role: { select: { rules: true } } }
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
realm: "project",
|
|
120
|
+
projectId,
|
|
121
|
+
subject: {
|
|
122
|
+
type: "service-account",
|
|
123
|
+
serviceAccountId: apiKey.serviceAccountId,
|
|
124
|
+
apiKeyId: apiKey.id
|
|
125
|
+
},
|
|
126
|
+
permissions: resolveProjectPermissions(roleBindings.flatMap((binding) => binding.role.rules.map((rule) => ({
|
|
127
|
+
permissions: rule.permissions,
|
|
128
|
+
restrictions: rule.restrictions ?? []
|
|
129
|
+
}))), apiKey.restrictionRules)
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function getBearerToken(context) {
|
|
133
|
+
return parseBearerToken(context.requestHeader.get("authorization"));
|
|
134
|
+
}
|
|
135
|
+
function resolveBackendPermissions(grants, restrictions) {
|
|
136
|
+
return resolvePermissions(grants, restrictions);
|
|
137
|
+
}
|
|
138
|
+
function resolveProjectPermissions(grants, restrictions) {
|
|
139
|
+
return resolvePermissions(grants, restrictions);
|
|
140
|
+
}
|
|
141
|
+
function resolvePermissions(grants, restrictions) {
|
|
142
|
+
const restrictionByPermission = new Map(restrictions.flatMap((rule) => rule.permissions.map((permission) => [permission, rule.restrictions ?? []])));
|
|
143
|
+
const permissions = new Map;
|
|
144
|
+
for (const grant of grants) {
|
|
145
|
+
for (const permission of grant.permissions) {
|
|
146
|
+
const keyRestrictions = restrictionByPermission.get(permission);
|
|
147
|
+
if (restrictions.length > 0 && !keyRestrictions) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const permissionGrants = permissions.get(permission) ?? [];
|
|
151
|
+
permissionGrants.push({ restrictions: [...grant.restrictions, ...keyRestrictions ?? []] });
|
|
152
|
+
permissions.set(permission, permissionGrants);
|
|
153
|
+
}
|
|
24
154
|
}
|
|
25
|
-
|
|
26
|
-
|
|
155
|
+
return permissions;
|
|
156
|
+
}
|
|
157
|
+
// src/shared/conversion.ts
|
|
158
|
+
import { create as create2, fromJson, toJson } from "@bufbuild/protobuf";
|
|
159
|
+
import { timestampFromDate, ValueSchema } from "@bufbuild/protobuf/wkt";
|
|
160
|
+
import {
|
|
161
|
+
ComponentKind,
|
|
162
|
+
ComponentSchema,
|
|
163
|
+
EntitySchema,
|
|
164
|
+
EvaluationStatus,
|
|
165
|
+
HubSchema,
|
|
166
|
+
InstanceCustomStatusSchema,
|
|
167
|
+
InstanceOperationStatus,
|
|
168
|
+
InstanceSchema,
|
|
169
|
+
InstanceSource,
|
|
170
|
+
InstanceStateSchema,
|
|
171
|
+
InstanceStatus,
|
|
172
|
+
LibrarySchema,
|
|
173
|
+
OperationLogSchema,
|
|
174
|
+
OperationPhaseSchema,
|
|
175
|
+
OperationPhaseType,
|
|
176
|
+
OperationSchema,
|
|
177
|
+
OperationStatus,
|
|
178
|
+
OperationType,
|
|
179
|
+
ProjectModelSchema,
|
|
180
|
+
ProjectSchema
|
|
181
|
+
} from "@highstate/api/v1";
|
|
182
|
+
import {
|
|
183
|
+
instanceCustomStatusInputSchema,
|
|
184
|
+
operationMetaSchema,
|
|
185
|
+
operationOptionsSchema,
|
|
186
|
+
operationPhaseSchema
|
|
187
|
+
} from "@highstate/backend/shared";
|
|
188
|
+
import {
|
|
189
|
+
instanceInputSchema,
|
|
190
|
+
instanceModelSchema,
|
|
191
|
+
z
|
|
192
|
+
} from "@highstate/contract";
|
|
193
|
+
var jsonValueSchema = z.lazy(() => z.union([
|
|
194
|
+
z.string(),
|
|
195
|
+
z.number(),
|
|
196
|
+
z.boolean(),
|
|
197
|
+
z.null(),
|
|
198
|
+
z.array(jsonValueSchema),
|
|
199
|
+
z.record(z.string(), jsonValueSchema)
|
|
200
|
+
]));
|
|
201
|
+
var jsonObjectSchema = z.record(z.string(), jsonValueSchema);
|
|
202
|
+
function toProject(project) {
|
|
203
|
+
return create2(ProjectSchema, {
|
|
204
|
+
id: project.id,
|
|
205
|
+
name: project.name,
|
|
206
|
+
meta: project.meta,
|
|
207
|
+
spaceId: project.spaceId,
|
|
208
|
+
modelStorageId: project.modelStorageId,
|
|
209
|
+
libraryId: project.libraryId,
|
|
210
|
+
pulumiBackendId: project.pulumiBackendId,
|
|
211
|
+
createdAt: toTimestamp(project.createdAt),
|
|
212
|
+
updatedAt: toTimestamp(project.updatedAt)
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
function toProjectModel(model) {
|
|
216
|
+
return create2(ProjectModelSchema, {
|
|
217
|
+
instances: [...model.instances, ...model.virtualInstances, ...model.ghostInstances].map(toInstance),
|
|
218
|
+
hubs: model.hubs.map(toHub)
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function toInstance(instance) {
|
|
222
|
+
return create2(InstanceSchema, {
|
|
223
|
+
id: instance.id,
|
|
224
|
+
kind: toComponentKind(instance.kind),
|
|
225
|
+
type: instance.type,
|
|
226
|
+
name: instance.name,
|
|
227
|
+
arguments: Object.entries(instance.args ?? {}).map(([key, value]) => ({
|
|
228
|
+
key,
|
|
229
|
+
value: fromJson(ValueSchema, value)
|
|
230
|
+
})),
|
|
231
|
+
inputs: toInstanceReferenceMap(instance.inputs),
|
|
232
|
+
hubInputs: Object.fromEntries(Object.entries(instance.hubInputs ?? {}).map(([key, values]) => [
|
|
233
|
+
key,
|
|
234
|
+
{ values: values.map((value) => ({ hubId: value.hubId })) }
|
|
235
|
+
])),
|
|
236
|
+
injectionInputs: (instance.injectionInputs ?? []).map((value) => ({ hubId: value.hubId })),
|
|
237
|
+
position: instance.position ?? undefined,
|
|
238
|
+
parentId: instance.parentId,
|
|
239
|
+
outputs: toInstanceReferenceMap(instance.outputs),
|
|
240
|
+
resolvedOutputs: toInstanceReferenceMap(instance.resolvedOutputs)
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
function fromInstance(instance) {
|
|
244
|
+
return instanceModelSchema.parse({
|
|
245
|
+
id: instance.id,
|
|
246
|
+
kind: fromComponentKind(instance.kind),
|
|
247
|
+
type: instance.type,
|
|
248
|
+
name: instance.name,
|
|
249
|
+
args: Object.fromEntries(instance.arguments.map((argument) => [
|
|
250
|
+
argument.key,
|
|
251
|
+
argument.value ? toJson(ValueSchema, argument.value) : null
|
|
252
|
+
])),
|
|
253
|
+
inputs: fromInstanceReferenceMap(instance.inputs),
|
|
254
|
+
hubInputs: Object.fromEntries(Object.entries(instance.hubInputs).map(([key, list]) => [
|
|
255
|
+
key,
|
|
256
|
+
list.values.map((value) => ({ hubId: value.hubId }))
|
|
257
|
+
])),
|
|
258
|
+
injectionInputs: instance.injectionInputs.map((value) => ({ hubId: value.hubId })),
|
|
259
|
+
position: instance.position
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
function toHub(hub) {
|
|
263
|
+
return create2(HubSchema, {
|
|
264
|
+
id: hub.id,
|
|
265
|
+
position: hub.position ?? undefined,
|
|
266
|
+
inputs: hub.inputs ?? [],
|
|
267
|
+
injectionInputs: (hub.injectionInputs ?? []).map((value) => ({ hubId: value.hubId }))
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
function fromHub(hub) {
|
|
271
|
+
return {
|
|
272
|
+
id: hub.id,
|
|
273
|
+
position: hub.position,
|
|
274
|
+
inputs: hub.inputs.map(fromInstanceReference),
|
|
275
|
+
injectionInputs: hub.injectionInputs.map((value) => ({ hubId: value.hubId }))
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function toInstancePatch(instance, paths) {
|
|
279
|
+
const patch = {};
|
|
280
|
+
for (const path of paths) {
|
|
281
|
+
switch (path) {
|
|
282
|
+
case "arguments":
|
|
283
|
+
patch.args = Object.fromEntries(instance.arguments.map((argument) => [
|
|
284
|
+
argument.key,
|
|
285
|
+
argument.value ? toJson(ValueSchema, argument.value) : null
|
|
286
|
+
]));
|
|
287
|
+
break;
|
|
288
|
+
case "inputs":
|
|
289
|
+
patch.inputs = fromInstanceReferenceMap(instance.inputs);
|
|
290
|
+
break;
|
|
291
|
+
case "hub_inputs":
|
|
292
|
+
patch.hubInputs = Object.fromEntries(Object.entries(instance.hubInputs).map(([key, list]) => [
|
|
293
|
+
key,
|
|
294
|
+
list.values.map((value) => ({ hubId: value.hubId }))
|
|
295
|
+
]));
|
|
296
|
+
break;
|
|
297
|
+
case "injection_inputs":
|
|
298
|
+
patch.injectionInputs = instance.injectionInputs.map((value) => ({ hubId: value.hubId }));
|
|
299
|
+
break;
|
|
300
|
+
case "position":
|
|
301
|
+
patch.position = instance.position ? { x: instance.position.x, y: instance.position.y } : null;
|
|
302
|
+
break;
|
|
303
|
+
case "position.x":
|
|
304
|
+
patch.position = { x: instance.position?.x ?? 0 };
|
|
305
|
+
break;
|
|
306
|
+
case "position.y":
|
|
307
|
+
patch.position = { y: instance.position?.y ?? 0 };
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return patch;
|
|
312
|
+
}
|
|
313
|
+
function toHubPatch(hub, paths) {
|
|
314
|
+
const patch = {};
|
|
315
|
+
for (const path of paths) {
|
|
316
|
+
switch (path) {
|
|
317
|
+
case "position":
|
|
318
|
+
patch.position = hub.position ? { x: hub.position.x, y: hub.position.y } : null;
|
|
319
|
+
break;
|
|
320
|
+
case "position.x":
|
|
321
|
+
patch.position = { x: hub.position?.x ?? 0 };
|
|
322
|
+
break;
|
|
323
|
+
case "position.y":
|
|
324
|
+
patch.position = { y: hub.position?.y ?? 0 };
|
|
325
|
+
break;
|
|
326
|
+
case "inputs":
|
|
327
|
+
patch.inputs = hub.inputs.map(fromInstanceReference);
|
|
328
|
+
break;
|
|
329
|
+
case "injection_inputs":
|
|
330
|
+
patch.injectionInputs = hub.injectionInputs.map((value) => ({ hubId: value.hubId }));
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return patch;
|
|
335
|
+
}
|
|
336
|
+
function toInstanceState(state) {
|
|
337
|
+
const model = state.model ? instanceModelSchema.parse(state.model) : undefined;
|
|
338
|
+
return create2(InstanceStateSchema, {
|
|
339
|
+
id: state.id,
|
|
340
|
+
instanceId: state.instanceId,
|
|
341
|
+
status: toInstanceStatus(state.status),
|
|
342
|
+
source: toInstanceSource(state.source),
|
|
343
|
+
kind: toComponentKind(state.kind),
|
|
344
|
+
parentInstanceId: state.parentInstanceId ?? undefined,
|
|
345
|
+
evaluationState: state.evaluationState ? {
|
|
346
|
+
status: toEvaluationStatus(state.evaluationState.status),
|
|
347
|
+
message: state.evaluationState.message ?? undefined,
|
|
348
|
+
model: state.evaluationState.model ? toInstance(instanceModelSchema.parse(state.evaluationState.model)) : undefined,
|
|
349
|
+
evaluatedAt: toTimestamp(state.evaluationState.evaluatedAt)
|
|
350
|
+
} : undefined,
|
|
351
|
+
lastOperationState: state.lastOperationState ? {
|
|
352
|
+
operationId: state.lastOperationState.operationId,
|
|
353
|
+
stateId: state.lastOperationState.stateId,
|
|
354
|
+
status: toInstanceOperationStatus(state.lastOperationState.status),
|
|
355
|
+
currentResourceCount: state.lastOperationState.currentResourceCount ?? undefined,
|
|
356
|
+
totalResourceCount: state.lastOperationState.totalResourceCount ?? undefined,
|
|
357
|
+
model: toInstance(instanceModelSchema.parse(state.lastOperationState.model)),
|
|
358
|
+
startedAt: toNullableTimestamp(state.lastOperationState.startedAt),
|
|
359
|
+
finishedAt: toNullableTimestamp(state.lastOperationState.finishedAt)
|
|
360
|
+
} : undefined,
|
|
361
|
+
terminalIds: state.terminalIds ?? [],
|
|
362
|
+
pageIds: state.pageIds ?? [],
|
|
363
|
+
panelIds: state.panelIds ?? [],
|
|
364
|
+
secretNames: state.secretNames ?? [],
|
|
365
|
+
customStatuses: (state.customStatuses ?? []).map(toInstanceCustomStatus),
|
|
366
|
+
currentResourceCount: state.currentResourceCount ?? undefined,
|
|
367
|
+
hasResourceHooks: state.hasResourceHooks,
|
|
368
|
+
statusFields: state.statusFields ? fromJson(ValueSchema, jsonValueSchema.parse(state.statusFields)) : undefined,
|
|
369
|
+
model: model ? toInstance(model) : undefined
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
function toInstanceCustomStatus(status) {
|
|
373
|
+
const meta = instanceCustomStatusInputSchema.shape.meta.parse(status.meta);
|
|
374
|
+
return create2(InstanceCustomStatusSchema, {
|
|
375
|
+
name: status.name,
|
|
376
|
+
meta: {
|
|
377
|
+
title: meta.title ?? status.name,
|
|
378
|
+
description: meta.description,
|
|
379
|
+
icon: meta.icon,
|
|
380
|
+
iconColor: meta.iconColor
|
|
381
|
+
},
|
|
382
|
+
value: status.value,
|
|
383
|
+
message: status.message ?? undefined,
|
|
384
|
+
order: status.order,
|
|
385
|
+
serviceAccountId: status.serviceAccountId,
|
|
386
|
+
createdAt: toTimestamp(status.createdAt),
|
|
387
|
+
updatedAt: toTimestamp(status.updatedAt)
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
function toOperation(operation) {
|
|
391
|
+
const meta = operationMetaSchema.parse(operation.meta);
|
|
392
|
+
const options = operationOptionsSchema.partial().parse(operation.options);
|
|
393
|
+
const phases = operation.phases ? operationPhaseSchema.array().parse(operation.phases) : [];
|
|
394
|
+
return create2(OperationSchema, {
|
|
395
|
+
id: operation.id,
|
|
396
|
+
meta,
|
|
397
|
+
type: toOperationType(operation.type),
|
|
398
|
+
status: toOperationStatus(operation.status),
|
|
399
|
+
options: {
|
|
400
|
+
forceUpdateDependencies: options.forceUpdateDependencies ?? false,
|
|
401
|
+
ignoreChangedDependencies: options.ignoreChangedDependencies ?? false,
|
|
402
|
+
ignoreDependencies: options.ignoreDependencies ?? false,
|
|
403
|
+
forceUpdateChildren: options.forceUpdateChildren ?? false,
|
|
404
|
+
onlyDestroyGhosts: options.onlyDestroyGhosts ?? false,
|
|
405
|
+
firstDestroyGhosts: options.firstDestroyGhosts ?? false,
|
|
406
|
+
ignoreGhosts: options.ignoreGhosts ?? false,
|
|
407
|
+
destroyDependentInstances: options.destroyDependentInstances ?? false,
|
|
408
|
+
invokeDestroyTriggers: options.invokeDestroyTriggers ?? false,
|
|
409
|
+
deleteUnreachableResources: options.deleteUnreachableResources ?? false,
|
|
410
|
+
forceDeleteState: options.forceDeleteState ?? false,
|
|
411
|
+
allowPartialCompositeInstanceUpdate: options.allowPartialCompositeInstanceUpdate ?? false,
|
|
412
|
+
allowPartialCompositeInstanceDestruction: options.allowPartialCompositeInstanceDestruction ?? false,
|
|
413
|
+
refresh: options.refresh ?? false,
|
|
414
|
+
debug: options.debug ?? false
|
|
415
|
+
},
|
|
416
|
+
requestedInstanceIds: z.string().array().parse(operation.requestedInstanceIds),
|
|
417
|
+
phases: phases.map(toOperationPhase),
|
|
418
|
+
startedAt: toTimestamp(operation.startedAt),
|
|
419
|
+
updatedAt: toTimestamp(operation.updatedAt),
|
|
420
|
+
finishedAt: toNullableTimestamp(operation.finishedAt)
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
function toOperationPhase(phase) {
|
|
424
|
+
return create2(OperationPhaseSchema, {
|
|
425
|
+
type: toOperationPhaseType(phase.type),
|
|
426
|
+
instances: phase.instances
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
function toOperationLog(operationId, log) {
|
|
430
|
+
return create2(OperationLogSchema, {
|
|
431
|
+
id: log.id,
|
|
432
|
+
operationId,
|
|
433
|
+
stateId: log.stateId ?? undefined,
|
|
434
|
+
isSystem: log.isSystem ?? false,
|
|
435
|
+
content: log.content
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
function toLibrary(library) {
|
|
439
|
+
return create2(LibrarySchema, {
|
|
440
|
+
components: Object.fromEntries(Object.entries(library.components).map(([type, component]) => [type, toComponent(component)])),
|
|
441
|
+
entities: Object.fromEntries(Object.entries(library.entities).map(([type, entity]) => [type, toEntity(entity)]))
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
function toComponent(component) {
|
|
445
|
+
try {
|
|
446
|
+
return create2(ComponentSchema, {
|
|
447
|
+
type: component.type,
|
|
448
|
+
kind: toComponentKind(component.kind),
|
|
449
|
+
arguments: Object.fromEntries(Object.entries(component.args).map(([name, argument]) => [
|
|
450
|
+
name,
|
|
451
|
+
{
|
|
452
|
+
schema: jsonObjectSchema.parse(argument.schema),
|
|
453
|
+
required: argument.required,
|
|
454
|
+
meta: argument.meta
|
|
455
|
+
}
|
|
456
|
+
])),
|
|
457
|
+
inputs: Object.fromEntries(Object.entries(component.inputs).map(([name, port]) => [name, toComponentPort(port)])),
|
|
458
|
+
outputs: Object.fromEntries(Object.entries(component.outputs).map(([name, port]) => [name, toComponentPort(port)])),
|
|
459
|
+
meta: component.meta,
|
|
460
|
+
definitionHash: component.definitionHash
|
|
461
|
+
});
|
|
462
|
+
} catch (error) {
|
|
463
|
+
throw new Error(`Failed to convert component "${component.type}" to an API message`, {
|
|
464
|
+
cause: error
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function toEntity(entity) {
|
|
469
|
+
return create2(EntitySchema, {
|
|
470
|
+
type: entity.type,
|
|
471
|
+
extensions: entity.extensions ?? [],
|
|
472
|
+
directExtensions: entity.directExtensions ?? [],
|
|
473
|
+
inclusions: entity.inclusions ?? [],
|
|
474
|
+
directInclusions: entity.directInclusions ?? [],
|
|
475
|
+
meta: entity.meta,
|
|
476
|
+
definitionHash: entity.definitionHash
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
function toTimestamp(value) {
|
|
480
|
+
if (!Number.isFinite(value.getTime())) {
|
|
481
|
+
throw new Error("Cannot convert invalid date to timestamp");
|
|
482
|
+
}
|
|
483
|
+
return timestampFromDate(value);
|
|
484
|
+
}
|
|
485
|
+
function toNullableTimestamp(value) {
|
|
486
|
+
return value ? toTimestamp(value) : undefined;
|
|
487
|
+
}
|
|
488
|
+
function toComponentPort(port) {
|
|
489
|
+
return {
|
|
490
|
+
entityType: port.type,
|
|
491
|
+
fromInput: port.fromInput,
|
|
492
|
+
required: port.required,
|
|
493
|
+
multiple: port.multiple,
|
|
494
|
+
meta: port.meta
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
function toInstanceReferenceMap(values) {
|
|
498
|
+
return Object.fromEntries(Object.entries(values ?? {}).map(([key, references]) => [
|
|
499
|
+
key,
|
|
500
|
+
{
|
|
501
|
+
values: references
|
|
502
|
+
}
|
|
503
|
+
]));
|
|
504
|
+
}
|
|
505
|
+
function fromInstanceReferenceMap(values) {
|
|
506
|
+
return Object.fromEntries(Object.entries(values).map(([key, list]) => [key, list.values.map(fromInstanceReference)]));
|
|
507
|
+
}
|
|
508
|
+
function fromInstanceReference(value) {
|
|
509
|
+
return instanceInputSchema.parse({
|
|
510
|
+
instanceId: value.instanceId,
|
|
511
|
+
output: value.output,
|
|
512
|
+
path: value.path
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
function toComponentKind(value) {
|
|
516
|
+
if (value === "unit")
|
|
517
|
+
return ComponentKind.UNIT;
|
|
518
|
+
if (value === "composite")
|
|
519
|
+
return ComponentKind.COMPOSITE;
|
|
520
|
+
throw new Error(`Unknown component kind "${value}"`);
|
|
521
|
+
}
|
|
522
|
+
function fromComponentKind(value) {
|
|
523
|
+
if (value === ComponentKind.UNIT)
|
|
524
|
+
return "unit";
|
|
525
|
+
if (value === ComponentKind.COMPOSITE)
|
|
526
|
+
return "composite";
|
|
527
|
+
throw new Error("Component kind must be specified");
|
|
528
|
+
}
|
|
529
|
+
function toInstanceStatus(value) {
|
|
530
|
+
const statuses = {
|
|
531
|
+
undeployed: InstanceStatus.UNDEPLOYED,
|
|
532
|
+
attempted: InstanceStatus.ATTEMPTED,
|
|
533
|
+
deployed: InstanceStatus.DEPLOYED,
|
|
534
|
+
failed: InstanceStatus.FAILED
|
|
535
|
+
};
|
|
536
|
+
const status = statuses[value];
|
|
537
|
+
if (status === undefined)
|
|
538
|
+
throw new Error(`Unknown instance status "${value}"`);
|
|
539
|
+
return status;
|
|
540
|
+
}
|
|
541
|
+
function toInstanceSource(value) {
|
|
542
|
+
if (value === "resident")
|
|
543
|
+
return InstanceSource.RESIDENT;
|
|
544
|
+
if (value === "virtual")
|
|
545
|
+
return InstanceSource.VIRTUAL;
|
|
546
|
+
throw new Error(`Unknown instance source "${value}"`);
|
|
547
|
+
}
|
|
548
|
+
function toEvaluationStatus(value) {
|
|
549
|
+
if (value === "evaluating")
|
|
550
|
+
return EvaluationStatus.EVALUATING;
|
|
551
|
+
if (value === "evaluated")
|
|
552
|
+
return EvaluationStatus.EVALUATED;
|
|
553
|
+
if (value === "error")
|
|
554
|
+
return EvaluationStatus.ERROR;
|
|
555
|
+
throw new Error(`Unknown evaluation status "${value}"`);
|
|
556
|
+
}
|
|
557
|
+
function toInstanceOperationStatus(value) {
|
|
558
|
+
const statuses = {
|
|
559
|
+
updating: InstanceOperationStatus.UPDATING,
|
|
560
|
+
processing_triggers: InstanceOperationStatus.PROCESSING_TRIGGERS,
|
|
561
|
+
previewing: InstanceOperationStatus.PREVIEWING,
|
|
562
|
+
destroying: InstanceOperationStatus.DESTROYING,
|
|
563
|
+
refreshing: InstanceOperationStatus.REFRESHING,
|
|
564
|
+
pending: InstanceOperationStatus.PENDING,
|
|
565
|
+
cancelling: InstanceOperationStatus.CANCELLING,
|
|
566
|
+
updated: InstanceOperationStatus.UPDATED,
|
|
567
|
+
previewed: InstanceOperationStatus.PREVIEWED,
|
|
568
|
+
skipped: InstanceOperationStatus.SKIPPED,
|
|
569
|
+
destroyed: InstanceOperationStatus.DESTROYED,
|
|
570
|
+
refreshed: InstanceOperationStatus.REFRESHED,
|
|
571
|
+
cancelled: InstanceOperationStatus.CANCELLED,
|
|
572
|
+
failed: InstanceOperationStatus.FAILED
|
|
573
|
+
};
|
|
574
|
+
const status = statuses[value];
|
|
575
|
+
if (status === undefined)
|
|
576
|
+
throw new Error(`Unknown instance operation status "${value}"`);
|
|
577
|
+
return status;
|
|
578
|
+
}
|
|
579
|
+
function fromOperationType(value) {
|
|
580
|
+
if (value === OperationType.UPDATE)
|
|
581
|
+
return "update";
|
|
582
|
+
if (value === OperationType.PREVIEW)
|
|
583
|
+
return "preview";
|
|
584
|
+
if (value === OperationType.DESTROY)
|
|
585
|
+
return "destroy";
|
|
586
|
+
if (value === OperationType.RECREATE)
|
|
587
|
+
return "recreate";
|
|
588
|
+
if (value === OperationType.REFRESH)
|
|
589
|
+
return "refresh";
|
|
590
|
+
throw new Error("Operation type must be specified");
|
|
591
|
+
}
|
|
592
|
+
function fromOperationPhase(phase) {
|
|
593
|
+
return operationPhaseSchema.parse({
|
|
594
|
+
type: fromOperationPhaseType(phase.type),
|
|
595
|
+
instances: phase.instances.map((instance) => ({
|
|
596
|
+
id: instance.id,
|
|
597
|
+
parentId: instance.parentId,
|
|
598
|
+
message: instance.message
|
|
599
|
+
}))
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
function toOperationType(value) {
|
|
603
|
+
const types = {
|
|
604
|
+
update: OperationType.UPDATE,
|
|
605
|
+
preview: OperationType.PREVIEW,
|
|
606
|
+
destroy: OperationType.DESTROY,
|
|
607
|
+
recreate: OperationType.RECREATE,
|
|
608
|
+
refresh: OperationType.REFRESH
|
|
609
|
+
};
|
|
610
|
+
const type = types[value];
|
|
611
|
+
if (type === undefined)
|
|
612
|
+
throw new Error(`Unknown operation type "${value}"`);
|
|
613
|
+
return type;
|
|
614
|
+
}
|
|
615
|
+
function toOperationStatus(value) {
|
|
616
|
+
const statuses = {
|
|
617
|
+
pending: OperationStatus.PENDING,
|
|
618
|
+
running: OperationStatus.RUNNING,
|
|
619
|
+
failing: OperationStatus.FAILING,
|
|
620
|
+
cancelling: OperationStatus.CANCELLING,
|
|
621
|
+
completed: OperationStatus.COMPLETED,
|
|
622
|
+
failed: OperationStatus.FAILED,
|
|
623
|
+
cancelled: OperationStatus.CANCELLED
|
|
624
|
+
};
|
|
625
|
+
const status = statuses[value];
|
|
626
|
+
if (status === undefined)
|
|
627
|
+
throw new Error(`Unknown operation status "${value}"`);
|
|
628
|
+
return status;
|
|
629
|
+
}
|
|
630
|
+
function toOperationPhaseType(value) {
|
|
631
|
+
const types = {
|
|
632
|
+
destroy: OperationPhaseType.DESTROY,
|
|
633
|
+
preview: OperationPhaseType.PREVIEW,
|
|
634
|
+
update: OperationPhaseType.UPDATE,
|
|
635
|
+
refresh: OperationPhaseType.REFRESH
|
|
636
|
+
};
|
|
637
|
+
const type = types[value];
|
|
638
|
+
if (type === undefined)
|
|
639
|
+
throw new Error(`Unknown operation phase type "${value}"`);
|
|
640
|
+
return type;
|
|
641
|
+
}
|
|
642
|
+
function fromOperationPhaseType(value) {
|
|
643
|
+
const types = {
|
|
644
|
+
[OperationPhaseType.DESTROY]: "destroy",
|
|
645
|
+
[OperationPhaseType.PREVIEW]: "preview",
|
|
646
|
+
[OperationPhaseType.UPDATE]: "update",
|
|
647
|
+
[OperationPhaseType.REFRESH]: "refresh"
|
|
648
|
+
};
|
|
649
|
+
const type = types[value];
|
|
650
|
+
if (!type)
|
|
651
|
+
throw new Error("Operation phase type must be specified");
|
|
652
|
+
return type;
|
|
27
653
|
}
|
|
28
654
|
// src/shared/error-handling.ts
|
|
29
|
-
import {
|
|
655
|
+
import { create as create3 } from "@bufbuild/protobuf";
|
|
656
|
+
import { durationFromMs } from "@bufbuild/protobuf/wkt";
|
|
657
|
+
import { Code as Code3, ConnectError as ConnectError2 } from "@connectrpc/connect";
|
|
658
|
+
import {
|
|
659
|
+
BadRequestSchema as BadRequestSchema2,
|
|
660
|
+
ErrorInfoSchema as ErrorInfoSchema2,
|
|
661
|
+
PreconditionFailureSchema,
|
|
662
|
+
RequestInfoSchema,
|
|
663
|
+
RetryInfoSchema
|
|
664
|
+
} from "@highstate/api/v1";
|
|
665
|
+
import { BackendError, BackendErrorCategory } from "@highstate/backend/shared";
|
|
30
666
|
import { isAbortError } from "abort-controller-x";
|
|
31
|
-
|
|
32
|
-
function
|
|
33
|
-
return async
|
|
667
|
+
var sensitiveMetadataKey = /credential|token|secret|encrypted|cause|stack|path/i;
|
|
668
|
+
function createErrorHandlingInterceptor(services) {
|
|
669
|
+
return (next) => async (request) => {
|
|
34
670
|
try {
|
|
35
|
-
return
|
|
671
|
+
return await next(request);
|
|
36
672
|
} catch (error) {
|
|
37
|
-
if (error instanceof
|
|
673
|
+
if (isAbortError(error) || error instanceof ConnectError2 && error.code === Code3.Canceled) {
|
|
38
674
|
throw error;
|
|
39
675
|
}
|
|
40
|
-
if (error instanceof
|
|
41
|
-
|
|
42
|
-
|
|
676
|
+
if (error instanceof BackendError) {
|
|
677
|
+
const code = categoryToCode(error.category);
|
|
678
|
+
if (code === Code3.Internal) {
|
|
679
|
+
services.logger.error({ error, method: request.method.name }, "unexpected backend error");
|
|
680
|
+
throw new ConnectError2("An unexpected error occurred", Code3.Internal);
|
|
681
|
+
}
|
|
682
|
+
throw new ConnectError2(error.message, code, undefined, backendErrorDetails(error, request));
|
|
683
|
+
}
|
|
684
|
+
if (error instanceof ConnectError2) {
|
|
685
|
+
throw error;
|
|
43
686
|
}
|
|
44
|
-
services.logger.error({ error }, "unexpected error");
|
|
45
|
-
throw new
|
|
687
|
+
services.logger.error({ error, method: request.method.name }, "unexpected error");
|
|
688
|
+
throw new ConnectError2("An unexpected error occurred", Code3.Internal);
|
|
46
689
|
}
|
|
47
690
|
};
|
|
48
691
|
}
|
|
692
|
+
function backendErrorDetails(error, request) {
|
|
693
|
+
const details = [
|
|
694
|
+
{
|
|
695
|
+
desc: ErrorInfoSchema2,
|
|
696
|
+
value: create3(ErrorInfoSchema2, {
|
|
697
|
+
reason: error.reason,
|
|
698
|
+
domain: "highstate.io",
|
|
699
|
+
metadata: Object.fromEntries(Object.entries(error.metadata).filter(([key]) => !sensitiveMetadataKey.test(key)))
|
|
700
|
+
})
|
|
701
|
+
}
|
|
702
|
+
];
|
|
703
|
+
if (error.fieldViolations.length > 0) {
|
|
704
|
+
details.push({
|
|
705
|
+
desc: BadRequestSchema2,
|
|
706
|
+
value: create3(BadRequestSchema2, {
|
|
707
|
+
fieldViolations: error.fieldViolations.map((violation) => ({
|
|
708
|
+
field: toProtobufPath(violation.field),
|
|
709
|
+
reason: violation.reason,
|
|
710
|
+
description: violation.description
|
|
711
|
+
}))
|
|
712
|
+
})
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
if (error.preconditionViolations.length > 0) {
|
|
716
|
+
details.push({
|
|
717
|
+
desc: PreconditionFailureSchema,
|
|
718
|
+
value: create3(PreconditionFailureSchema, {
|
|
719
|
+
violations: error.preconditionViolations.map((violation) => ({ ...violation }))
|
|
720
|
+
})
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
if (error.retry && Number.isFinite(error.retry.delayMs) && error.retry.delayMs >= 0) {
|
|
724
|
+
details.push({
|
|
725
|
+
desc: RetryInfoSchema,
|
|
726
|
+
value: create3(RetryInfoSchema, { retryDelay: durationFromMs(error.retry.delayMs) })
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
const requestId = request.header.get("x-request-id")?.trim();
|
|
730
|
+
if (requestId) {
|
|
731
|
+
details.push({
|
|
732
|
+
desc: RequestInfoSchema,
|
|
733
|
+
value: create3(RequestInfoSchema, { requestId })
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
return details;
|
|
737
|
+
}
|
|
738
|
+
function toProtobufPath(path) {
|
|
739
|
+
return path.split(".").map((segment) => segment.replace(/[A-Z]/g, (character) => `_${character.toLowerCase()}`)).join(".");
|
|
740
|
+
}
|
|
741
|
+
function categoryToCode(category) {
|
|
742
|
+
switch (category) {
|
|
743
|
+
case BackendErrorCategory.InvalidArgument:
|
|
744
|
+
return Code3.InvalidArgument;
|
|
745
|
+
case BackendErrorCategory.Unauthenticated:
|
|
746
|
+
return Code3.Unauthenticated;
|
|
747
|
+
case BackendErrorCategory.PermissionDenied:
|
|
748
|
+
return Code3.PermissionDenied;
|
|
749
|
+
case BackendErrorCategory.NotFound:
|
|
750
|
+
return Code3.NotFound;
|
|
751
|
+
case BackendErrorCategory.AlreadyExists:
|
|
752
|
+
return Code3.AlreadyExists;
|
|
753
|
+
case BackendErrorCategory.FailedPrecondition:
|
|
754
|
+
return Code3.FailedPrecondition;
|
|
755
|
+
case BackendErrorCategory.Aborted:
|
|
756
|
+
return Code3.Aborted;
|
|
757
|
+
case BackendErrorCategory.Unavailable:
|
|
758
|
+
return Code3.Unavailable;
|
|
759
|
+
case BackendErrorCategory.Internal:
|
|
760
|
+
return Code3.Internal;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
// src/shared/field-mask.ts
|
|
764
|
+
import { Code as Code4 } from "@connectrpc/connect";
|
|
765
|
+
function validateUpdateMask(mask, schema, mutablePaths) {
|
|
766
|
+
if (!mask || mask.paths.length === 0) {
|
|
767
|
+
throw fieldMaskError("update_mask", "REQUIRED", "The update mask must not be empty");
|
|
768
|
+
}
|
|
769
|
+
const paths = new Set;
|
|
770
|
+
for (const path of mask.paths) {
|
|
771
|
+
const normalized = normalizePath(schema, path);
|
|
772
|
+
if (!mutablePaths.has(normalized)) {
|
|
773
|
+
throw fieldMaskError(`update_mask.paths`, "IMMUTABLE_OR_UNKNOWN", `The path "${path}" is unknown, immutable, output-only, or traverses a collection`);
|
|
774
|
+
}
|
|
775
|
+
paths.add(normalized);
|
|
776
|
+
}
|
|
777
|
+
return [...paths];
|
|
778
|
+
}
|
|
779
|
+
function normalizePath(schema, path) {
|
|
780
|
+
if (!path || path === "*")
|
|
781
|
+
return path;
|
|
782
|
+
let descriptor = schema;
|
|
783
|
+
const normalized = [];
|
|
784
|
+
const segments = path.split(".");
|
|
785
|
+
for (const [index, segment] of segments.entries()) {
|
|
786
|
+
const field = descriptor.fields.find((candidate) => candidate.name === segment || candidate.localName === segment || candidate.jsonName === segment);
|
|
787
|
+
if (!field)
|
|
788
|
+
return path;
|
|
789
|
+
normalized.push(field.name);
|
|
790
|
+
if (index === segments.length - 1)
|
|
791
|
+
break;
|
|
792
|
+
if (field.fieldKind !== "message" || !field.message)
|
|
793
|
+
return path;
|
|
794
|
+
descriptor = field.message;
|
|
795
|
+
}
|
|
796
|
+
return normalized.join(".");
|
|
797
|
+
}
|
|
798
|
+
function fieldMaskError(field, reason, description) {
|
|
799
|
+
return createApiError({
|
|
800
|
+
message: "Invalid update mask",
|
|
801
|
+
code: Code4.InvalidArgument,
|
|
802
|
+
reason: "UPDATE_MASK_INVALID",
|
|
803
|
+
fieldViolations: [{ field, reason, description }]
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
// src/shared/serialization.ts
|
|
807
|
+
function toJsonObject(value) {
|
|
808
|
+
return normalizeJsonValue(value);
|
|
809
|
+
}
|
|
810
|
+
function normalizeJsonValue(value) {
|
|
811
|
+
if (value instanceof Date) {
|
|
812
|
+
return value.toISOString();
|
|
813
|
+
}
|
|
814
|
+
if (Array.isArray(value)) {
|
|
815
|
+
return value.map(normalizeJsonValue);
|
|
816
|
+
}
|
|
817
|
+
if (value && typeof value === "object") {
|
|
818
|
+
return Object.fromEntries(Object.entries(value).flatMap(([key, entry]) => {
|
|
819
|
+
if (entry === undefined) {
|
|
820
|
+
return [];
|
|
821
|
+
}
|
|
822
|
+
return [[key, normalizeJsonValue(entry)]];
|
|
823
|
+
}));
|
|
824
|
+
}
|
|
825
|
+
return value;
|
|
826
|
+
}
|
|
49
827
|
// src/shared/validation.ts
|
|
50
|
-
import {
|
|
828
|
+
import { Code as Code5 } from "@connectrpc/connect";
|
|
51
829
|
function parseArgument(request, argumentName, schema) {
|
|
52
830
|
const result = schema.safeParse(request[argumentName]);
|
|
53
831
|
if (!result.success) {
|
|
54
|
-
throw
|
|
832
|
+
throw validationError(`Invalid argument "${argumentName}"`, argumentName, result.error);
|
|
55
833
|
}
|
|
56
834
|
return result.data;
|
|
57
835
|
}
|
|
58
|
-
|
|
59
|
-
|
|
836
|
+
function parseValue(value, name, schema) {
|
|
837
|
+
const result = schema.safeParse(value);
|
|
838
|
+
if (!result.success) {
|
|
839
|
+
throw validationError(`Invalid ${name}`, name, result.error);
|
|
840
|
+
}
|
|
841
|
+
return result.data;
|
|
842
|
+
}
|
|
843
|
+
function validationError(message, field, error) {
|
|
844
|
+
return createApiError({
|
|
845
|
+
message,
|
|
846
|
+
code: Code5.InvalidArgument,
|
|
847
|
+
reason: "REQUEST_INVALID",
|
|
848
|
+
fieldViolations: error.issues.map((issue) => ({
|
|
849
|
+
field: [field, ...issue.path.map(String)].join("."),
|
|
850
|
+
reason: issue.code.toUpperCase(),
|
|
851
|
+
description: issue.message
|
|
852
|
+
}))
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
// src/handlers/instance-state.ts
|
|
856
|
+
function createInstanceStateService(services) {
|
|
60
857
|
return {
|
|
858
|
+
async listInstanceStates(request, context) {
|
|
859
|
+
const requestContext = await authenticateProject(services, request, context);
|
|
860
|
+
const page = await services.instanceStateService.getInstanceStates(requestContext, {
|
|
861
|
+
includeEvaluationState: request.includeEvaluationState,
|
|
862
|
+
includeLastOperationState: request.includeLastOperationState,
|
|
863
|
+
includeParentInstanceId: request.includeParentInstanceId,
|
|
864
|
+
includeExtra: request.includeExtra,
|
|
865
|
+
loadCustomStatuses: request.includeCustomStatuses
|
|
866
|
+
}, { pageSize: request.pageSize, pageToken: request.pageToken });
|
|
867
|
+
return { states: page.items.map(toInstanceState), nextPageToken: page.nextPageToken };
|
|
868
|
+
},
|
|
869
|
+
async getInstanceState(request, context) {
|
|
870
|
+
const requestContext = await authenticateProject(services, request, context);
|
|
871
|
+
const stateId = parseArgument(request, "stateId", z2.cuid2());
|
|
872
|
+
const state = await services.instanceStateService.getInstanceStateOrThrow(requestContext, stateId, {
|
|
873
|
+
includeEvaluationState: request.includeEvaluationState,
|
|
874
|
+
includeLastOperationState: request.includeLastOperationState,
|
|
875
|
+
includeParentInstanceId: request.includeParentInstanceId,
|
|
876
|
+
includeExtra: request.includeExtra,
|
|
877
|
+
loadCustomStatuses: request.includeCustomStatuses
|
|
878
|
+
});
|
|
879
|
+
return { state: toInstanceState(state) };
|
|
880
|
+
},
|
|
61
881
|
async updateCustomStatus(request, context) {
|
|
62
|
-
const
|
|
63
|
-
const stateId = parseArgument(request, "stateId",
|
|
64
|
-
const customStatus = parseArgument(request, "status",
|
|
65
|
-
await services.instanceStateService.updateCustomStatus(
|
|
66
|
-
|
|
882
|
+
const requestContext = await authenticateProject(services, request, context);
|
|
883
|
+
const stateId = parseArgument(request, "stateId", z2.cuid2());
|
|
884
|
+
const customStatus = parseArgument(request, "status", instanceCustomStatusInputSchema2);
|
|
885
|
+
await services.instanceStateService.updateCustomStatus(requestContext, stateId, requestContext.subject.serviceAccountId, customStatus);
|
|
886
|
+
const state = await services.instanceStateService.getInstanceStateOrThrow(requestContext, stateId, { loadCustomStatuses: true });
|
|
887
|
+
const status = state.customStatuses?.find((value) => value.name === customStatus.name && value.serviceAccountId === requestContext.subject.serviceAccountId);
|
|
888
|
+
if (!status) {
|
|
889
|
+
throw new Error("Updated custom status was not returned by the backend");
|
|
890
|
+
}
|
|
891
|
+
return { status: toInstanceCustomStatus(status) };
|
|
67
892
|
},
|
|
68
893
|
async removeCustomStatus(request, context) {
|
|
69
|
-
const
|
|
70
|
-
const stateId = parseArgument(request, "stateId",
|
|
71
|
-
|
|
894
|
+
const requestContext = await authenticateProject(services, request, context);
|
|
895
|
+
const stateId = parseArgument(request, "stateId", z2.cuid2());
|
|
896
|
+
const statusName = parseArgument(request, "statusName", z2.string().min(1));
|
|
897
|
+
await services.instanceStateService.removeCustomStatus(requestContext, stateId, requestContext.subject.serviceAccountId, statusName);
|
|
898
|
+
return {};
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// src/handlers/library.ts
|
|
904
|
+
import { ComponentKind as ComponentKind2 } from "@highstate/api/v1";
|
|
905
|
+
import { ComponentNotFoundError } from "@highstate/backend/shared";
|
|
906
|
+
import { z as z3 } from "@highstate/contract";
|
|
907
|
+
function createLibraryService(services) {
|
|
908
|
+
return {
|
|
909
|
+
async getLibrary(request, context) {
|
|
910
|
+
const requestContext = await authenticateProject(services, request, context);
|
|
911
|
+
const library = await services.libraryService.getLibraryModel(requestContext, context.signal);
|
|
912
|
+
return {
|
|
913
|
+
library: toLibrary({
|
|
914
|
+
components: { ...library.components },
|
|
915
|
+
entities: { ...library.entities }
|
|
916
|
+
})
|
|
917
|
+
};
|
|
918
|
+
},
|
|
919
|
+
async listComponents(request, context) {
|
|
920
|
+
const requestContext = await authenticateProject(services, request, context);
|
|
921
|
+
const page = await services.libraryService.getComponents(requestContext, { pageSize: request.pageSize, pageToken: request.pageToken }, context.signal);
|
|
922
|
+
return {
|
|
923
|
+
components: page.items.map((component) => ({
|
|
924
|
+
type: component.type,
|
|
925
|
+
kind: component.kind === "unit" ? ComponentKind2.UNIT : ComponentKind2.COMPOSITE,
|
|
926
|
+
meta: {
|
|
927
|
+
title: component.meta.title,
|
|
928
|
+
description: component.meta.description,
|
|
929
|
+
color: component.meta.color,
|
|
930
|
+
icon: component.meta.icon,
|
|
931
|
+
iconColor: component.meta.iconColor,
|
|
932
|
+
secondaryIcon: component.meta.secondaryIcon,
|
|
933
|
+
secondaryIconColor: component.meta.secondaryIconColor,
|
|
934
|
+
category: component.meta.category,
|
|
935
|
+
defaultNamePrefix: component.meta.defaultNamePrefix
|
|
936
|
+
}
|
|
937
|
+
})),
|
|
938
|
+
nextPageToken: page.nextPageToken
|
|
939
|
+
};
|
|
940
|
+
},
|
|
941
|
+
async getComponent(request, context) {
|
|
942
|
+
const requestContext = await authenticateProject(services, request, context);
|
|
943
|
+
const type = parseArgument(request, "type", z3.string().min(1));
|
|
944
|
+
const library = await services.libraryService.getLibraryModel(requestContext, context.signal);
|
|
945
|
+
const component = library.components[type];
|
|
946
|
+
if (!component) {
|
|
947
|
+
throw new ComponentNotFoundError(requestContext.projectId, type);
|
|
948
|
+
}
|
|
949
|
+
return { component: toComponent(component) };
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// src/handlers/operation.ts
|
|
955
|
+
import { operationLaunchInputSchema, operationPlanInputSchema } from "@highstate/backend/shared";
|
|
956
|
+
import { instanceIdSchema, z as z4 } from "@highstate/contract";
|
|
957
|
+
function createOperationService(services) {
|
|
958
|
+
return {
|
|
959
|
+
async planOperation(request, context) {
|
|
960
|
+
const requestContext = await authenticateProject(services, request, context);
|
|
961
|
+
const input = parseValue({ ...request, projectId: requestContext.projectId, type: fromOperationType(request.type) }, "request", operationPlanInputSchema);
|
|
962
|
+
const phases = await services.operationManager.plan(input);
|
|
963
|
+
return { phases: phases.map(toOperationPhase) };
|
|
964
|
+
},
|
|
965
|
+
async launchOperation(request, context) {
|
|
966
|
+
const requestContext = await authenticateProject(services, request, context);
|
|
967
|
+
const input = parseValue({
|
|
968
|
+
...request,
|
|
969
|
+
projectId: requestContext.projectId,
|
|
970
|
+
type: fromOperationType(request.type),
|
|
971
|
+
plan: request.plan.length > 0 ? request.plan.map(fromOperationPhase) : undefined
|
|
972
|
+
}, "request", operationLaunchInputSchema);
|
|
973
|
+
const operation = await services.operationManager.launch(input);
|
|
974
|
+
return { operation: toOperation(operation) };
|
|
975
|
+
},
|
|
976
|
+
async getOperation(request, context) {
|
|
977
|
+
const requestContext = await authenticateProject(services, request, context);
|
|
978
|
+
const operationId = parseValue(request.operationId, "operationId", z4.string().min(1));
|
|
979
|
+
const operation = await services.operationService.getOperationOrThrow(requestContext, operationId);
|
|
980
|
+
return { operation: toOperation(operation) };
|
|
981
|
+
},
|
|
982
|
+
async listOperations(request, context) {
|
|
983
|
+
const requestContext = await authenticateProject(services, request, context);
|
|
984
|
+
const page = await services.operationService.getOperations(requestContext, {
|
|
985
|
+
pageSize: request.pageSize,
|
|
986
|
+
pageToken: request.pageToken
|
|
987
|
+
});
|
|
988
|
+
return { operations: page.items.map(toOperation), nextPageToken: page.nextPageToken };
|
|
989
|
+
},
|
|
990
|
+
async listOperationLogs(request, context) {
|
|
991
|
+
const requestContext = await authenticateProject(services, request, context);
|
|
992
|
+
const operationId = parseValue(request.operationId, "operationId", z4.string().min(1));
|
|
993
|
+
const stateId = parseValue(request.stateId, "stateId", z4.cuid2().optional());
|
|
994
|
+
await services.operationService.getOperationOrThrow(requestContext, operationId);
|
|
995
|
+
const page = await services.operationService.getOperationLogs(requestContext, operationId, stateId, { pageSize: request.pageSize, pageToken: request.pageToken });
|
|
996
|
+
return {
|
|
997
|
+
logs: page.items.map((log) => toOperationLog(operationId, log)),
|
|
998
|
+
nextPageToken: page.nextPageToken
|
|
999
|
+
};
|
|
1000
|
+
},
|
|
1001
|
+
async cancelOperation(request, context) {
|
|
1002
|
+
const requestContext = await authenticateProject(services, request, context);
|
|
1003
|
+
const operationId = parseValue(request.operationId, "operationId", z4.string().min(1));
|
|
1004
|
+
await services.operationService.getOperationOrThrow(requestContext, operationId);
|
|
1005
|
+
services.operationManager.cancel(operationId);
|
|
1006
|
+
return {};
|
|
1007
|
+
},
|
|
1008
|
+
async cancelInstanceOperation(request, context) {
|
|
1009
|
+
const requestContext = await authenticateProject(services, request, context);
|
|
1010
|
+
const operationId = parseValue(request.operationId, "operationId", z4.string().min(1));
|
|
1011
|
+
const instanceId = parseValue(request.instanceId, "instanceId", instanceIdSchema);
|
|
1012
|
+
await services.operationService.getOperationOrThrow(requestContext, operationId);
|
|
1013
|
+
services.operationManager.cancelInstance(operationId, instanceId);
|
|
72
1014
|
return {};
|
|
73
1015
|
}
|
|
74
1016
|
};
|
|
@@ -76,54 +1018,166 @@ function createInstanceService(services) {
|
|
|
76
1018
|
|
|
77
1019
|
// src/handlers/panel.ts
|
|
78
1020
|
import { panelInputSchema } from "@highstate/backend/shared";
|
|
79
|
-
import { z as
|
|
1021
|
+
import { z as z5 } from "@highstate/contract";
|
|
80
1022
|
function createPanelService(services) {
|
|
81
1023
|
return {
|
|
82
1024
|
async setUnitPanels(request, context) {
|
|
83
|
-
const
|
|
84
|
-
const workerVersionId = parseArgument(request, "workerVersionId",
|
|
85
|
-
const workerInstanceId = parseArgument(request, "workerInstanceId",
|
|
86
|
-
const stateId = parseArgument(request, "stateId",
|
|
87
|
-
const panels = parseArgument(request, "panels",
|
|
88
|
-
const panelIds = await services.panelService.setUnitPanels(
|
|
1025
|
+
const requestContext = await authenticateProject(services, request, context);
|
|
1026
|
+
const workerVersionId = parseArgument(request, "workerVersionId", z5.cuid2());
|
|
1027
|
+
const workerInstanceId = parseArgument(request, "workerInstanceId", z5.cuid2());
|
|
1028
|
+
const stateId = parseArgument(request, "stateId", z5.cuid2());
|
|
1029
|
+
const panels = parseArgument(request, "panels", z5.array(panelInputSchema));
|
|
1030
|
+
const panelIds = await services.panelService.setUnitPanels(requestContext, stateId, requestContext.subject.apiKeyId, workerVersionId, panels, workerInstanceId);
|
|
89
1031
|
return { panelIds };
|
|
90
1032
|
}
|
|
91
1033
|
};
|
|
92
1034
|
}
|
|
93
1035
|
|
|
1036
|
+
// src/handlers/project.ts
|
|
1037
|
+
function createProjectService(services) {
|
|
1038
|
+
return {
|
|
1039
|
+
async getProject(request, context) {
|
|
1040
|
+
const requestContext = await authenticateProject(services, request, context);
|
|
1041
|
+
const [project, unlockState] = await Promise.all([
|
|
1042
|
+
services.projectService.getProjectOrThrowCore(requestContext.projectId),
|
|
1043
|
+
services.projectUnlockService.getProjectUnlockStateCore(requestContext.projectId)
|
|
1044
|
+
]);
|
|
1045
|
+
return {
|
|
1046
|
+
project: toProject(project),
|
|
1047
|
+
isLocked: unlockState.type === "locked"
|
|
1048
|
+
};
|
|
1049
|
+
},
|
|
1050
|
+
async listProjects(request, context) {
|
|
1051
|
+
const requestContext = await authenticateBackend(services, context);
|
|
1052
|
+
const result = await services.projectService.getProjects(requestContext, {
|
|
1053
|
+
pageSize: request.pageSize,
|
|
1054
|
+
pageToken: request.pageToken
|
|
1055
|
+
});
|
|
1056
|
+
const projects = await Promise.all(result.items.map(async (project) => {
|
|
1057
|
+
const unlockState = await services.projectUnlockService.getProjectUnlockStateCore(project.id);
|
|
1058
|
+
return {
|
|
1059
|
+
project: toProject(project),
|
|
1060
|
+
isLocked: unlockState.type === "locked"
|
|
1061
|
+
};
|
|
1062
|
+
}));
|
|
1063
|
+
return {
|
|
1064
|
+
projects,
|
|
1065
|
+
nextPageToken: result.nextPageToken
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// src/handlers/project-model.ts
|
|
1072
|
+
import {
|
|
1073
|
+
HubSchema as HubSchema2,
|
|
1074
|
+
InstanceSchema as InstanceSchema2
|
|
1075
|
+
} from "@highstate/api/v1";
|
|
1076
|
+
import { projectModelInstanceSchema } from "@highstate/backend/shared";
|
|
1077
|
+
import { hubModelSchema, instanceIdSchema as instanceIdSchema2, z as z6 } from "@highstate/contract";
|
|
1078
|
+
var instanceMutablePaths = new Set([
|
|
1079
|
+
"arguments",
|
|
1080
|
+
"inputs",
|
|
1081
|
+
"hub_inputs",
|
|
1082
|
+
"injection_inputs",
|
|
1083
|
+
"position",
|
|
1084
|
+
"position.x",
|
|
1085
|
+
"position.y"
|
|
1086
|
+
]);
|
|
1087
|
+
var hubMutablePaths = new Set([
|
|
1088
|
+
"position",
|
|
1089
|
+
"position.x",
|
|
1090
|
+
"position.y",
|
|
1091
|
+
"inputs",
|
|
1092
|
+
"injection_inputs"
|
|
1093
|
+
]);
|
|
1094
|
+
function createProjectModelService(services) {
|
|
1095
|
+
return {
|
|
1096
|
+
async getProjectModel(request, context) {
|
|
1097
|
+
const requestContext = await authenticateProject(services, request, context);
|
|
1098
|
+
const [model] = await services.projectModelService.getProjectModel(requestContext, {
|
|
1099
|
+
includeVirtualInstances: request.includeVirtualInstances,
|
|
1100
|
+
includeGhostInstances: request.includeGhostInstances
|
|
1101
|
+
});
|
|
1102
|
+
return { model: toProjectModel(model) };
|
|
1103
|
+
},
|
|
1104
|
+
async createNodes(request, context) {
|
|
1105
|
+
const requestContext = await authenticateProject(services, request, context);
|
|
1106
|
+
const instances = parseValue(request.instances.map(fromInstance), "instances", projectModelInstanceSchema.array());
|
|
1107
|
+
const hubs = parseValue(request.hubs.map(fromHub), "hubs", hubModelSchema.array());
|
|
1108
|
+
await services.projectService.createNodes(requestContext, instances, hubs);
|
|
1109
|
+
return {};
|
|
1110
|
+
},
|
|
1111
|
+
async updateInstance(request, context) {
|
|
1112
|
+
const requestContext = await authenticateProject(services, request, context);
|
|
1113
|
+
const requestInstance = parseArgument(request, "instance", z6.custom());
|
|
1114
|
+
const instanceId = parseArgument(requestInstance, "id", instanceIdSchema2);
|
|
1115
|
+
const paths = validateUpdateMask(request.updateMask, InstanceSchema2, instanceMutablePaths);
|
|
1116
|
+
const patch = toInstancePatch(requestInstance, paths);
|
|
1117
|
+
const instance = await services.projectService.updateInstance(requestContext, instanceId, patch);
|
|
1118
|
+
return { instance: toInstance(instance) };
|
|
1119
|
+
},
|
|
1120
|
+
async renameInstance(request, context) {
|
|
1121
|
+
const requestContext = await authenticateProject(services, request, context);
|
|
1122
|
+
const instanceId = parseArgument(request, "instanceId", instanceIdSchema2);
|
|
1123
|
+
const newName = parseArgument(request, "newName", z6.string().min(1));
|
|
1124
|
+
const instance = await services.projectService.renameInstance(requestContext, instanceId, newName);
|
|
1125
|
+
return { instance: toInstance(instance) };
|
|
1126
|
+
},
|
|
1127
|
+
async deleteInstance(request, context) {
|
|
1128
|
+
const requestContext = await authenticateProject(services, request, context);
|
|
1129
|
+
const instanceId = parseArgument(request, "instanceId", instanceIdSchema2);
|
|
1130
|
+
await services.projectService.deleteInstance(requestContext, instanceId);
|
|
1131
|
+
return {};
|
|
1132
|
+
},
|
|
1133
|
+
async updateHub(request, context) {
|
|
1134
|
+
const requestContext = await authenticateProject(services, request, context);
|
|
1135
|
+
const requestHub = parseArgument(request, "hub", z6.custom());
|
|
1136
|
+
const hubId = parseArgument(requestHub, "id", z6.cuid2());
|
|
1137
|
+
const paths = validateUpdateMask(request.updateMask, HubSchema2, hubMutablePaths);
|
|
1138
|
+
const patch = toHubPatch(requestHub, paths);
|
|
1139
|
+
const hub = await services.projectService.updateHub(requestContext, hubId, patch);
|
|
1140
|
+
return { hub: toHub(hub) };
|
|
1141
|
+
},
|
|
1142
|
+
async deleteHub(request, context) {
|
|
1143
|
+
const requestContext = await authenticateProject(services, request, context);
|
|
1144
|
+
const hubId = parseArgument(request, "hubId", z6.cuid2());
|
|
1145
|
+
await services.projectService.deleteHub(requestContext, hubId);
|
|
1146
|
+
return {};
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
|
|
94
1151
|
// src/handlers/secret.ts
|
|
95
1152
|
function createSecretService(services) {
|
|
96
1153
|
return {
|
|
97
|
-
async getSecretContent(
|
|
98
|
-
|
|
1154
|
+
async getSecretContent(request, context) {
|
|
1155
|
+
await authenticateProject(services, request, context);
|
|
99
1156
|
throw new Error("Not implemented");
|
|
100
1157
|
}
|
|
101
1158
|
};
|
|
102
1159
|
}
|
|
103
1160
|
|
|
104
1161
|
// src/handlers/worker.ts
|
|
105
|
-
import {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
workerMeta: commonObjectMetaSchema,
|
|
110
|
-
serviceAccountMeta: serviceAccountMetaSchema
|
|
111
|
-
})
|
|
112
|
-
]);
|
|
1162
|
+
import { create as create4 } from "@bufbuild/protobuf";
|
|
1163
|
+
import { ConnectResponseSchema } from "@highstate/api/worker.v1";
|
|
1164
|
+
import { WorkerOwnershipError } from "@highstate/backend/shared";
|
|
1165
|
+
import { commonObjectMetaSchema, serviceAccountMetaSchema, z as z7 } from "@highstate/contract";
|
|
113
1166
|
function createWorkerService(services) {
|
|
114
1167
|
return {
|
|
115
1168
|
async* connect(request, context) {
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
const
|
|
1169
|
+
const requestContext = await authenticateProject(services, request, context);
|
|
1170
|
+
const { projectId } = requestContext;
|
|
1171
|
+
const workerVersionId = parseArgument(request, "workerVersionId", z7.cuid2());
|
|
1172
|
+
const workerInstanceId = parseArgument(request, "workerInstanceId", z7.cuid2());
|
|
1173
|
+
const dataEndpoint = parseArgument(request, "dataEndpoint", z7.string().min(1));
|
|
120
1174
|
const database = await services.database.forProject(projectId);
|
|
121
1175
|
const workerVersion = await database.workerVersion.findFirst({
|
|
122
|
-
where: { id: workerVersionId, apiKeyId:
|
|
1176
|
+
where: { id: workerVersionId, apiKeyId: requestContext.subject.apiKeyId },
|
|
123
1177
|
select: { id: true }
|
|
124
1178
|
});
|
|
125
1179
|
if (!workerVersion) {
|
|
126
|
-
throw new
|
|
1180
|
+
throw new WorkerOwnershipError(projectId, workerVersionId);
|
|
127
1181
|
}
|
|
128
1182
|
services.workerManager.assertWorkerInstance(projectId, workerVersionId, workerInstanceId);
|
|
129
1183
|
services.panelEndpointManager.connect(projectId, workerVersionId, workerInstanceId, dataEndpoint);
|
|
@@ -134,15 +1188,15 @@ function createWorkerService(services) {
|
|
|
134
1188
|
select: { stateId: true, params: true }
|
|
135
1189
|
});
|
|
136
1190
|
for (const registration of existingRegistrations) {
|
|
137
|
-
yield {
|
|
1191
|
+
yield create4(ConnectResponseSchema, {
|
|
138
1192
|
event: {
|
|
139
|
-
|
|
1193
|
+
case: "unitRegistration",
|
|
140
1194
|
value: {
|
|
141
1195
|
stateId: registration.stateId,
|
|
142
|
-
params: registration.params
|
|
1196
|
+
params: toJsonObject(registration.params)
|
|
143
1197
|
}
|
|
144
1198
|
}
|
|
145
|
-
};
|
|
1199
|
+
});
|
|
146
1200
|
}
|
|
147
1201
|
const registrationStream = await services.pubsubManager.subscribe([
|
|
148
1202
|
"worker-unit-registration",
|
|
@@ -151,19 +1205,19 @@ function createWorkerService(services) {
|
|
|
151
1205
|
]);
|
|
152
1206
|
for await (const event of registrationStream) {
|
|
153
1207
|
if (event.type === "registered") {
|
|
154
|
-
yield {
|
|
1208
|
+
yield create4(ConnectResponseSchema, {
|
|
155
1209
|
event: {
|
|
156
|
-
|
|
157
|
-
value: { stateId: event.stateId, params: event.params }
|
|
1210
|
+
case: "unitRegistration",
|
|
1211
|
+
value: { stateId: event.stateId, params: toJsonObject(event.params) }
|
|
158
1212
|
}
|
|
159
|
-
};
|
|
1213
|
+
});
|
|
160
1214
|
} else if (event.type === "deregistered") {
|
|
161
|
-
yield {
|
|
1215
|
+
yield create4(ConnectResponseSchema, {
|
|
162
1216
|
event: {
|
|
163
|
-
|
|
1217
|
+
case: "unitDeregistration",
|
|
164
1218
|
value: { stateId: event.stateId }
|
|
165
1219
|
}
|
|
166
|
-
};
|
|
1220
|
+
});
|
|
167
1221
|
}
|
|
168
1222
|
}
|
|
169
1223
|
} finally {
|
|
@@ -171,38 +1225,93 @@ function createWorkerService(services) {
|
|
|
171
1225
|
}
|
|
172
1226
|
},
|
|
173
1227
|
async updateWorkerVersionMeta(request, context) {
|
|
174
|
-
const
|
|
175
|
-
const workerVersionId = parseArgument(request, "workerVersionId",
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
await services.workerService.updateWorkerVersionMeta(projectId, workerVersionId, workerMeta, serviceAccountMeta);
|
|
1228
|
+
const requestContext = await authenticateProject(services, request, context);
|
|
1229
|
+
const workerVersionId = parseArgument(request, "workerVersionId", z7.string());
|
|
1230
|
+
const workerMeta = parseArgument(request, "workerMeta", commonObjectMetaSchema);
|
|
1231
|
+
const serviceAccountMeta = parseArgument(request, "serviceAccountMeta", serviceAccountMetaSchema.optional());
|
|
1232
|
+
await services.workerService.updateWorkerVersionMeta(requestContext, workerVersionId, workerMeta, serviceAccountMeta);
|
|
180
1233
|
return {};
|
|
181
1234
|
}
|
|
182
1235
|
};
|
|
183
1236
|
}
|
|
184
1237
|
|
|
185
1238
|
// src/index.ts
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
1239
|
+
function createBackendApiHandler(services, options = {}) {
|
|
1240
|
+
return connectNodeAdapter({
|
|
1241
|
+
interceptors: [createErrorHandlingInterceptor(services)],
|
|
1242
|
+
requestPathPrefix: options.requestPathPrefix,
|
|
1243
|
+
routes(router) {
|
|
1244
|
+
router.service(InstanceStateService, createInstanceStateService(services));
|
|
1245
|
+
router.service(LibraryService, createLibraryService(services));
|
|
1246
|
+
router.service(OperationService, createOperationService(services));
|
|
1247
|
+
router.service(PanelService, createPanelService(services));
|
|
1248
|
+
router.service(ProjectModelService, createProjectModelService(services));
|
|
1249
|
+
router.service(ProjectService, createProjectService(services));
|
|
1250
|
+
router.service(SecretService, createSecretService(services));
|
|
1251
|
+
router.service(WorkerService, createWorkerService(services));
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
async function startBackendApi(services, options = {}) {
|
|
193
1256
|
const uid = process.geteuid?.();
|
|
194
|
-
const
|
|
1257
|
+
const workerSocketPath = options.workerSocketPath ?? `/run/user/${uid}/highstate.sock`;
|
|
1258
|
+
const workerAddress = `unix:${workerSocketPath}`;
|
|
1259
|
+
const address = options.address ?? workerAddress;
|
|
1260
|
+
const addresses = address === workerAddress ? [workerAddress] : [workerAddress, address];
|
|
1261
|
+
const handler = createBackendApiHandler(services);
|
|
1262
|
+
const servers = addresses.map(() => createServer(handler));
|
|
195
1263
|
try {
|
|
196
|
-
await rm(
|
|
1264
|
+
await rm(workerSocketPath, { force: true });
|
|
197
1265
|
} catch (error) {
|
|
198
1266
|
if (!(error instanceof Error && ("code" in error) && error.code === "ENOENT")) {
|
|
199
1267
|
services.logger.error({ error }, "failed to remove existing socket file");
|
|
200
1268
|
}
|
|
201
1269
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
1270
|
+
try {
|
|
1271
|
+
for (const [index, server] of servers.entries()) {
|
|
1272
|
+
const serverAddress = addresses[index];
|
|
1273
|
+
await listen(server, serverAddress);
|
|
1274
|
+
services.logger.info(`api listening at "%s"`, serverAddress);
|
|
1275
|
+
}
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
await Promise.allSettled(servers.map(close));
|
|
1278
|
+
await rm(workerSocketPath, { force: true });
|
|
1279
|
+
throw new Error("Failed to start backend api", { cause: error });
|
|
1280
|
+
}
|
|
1281
|
+
services.workerManager.config.HIGHSTATE_WORKER_API_PATH = workerSocketPath;
|
|
1282
|
+
return {
|
|
1283
|
+
address,
|
|
1284
|
+
async shutdown() {
|
|
1285
|
+
await Promise.all(servers.map(close));
|
|
1286
|
+
await rm(workerSocketPath, { force: true });
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
async function listen(server, address) {
|
|
1291
|
+
await new Promise((resolve, reject) => {
|
|
1292
|
+
server.once("error", reject);
|
|
1293
|
+
server.once("listening", resolve);
|
|
1294
|
+
if (address.startsWith("unix:")) {
|
|
1295
|
+
server.listen(address.slice("unix:".length));
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
const url = new URL(address.includes("://") ? address : `http://${address}`);
|
|
1299
|
+
server.listen(Number(url.port), url.hostname);
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
async function close(server) {
|
|
1303
|
+
await new Promise((resolve, reject) => {
|
|
1304
|
+
server.close((error) => {
|
|
1305
|
+
if (error) {
|
|
1306
|
+
reject(error);
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
resolve();
|
|
1310
|
+
});
|
|
1311
|
+
server.closeAllConnections();
|
|
1312
|
+
});
|
|
205
1313
|
}
|
|
206
1314
|
export {
|
|
207
|
-
|
|
1315
|
+
startBackendApi,
|
|
1316
|
+
createBackendApiHandler
|
|
208
1317
|
};
|