@highstate/backend 0.18.0 → 0.20.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/{chunk-JT4KWE3B.js → chunk-52MY2TCE.js} +348 -19
- package/dist/chunk-52MY2TCE.js.map +1 -0
- package/dist/{chunk-I7BWSAN6.js → chunk-UAWBPTDW.js} +3 -3
- package/dist/{chunk-I7BWSAN6.js.map → chunk-UAWBPTDW.js.map} +1 -1
- package/dist/highstate.manifest.json +4 -4
- package/dist/index.js +4159 -785
- package/dist/index.js.map +1 -1
- package/dist/library/worker/main.js +5 -2
- package/dist/library/worker/main.js.map +1 -1
- package/dist/shared/index.js +2 -2
- package/package.json +7 -7
- package/prisma/backend/_schema/object.prisma +12 -0
- package/prisma/backend/sqlite/migrations/20260222113554_add_object_tracking/migration.sql +7 -0
- package/prisma/project/artifact.prisma +3 -0
- package/prisma/project/entity.prisma +125 -0
- package/prisma/project/instance.prisma +6 -0
- package/prisma/project/migrations/20260301210131_add_entity_tracking/migration.sql +70 -0
- package/prisma/project/migrations/20260302212734_add_resource_hooks_flag/migration.sql +1 -0
- package/prisma/project/operation.prisma +3 -0
- package/src/business/artifact.test.ts +22 -2
- package/src/business/artifact.ts +7 -1
- package/src/business/entity-snapshot.test.ts +684 -0
- package/src/business/entity-snapshot.ts +904 -0
- package/src/business/evaluation.test.ts +56 -0
- package/src/business/evaluation.ts +102 -22
- package/src/business/global-search.test.ts +344 -0
- package/src/business/global-search.ts +902 -0
- package/src/business/index.ts +4 -0
- package/src/business/instance-lock.ts +58 -74
- package/src/business/instance-state.test.ts +15 -1
- package/src/business/instance-state.ts +37 -14
- package/src/business/object-ref-index.test.ts +140 -0
- package/src/business/object-ref-index.ts +193 -0
- package/src/business/operation.test.ts +15 -1
- package/src/business/operation.ts +4 -0
- package/src/business/project-model.ts +154 -13
- package/src/business/project-unlock.ts +25 -2
- package/src/business/project.ts +9 -0
- package/src/business/secret.test.ts +35 -2
- package/src/business/secret.ts +32 -9
- package/src/business/settings.ts +761 -0
- package/src/business/unit-output.test.ts +477 -0
- package/src/business/unit-output.ts +461 -0
- package/src/business/worker.ts +55 -4
- package/src/database/_generated/backend/postgresql/browser.ts +6 -0
- package/src/database/_generated/backend/postgresql/client.ts +6 -0
- package/src/database/_generated/backend/postgresql/internal/class.ts +23 -5
- package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +89 -5
- package/src/database/_generated/backend/postgresql/internal/prismaNamespaceBrowser.ts +9 -0
- package/src/database/_generated/backend/postgresql/models/Object.ts +1076 -0
- package/src/database/_generated/backend/postgresql/models.ts +1 -0
- package/src/database/_generated/backend/sqlite/browser.ts +6 -0
- package/src/database/_generated/backend/sqlite/client.ts +6 -0
- package/src/database/_generated/backend/sqlite/internal/class.ts +23 -5
- package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +89 -5
- package/src/database/_generated/backend/sqlite/internal/prismaNamespaceBrowser.ts +9 -0
- package/src/database/_generated/backend/sqlite/models/Object.ts +1074 -0
- package/src/database/_generated/backend/sqlite/models.ts +1 -0
- package/src/database/_generated/project/browser.ts +23 -0
- package/src/database/_generated/project/client.ts +23 -0
- package/src/database/_generated/project/commonInputTypes.ts +87 -53
- package/src/database/_generated/project/enums.ts +8 -0
- package/src/database/_generated/project/internal/class.ts +53 -5
- package/src/database/_generated/project/internal/prismaNamespace.ts +367 -13
- package/src/database/_generated/project/internal/prismaNamespaceBrowser.ts +48 -1
- package/src/database/_generated/project/models/Artifact.ts +199 -11
- package/src/database/_generated/project/models/Entity.ts +1274 -0
- package/src/database/_generated/project/models/EntitySnapshot.ts +2389 -0
- package/src/database/_generated/project/models/EntitySnapshotContent.ts +1260 -0
- package/src/database/_generated/project/models/EntitySnapshotReference.ts +1449 -0
- package/src/database/_generated/project/models/InstanceState.ts +361 -1
- package/src/database/_generated/project/models/Operation.ts +148 -3
- package/src/database/_generated/project/models/OperationLog.ts +0 -4
- package/src/database/_generated/project/models.ts +4 -0
- package/src/database/migration.ts +3 -0
- package/src/library/worker/evaluator.ts +7 -1
- package/src/orchestrator/manager.ts +7 -0
- package/src/orchestrator/operation-context.captured-outputs.test.ts +118 -0
- package/src/orchestrator/operation-context.ts +154 -16
- package/src/orchestrator/operation-plan.destroy.test.md +33 -12
- package/src/orchestrator/operation-plan.destroy.test.ts +140 -2
- package/src/orchestrator/operation-plan.fixtures.ts +2 -0
- package/src/orchestrator/operation-plan.md +4 -1
- package/src/orchestrator/operation-plan.ts +286 -92
- package/src/orchestrator/operation-plan.update.test.md +286 -11
- package/src/orchestrator/operation-plan.update.test.ts +656 -5
- package/src/orchestrator/operation-workset.ts +72 -22
- package/src/orchestrator/operation.cancel.test.ts +4 -0
- package/src/orchestrator/operation.composite.test.ts +341 -0
- package/src/orchestrator/operation.destroy.test.ts +4 -0
- package/src/orchestrator/operation.output-validation.failure.test.ts +124 -0
- package/src/orchestrator/operation.preview.test.ts +4 -0
- package/src/orchestrator/operation.refresh.test.ts +4 -0
- package/src/orchestrator/operation.test-utils.ts +52 -13
- package/src/orchestrator/operation.ts +228 -68
- package/src/orchestrator/operation.update.failure.test.ts +4 -0
- package/src/orchestrator/operation.update.skip.test.ts +110 -0
- package/src/orchestrator/operation.update.test.ts +4 -0
- package/src/orchestrator/plan-test-builder.ts +1 -0
- package/src/orchestrator/unit-input-values.test.ts +450 -0
- package/src/orchestrator/unit-input-values.ts +281 -0
- package/src/pubsub/manager.ts +3 -0
- package/src/runner/abstractions.ts +23 -54
- package/src/runner/local.ts +109 -85
- package/src/services.ts +52 -1
- package/src/shared/models/prisma.ts +1 -0
- package/src/shared/models/project/entity.ts +121 -0
- package/src/shared/models/project/index.ts +1 -0
- package/src/shared/models/project/operation.ts +61 -3
- package/src/shared/models/project/state.ts +10 -0
- package/src/shared/models/project/worker.ts +7 -0
- package/src/shared/resolvers/effective-output-type.test.ts +494 -0
- package/src/shared/resolvers/effective-output-type.ts +162 -0
- package/src/shared/resolvers/index.ts +1 -0
- package/src/shared/resolvers/input.ts +61 -9
- package/src/shared/utils/index.ts +1 -0
- package/src/shared/utils/stable-json.ts +41 -0
- package/src/terminal/manager.ts +6 -0
- package/src/worker/manager.ts +97 -1
- package/dist/chunk-JT4KWE3B.js.map +0 -1
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import type { Logger } from "pino"
|
|
2
|
+
import type { LibraryBackend } from "../library"
|
|
3
|
+
import { crc32 } from "node:zlib"
|
|
4
|
+
import {
|
|
5
|
+
type EntityWithMeta,
|
|
6
|
+
getEntityId,
|
|
7
|
+
type InstanceStatusField,
|
|
8
|
+
instanceStatusFieldSchema,
|
|
9
|
+
isAssignableTo,
|
|
10
|
+
type UnitPage,
|
|
11
|
+
type UnitTerminal,
|
|
12
|
+
type UnitTrigger,
|
|
13
|
+
type UnitWorker,
|
|
14
|
+
unitArtifactId,
|
|
15
|
+
unitArtifactSchema,
|
|
16
|
+
unitPageSchema,
|
|
17
|
+
unitTerminalSchema,
|
|
18
|
+
unitTriggerSchema,
|
|
19
|
+
unitWorkerSchema,
|
|
20
|
+
type VersionedName,
|
|
21
|
+
} from "@highstate/contract"
|
|
22
|
+
import { encode } from "@msgpack/msgpack"
|
|
23
|
+
import { sha256 } from "@noble/hashes/sha2"
|
|
24
|
+
import { mapValues, omitBy } from "remeda"
|
|
25
|
+
import { z } from "zod"
|
|
26
|
+
|
|
27
|
+
export type RawPulumiOutputValue = {
|
|
28
|
+
value: unknown
|
|
29
|
+
secret?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type RawPulumiOutputs = Record<string, RawPulumiOutputValue>
|
|
33
|
+
|
|
34
|
+
export type UnitEntitySnapshotPayload = {
|
|
35
|
+
nodes: UnitEntitySnapshotNode[]
|
|
36
|
+
implicitReferences: UnitEntitySnapshotReference[]
|
|
37
|
+
explicitReferences: UnitEntitySnapshotReference[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type UnitEntitySnapshotNode = {
|
|
41
|
+
entityId: string
|
|
42
|
+
entityType: VersionedName
|
|
43
|
+
identity: string
|
|
44
|
+
meta: {
|
|
45
|
+
title?: string
|
|
46
|
+
description?: string
|
|
47
|
+
icon?: string
|
|
48
|
+
iconColor?: string
|
|
49
|
+
} | null
|
|
50
|
+
content: Record<string, unknown>
|
|
51
|
+
referencedOutputs: string[]
|
|
52
|
+
exportedOutputs: string[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type UnitEntitySnapshotReference = {
|
|
56
|
+
fromEntityId: string
|
|
57
|
+
toEntityId: string
|
|
58
|
+
group: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type ParsedUnitOutputs = {
|
|
62
|
+
outputHash: number | null
|
|
63
|
+
statusFields: InstanceStatusField[] | null
|
|
64
|
+
terminals: UnitTerminal[] | null
|
|
65
|
+
pages: UnitPage[] | null
|
|
66
|
+
triggers: UnitTrigger[] | null
|
|
67
|
+
secrets: Record<string, unknown> | null
|
|
68
|
+
workers: UnitWorker[] | null
|
|
69
|
+
exportedArtifactIds: Record<string, string[]> | null
|
|
70
|
+
hasResourceHooks: boolean
|
|
71
|
+
entitySnapshotError: string | null
|
|
72
|
+
entitySnapshotPayload: UnitEntitySnapshotPayload | null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class UnitOutputService {
|
|
76
|
+
constructor(
|
|
77
|
+
private readonly libraryBackend: LibraryBackend,
|
|
78
|
+
readonly _logger: Logger,
|
|
79
|
+
) {}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Parses raw Pulumi outputs returned by the runner.
|
|
83
|
+
*
|
|
84
|
+
* It extracts Highstate-specific "$..." outputs, computes output hash,
|
|
85
|
+
* and builds entity snapshot payload based on static component/entity models.
|
|
86
|
+
*
|
|
87
|
+
* @param options The raw parsing inputs.
|
|
88
|
+
*/
|
|
89
|
+
async parseUnitOutputs(options: {
|
|
90
|
+
libraryId: string
|
|
91
|
+
instanceType: VersionedName
|
|
92
|
+
outputs: RawPulumiOutputs
|
|
93
|
+
signal?: AbortSignal
|
|
94
|
+
}): Promise<ParsedUnitOutputs> {
|
|
95
|
+
const unitOutputs = omitBy(options.outputs, (_value, key) => key.startsWith("$"))
|
|
96
|
+
const outputNames = Object.keys(unitOutputs)
|
|
97
|
+
|
|
98
|
+
const outputHash = outputNames.length > 0 ? crc32(sha256(encode(unitOutputs))) : null
|
|
99
|
+
|
|
100
|
+
const statusFields = options.outputs.$statusFields
|
|
101
|
+
? z.array(instanceStatusFieldSchema).parse(options.outputs.$statusFields.value)
|
|
102
|
+
: null
|
|
103
|
+
|
|
104
|
+
const terminals = options.outputs.$terminals
|
|
105
|
+
? z.array(unitTerminalSchema).parse(options.outputs.$terminals.value)
|
|
106
|
+
: null
|
|
107
|
+
|
|
108
|
+
const pages = options.outputs.$pages
|
|
109
|
+
? z.array(unitPageSchema).parse(options.outputs.$pages.value)
|
|
110
|
+
: null
|
|
111
|
+
|
|
112
|
+
const triggers = options.outputs.$triggers
|
|
113
|
+
? z.array(unitTriggerSchema).parse(options.outputs.$triggers.value)
|
|
114
|
+
: null
|
|
115
|
+
|
|
116
|
+
const workers = options.outputs.$workers
|
|
117
|
+
? z.array(unitWorkerSchema).parse(options.outputs.$workers.value)
|
|
118
|
+
: null
|
|
119
|
+
|
|
120
|
+
const secrets = options.outputs.$secrets
|
|
121
|
+
? z.record(z.string(), z.unknown()).parse(options.outputs.$secrets.value)
|
|
122
|
+
: null
|
|
123
|
+
|
|
124
|
+
const exportedArtifactIds = this.parseExportedArtifactIds(options.outputs)
|
|
125
|
+
|
|
126
|
+
const hasResourceHooks = options.outputs.$hasResourceHooks
|
|
127
|
+
? z.boolean().parse(options.outputs.$hasResourceHooks.value)
|
|
128
|
+
: false
|
|
129
|
+
|
|
130
|
+
let entitySnapshotPayload: UnitEntitySnapshotPayload | null = null
|
|
131
|
+
let entitySnapshotError: string | null = null
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
entitySnapshotPayload = await this.parseEntitySnapshotPayload({
|
|
135
|
+
libraryId: options.libraryId,
|
|
136
|
+
instanceType: options.instanceType,
|
|
137
|
+
unitOutputs,
|
|
138
|
+
signal: options.signal,
|
|
139
|
+
})
|
|
140
|
+
} catch (error) {
|
|
141
|
+
entitySnapshotError = error instanceof Error ? error.message : String(error)
|
|
142
|
+
entitySnapshotPayload = null
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
outputHash,
|
|
147
|
+
statusFields,
|
|
148
|
+
terminals,
|
|
149
|
+
pages,
|
|
150
|
+
triggers,
|
|
151
|
+
secrets,
|
|
152
|
+
workers,
|
|
153
|
+
exportedArtifactIds,
|
|
154
|
+
hasResourceHooks,
|
|
155
|
+
entitySnapshotError,
|
|
156
|
+
entitySnapshotPayload,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private parseExportedArtifactIds(outputs: RawPulumiOutputs): Record<string, string[]> | null {
|
|
161
|
+
const rawArtifacts = outputs.$artifacts
|
|
162
|
+
if (!rawArtifacts) {
|
|
163
|
+
return null
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const rawArtifactsByOutput = z
|
|
167
|
+
.record(z.string(), z.array(z.unknown()))
|
|
168
|
+
.parse(rawArtifacts.value)
|
|
169
|
+
|
|
170
|
+
return mapValues(rawArtifactsByOutput, artifacts => {
|
|
171
|
+
return artifacts.map(rawArtifact => {
|
|
172
|
+
if (typeof rawArtifact !== "object" || rawArtifact === null) {
|
|
173
|
+
throw new Error("Invalid artifact value")
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const validated = unitArtifactSchema.parse(rawArtifact)
|
|
177
|
+
const id = (rawArtifact as Record<string | symbol, unknown>)[unitArtifactId]
|
|
178
|
+
if (typeof id === "string" && id.length > 0) {
|
|
179
|
+
return id
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
throw new Error(`Failed to determine artifact ID for artifact with hash ${validated.hash}`)
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private async parseEntitySnapshotPayload(options: {
|
|
188
|
+
libraryId: string
|
|
189
|
+
instanceType: VersionedName
|
|
190
|
+
unitOutputs: RawPulumiOutputs
|
|
191
|
+
signal?: AbortSignal
|
|
192
|
+
}): Promise<UnitEntitySnapshotPayload | null> {
|
|
193
|
+
const unitOutputNames = Object.keys(options.unitOutputs)
|
|
194
|
+
if (unitOutputNames.length === 0) {
|
|
195
|
+
return null
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const library = await this.libraryBackend.loadLibrary(options.libraryId, options.signal)
|
|
199
|
+
const component = library.components[options.instanceType]
|
|
200
|
+
|
|
201
|
+
if (!component) {
|
|
202
|
+
throw new Error(`Component "${options.instanceType}" is not defined in the library`)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const nodeByEntityId = new Map<
|
|
206
|
+
string,
|
|
207
|
+
{
|
|
208
|
+
node: Omit<UnitEntitySnapshotNode, "exportedOutputs" | "referencedOutputs"> & {
|
|
209
|
+
exportedOutputs: Set<string>
|
|
210
|
+
referencedOutputs: Set<string>
|
|
211
|
+
}
|
|
212
|
+
entityModelInclusions: Array<{
|
|
213
|
+
type: VersionedName
|
|
214
|
+
required: boolean
|
|
215
|
+
multiple: boolean
|
|
216
|
+
field: string
|
|
217
|
+
}>
|
|
218
|
+
}
|
|
219
|
+
>()
|
|
220
|
+
|
|
221
|
+
const implicitReferencesByKey = new Map<string, UnitEntitySnapshotReference>()
|
|
222
|
+
const explicitReferencesByKey = new Map<string, UnitEntitySnapshotReference>()
|
|
223
|
+
|
|
224
|
+
const isRecord = (value: unknown): value is Record<string, unknown> => {
|
|
225
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function assertEntityWithMeta(
|
|
229
|
+
expectedType: VersionedName,
|
|
230
|
+
output: string,
|
|
231
|
+
value: unknown,
|
|
232
|
+
): asserts value is EntityWithMeta {
|
|
233
|
+
if (!isRecord(value)) {
|
|
234
|
+
throw new Error(`Output "${output}" must be an object`)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const meta = (value as Record<string, unknown>).$meta
|
|
238
|
+
if (!isRecord(meta)) {
|
|
239
|
+
throw new Error(`Output "${output}" must include a "$meta" object`)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const metaType = meta.type
|
|
243
|
+
if (typeof metaType !== "string") {
|
|
244
|
+
throw new Error(
|
|
245
|
+
`Output "${output}" has invalid "$meta.type": expected "${expectedType}", got "${String(metaType)}"`,
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const entityModel = library.entities[metaType as VersionedName]
|
|
250
|
+
if (!entityModel) {
|
|
251
|
+
throw new Error(`Entity type "${metaType}" is not defined in the library`)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!isAssignableTo(entityModel, expectedType)) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
`Output "${output}" has invalid "$meta.type": expected assignable to "${expectedType}", got "${String(metaType)}"`,
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const metaIdentity = meta.identity
|
|
261
|
+
if (typeof metaIdentity !== "string" || metaIdentity.length === 0) {
|
|
262
|
+
throw new Error(
|
|
263
|
+
`Output "${output}" has invalid "$meta.identity": expected a non-empty string`,
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const normalizeSnapshotContent = (
|
|
269
|
+
entityType: VersionedName,
|
|
270
|
+
record: Record<string, unknown>,
|
|
271
|
+
): Record<string, unknown> => {
|
|
272
|
+
const entityModel = library.entities[entityType]
|
|
273
|
+
if (!entityModel) {
|
|
274
|
+
throw new Error(`Entity type "${entityType}" is not defined in the library`)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const { $meta: _ignoredMeta, ...content } = record
|
|
278
|
+
for (const inclusion of entityModel.inclusions ?? []) {
|
|
279
|
+
delete (content as Record<string, unknown>)[inclusion.field]
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return content
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const collectEntityValue = async (options: {
|
|
286
|
+
expectedType: VersionedName
|
|
287
|
+
output: string
|
|
288
|
+
value: unknown
|
|
289
|
+
relation: "exported" | "referenced"
|
|
290
|
+
stack: string[]
|
|
291
|
+
}): Promise<string> => {
|
|
292
|
+
assertEntityWithMeta(options.expectedType, options.output, options.value)
|
|
293
|
+
|
|
294
|
+
const record = options.value as unknown as Record<string, unknown>
|
|
295
|
+
|
|
296
|
+
const entityWithMeta = options.value as EntityWithMeta
|
|
297
|
+
const { identity } = entityWithMeta.$meta
|
|
298
|
+
const meta = entityWithMeta.$meta
|
|
299
|
+
const entityType = entityWithMeta.$meta.type
|
|
300
|
+
|
|
301
|
+
const entityId = getEntityId(entityWithMeta)
|
|
302
|
+
if (options.stack.includes(entityId)) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
`Detected entity inclusion cycle while collecting output "${options.output}": "${entityId}"`,
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const existing = nodeByEntityId.get(entityId)
|
|
309
|
+
if (!existing) {
|
|
310
|
+
const entityModel = library.entities[entityType]
|
|
311
|
+
if (!entityModel) {
|
|
312
|
+
throw new Error(`Entity type "${entityType}" is not defined in the library`)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const normalizedContent = normalizeSnapshotContent(entityType, record)
|
|
316
|
+
|
|
317
|
+
const snapshotMeta = {
|
|
318
|
+
...(typeof meta.title === "string" ? { title: meta.title } : {}),
|
|
319
|
+
...(typeof meta.description === "string" ? { description: meta.description } : {}),
|
|
320
|
+
...(typeof meta.icon === "string" ? { icon: meta.icon } : {}),
|
|
321
|
+
...(typeof meta.iconColor === "string" ? { iconColor: meta.iconColor } : {}),
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
nodeByEntityId.set(entityId, {
|
|
325
|
+
node: {
|
|
326
|
+
entityId,
|
|
327
|
+
entityType,
|
|
328
|
+
identity,
|
|
329
|
+
meta: Object.keys(snapshotMeta).length > 0 ? snapshotMeta : null,
|
|
330
|
+
content: normalizedContent,
|
|
331
|
+
referencedOutputs: new Set<string>(),
|
|
332
|
+
exportedOutputs: new Set<string>(),
|
|
333
|
+
},
|
|
334
|
+
entityModelInclusions: (entityModel.inclusions ?? []).map(i => ({
|
|
335
|
+
type: i.type,
|
|
336
|
+
required: i.required,
|
|
337
|
+
multiple: i.multiple,
|
|
338
|
+
field: i.field,
|
|
339
|
+
})),
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const current = nodeByEntityId.get(entityId)!
|
|
344
|
+
if (options.relation === "exported") {
|
|
345
|
+
current.node.exportedOutputs.add(options.output)
|
|
346
|
+
} else {
|
|
347
|
+
current.node.referencedOutputs.add(options.output)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const rawReferences = meta.references
|
|
351
|
+
if (rawReferences) {
|
|
352
|
+
for (const [group, ids] of Object.entries(rawReferences)) {
|
|
353
|
+
for (const id of ids) {
|
|
354
|
+
explicitReferencesByKey.set(`${entityId}:${id}:${group}`, {
|
|
355
|
+
fromEntityId: entityId,
|
|
356
|
+
toEntityId: id,
|
|
357
|
+
group,
|
|
358
|
+
})
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
for (const inclusion of current.entityModelInclusions) {
|
|
364
|
+
const rawIncluded = record[inclusion.field]
|
|
365
|
+
if (rawIncluded === undefined || rawIncluded === null) {
|
|
366
|
+
continue
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (inclusion.multiple) {
|
|
370
|
+
for (const item of rawIncluded as unknown[]) {
|
|
371
|
+
const childEntityId = await collectEntityValue({
|
|
372
|
+
expectedType: inclusion.type,
|
|
373
|
+
output: options.output,
|
|
374
|
+
value: item,
|
|
375
|
+
relation: "referenced",
|
|
376
|
+
stack: [...options.stack, entityId],
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
implicitReferencesByKey.set(`${entityId}:${childEntityId}:${inclusion.field}`, {
|
|
380
|
+
fromEntityId: entityId,
|
|
381
|
+
toEntityId: childEntityId,
|
|
382
|
+
group: inclusion.field,
|
|
383
|
+
})
|
|
384
|
+
}
|
|
385
|
+
} else {
|
|
386
|
+
const childEntityId = await collectEntityValue({
|
|
387
|
+
expectedType: inclusion.type,
|
|
388
|
+
output: options.output,
|
|
389
|
+
value: rawIncluded,
|
|
390
|
+
relation: "referenced",
|
|
391
|
+
stack: [...options.stack, entityId],
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
implicitReferencesByKey.set(`${entityId}:${childEntityId}:${inclusion.field}`, {
|
|
395
|
+
fromEntityId: entityId,
|
|
396
|
+
toEntityId: childEntityId,
|
|
397
|
+
group: inclusion.field,
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return entityId
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
for (const outputName of unitOutputNames) {
|
|
406
|
+
const outputSpec = component.outputs[outputName]
|
|
407
|
+
if (!outputSpec) {
|
|
408
|
+
throw new Error(`Output "${outputName}" is not defined on component "${component.type}"`)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const outputValue = options.unitOutputs[outputName]?.value
|
|
412
|
+
if (outputValue === undefined || outputValue === null) {
|
|
413
|
+
continue
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (outputSpec.multiple) {
|
|
417
|
+
if (!Array.isArray(outputValue)) {
|
|
418
|
+
throw new Error(`Output "${outputName}" must be an array`)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
for (const item of outputValue) {
|
|
422
|
+
await collectEntityValue({
|
|
423
|
+
expectedType: outputSpec.type,
|
|
424
|
+
output: outputName,
|
|
425
|
+
value: item,
|
|
426
|
+
relation: "exported",
|
|
427
|
+
stack: [],
|
|
428
|
+
})
|
|
429
|
+
}
|
|
430
|
+
} else {
|
|
431
|
+
await collectEntityValue({
|
|
432
|
+
expectedType: outputSpec.type,
|
|
433
|
+
output: outputName,
|
|
434
|
+
value: outputValue,
|
|
435
|
+
relation: "exported",
|
|
436
|
+
stack: [],
|
|
437
|
+
})
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (nodeByEntityId.size === 0) {
|
|
442
|
+
return null
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const nodes = Array.from(nodeByEntityId.values()).map(({ node }) => ({
|
|
446
|
+
entityId: node.entityId,
|
|
447
|
+
entityType: node.entityType,
|
|
448
|
+
identity: node.identity,
|
|
449
|
+
meta: node.meta,
|
|
450
|
+
content: node.content,
|
|
451
|
+
referencedOutputs: Array.from(node.referencedOutputs),
|
|
452
|
+
exportedOutputs: Array.from(node.exportedOutputs),
|
|
453
|
+
}))
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
nodes,
|
|
457
|
+
implicitReferences: Array.from(implicitReferencesByKey.values()),
|
|
458
|
+
explicitReferences: Array.from(explicitReferencesByKey.values()),
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
package/src/business/worker.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CommonObjectMeta, UnitWorker } from "@highstate/contract"
|
|
1
|
+
import type { CommonObjectMeta, ServiceAccountMeta, UnitWorker } from "@highstate/contract"
|
|
2
2
|
import type { Logger } from "pino"
|
|
3
3
|
import type {
|
|
4
4
|
DatabaseManager,
|
|
@@ -13,6 +13,7 @@ import { randomBytes } from "node:crypto"
|
|
|
13
13
|
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/client"
|
|
14
14
|
import { createProjectLogger } from "../common"
|
|
15
15
|
import {
|
|
16
|
+
type ApiKeyMeta,
|
|
16
17
|
extractDigestFromImage,
|
|
17
18
|
getWorkerIdentity,
|
|
18
19
|
type WorkerUnitRegistrationEvent,
|
|
@@ -41,7 +42,9 @@ export class WorkerService {
|
|
|
41
42
|
projectId: string,
|
|
42
43
|
stateId: string,
|
|
43
44
|
unitWorkers: UnitWorker[],
|
|
44
|
-
): Promise<
|
|
45
|
+
): Promise<string[]> {
|
|
46
|
+
const objectIds = new Set<string>()
|
|
47
|
+
|
|
45
48
|
// parse images first
|
|
46
49
|
const parsedWorkers = unitWorkers.map(w => {
|
|
47
50
|
const digest = extractDigestFromImage(w.image)
|
|
@@ -67,6 +70,13 @@ export class WorkerService {
|
|
|
67
70
|
const workerRecord = await this.ensureWorker(tx, worker.identity)
|
|
68
71
|
const workerVersionRecord = await this.ensureWorkerVersion(tx, workerRecord, worker.digest)
|
|
69
72
|
|
|
73
|
+
objectIds.add(workerRecord.id)
|
|
74
|
+
objectIds.add(workerRecord.serviceAccountId)
|
|
75
|
+
objectIds.add(workerVersionRecord.id)
|
|
76
|
+
if (workerVersionRecord.apiKeyId) {
|
|
77
|
+
objectIds.add(workerVersionRecord.apiKeyId)
|
|
78
|
+
}
|
|
79
|
+
|
|
70
80
|
const existing = existingRegistrations.find(r => r.name === worker.name)
|
|
71
81
|
const stringifiedParams = JSON.stringify(worker.params)
|
|
72
82
|
|
|
@@ -145,6 +155,8 @@ export class WorkerService {
|
|
|
145
155
|
void this.workerManager.syncWorkers(projectId)
|
|
146
156
|
|
|
147
157
|
logger.info(`updated worker registrations for instance state "%s"`, stateId)
|
|
158
|
+
|
|
159
|
+
return Array.from(objectIds)
|
|
148
160
|
}
|
|
149
161
|
|
|
150
162
|
private async ensureWorker(tx: ProjectTransaction, identity: string): Promise<Worker> {
|
|
@@ -230,7 +242,7 @@ export class WorkerService {
|
|
|
230
242
|
where: {
|
|
231
243
|
unitRegistrations: { none: {} },
|
|
232
244
|
},
|
|
233
|
-
select: { id: true },
|
|
245
|
+
select: { id: true, apiKeyId: true },
|
|
234
246
|
})
|
|
235
247
|
|
|
236
248
|
if (unused.length === 0) {
|
|
@@ -240,6 +252,22 @@ export class WorkerService {
|
|
|
240
252
|
await tx.workerVersion.deleteMany({
|
|
241
253
|
where: { id: { in: unused.map(u => u.id) } },
|
|
242
254
|
})
|
|
255
|
+
|
|
256
|
+
await tx.apiKey.deleteMany({
|
|
257
|
+
where: { id: { in: unused.map(u => u.apiKeyId) } },
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private createWorkerApiKeyMeta(
|
|
262
|
+
workerIdentity: string,
|
|
263
|
+
serviceAccountMeta: Pick<ServiceAccountMeta, "title" | "description">,
|
|
264
|
+
): ApiKeyMeta {
|
|
265
|
+
return {
|
|
266
|
+
title: `${serviceAccountMeta.title} API Key`,
|
|
267
|
+
description:
|
|
268
|
+
serviceAccountMeta.description ??
|
|
269
|
+
`Automatically managed API key for worker "${workerIdentity}" service account.`,
|
|
270
|
+
}
|
|
243
271
|
}
|
|
244
272
|
|
|
245
273
|
/**
|
|
@@ -253,15 +281,38 @@ export class WorkerService {
|
|
|
253
281
|
projectId: string,
|
|
254
282
|
workerVersionId: string,
|
|
255
283
|
meta: CommonObjectMeta,
|
|
284
|
+
serviceAccountMeta?: ServiceAccountMeta,
|
|
256
285
|
): Promise<void> {
|
|
257
286
|
const database = await this.database.forProject(projectId)
|
|
258
287
|
const logger = createProjectLogger(this.logger, projectId)
|
|
259
288
|
|
|
260
289
|
try {
|
|
261
|
-
await database.workerVersion.update({
|
|
290
|
+
const workerVersion = await database.workerVersion.update({
|
|
262
291
|
where: { id: workerVersionId },
|
|
292
|
+
select: {
|
|
293
|
+
worker: {
|
|
294
|
+
select: {
|
|
295
|
+
identity: true,
|
|
296
|
+
serviceAccountId: true,
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
},
|
|
263
300
|
data: { meta },
|
|
264
301
|
})
|
|
302
|
+
|
|
303
|
+
if (serviceAccountMeta) {
|
|
304
|
+
await database.serviceAccount.update({
|
|
305
|
+
where: { id: workerVersion.worker.serviceAccountId },
|
|
306
|
+
data: { meta: serviceAccountMeta },
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
await database.apiKey.updateMany({
|
|
310
|
+
where: { serviceAccountId: workerVersion.worker.serviceAccountId },
|
|
311
|
+
data: {
|
|
312
|
+
meta: this.createWorkerApiKeyMeta(workerVersion.worker.identity, serviceAccountMeta),
|
|
313
|
+
},
|
|
314
|
+
})
|
|
315
|
+
}
|
|
265
316
|
} catch (error) {
|
|
266
317
|
if (error instanceof PrismaClientKnownRequestError && error.code === "P2025") {
|
|
267
318
|
throw new WorkerVersionNotFoundError(projectId, workerVersionId)
|
|
@@ -27,6 +27,12 @@ export type UserWorkspaceLayout = Prisma.UserWorkspaceLayoutModel
|
|
|
27
27
|
*
|
|
28
28
|
*/
|
|
29
29
|
export type Library = Prisma.LibraryModel
|
|
30
|
+
/**
|
|
31
|
+
* Model Object
|
|
32
|
+
* The object allows to track arbitrary object across multiple projects and search them globally by their IDs.
|
|
33
|
+
* This also allow to correlate different entities across different projects.
|
|
34
|
+
*/
|
|
35
|
+
export type Object = Prisma.ObjectModel
|
|
30
36
|
/**
|
|
31
37
|
* Model Project
|
|
32
38
|
*
|
|
@@ -49,6 +49,12 @@ export type UserWorkspaceLayout = Prisma.UserWorkspaceLayoutModel
|
|
|
49
49
|
*
|
|
50
50
|
*/
|
|
51
51
|
export type Library = Prisma.LibraryModel
|
|
52
|
+
/**
|
|
53
|
+
* Model Object
|
|
54
|
+
* The object allows to track arbitrary object across multiple projects and search them globally by their IDs.
|
|
55
|
+
* This also allow to correlate different entities across different projects.
|
|
56
|
+
*/
|
|
57
|
+
export type Object = Prisma.ObjectModel
|
|
52
58
|
/**
|
|
53
59
|
* Model Project
|
|
54
60
|
*
|