@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.
Files changed (120) hide show
  1. package/dist/{chunk-JT4KWE3B.js → chunk-52MY2TCE.js} +348 -19
  2. package/dist/chunk-52MY2TCE.js.map +1 -0
  3. package/dist/{chunk-I7BWSAN6.js → chunk-UAWBPTDW.js} +3 -3
  4. package/dist/{chunk-I7BWSAN6.js.map → chunk-UAWBPTDW.js.map} +1 -1
  5. package/dist/highstate.manifest.json +4 -4
  6. package/dist/index.js +4159 -785
  7. package/dist/index.js.map +1 -1
  8. package/dist/library/worker/main.js +5 -2
  9. package/dist/library/worker/main.js.map +1 -1
  10. package/dist/shared/index.js +2 -2
  11. package/package.json +7 -7
  12. package/prisma/backend/_schema/object.prisma +12 -0
  13. package/prisma/backend/sqlite/migrations/20260222113554_add_object_tracking/migration.sql +7 -0
  14. package/prisma/project/artifact.prisma +3 -0
  15. package/prisma/project/entity.prisma +125 -0
  16. package/prisma/project/instance.prisma +6 -0
  17. package/prisma/project/migrations/20260301210131_add_entity_tracking/migration.sql +70 -0
  18. package/prisma/project/migrations/20260302212734_add_resource_hooks_flag/migration.sql +1 -0
  19. package/prisma/project/operation.prisma +3 -0
  20. package/src/business/artifact.test.ts +22 -2
  21. package/src/business/artifact.ts +7 -1
  22. package/src/business/entity-snapshot.test.ts +684 -0
  23. package/src/business/entity-snapshot.ts +904 -0
  24. package/src/business/evaluation.test.ts +56 -0
  25. package/src/business/evaluation.ts +102 -22
  26. package/src/business/global-search.test.ts +344 -0
  27. package/src/business/global-search.ts +902 -0
  28. package/src/business/index.ts +4 -0
  29. package/src/business/instance-lock.ts +58 -74
  30. package/src/business/instance-state.test.ts +15 -1
  31. package/src/business/instance-state.ts +37 -14
  32. package/src/business/object-ref-index.test.ts +140 -0
  33. package/src/business/object-ref-index.ts +193 -0
  34. package/src/business/operation.test.ts +15 -1
  35. package/src/business/operation.ts +4 -0
  36. package/src/business/project-model.ts +154 -13
  37. package/src/business/project-unlock.ts +25 -2
  38. package/src/business/project.ts +9 -0
  39. package/src/business/secret.test.ts +35 -2
  40. package/src/business/secret.ts +32 -9
  41. package/src/business/settings.ts +761 -0
  42. package/src/business/unit-output.test.ts +477 -0
  43. package/src/business/unit-output.ts +461 -0
  44. package/src/business/worker.ts +55 -4
  45. package/src/database/_generated/backend/postgresql/browser.ts +6 -0
  46. package/src/database/_generated/backend/postgresql/client.ts +6 -0
  47. package/src/database/_generated/backend/postgresql/internal/class.ts +23 -5
  48. package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +89 -5
  49. package/src/database/_generated/backend/postgresql/internal/prismaNamespaceBrowser.ts +9 -0
  50. package/src/database/_generated/backend/postgresql/models/Object.ts +1076 -0
  51. package/src/database/_generated/backend/postgresql/models.ts +1 -0
  52. package/src/database/_generated/backend/sqlite/browser.ts +6 -0
  53. package/src/database/_generated/backend/sqlite/client.ts +6 -0
  54. package/src/database/_generated/backend/sqlite/internal/class.ts +23 -5
  55. package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +89 -5
  56. package/src/database/_generated/backend/sqlite/internal/prismaNamespaceBrowser.ts +9 -0
  57. package/src/database/_generated/backend/sqlite/models/Object.ts +1074 -0
  58. package/src/database/_generated/backend/sqlite/models.ts +1 -0
  59. package/src/database/_generated/project/browser.ts +23 -0
  60. package/src/database/_generated/project/client.ts +23 -0
  61. package/src/database/_generated/project/commonInputTypes.ts +87 -53
  62. package/src/database/_generated/project/enums.ts +8 -0
  63. package/src/database/_generated/project/internal/class.ts +53 -5
  64. package/src/database/_generated/project/internal/prismaNamespace.ts +367 -13
  65. package/src/database/_generated/project/internal/prismaNamespaceBrowser.ts +48 -1
  66. package/src/database/_generated/project/models/Artifact.ts +199 -11
  67. package/src/database/_generated/project/models/Entity.ts +1274 -0
  68. package/src/database/_generated/project/models/EntitySnapshot.ts +2389 -0
  69. package/src/database/_generated/project/models/EntitySnapshotContent.ts +1260 -0
  70. package/src/database/_generated/project/models/EntitySnapshotReference.ts +1449 -0
  71. package/src/database/_generated/project/models/InstanceState.ts +361 -1
  72. package/src/database/_generated/project/models/Operation.ts +148 -3
  73. package/src/database/_generated/project/models/OperationLog.ts +0 -4
  74. package/src/database/_generated/project/models.ts +4 -0
  75. package/src/database/migration.ts +3 -0
  76. package/src/library/worker/evaluator.ts +7 -1
  77. package/src/orchestrator/manager.ts +7 -0
  78. package/src/orchestrator/operation-context.captured-outputs.test.ts +118 -0
  79. package/src/orchestrator/operation-context.ts +154 -16
  80. package/src/orchestrator/operation-plan.destroy.test.md +33 -12
  81. package/src/orchestrator/operation-plan.destroy.test.ts +140 -2
  82. package/src/orchestrator/operation-plan.fixtures.ts +2 -0
  83. package/src/orchestrator/operation-plan.md +4 -1
  84. package/src/orchestrator/operation-plan.ts +286 -92
  85. package/src/orchestrator/operation-plan.update.test.md +286 -11
  86. package/src/orchestrator/operation-plan.update.test.ts +656 -5
  87. package/src/orchestrator/operation-workset.ts +72 -22
  88. package/src/orchestrator/operation.cancel.test.ts +4 -0
  89. package/src/orchestrator/operation.composite.test.ts +341 -0
  90. package/src/orchestrator/operation.destroy.test.ts +4 -0
  91. package/src/orchestrator/operation.output-validation.failure.test.ts +124 -0
  92. package/src/orchestrator/operation.preview.test.ts +4 -0
  93. package/src/orchestrator/operation.refresh.test.ts +4 -0
  94. package/src/orchestrator/operation.test-utils.ts +52 -13
  95. package/src/orchestrator/operation.ts +228 -68
  96. package/src/orchestrator/operation.update.failure.test.ts +4 -0
  97. package/src/orchestrator/operation.update.skip.test.ts +110 -0
  98. package/src/orchestrator/operation.update.test.ts +4 -0
  99. package/src/orchestrator/plan-test-builder.ts +1 -0
  100. package/src/orchestrator/unit-input-values.test.ts +450 -0
  101. package/src/orchestrator/unit-input-values.ts +281 -0
  102. package/src/pubsub/manager.ts +3 -0
  103. package/src/runner/abstractions.ts +23 -54
  104. package/src/runner/local.ts +109 -85
  105. package/src/services.ts +52 -1
  106. package/src/shared/models/prisma.ts +1 -0
  107. package/src/shared/models/project/entity.ts +121 -0
  108. package/src/shared/models/project/index.ts +1 -0
  109. package/src/shared/models/project/operation.ts +61 -3
  110. package/src/shared/models/project/state.ts +10 -0
  111. package/src/shared/models/project/worker.ts +7 -0
  112. package/src/shared/resolvers/effective-output-type.test.ts +494 -0
  113. package/src/shared/resolvers/effective-output-type.ts +162 -0
  114. package/src/shared/resolvers/index.ts +1 -0
  115. package/src/shared/resolvers/input.ts +61 -9
  116. package/src/shared/utils/index.ts +1 -0
  117. package/src/shared/utils/stable-json.ts +41 -0
  118. package/src/terminal/manager.ts +6 -0
  119. package/src/worker/manager.ts +97 -1
  120. package/dist/chunk-JT4KWE3B.js.map +0 -1
