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