@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@highstate/backend-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "highstate build",
|
|
26
|
+
"test": "bun run --bun vitest run",
|
|
26
27
|
"typecheck": "tsgo --noEmit --skipLibCheck",
|
|
27
28
|
"biome": "biome check --write --unsafe --error-on-warnings",
|
|
28
29
|
"biome:check": "biome check --error-on-warnings"
|
|
@@ -40,19 +41,20 @@
|
|
|
40
41
|
}
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
43
|
-
"@bufbuild/protobuf": "^2.
|
|
44
|
-
"@
|
|
45
|
-
"@
|
|
46
|
-
"@highstate/
|
|
47
|
-
"@highstate/
|
|
44
|
+
"@bufbuild/protobuf": "^2.14.0",
|
|
45
|
+
"@connectrpc/connect": "^2.1.2",
|
|
46
|
+
"@connectrpc/connect-node": "^2.1.2",
|
|
47
|
+
"@highstate/api": "0.27.0",
|
|
48
|
+
"@highstate/backend": "0.27.0",
|
|
49
|
+
"@highstate/cli": "0.27.0",
|
|
50
|
+
"@highstate/contract": "0.27.0",
|
|
48
51
|
"abort-controller-x": "^0.4.3",
|
|
49
|
-
"nice-grpc": "^2.1.12",
|
|
50
|
-
"nice-grpc-common": "^2.0.2",
|
|
51
52
|
"zod": "^4.0.5"
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
|
54
|
-
"@biomejs/biome": "2.
|
|
55
|
-
"@typescript/native-preview": "^7.0.0-dev.20250920.1"
|
|
55
|
+
"@biomejs/biome": "2.5.0",
|
|
56
|
+
"@typescript/native-preview": "^7.0.0-dev.20250920.1",
|
|
57
|
+
"vitest": "^3.2.4"
|
|
56
58
|
},
|
|
57
59
|
"repository": {
|
|
58
60
|
"url": "https://github.com/highstate-io/highstate"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { ServiceImpl } from "@connectrpc/connect"
|
|
2
|
+
import type { InstanceStateService } from "@highstate/api/v1"
|
|
3
|
+
import type { Services } from "@highstate/backend"
|
|
4
|
+
import { instanceCustomStatusInputSchema } from "@highstate/backend/shared"
|
|
5
|
+
import { z } from "@highstate/contract"
|
|
6
|
+
import {
|
|
7
|
+
authenticateProject,
|
|
8
|
+
parseArgument,
|
|
9
|
+
toInstanceCustomStatus,
|
|
10
|
+
toInstanceState,
|
|
11
|
+
} from "../shared"
|
|
12
|
+
|
|
13
|
+
export function createInstanceStateService(
|
|
14
|
+
services: Services,
|
|
15
|
+
): ServiceImpl<typeof InstanceStateService> {
|
|
16
|
+
return {
|
|
17
|
+
async listInstanceStates(request, context) {
|
|
18
|
+
const requestContext = await authenticateProject(services, request, context)
|
|
19
|
+
const page = await services.instanceStateService.getInstanceStates(
|
|
20
|
+
requestContext,
|
|
21
|
+
{
|
|
22
|
+
includeEvaluationState: request.includeEvaluationState,
|
|
23
|
+
includeLastOperationState: request.includeLastOperationState,
|
|
24
|
+
includeParentInstanceId: request.includeParentInstanceId,
|
|
25
|
+
includeExtra: request.includeExtra,
|
|
26
|
+
loadCustomStatuses: request.includeCustomStatuses,
|
|
27
|
+
},
|
|
28
|
+
{ pageSize: request.pageSize, pageToken: request.pageToken },
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return { states: page.items.map(toInstanceState), nextPageToken: page.nextPageToken }
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
async getInstanceState(request, context) {
|
|
35
|
+
const requestContext = await authenticateProject(services, request, context)
|
|
36
|
+
const stateId = parseArgument(request, "stateId", z.cuid2())
|
|
37
|
+
const state = await services.instanceStateService.getInstanceStateOrThrow(
|
|
38
|
+
requestContext,
|
|
39
|
+
stateId,
|
|
40
|
+
{
|
|
41
|
+
includeEvaluationState: request.includeEvaluationState,
|
|
42
|
+
includeLastOperationState: request.includeLastOperationState,
|
|
43
|
+
includeParentInstanceId: request.includeParentInstanceId,
|
|
44
|
+
includeExtra: request.includeExtra,
|
|
45
|
+
loadCustomStatuses: request.includeCustomStatuses,
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return { state: toInstanceState(state) }
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
async updateCustomStatus(request, context) {
|
|
53
|
+
const requestContext = await authenticateProject(services, request, context)
|
|
54
|
+
const stateId = parseArgument(request, "stateId", z.cuid2())
|
|
55
|
+
const customStatus = parseArgument(request, "status", instanceCustomStatusInputSchema)
|
|
56
|
+
|
|
57
|
+
await services.instanceStateService.updateCustomStatus(
|
|
58
|
+
requestContext,
|
|
59
|
+
stateId,
|
|
60
|
+
requestContext.subject.serviceAccountId,
|
|
61
|
+
customStatus,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
const state = await services.instanceStateService.getInstanceStateOrThrow(
|
|
65
|
+
requestContext,
|
|
66
|
+
stateId,
|
|
67
|
+
{ loadCustomStatuses: true },
|
|
68
|
+
)
|
|
69
|
+
const status = state.customStatuses?.find(
|
|
70
|
+
value =>
|
|
71
|
+
value.name === customStatus.name &&
|
|
72
|
+
value.serviceAccountId === requestContext.subject.serviceAccountId,
|
|
73
|
+
)
|
|
74
|
+
if (!status) {
|
|
75
|
+
throw new Error("Updated custom status was not returned by the backend")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { status: toInstanceCustomStatus(status) }
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
async removeCustomStatus(request, context) {
|
|
82
|
+
const requestContext = await authenticateProject(services, request, context)
|
|
83
|
+
const stateId = parseArgument(request, "stateId", z.cuid2())
|
|
84
|
+
const statusName = parseArgument(request, "statusName", z.string().min(1))
|
|
85
|
+
|
|
86
|
+
await services.instanceStateService.removeCustomStatus(
|
|
87
|
+
requestContext,
|
|
88
|
+
stateId,
|
|
89
|
+
requestContext.subject.serviceAccountId,
|
|
90
|
+
statusName,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return {}
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ServiceImpl } from "@connectrpc/connect"
|
|
2
|
+
import type { Services } from "@highstate/backend"
|
|
3
|
+
import { ComponentKind, type LibraryService } from "@highstate/api/v1"
|
|
4
|
+
import { ComponentNotFoundError } from "@highstate/backend/shared"
|
|
5
|
+
import { z } from "@highstate/contract"
|
|
6
|
+
import { authenticateProject, parseArgument, toComponent, toLibrary } from "../shared"
|
|
7
|
+
|
|
8
|
+
export function createLibraryService(services: Services): ServiceImpl<typeof LibraryService> {
|
|
9
|
+
return {
|
|
10
|
+
async getLibrary(request, context) {
|
|
11
|
+
const requestContext = await authenticateProject(services, request, context)
|
|
12
|
+
const library = await services.libraryService.getLibraryModel(requestContext, context.signal)
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
library: toLibrary({
|
|
16
|
+
components: { ...library.components },
|
|
17
|
+
entities: { ...library.entities },
|
|
18
|
+
}),
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
async listComponents(request, context) {
|
|
23
|
+
const requestContext = await authenticateProject(services, request, context)
|
|
24
|
+
const page = await services.libraryService.getComponents(
|
|
25
|
+
requestContext,
|
|
26
|
+
{ pageSize: request.pageSize, pageToken: request.pageToken },
|
|
27
|
+
context.signal,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
components: page.items.map(component => ({
|
|
32
|
+
type: component.type,
|
|
33
|
+
kind: component.kind === "unit" ? ComponentKind.UNIT : ComponentKind.COMPOSITE,
|
|
34
|
+
meta: {
|
|
35
|
+
title: component.meta.title,
|
|
36
|
+
description: component.meta.description,
|
|
37
|
+
color: component.meta.color,
|
|
38
|
+
icon: component.meta.icon,
|
|
39
|
+
iconColor: component.meta.iconColor,
|
|
40
|
+
secondaryIcon: component.meta.secondaryIcon,
|
|
41
|
+
secondaryIconColor: component.meta.secondaryIconColor,
|
|
42
|
+
category: component.meta.category,
|
|
43
|
+
defaultNamePrefix: component.meta.defaultNamePrefix,
|
|
44
|
+
},
|
|
45
|
+
})),
|
|
46
|
+
nextPageToken: page.nextPageToken,
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
async getComponent(request, context) {
|
|
51
|
+
const requestContext = await authenticateProject(services, request, context)
|
|
52
|
+
const type = parseArgument(request, "type", z.string().min(1))
|
|
53
|
+
const library = await services.libraryService.getLibraryModel(requestContext, context.signal)
|
|
54
|
+
const component = library.components[type]
|
|
55
|
+
|
|
56
|
+
if (!component) {
|
|
57
|
+
throw new ComponentNotFoundError(requestContext.projectId, type)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { component: toComponent(component) }
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { ServiceImpl } from "@connectrpc/connect"
|
|
2
|
+
import type { OperationService } from "@highstate/api/v1"
|
|
3
|
+
import type { Services } from "@highstate/backend"
|
|
4
|
+
import { operationLaunchInputSchema, operationPlanInputSchema } from "@highstate/backend/shared"
|
|
5
|
+
import { instanceIdSchema, z } from "@highstate/contract"
|
|
6
|
+
import {
|
|
7
|
+
authenticateProject,
|
|
8
|
+
fromOperationPhase,
|
|
9
|
+
fromOperationType,
|
|
10
|
+
parseValue,
|
|
11
|
+
toOperation,
|
|
12
|
+
toOperationLog,
|
|
13
|
+
toOperationPhase,
|
|
14
|
+
} from "../shared"
|
|
15
|
+
|
|
16
|
+
export function createOperationService(services: Services): ServiceImpl<typeof OperationService> {
|
|
17
|
+
return {
|
|
18
|
+
async planOperation(request, context) {
|
|
19
|
+
const requestContext = await authenticateProject(services, request, context)
|
|
20
|
+
const input = parseValue(
|
|
21
|
+
{ ...request, projectId: requestContext.projectId, type: fromOperationType(request.type) },
|
|
22
|
+
"request",
|
|
23
|
+
operationPlanInputSchema,
|
|
24
|
+
)
|
|
25
|
+
const phases = await services.operationManager.plan(input)
|
|
26
|
+
|
|
27
|
+
return { phases: phases.map(toOperationPhase) }
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
async launchOperation(request, context) {
|
|
31
|
+
const requestContext = await authenticateProject(services, request, context)
|
|
32
|
+
const input = parseValue(
|
|
33
|
+
{
|
|
34
|
+
...request,
|
|
35
|
+
projectId: requestContext.projectId,
|
|
36
|
+
type: fromOperationType(request.type),
|
|
37
|
+
plan: request.plan.length > 0 ? request.plan.map(fromOperationPhase) : undefined,
|
|
38
|
+
},
|
|
39
|
+
"request",
|
|
40
|
+
operationLaunchInputSchema,
|
|
41
|
+
)
|
|
42
|
+
const operation = await services.operationManager.launch(input)
|
|
43
|
+
|
|
44
|
+
return { operation: toOperation(operation) }
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
async getOperation(request, context) {
|
|
48
|
+
const requestContext = await authenticateProject(services, request, context)
|
|
49
|
+
const operationId = parseValue(request.operationId, "operationId", z.string().min(1))
|
|
50
|
+
const operation = await services.operationService.getOperationOrThrow(
|
|
51
|
+
requestContext,
|
|
52
|
+
operationId,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return { operation: toOperation(operation) }
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
async listOperations(request, context) {
|
|
59
|
+
const requestContext = await authenticateProject(services, request, context)
|
|
60
|
+
const page = await services.operationService.getOperations(requestContext, {
|
|
61
|
+
pageSize: request.pageSize,
|
|
62
|
+
pageToken: request.pageToken,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
return { operations: page.items.map(toOperation), nextPageToken: page.nextPageToken }
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
async listOperationLogs(request, context) {
|
|
69
|
+
const requestContext = await authenticateProject(services, request, context)
|
|
70
|
+
const operationId = parseValue(request.operationId, "operationId", z.string().min(1))
|
|
71
|
+
const stateId = parseValue(request.stateId, "stateId", z.cuid2().optional())
|
|
72
|
+
await services.operationService.getOperationOrThrow(requestContext, operationId)
|
|
73
|
+
|
|
74
|
+
const page = await services.operationService.getOperationLogs(
|
|
75
|
+
requestContext,
|
|
76
|
+
operationId,
|
|
77
|
+
stateId,
|
|
78
|
+
{ pageSize: request.pageSize, pageToken: request.pageToken },
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
logs: page.items.map(log => toOperationLog(operationId, log)),
|
|
83
|
+
nextPageToken: page.nextPageToken,
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
async cancelOperation(request, context) {
|
|
88
|
+
const requestContext = await authenticateProject(services, request, context)
|
|
89
|
+
const operationId = parseValue(request.operationId, "operationId", z.string().min(1))
|
|
90
|
+
await services.operationService.getOperationOrThrow(requestContext, operationId)
|
|
91
|
+
|
|
92
|
+
services.operationManager.cancel(operationId)
|
|
93
|
+
|
|
94
|
+
return {}
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
async cancelInstanceOperation(request, context) {
|
|
98
|
+
const requestContext = await authenticateProject(services, request, context)
|
|
99
|
+
const operationId = parseValue(request.operationId, "operationId", z.string().min(1))
|
|
100
|
+
const instanceId = parseValue(request.instanceId, "instanceId", instanceIdSchema)
|
|
101
|
+
await services.operationService.getOperationOrThrow(requestContext, operationId)
|
|
102
|
+
|
|
103
|
+
services.operationManager.cancelInstance(operationId, instanceId)
|
|
104
|
+
|
|
105
|
+
return {}
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
}
|
package/src/handlers/panel.ts
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ServiceImpl } from "@connectrpc/connect"
|
|
2
|
+
import type { PanelService } from "@highstate/api/v1"
|
|
2
3
|
import type { Services } from "@highstate/backend"
|
|
3
4
|
import { panelInputSchema } from "@highstate/backend/shared"
|
|
4
5
|
import { z } from "@highstate/contract"
|
|
5
|
-
import {
|
|
6
|
+
import { authenticateProject, parseArgument } from "../shared"
|
|
6
7
|
|
|
7
|
-
export function createPanelService(services: Services):
|
|
8
|
+
export function createPanelService(services: Services): ServiceImpl<typeof PanelService> {
|
|
8
9
|
return {
|
|
9
10
|
async setUnitPanels(request, context) {
|
|
10
|
-
const
|
|
11
|
+
const requestContext = await authenticateProject(services, request, context)
|
|
11
12
|
const workerVersionId = parseArgument(request, "workerVersionId", z.cuid2())
|
|
12
13
|
const workerInstanceId = parseArgument(request, "workerInstanceId", z.cuid2())
|
|
13
14
|
const stateId = parseArgument(request, "stateId", z.cuid2())
|
|
14
15
|
const panels = parseArgument(request, "panels", z.array(panelInputSchema))
|
|
15
16
|
|
|
16
17
|
const panelIds = await services.panelService.setUnitPanels(
|
|
17
|
-
|
|
18
|
+
requestContext,
|
|
18
19
|
stateId,
|
|
19
|
-
|
|
20
|
+
requestContext.subject.apiKeyId,
|
|
20
21
|
workerVersionId,
|
|
21
22
|
panels,
|
|
22
23
|
workerInstanceId,
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { ServiceImpl } from "@connectrpc/connect"
|
|
2
|
+
import type { Services } from "@highstate/backend"
|
|
3
|
+
import {
|
|
4
|
+
HubSchema,
|
|
5
|
+
InstanceSchema,
|
|
6
|
+
type ProjectModelService,
|
|
7
|
+
type UpdateHubRequest,
|
|
8
|
+
type UpdateInstanceRequest,
|
|
9
|
+
} from "@highstate/api/v1"
|
|
10
|
+
import { projectModelInstanceSchema } from "@highstate/backend/shared"
|
|
11
|
+
import { hubModelSchema, instanceIdSchema, z } from "@highstate/contract"
|
|
12
|
+
import {
|
|
13
|
+
authenticateProject,
|
|
14
|
+
fromHub,
|
|
15
|
+
fromInstance,
|
|
16
|
+
parseArgument,
|
|
17
|
+
parseValue,
|
|
18
|
+
toHub,
|
|
19
|
+
toHubPatch,
|
|
20
|
+
toInstance,
|
|
21
|
+
toInstancePatch,
|
|
22
|
+
toProjectModel,
|
|
23
|
+
validateUpdateMask,
|
|
24
|
+
} from "../shared"
|
|
25
|
+
|
|
26
|
+
type UpdateInstance = NonNullable<UpdateInstanceRequest["instance"]>
|
|
27
|
+
type UpdateHub = NonNullable<UpdateHubRequest["hub"]>
|
|
28
|
+
|
|
29
|
+
const instanceMutablePaths = new Set([
|
|
30
|
+
"arguments",
|
|
31
|
+
"inputs",
|
|
32
|
+
"hub_inputs",
|
|
33
|
+
"injection_inputs",
|
|
34
|
+
"position",
|
|
35
|
+
"position.x",
|
|
36
|
+
"position.y",
|
|
37
|
+
])
|
|
38
|
+
const hubMutablePaths = new Set([
|
|
39
|
+
"position",
|
|
40
|
+
"position.x",
|
|
41
|
+
"position.y",
|
|
42
|
+
"inputs",
|
|
43
|
+
"injection_inputs",
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
export function createProjectModelService(
|
|
47
|
+
services: Services,
|
|
48
|
+
): ServiceImpl<typeof ProjectModelService> {
|
|
49
|
+
return {
|
|
50
|
+
async getProjectModel(request, context) {
|
|
51
|
+
const requestContext = await authenticateProject(services, request, context)
|
|
52
|
+
const [model] = await services.projectModelService.getProjectModel(requestContext, {
|
|
53
|
+
includeVirtualInstances: request.includeVirtualInstances,
|
|
54
|
+
includeGhostInstances: request.includeGhostInstances,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
return { model: toProjectModel(model) }
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
async createNodes(request, context) {
|
|
61
|
+
const requestContext = await authenticateProject(services, request, context)
|
|
62
|
+
const instances = parseValue(
|
|
63
|
+
request.instances.map(fromInstance),
|
|
64
|
+
"instances",
|
|
65
|
+
projectModelInstanceSchema.array(),
|
|
66
|
+
)
|
|
67
|
+
const hubs = parseValue(request.hubs.map(fromHub), "hubs", hubModelSchema.array())
|
|
68
|
+
|
|
69
|
+
await services.projectService.createNodes(requestContext, instances, hubs)
|
|
70
|
+
|
|
71
|
+
return {}
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async updateInstance(request, context) {
|
|
75
|
+
const requestContext = await authenticateProject(services, request, context)
|
|
76
|
+
const requestInstance = parseArgument(request, "instance", z.custom<UpdateInstance>())
|
|
77
|
+
const instanceId = parseArgument(requestInstance, "id", instanceIdSchema)
|
|
78
|
+
const paths = validateUpdateMask(request.updateMask, InstanceSchema, instanceMutablePaths)
|
|
79
|
+
const patch = toInstancePatch(requestInstance, paths)
|
|
80
|
+
const instance = await services.projectService.updateInstance(
|
|
81
|
+
requestContext,
|
|
82
|
+
instanceId,
|
|
83
|
+
patch,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return { instance: toInstance(instance) }
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
async renameInstance(request, context) {
|
|
90
|
+
const requestContext = await authenticateProject(services, request, context)
|
|
91
|
+
const instanceId = parseArgument(request, "instanceId", instanceIdSchema)
|
|
92
|
+
const newName = parseArgument(request, "newName", z.string().min(1))
|
|
93
|
+
const instance = await services.projectService.renameInstance(
|
|
94
|
+
requestContext,
|
|
95
|
+
instanceId,
|
|
96
|
+
newName,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return { instance: toInstance(instance) }
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
async deleteInstance(request, context) {
|
|
103
|
+
const requestContext = await authenticateProject(services, request, context)
|
|
104
|
+
const instanceId = parseArgument(request, "instanceId", instanceIdSchema)
|
|
105
|
+
|
|
106
|
+
await services.projectService.deleteInstance(requestContext, instanceId)
|
|
107
|
+
|
|
108
|
+
return {}
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
async updateHub(request, context) {
|
|
112
|
+
const requestContext = await authenticateProject(services, request, context)
|
|
113
|
+
const requestHub = parseArgument(request, "hub", z.custom<UpdateHub>())
|
|
114
|
+
const hubId = parseArgument(requestHub, "id", z.cuid2())
|
|
115
|
+
const paths = validateUpdateMask(request.updateMask, HubSchema, hubMutablePaths)
|
|
116
|
+
const patch = toHubPatch(requestHub, paths)
|
|
117
|
+
const hub = await services.projectService.updateHub(requestContext, hubId, patch)
|
|
118
|
+
|
|
119
|
+
return { hub: toHub(hub) }
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
async deleteHub(request, context) {
|
|
123
|
+
const requestContext = await authenticateProject(services, request, context)
|
|
124
|
+
const hubId = parseArgument(request, "hubId", z.cuid2())
|
|
125
|
+
|
|
126
|
+
await services.projectService.deleteHub(requestContext, hubId)
|
|
127
|
+
|
|
128
|
+
return {}
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ServiceImpl } from "@connectrpc/connect"
|
|
2
|
+
import type { ProjectService } from "@highstate/api/v1"
|
|
3
|
+
import type { Services } from "@highstate/backend"
|
|
4
|
+
import { authenticateBackend, authenticateProject, toProject } from "../shared"
|
|
5
|
+
|
|
6
|
+
export function createProjectService(services: Services): ServiceImpl<typeof ProjectService> {
|
|
7
|
+
return {
|
|
8
|
+
async getProject(request, context) {
|
|
9
|
+
const requestContext = await authenticateProject(services, request, context)
|
|
10
|
+
const [project, unlockState] = await Promise.all([
|
|
11
|
+
services.projectService.getProjectOrThrowCore(requestContext.projectId),
|
|
12
|
+
services.projectUnlockService.getProjectUnlockStateCore(requestContext.projectId),
|
|
13
|
+
])
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
project: toProject(project),
|
|
17
|
+
isLocked: unlockState.type === "locked",
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
async listProjects(request, context) {
|
|
22
|
+
const requestContext = await authenticateBackend(services, context)
|
|
23
|
+
const result = await services.projectService.getProjects(requestContext, {
|
|
24
|
+
pageSize: request.pageSize,
|
|
25
|
+
pageToken: request.pageToken,
|
|
26
|
+
})
|
|
27
|
+
const projects = await Promise.all(
|
|
28
|
+
result.items.map(async project => {
|
|
29
|
+
const unlockState = await services.projectUnlockService.getProjectUnlockStateCore(
|
|
30
|
+
project.id,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
project: toProject(project),
|
|
35
|
+
isLocked: unlockState.type === "locked",
|
|
36
|
+
}
|
|
37
|
+
}),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
projects,
|
|
42
|
+
nextPageToken: result.nextPageToken,
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/handlers/secret.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ServiceImpl } from "@connectrpc/connect"
|
|
2
|
+
import type { SecretService } from "@highstate/api/v1"
|
|
2
3
|
import type { Services } from "@highstate/backend"
|
|
3
|
-
import {
|
|
4
|
+
import { authenticateProject } from "../shared"
|
|
4
5
|
|
|
5
|
-
export function createSecretService(services: Services):
|
|
6
|
+
export function createSecretService(services: Services): ServiceImpl<typeof SecretService> {
|
|
6
7
|
return {
|
|
7
|
-
async getSecretContent(
|
|
8
|
-
|
|
8
|
+
async getSecretContent(request, context) {
|
|
9
|
+
await authenticateProject(services, request, context)
|
|
9
10
|
|
|
10
11
|
// TODO: validate secret access
|
|
11
12
|
|
package/src/handlers/worker.ts
CHANGED
|
@@ -1,31 +1,27 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ServiceImpl } from "@connectrpc/connect"
|
|
2
2
|
import type { Services } from "@highstate/backend"
|
|
3
|
+
import { create } from "@bufbuild/protobuf"
|
|
4
|
+
import { ConnectResponseSchema, type WorkerService } from "@highstate/api/worker.v1"
|
|
5
|
+
import { WorkerOwnershipError } from "@highstate/backend/shared"
|
|
3
6
|
import { commonObjectMetaSchema, serviceAccountMetaSchema, z } from "@highstate/contract"
|
|
4
|
-
import {
|
|
7
|
+
import { authenticateProject, parseArgument, toJsonObject } from "../shared"
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
commonObjectMetaSchema,
|
|
8
|
-
z.object({
|
|
9
|
-
workerMeta: commonObjectMetaSchema,
|
|
10
|
-
serviceAccountMeta: serviceAccountMetaSchema,
|
|
11
|
-
}),
|
|
12
|
-
])
|
|
13
|
-
|
|
14
|
-
export function createWorkerService(services: Services): WorkerServiceImplementation {
|
|
9
|
+
export function createWorkerService(services: Services): ServiceImpl<typeof WorkerService> {
|
|
15
10
|
return {
|
|
16
11
|
async *connect(request, context) {
|
|
17
|
-
const
|
|
12
|
+
const requestContext = await authenticateProject(services, request, context)
|
|
13
|
+
const { projectId } = requestContext
|
|
18
14
|
|
|
19
15
|
const workerVersionId = parseArgument(request, "workerVersionId", z.cuid2())
|
|
20
16
|
const workerInstanceId = parseArgument(request, "workerInstanceId", z.cuid2())
|
|
21
17
|
const dataEndpoint = parseArgument(request, "dataEndpoint", z.string().min(1))
|
|
22
18
|
const database = await services.database.forProject(projectId)
|
|
23
19
|
const workerVersion = await database.workerVersion.findFirst({
|
|
24
|
-
where: { id: workerVersionId, apiKeyId:
|
|
20
|
+
where: { id: workerVersionId, apiKeyId: requestContext.subject.apiKeyId },
|
|
25
21
|
select: { id: true },
|
|
26
22
|
})
|
|
27
23
|
if (!workerVersion) {
|
|
28
|
-
throw new
|
|
24
|
+
throw new WorkerOwnershipError(projectId, workerVersionId)
|
|
29
25
|
}
|
|
30
26
|
|
|
31
27
|
services.workerManager.assertWorkerInstance(projectId, workerVersionId, workerInstanceId)
|
|
@@ -43,15 +39,15 @@ export function createWorkerService(services: Services): WorkerServiceImplementa
|
|
|
43
39
|
select: { stateId: true, params: true },
|
|
44
40
|
})
|
|
45
41
|
for (const registration of existingRegistrations) {
|
|
46
|
-
yield {
|
|
42
|
+
yield create(ConnectResponseSchema, {
|
|
47
43
|
event: {
|
|
48
|
-
|
|
44
|
+
case: "unitRegistration",
|
|
49
45
|
value: {
|
|
50
46
|
stateId: registration.stateId,
|
|
51
|
-
params: registration.params,
|
|
47
|
+
params: toJsonObject(registration.params),
|
|
52
48
|
},
|
|
53
49
|
},
|
|
54
|
-
}
|
|
50
|
+
})
|
|
55
51
|
}
|
|
56
52
|
const registrationStream = await services.pubsubManager.subscribe([
|
|
57
53
|
"worker-unit-registration",
|
|
@@ -60,19 +56,19 @@ export function createWorkerService(services: Services): WorkerServiceImplementa
|
|
|
60
56
|
])
|
|
61
57
|
for await (const event of registrationStream) {
|
|
62
58
|
if (event.type === "registered") {
|
|
63
|
-
yield {
|
|
59
|
+
yield create(ConnectResponseSchema, {
|
|
64
60
|
event: {
|
|
65
|
-
|
|
66
|
-
value: { stateId: event.stateId, params: event.params },
|
|
61
|
+
case: "unitRegistration",
|
|
62
|
+
value: { stateId: event.stateId, params: toJsonObject(event.params) },
|
|
67
63
|
},
|
|
68
|
-
}
|
|
64
|
+
})
|
|
69
65
|
} else if (event.type === "deregistered") {
|
|
70
|
-
yield {
|
|
66
|
+
yield create(ConnectResponseSchema, {
|
|
71
67
|
event: {
|
|
72
|
-
|
|
68
|
+
case: "unitDeregistration",
|
|
73
69
|
value: { stateId: event.stateId },
|
|
74
70
|
},
|
|
75
|
-
}
|
|
71
|
+
})
|
|
76
72
|
}
|
|
77
73
|
}
|
|
78
74
|
} finally {
|
|
@@ -81,17 +77,18 @@ export function createWorkerService(services: Services): WorkerServiceImplementa
|
|
|
81
77
|
},
|
|
82
78
|
|
|
83
79
|
async updateWorkerVersionMeta(request, context) {
|
|
84
|
-
const
|
|
80
|
+
const requestContext = await authenticateProject(services, request, context)
|
|
85
81
|
|
|
86
82
|
const workerVersionId = parseArgument(request, "workerVersionId", z.string())
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
83
|
+
const workerMeta = parseArgument(request, "workerMeta", commonObjectMetaSchema)
|
|
84
|
+
const serviceAccountMeta = parseArgument(
|
|
85
|
+
request,
|
|
86
|
+
"serviceAccountMeta",
|
|
87
|
+
serviceAccountMetaSchema.optional(),
|
|
88
|
+
)
|
|
92
89
|
|
|
93
90
|
await services.workerService.updateWorkerVersionMeta(
|
|
94
|
-
|
|
91
|
+
requestContext,
|
|
95
92
|
workerVersionId,
|
|
96
93
|
workerMeta,
|
|
97
94
|
serviceAccountMeta,
|