@@ -0,0 +1,281 @@
1
+ import type { UnitInputValue, VersionedName } from "@highstate/contract"
2
+ import type { CapturedEntitySnapshotValue } from "../business"
3
+ import type { LibraryModel, ResolvedInstanceInput } from "../shared"
4
+
5
+ type InclusionStep = {
6
+ field: string
7
+ multiple: boolean
8
+ type: string
9
+ }
10
+
11
+ type ParsedPathSegment = {
12
+ field: string
13
+ }
14
+
15
+ type EntityModelLike = {
16
+ type: string
17
+ extensions?: string[]
18
+ inclusions?: {
19
+ field: string
20
+ type: string
21
+ multiple: boolean
22
+ }[]
23
+ }
24
+
25
+ function parseInclusionPath(path: string): ParsedPathSegment[] {
26
+ if (path.length === 0) {
27
+ throw new Error("Input path cannot be empty")
28
+ }
29
+
30
+ const segments = path.split(".")
31
+
32
+ return segments.map(segment => {
33
+ const match = /^([^.[\]]+)$/.exec(segment)
34
+ if (!match) {
35
+ throw new Error(`Invalid input path segment "${segment}". Expected format "field".`)
36
+ }
37
+
38
+ return {
39
+ field: match[1],
40
+ }
41
+ })
42
+ }
43
+
44
+ function resolveInclusionPath(options: {
45
+ library: LibraryModel
46
+ rootEntity: EntityModelLike
47
+ path: string
48
+ }): InclusionStep[] {
49
+ const segments = parseInclusionPath(options.path)
50
+ const steps: InclusionStep[] = []
51
+ let current = options.rootEntity
52
+
53
+ for (const segment of segments) {
54
+ const inclusion = current.inclusions?.find(inc => inc.field === segment.field)
55
+ if (!inclusion) {
56
+ throw new Error(
57
+ `Invalid input path "${options.path}": inclusion "${segment.field}" is not defined on entity "${current.type}".`,
58
+ )
59
+ }
60
+
61
+ steps.push(inclusion)
62
+
63
+ const nextEntity = options.library.entities[inclusion.type]
64
+ if (!nextEntity) {
65
+ throw new Error(
66
+ `Invalid input path "${options.path}": entity type "${inclusion.type}" is not defined in the library.`,
67
+ )
68
+ }
69
+
70
+ current = nextEntity
71
+ }
72
+
73
+ return steps
74
+ }
75
+
76
+ function extractByInclusionPath(options: {
77
+ values: unknown[]
78
+ steps: InclusionStep[]
79
+ outputName: string
80
+ instanceId: string
81
+ path: string
82
+ }): unknown[] {
83
+ let currentValues = options.values
84
+
85
+ for (const step of options.steps) {
86
+ const nextValues: unknown[] = []
87
+
88
+ for (const value of currentValues) {
89
+ if (typeof value !== "object" || value === null) {
90
+ throw new Error(
91
+ `Cannot extract inclusion path "${options.path}" from non-object output "${options.outputName}" of instance "${options.instanceId}".`,
92
+ )
93
+ }
94
+
95
+ const extracted = (value as Record<string, unknown>)[step.field]
96
+
97
+ if (extracted === undefined || extracted === null) {
98
+ continue
99
+ }
100
+
101
+ if (step.multiple) {
102
+ if (!Array.isArray(extracted)) {
103
+ throw new Error(
104
+ `Expected inclusion field "${step.field}" on output "${options.outputName}" to be an array while resolving path "${options.path}".`,
105
+ )
106
+ }
107
+
108
+ for (const item of extracted) {
109
+ nextValues.push(item)
110
+ }
111
+
112
+ continue
113
+ }
114
+
115
+ nextValues.push(extracted)
116
+ }
117
+
118
+ currentValues = nextValues
119
+ }
120
+
121
+ return currentValues
122
+ }
123
+
124
+ function isEntityAssignableTo(entity: EntityModelLike, requiredType: string): boolean {
125
+ return entity.type === requiredType || !!entity.extensions?.includes(requiredType)
126
+ }
127
+
128
+ export type ResolveUnitInputValuesOptions = {
129
+ library: LibraryModel
130
+ inputName: string
131
+ resolvedInput: ResolvedInstanceInput
132
+ dependencyInstanceType: VersionedName
133
+ captured: CapturedEntitySnapshotValue[]
134
+ effectiveOutputType?: string
135
+ effectiveRootOutputType?: string
136
+ }
137
+
138
+ export function resolveUnitInputValues(options: ResolveUnitInputValuesOptions): UnitInputValue[] {
139
+ const dependencyComponent = options.library.components[options.dependencyInstanceType]!
140
+
141
+ const outputSpec = dependencyComponent.outputs[options.resolvedInput.input.output]
142
+ if (!outputSpec) {
143
+ throw new Error(
144
+ `Output "${options.resolvedInput.input.output}" is not defined on component "${options.dependencyInstanceType}"`,
145
+ )
146
+ }
147
+
148
+ const effectiveOutputType = options.effectiveOutputType ?? outputSpec.type
149
+ const effectiveRootOutputType = options.effectiveRootOutputType ?? effectiveOutputType
150
+
151
+ const outputEntity = options.library.entities[effectiveRootOutputType]
152
+ if (!outputEntity) {
153
+ throw new Error(`Entity type "${effectiveRootOutputType}" is not defined in the library`)
154
+ }
155
+
156
+ const effectiveOutputEntity = options.library.entities[effectiveOutputType]
157
+ if (!effectiveOutputEntity) {
158
+ throw new Error(`Entity type "${effectiveOutputType}" is not defined in the library`)
159
+ }
160
+
161
+ if (options.captured.length === 0) {
162
+ return []
163
+ }
164
+
165
+ const capturedValues = options.captured.map(item => {
166
+ if (item.ok) {
167
+ return item.value
168
+ }
169
+
170
+ throw new Error(
171
+ `Cannot use captured output "${options.resolvedInput.input.output}" from instance "${options.resolvedInput.input.instanceId}" for input "${options.inputName}": failed to reconstruct entity snapshot "${item.error.snapshotId}": ${item.error.message}`,
172
+ )
173
+ })
174
+
175
+ const explicitPath = options.resolvedInput.input.path
176
+ if (explicitPath) {
177
+ const inclusionSteps = resolveInclusionPath({
178
+ library: options.library,
179
+ rootEntity: outputEntity,
180
+ path: explicitPath,
181
+ })
182
+
183
+ const targetType = inclusionSteps.at(-1)?.type
184
+ if (!targetType) {
185
+ throw new Error(
186
+ `Invalid input path "${explicitPath}" for output "${options.resolvedInput.input.output}": no inclusion steps resolved.`,
187
+ )
188
+ }
189
+
190
+ const targetEntity = options.library.entities[targetType]
191
+ if (!targetEntity) {
192
+ throw new Error(`Entity type "${targetType}" is not defined in the library`)
193
+ }
194
+
195
+ if (!isEntityAssignableTo(targetEntity, options.resolvedInput.type)) {
196
+ throw new Error(
197
+ `Cannot use output "${options.resolvedInput.input.output}" path "${explicitPath}" of type "${effectiveRootOutputType}" from instance "${options.resolvedInput.input.instanceId}" for input "${options.inputName}" of type "${options.resolvedInput.type}": resolved path type is "${targetEntity.type}".`,
198
+ )
199
+ }
200
+
201
+ const extractedValues = extractByInclusionPath({
202
+ values: capturedValues,
203
+ steps: inclusionSteps,
204
+ outputName: options.resolvedInput.input.output,
205
+ instanceId: options.resolvedInput.input.instanceId,
206
+ path: explicitPath,
207
+ })
208
+
209
+ return extractedValues.map(extracted => ({
210
+ value: extracted,
211
+ source: {
212
+ instanceId: options.resolvedInput.input.instanceId,
213
+ output: options.resolvedInput.input.output,
214
+ path: explicitPath,
215
+ },
216
+ }))
217
+ }
218
+
219
+ // if output type matches input type or extends it, no transformation needed
220
+ if (
221
+ options.resolvedInput.type === effectiveOutputType ||
222
+ effectiveOutputEntity.extensions?.includes(options.resolvedInput.type)
223
+ ) {
224
+ return capturedValues.map(value => ({
225
+ value: value,
226
+ source: {
227
+ instanceId: options.resolvedInput.input.instanceId,
228
+ output: options.resolvedInput.input.output,
229
+ path: options.resolvedInput.input.path,
230
+ },
231
+ }))
232
+ }
233
+
234
+ // otherwise, find matching inclusion to perform transformation
235
+ const inclusion = outputEntity.inclusions?.find(inc => inc.type === options.resolvedInput.type)
236
+ if (!inclusion) {
237
+ throw new Error(
238
+ `Cannot use output "${options.resolvedInput.input.output}" of type "${effectiveOutputType}" from instance "${options.resolvedInput.input.instanceId}" for input "${options.inputName}" of type "${options.resolvedInput.type}": no matching inclusion found in entity "${outputEntity.type}"`,
239
+ )
240
+ }
241
+
242
+ const extractedValues: unknown[] = []
243
+
244
+ for (const value of capturedValues) {
245
+ if (typeof value !== "object" || value === null) {
246
+ throw new Error(
247
+ `Cannot extract field "${inclusion.field}" from non-object output "${options.resolvedInput.input.output}" of instance "${options.resolvedInput.input.instanceId}".`,
248
+ )
249
+ }
250
+
251
+ const extracted = (value as Record<string, unknown>)[inclusion.field]
252
+
253
+ if (extracted === undefined || extracted === null) {
254
+ continue
255
+ }
256
+
257
+ if (inclusion.multiple) {
258
+ if (!Array.isArray(extracted)) {
259
+ throw new Error(
260
+ `Expected inclusion field "${inclusion.field}" on output "${options.resolvedInput.input.output}" to be an array.`,
261
+ )
262
+ }
263
+
264
+ for (const item of extracted) {
265
+ extractedValues.push(item)
266
+ }
267
+ continue
268
+ }
269
+
270
+ extractedValues.push(extracted)
271
+ }
272
+
273
+ return extractedValues.map(extracted => ({
274
+ value: extracted,
275
+ source: {
276
+ instanceId: options.resolvedInput.input.instanceId,
277
+ output: options.resolvedInput.input.output,
278
+ path: options.resolvedInput.input.path,
279
+ },
280
+ }))
281
+ }
@@ -11,6 +11,7 @@ import {
11
11
  projectUnlockStateSchema,
12
12
  terminalSessionOutputSchema,
13
13
  workerUnitRegistrationEventSchema,
14
+ workerVersionStatusEventSchema,
14
15
  } from "../shared"
15
16
 
16
17
  export type PubSubEventMap = {
@@ -23,6 +24,7 @@ export type PubSubEventMap = {
23
24
  "operation-instance-log": [operationId: string, instanceId: string]
24
25
  "worker-unit-registration": [projectId: string, workerVersionId: string]
25
26
  "worker-version-log": [projectId: string, workerVersionId: string]
27
+ "worker-version-status": [projectId: string]
26
28
  }
27
29
 
28
30
  const eventSchemas = {
@@ -35,6 +37,7 @@ const eventSchemas = {
35
37
  "operation-instance-log": z.custom<OperationLog>(),
36
38
  "worker-unit-registration": workerUnitRegistrationEventSchema,
37
39
  "worker-version-log": z.custom<WorkerVersionLog>(),
40
+ "worker-version-status": workerVersionStatusEventSchema,
38
41
  }
39
42
 
40
43
  type PubSubEventSchemas = typeof eventSchemas
@@ -1,16 +1,13 @@
1
- import type {
2
- GenericName,
3
- InstanceId,
4
- InstanceStatusField,
5
- UnitConfig,
6
- UnitPage,
7
- UnitTerminal,
8
- UnitTrigger,
9
- UnitWorker,
10
- VersionedName,
11
- } from "@highstate/contract"
1
+ import type { GenericName, InstanceId, UnitConfig, VersionedName } from "@highstate/contract"
12
2
  import type { Artifact } from "../database"
13
3
 
4
+ export type RawPulumiOutputValue = {
5
+ value: unknown
6
+ secret?: boolean
7
+ }
8
+
9
+ export type RawPulumiOutputs = Record<string, RawPulumiOutputValue>
10
+
14
11
  export type OperationType = "update" | "destroy" | "refresh"
15
12
 
16
13
  export type UnitStateUpdate = {
@@ -57,46 +54,11 @@ export type UnitStateUpdate = {
57
54
  operationType: OperationType
58
55
 
59
56
  /**
60
- * The CRC32 hash of the output produced by the unit.
61
- */
62
- outputHash?: number | null
63
-
64
- /**
65
- * The status fields produced by the unit.
66
- */
67
- statusFields?: InstanceStatusField[] | null
68
-
69
- /**
70
- * The terminals produced by the unit.
71
- */
72
- terminals?: UnitTerminal[] | null
73
-
74
- /**
75
- * The pages produced by the unit.
76
- */
77
- pages?: UnitPage[] | null
78
-
79
- /**
80
- * The triggers produced by the unit.
81
- */
82
- triggers?: UnitTrigger[] | null
83
-
84
- /**
85
- * The values of the secrets produced by the unit.
86
- */
87
- secrets?: Record<string, unknown> | null
88
-
89
- /**
90
- * The workers produced by the unit.
91
- */
92
- workers?: UnitWorker[] | null
93
-
94
- /**
95
- * The IDs of the artifacts exported by the unit.
57
+ * Raw Pulumi outputs produced by the unit.
96
58
  *
97
- * The keys are the output names, and the values are the IDs of the artifacts exported for that output.
59
+ * Parsing and persistence is handled in the business layer.
98
60
  */
99
- exportedArtifactIds?: Record<string, string[]> | null
61
+ rawOutputs?: RawPulumiOutputs | null
100
62
  }
101
63
  )
102
64
 
@@ -108,6 +70,11 @@ export type UnitOptions = {
108
70
  */
109
71
  projectId: string
110
72
 
73
+ /**
74
+ * The operation ID that triggered this run.
75
+ */
76
+ operationId?: string
77
+
111
78
  /**
112
79
  * The state ID to use as the stack name.
113
80
  */
@@ -152,11 +119,6 @@ export type UnitUpdateOptions = UnitOptions & {
152
119
  */
153
120
  config: UnitConfig
154
121
 
155
- /**
156
- * The secret values to pass to the unit.
157
- */
158
- secrets: Record<string, unknown>
159
-
160
122
  /**
161
123
  * Artifact required by this instance.
162
124
  */
@@ -193,6 +155,13 @@ export type UnitDestroyOptions = UnitOptions & {
193
155
  * Delete the stack state even if the destroy operation fails.
194
156
  */
195
157
  forceDeleteState?: boolean
158
+
159
+ /**
160
+ * Whether the instance has resource hooks.
161
+ *
162
+ * If false, the runner may skip running the Pulumi program on destroy.
163
+ */
164
+ hasResourceHooks?: boolean
196
165
  }
197
166
 
198
167
  export interface RunnerBackend {