@highstate/backend 0.9.4 → 0.9.6

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.
@@ -2,7 +2,12 @@ import type { LibraryBackend } from "../library"
2
2
  import type { ProjectBackend } from "../project"
3
3
  import type { StateBackend, StateManager } from "../state"
4
4
  import type { Logger } from "pino"
5
- import { isUnitModel, type ComponentModel, type InstanceModel } from "@highstate/contract"
5
+ import {
6
+ isUnitModel,
7
+ parseInstanceId,
8
+ type ComponentModel,
9
+ type InstanceModel,
10
+ } from "@highstate/contract"
6
11
  import { unique } from "remeda"
7
12
  import {
8
13
  applyPartialInstanceState,
@@ -22,8 +27,12 @@ import {
22
27
  type ResolvedInstanceInput,
23
28
  } from "../shared"
24
29
 
30
+ export type OperationPhase = "update" | "destroy" | "refresh"
31
+
25
32
  export class OperationWorkset {
26
- private readonly affectedInstanceIdSet = new Set<string>()
33
+ private readonly instanceIdsToUpdate = new Set<string>()
34
+ private readonly instanceIdsToDestroy = new Set<string>()
35
+
27
36
  private readonly instanceMap = new Map<string, InstanceModel>()
28
37
  private readonly instanceChildrenMap = new Map<string, InstanceModel[]>()
29
38
 
@@ -67,8 +76,24 @@ export class OperationWorkset {
67
76
  return instance
68
77
  }
69
78
 
70
- public isAffected(instanceId: string): boolean {
71
- return this.affectedInstanceIdSet.has(instanceId)
79
+ public getAffectedInstanceIds(phase: OperationPhase): string[] {
80
+ if (phase === "destroy") {
81
+ return Array.from(this.instanceIdsToDestroy)
82
+ }
83
+
84
+ return Array.from(this.instanceIdsToUpdate)
85
+ }
86
+
87
+ public getInstanceOrUndefined(instanceId: string): InstanceModel | undefined {
88
+ return this.instanceMap.get(instanceId)
89
+ }
90
+
91
+ public isAffected(instanceId: string, phase: OperationPhase): boolean {
92
+ if (phase === "destroy") {
93
+ return this.instanceIdsToDestroy.has(instanceId)
94
+ }
95
+
96
+ return this.instanceIdsToUpdate.has(instanceId)
72
97
  }
73
98
 
74
99
  public updateState(update: InstanceStateUpdate): InstanceState {
@@ -76,7 +101,7 @@ export class OperationWorkset {
76
101
  this.stateManager.emitStatePatch(this.operation.projectId, createInstanceStatePatch(update))
77
102
 
78
103
  if (finalState.parentId) {
79
- this.recalculateCompositeState(finalState.parentId)
104
+ this.recalculateCompositeInstanceState(finalState.parentId)
80
105
  }
81
106
 
82
107
  return finalState
@@ -128,25 +153,44 @@ export class OperationWorkset {
128
153
 
129
154
  public emitAffectedInitialStates(): void {
130
155
  for (const state of this.initialStateMap.values()) {
131
- if (this.affectedInstanceIdSet.has(state.id)) {
156
+ if (this.instanceIdsToUpdate.has(state.id)) {
132
157
  this.stateManager.emitStatePatch(this.operation.projectId, state)
133
158
  }
134
159
  }
135
160
  }
136
161
 
137
- public getAffectedCompositeChildren(instanceId: string): InstanceModel[] {
162
+ public getAffectedCompositeChildren(instanceId: string, phase: OperationPhase): InstanceModel[] {
138
163
  const children = this.instanceChildrenMap.get(instanceId)
139
164
  if (!children) {
140
165
  return []
141
166
  }
142
167
 
143
- return children.filter(child => this.affectedInstanceIdSet.has(child.id))
168
+ if (phase === "destroy") {
169
+ return children.filter(child => this.instanceIdsToDestroy.has(child.id))
170
+ }
171
+
172
+ return children.filter(child => this.instanceIdsToUpdate.has(child.id))
144
173
  }
145
174
 
146
175
  public getState(instanceId: string): InstanceState | undefined {
147
176
  return this.stateMap.get(instanceId)
148
177
  }
149
178
 
179
+ public getParentId(instanceId: string, phase: OperationPhase): string | null {
180
+ if (phase === "destroy") {
181
+ const state = this.stateMap.get(instanceId)
182
+ if (!state) {
183
+ return null
184
+ }
185
+
186
+ return state.parentId
187
+ }
188
+
189
+ const instance = this.getInstance(instanceId)
190
+
191
+ return instance.parentId ?? null
192
+ }
193
+
150
194
  public getDependentStates(instanceId: string): InstanceState[] {
151
195
  const dependentStateIds = this.dependentStateIdMap.get(instanceId)
152
196
  if (!dependentStateIds) {
@@ -158,7 +202,7 @@ export class OperationWorkset {
158
202
  .filter((state): state is InstanceState => !!state)
159
203
  }
160
204
 
161
- private recalculateCompositeState(instanceId: string): void {
205
+ private recalculateCompositeInstanceState(instanceId: string): void {
162
206
  const state = this.stateMap.get(instanceId) ?? createInstanceState(instanceId)
163
207
  let currentResourceCount = 0
164
208
  let totalResourceCount = 0
@@ -186,7 +230,7 @@ export class OperationWorkset {
186
230
  this.stateManager.emitStatePatch(this.operation.projectId, updatedState)
187
231
 
188
232
  if (state.parentId) {
189
- this.recalculateCompositeState(state.parentId)
233
+ this.recalculateCompositeInstanceState(state.parentId)
190
234
  }
191
235
  }
192
236
 
@@ -215,9 +259,9 @@ export class OperationWorkset {
215
259
  }
216
260
  }
217
261
 
218
- private async extendForUpdate(): Promise<void> {
262
+ private async calculateInstanceIdsToUpdate(): Promise<void> {
219
263
  const traverse = async (instanceId: string) => {
220
- if (this.affectedInstanceIdSet.has(instanceId)) {
264
+ if (this.instanceIdsToUpdate.has(instanceId)) {
221
265
  return
222
266
  }
223
267
 
@@ -238,51 +282,62 @@ export class OperationWorkset {
238
282
  const { inputHash: expectedInputHash } = await this.inputHashResolver(instance.id)
239
283
 
240
284
  if (this.operation.options.forceUpdateDependencies) {
241
- this.affectedInstanceIdSet.add(instanceId)
285
+ this.instanceIdsToUpdate.add(instanceId)
242
286
  return
243
287
  }
244
288
 
245
289
  if (state?.status !== "created" || state.inputHash !== expectedInputHash) {
246
- this.affectedInstanceIdSet.add(instanceId)
290
+ this.instanceIdsToUpdate.add(instanceId)
247
291
  }
248
292
  }
249
293
 
250
294
  // 1. extend affected instance IDs with their not-created or not-up-to-date dependencies (only for "update" operations)
251
- for (const instanceId of this.operation.instanceIds) {
295
+ for (const instanceId of this.operation.requestedInstanceIds) {
252
296
  if (this.operation.type === "update") {
253
297
  await traverse(instanceId)
254
298
  }
255
299
 
256
- this.affectedInstanceIdSet.add(instanceId)
300
+ this.instanceIdsToUpdate.add(instanceId)
257
301
  }
258
302
 
259
303
  // 2. extend affected instance IDs with the children of the affected composite instances
260
- const compositeInstanceQueue = Array.from(this.affectedInstanceIdSet)
304
+ const compositeInstanceQueue = Array.from(this.instanceIdsToUpdate)
261
305
  while (compositeInstanceQueue.length > 0) {
262
306
  const childId = compositeInstanceQueue.pop()!
263
307
  const children = this.instanceChildrenMap.get(childId) ?? []
264
308
 
265
309
  for (const child of children) {
266
310
  compositeInstanceQueue.push(child.id)
267
- this.affectedInstanceIdSet.add(child.id)
311
+
312
+ if (this.operation.options.forceUpdateChildren) {
313
+ this.instanceIdsToUpdate.add(child.id)
314
+ continue
315
+ }
316
+
317
+ const state = this.stateMap.get(child.id)
318
+ const { inputHash: expectedInputHash } = await this.inputHashResolver(child.id)
319
+
320
+ if (state?.status !== "created" || state.inputHash !== expectedInputHash) {
321
+ this.instanceIdsToUpdate.add(child.id)
322
+ }
268
323
  }
269
324
  }
270
325
 
271
326
  // 3. detect composite instance children and include their parents (recursively)
272
- for (const instanceId of this.affectedInstanceIdSet) {
327
+ for (const instanceId of this.instanceIdsToUpdate) {
273
328
  let instance = this.instanceMap.get(instanceId)
274
329
  while (instance?.parentId) {
275
- this.affectedInstanceIdSet.add(instance.parentId)
330
+ this.instanceIdsToUpdate.add(instance.parentId)
276
331
  instance = this.instanceMap.get(instance.parentId)
277
332
  }
278
333
  }
279
334
 
280
- this.operation.affectedInstanceIds = Array.from(this.affectedInstanceIdSet)
335
+ this.operation.instanceIdsToUpdate = Array.from(this.instanceIdsToUpdate)
281
336
  }
282
337
 
283
- private extendForDestroy() {
338
+ private calculateInstanceIdsToDestroy() {
284
339
  const traverse = (instanceKey: string) => {
285
- if (this.affectedInstanceIdSet.has(instanceKey)) {
340
+ if (this.instanceIdsToDestroy.has(instanceKey)) {
286
341
  return
287
342
  }
288
343
 
@@ -295,46 +350,74 @@ export class OperationWorkset {
295
350
 
296
351
  for (const dependentId of dependentIds) {
297
352
  traverse(dependentId)
298
- this.affectedInstanceIdSet.add(instanceKey)
353
+ this.instanceIdsToDestroy.add(dependentId)
299
354
  }
300
355
  }
301
356
 
302
- // 1. extend affected instance IDs with their created dependents (if not forbidden by the operation options)
303
- for (const instanceId of this.operation.instanceIds) {
304
- const instance = this.instanceMap.get(instanceId)
305
- if (!instance) {
306
- throw new Error(`Instance not found: ${instanceId}`)
307
- }
357
+ if (this.operation.type === "destroy" || this.operation.type === "recreate") {
358
+ // 1.a. extend affected instance IDs with their created dependents (if not forbidden by the operation options)
359
+ for (const instanceId of this.operation.requestedInstanceIds) {
360
+ const instance = this.instanceMap.get(instanceId)
361
+ if (!instance) {
362
+ throw new Error(`Instance not found: ${instanceId}`)
363
+ }
308
364
 
309
- if (this.operation.options.destroyDependentInstances) {
310
- traverse(instance.id)
365
+ if (this.operation.options.destroyDependentInstances) {
366
+ traverse(instance.id)
367
+ }
368
+
369
+ this.instanceIdsToDestroy.add(instanceId)
311
370
  }
371
+ } else if (this.operation.type === "update") {
372
+ // 1.b. find all children instances of the affected to-update instances which are in the state map, but not in the instance map
373
+ // in other words, this code cleans up child instances which are not produced by composite instances anymore
374
+
375
+ for (const instanceId of this.operation.instanceIdsToUpdate) {
376
+ const [type] = parseInstanceId(instanceId)
377
+ const component = this.library.components[type]
312
378
 
313
- this.affectedInstanceIdSet.add(instanceId)
379
+ if (!component || isUnitModel(component)) {
380
+ // ignore non-composite instances
381
+ continue
382
+ }
383
+
384
+ const childrenQueue = [...(this.stateChildIdMap.get(instanceId) ?? [])]
385
+
386
+ while (childrenQueue.length > 0) {
387
+ const childId = childrenQueue.pop()!
388
+ if (!this.instanceMap.has(childId)) {
389
+ this.instanceIdsToDestroy.add(childId)
390
+ }
391
+
392
+ childrenQueue.push(...(this.stateChildIdMap.get(childId) ?? []))
393
+ }
394
+ }
395
+ } else {
396
+ return
314
397
  }
315
398
 
316
399
  // 2. extend affected instance IDs with the children of the affected composite instances
317
- const compositeInstanceQueue = Array.from(this.affectedInstanceIdSet)
400
+ const compositeInstanceQueue = Array.from(this.instanceIdsToDestroy)
318
401
  while (compositeInstanceQueue.length > 0) {
319
402
  const childId = compositeInstanceQueue.pop()!
320
403
  const children = this.stateChildIdMap.get(childId) ?? []
321
404
 
322
405
  for (const child of children) {
323
406
  compositeInstanceQueue.push(child)
324
- this.affectedInstanceIdSet.add(child)
407
+ this.instanceIdsToDestroy.add(child)
325
408
  }
326
409
  }
327
410
 
328
411
  // 3. detect composite instance children and include their parents (recursively)
329
- for (const instanceId of this.affectedInstanceIdSet) {
412
+ for (const instanceId of this.instanceIdsToDestroy) {
330
413
  let state = this.stateMap.get(instanceId)
331
414
  while (state?.parentId) {
332
- this.affectedInstanceIdSet.add(state.parentId)
415
+ this.instanceIdsToDestroy.add(state.parentId)
333
416
  state = this.stateMap.get(state.parentId)
334
417
  }
335
418
  }
336
419
 
337
- this.operation.affectedInstanceIds = Array.from(this.affectedInstanceIdSet)
420
+ this.operation.instanceIdsToDestroy = Array.from(this.instanceIdsToDestroy)
338
421
  }
339
422
 
340
423
  private getSourceHashIfApplicable(
@@ -366,18 +449,7 @@ export class OperationWorkset {
366
449
  }
367
450
 
368
451
  public getLockInstanceIds(): string[] {
369
- const instanceIds = new Set<string>(this.operation.affectedInstanceIds)
370
-
371
- // enrich with the parent IDs of the affected instances in order to lock and update parent states
372
- for (const instanceId of this.operation.affectedInstanceIds) {
373
- const instance = this.getInstance(instanceId)
374
-
375
- if (instance.parentId) {
376
- instanceIds.add(instance.parentId)
377
- }
378
- }
379
-
380
- return Array.from(instanceIds)
452
+ return Array.from(this.instanceIdsToUpdate.union(this.instanceIdsToDestroy))
381
453
  }
382
454
 
383
455
  public static async load(
@@ -422,8 +494,13 @@ export class OperationWorkset {
422
494
  if (worksetInstance) {
423
495
  worksetInstance.outputs = instance.instance.outputs
424
496
  worksetInstance.resolvedOutputs = instance.instance.resolvedOutputs
425
- } else {
497
+ } else if (instance.instance.parentId) {
426
498
  workset.addInstance(instance.instance)
499
+ } else {
500
+ workset.logger.warn(
501
+ `ignoring instance "${instance.instance.id}" from composite instance state because it is not in the project or is not a part of known composite instance`,
502
+ )
503
+ continue
427
504
  }
428
505
 
429
506
  for (const child of instance.children) {
@@ -492,20 +569,12 @@ export class OperationWorkset {
492
569
  { promiseCache: workset.inputHashResolverPromiseCache },
493
570
  )
494
571
 
495
- switch (operation.type) {
496
- case "update":
497
- case "preview":
498
- case "refresh":
499
- await workset.extendForUpdate()
500
- break
501
- case "recreate":
502
- workset.extendForDestroy()
503
- break
504
- case "destroy":
505
- workset.extendForDestroy()
506
- break
572
+ if (operation.type !== "destroy") {
573
+ await workset.calculateInstanceIdsToUpdate()
507
574
  }
508
575
 
576
+ workset.calculateInstanceIdsToDestroy()
577
+
509
578
  return workset
510
579
  }
511
580
  }
@@ -24,9 +24,7 @@ import {
24
24
  tryWrapAbortErrorLike,
25
25
  valueToString,
26
26
  } from "../common"
27
- import { OperationWorkset } from "./operation-workset"
28
-
29
- type OperationPhase = "update" | "destroy" | "refresh"
27
+ import { OperationWorkset, type OperationPhase } from "./operation-workset"
30
28
 
31
29
  export class RuntimeOperation {
32
30
  private readonly abortController = new AbortController()
@@ -117,10 +115,14 @@ export class RuntimeOperation {
117
115
  }
118
116
 
119
117
  private async processOperation(): Promise<void> {
120
- this.operation.affectedInstanceIds = this.workset.operation.affectedInstanceIds
118
+ this.operation.instanceIdsToUpdate = this.workset.operation.instanceIdsToUpdate
119
+ this.operation.instanceIdsToDestroy = this.workset.operation.instanceIdsToDestroy
121
120
 
122
121
  this.logger.info(
123
- { affectedInstanceIds: this.operation.affectedInstanceIds },
122
+ {
123
+ instanceIdsToUpdate: this.operation.instanceIdsToUpdate,
124
+ instanceIdsToDestroy: this.operation.instanceIdsToDestroy,
125
+ },
124
126
  "operation started",
125
127
  )
126
128
 
@@ -130,9 +132,9 @@ export class RuntimeOperation {
130
132
  this.currentPhase = phase
131
133
 
132
134
  const promises: Promise<void>[] = []
133
- for (const instanceId of this.operation.affectedInstanceIds) {
134
- const instance = this.workset.getInstance(instanceId)
135
- if (instance.parentId && this.workset.isAffected(instance.parentId)) {
135
+ for (const instanceId of this.workset.getAffectedInstanceIds(phase)) {
136
+ const parentId = this.workset.getParentId(instanceId, phase)
137
+ if (parentId && this.workset.isAffected(parentId, phase)) {
136
138
  // do not call the operation for child instances of affected composites,
137
139
  // they will be called by their parent instance
138
140
  continue
@@ -163,19 +165,25 @@ export class RuntimeOperation {
163
165
  }
164
166
 
165
167
  private getInstancePromiseForOperation(instanceId: string): Promise<void> {
166
- const instance = this.workset.getInstance(instanceId)
167
- const component = this.workset.library.components[instance.type]
168
+ const [instanceType] = parseInstanceId(instanceId)
169
+ const component = this.workset.library.components[instanceType]
168
170
 
169
171
  if (isUnitModel(component)) {
170
- return this.getUnitPromise(instance)
172
+ return this.getUnitPromise(instanceId)
171
173
  }
172
174
 
173
- return this.getCompositePromise(instance)
175
+ return this.getCompositePromise(instanceId)
174
176
  }
175
177
 
176
178
  private getOperationPhases(): OperationPhase[] {
177
179
  switch (this.operation.type) {
178
- case "update":
180
+ case "update": {
181
+ if (this.operation.instanceIdsToDestroy.length > 0) {
182
+ return ["destroy", "update"]
183
+ }
184
+
185
+ return ["update"]
186
+ }
179
187
  case "preview":
180
188
  return ["update"]
181
189
  case "recreate":
@@ -187,35 +195,36 @@ export class RuntimeOperation {
187
195
  }
188
196
  }
189
197
 
190
- private async getUnitPromise(instance: InstanceModel): Promise<void> {
198
+ private async getUnitPromise(instanceId: string): Promise<void> {
191
199
  switch (this.currentPhase) {
192
200
  case "update": {
193
- return this.updateUnit(instance)
201
+ return this.updateUnit(instanceId)
194
202
  }
195
203
  case "destroy": {
196
- return this.destroyUnit(instance.id)
204
+ return this.destroyUnit(instanceId)
197
205
  }
198
206
  case "refresh": {
199
- return this.refreshUnit(instance.id)
207
+ return this.refreshUnit(instanceId)
200
208
  }
201
209
  }
202
210
  }
203
211
 
204
- private async getCompositePromise(instance: InstanceModel): Promise<void> {
205
- const logger = this.logger.child({ instanceId: instance.id })
212
+ private async getCompositePromise(instanceId: string): Promise<void> {
213
+ const logger = this.logger.child({ instanceId })
206
214
 
207
- return this.getInstancePromise(instance.id, async () => {
208
- const state = this.workset.getState(instance.id) ?? createInstanceState(instance.id)
215
+ return this.getInstancePromise(instanceId, async () => {
216
+ const state = this.workset.getState(instanceId) ?? createInstanceState(instanceId)
217
+ const instance = this.workset.getInstanceOrUndefined(instanceId)
209
218
 
210
219
  this.updateInstanceState({
211
220
  ...state,
212
- parentId: instance.parentId,
221
+ parentId: instance?.parentId,
213
222
  latestOperationId: this.operation.id,
214
223
  status: this.getStatusByOperationType(),
215
224
  error: null,
216
225
  })
217
226
 
218
- const children = this.workset.getAffectedCompositeChildren(instance.id)
227
+ const children = this.workset.getAffectedCompositeChildren(instanceId, this.currentPhase)
219
228
  const childPromises: Promise<void>[] = []
220
229
 
221
230
  if (children.length) {
@@ -225,6 +234,14 @@ export class RuntimeOperation {
225
234
  }
226
235
 
227
236
  for (const child of children) {
237
+ if (
238
+ !this.operation.options.forceUpdateChildren &&
239
+ !this.workset.isAffected(child.id, this.currentPhase)
240
+ ) {
241
+ // skip children that are not affected by the operation
242
+ continue
243
+ }
244
+
228
245
  logger.debug(`waiting for child: "%s"`, child.id)
229
246
  childPromises.push(this.getInstancePromiseForOperation(child.id))
230
247
  }
@@ -237,18 +254,18 @@ export class RuntimeOperation {
237
254
  }
238
255
 
239
256
  this.updateInstanceState({
240
- id: instance.id,
257
+ id: instanceId,
241
258
  status: this.operation.type === "destroy" ? "not_created" : "created",
242
- inputHash: await this.workset.getUpToDateInputHash(instance),
259
+ inputHash: instance ? await this.workset.getUpToDateInputHash(instance) : undefined,
243
260
  })
244
261
  } catch (error) {
245
262
  if (isAbortErrorLike(error)) {
246
- this.workset.restoreInitialStatus(instance.id)
263
+ this.workset.restoreInitialStatus(instanceId)
247
264
  return
248
265
  }
249
266
 
250
267
  this.updateInstanceState({
251
- id: instance.id,
268
+ id: instanceId,
252
269
  status: "error",
253
270
  error: errorToString(error),
254
271
  })
@@ -256,8 +273,10 @@ export class RuntimeOperation {
256
273
  })
257
274
  }
258
275
 
259
- private updateUnit(instance: InstanceModel): Promise<void> {
260
- return this.getInstancePromise(instance.id, async logger => {
276
+ private updateUnit(instanceId: string): Promise<void> {
277
+ return this.getInstancePromise(instanceId, async logger => {
278
+ const instance = this.workset.getInstance(instanceId)
279
+
261
280
  this.updateInstanceState({
262
281
  id: instance.id,
263
282
  parentId: instance.parentId,
@@ -323,7 +342,7 @@ export class RuntimeOperation {
323
342
  const dependencyPromises: Promise<void>[] = []
324
343
 
325
344
  for (const dependency of dependencies) {
326
- if (!this.operation.affectedInstanceIds.includes(dependency.id)) {
345
+ if (!this.operation.instanceIdsToUpdate.includes(dependency.id)) {
327
346
  // skip dependencies that are not affected by the operation
328
347
  continue
329
348
  }
@@ -19,6 +19,10 @@ import {
19
19
  } from "../shared"
20
20
 
21
21
  type CompositeInstanceEvent =
22
+ | {
23
+ type: "evaluation-started"
24
+ instanceId: string
25
+ }
22
26
  | {
23
27
  type: "updated"
24
28
  instance: CompositeInstance
@@ -157,6 +161,10 @@ export class ProjectManager {
157
161
  await this.projectLockManager.getLock(projectId).lockInstances(instanceIds, async () => {
158
162
  this.logger.debug({ instanceIds }, "evaluating composite instances")
159
163
 
164
+ for (const instanceId of instanceIds) {
165
+ this.compositeInstanceEE.emit(projectId, { type: "evaluation-started", instanceId })
166
+ }
167
+
160
168
  const [
161
169
  { instances, resolvedInputs, stateMap, resolveInputHash },
162
170
  topLevelCompositeChildrenIds,
@@ -6,6 +6,9 @@ export type LibraryModel = {
6
6
  }
7
7
 
8
8
  export type LibraryUpdate =
9
+ | {
10
+ type: "reload-started" | "reload-completed"
11
+ }
9
12
  | {
10
13
  type: "component-updated"
11
14
  component: ComponentModel
@@ -19,6 +19,14 @@ export const operationOptionsSchema = z.object({
19
19
  */
20
20
  forceUpdateDependencies: z.boolean().default(false),
21
21
 
22
+ /**
23
+ * Whether to force update all children of the composite instances even if they are not changed.
24
+ *
25
+ * Only applicable for `update`, `preview`, `recreate`, and `refresh` operations.
26
+ * By default, `false`.
27
+ */
28
+ forceUpdateChildren: z.boolean().default(false),
29
+
22
30
  /**
23
31
  * Whether to destroy all dependents of the instances when destroying them.
24
32
  *
@@ -72,8 +80,11 @@ export const projectOperationSchema = z.object({
72
80
 
73
81
  projectId: z.string(),
74
82
  type: operationTypeSchema,
75
- instanceIds: z.array(z.string()),
76
- affectedInstanceIds: z.array(z.string()).default(() => []),
83
+
84
+ requestedInstanceIds: z.array(z.string()),
85
+
86
+ instanceIdsToUpdate: z.array(z.string()).default(() => []),
87
+ instanceIdsToDestroy: z.array(z.string()).default(() => []),
77
88
 
78
89
  options: operationOptionsSchema.default(() => ({})),
79
90
 
@@ -59,7 +59,7 @@ export const createValidationResolver = defineGraphResolver<
59
59
 
60
60
  return {
61
61
  status: "invalid-args",
62
- errorText: ajv.errorsText(),
62
+ errorText: `invalid argument "${name}": ${ajv.errorsText()}`,
63
63
  }
64
64
  }
65
65
  }
@@ -70,14 +70,14 @@ export const createValidationResolver = defineGraphResolver<
70
70
  if (inputInstance?.status !== "ok") {
71
71
  return {
72
72
  status: "invalid-inputs",
73
- errorText: `instance "${input.input.instanceId}" is invalid`,
73
+ errorText: `instance "${input.input.instanceId}" has errors`,
74
74
  }
75
75
  }
76
76
  }
77
77
  }
78
78
 
79
79
  for (const [name, input] of Object.entries(component.inputs)) {
80
- if (!input.required || input.multiple) {
80
+ if (!input.required) {
81
81
  continue
82
82
  }
83
83
 
@@ -13,9 +13,16 @@ export const instanceStatusSchema = z.enum([
13
13
  "unknown",
14
14
  ])
15
15
 
16
+ export const instanceStatusFieldValueSchema = z.union([
17
+ z.string(),
18
+ z.number(),
19
+ z.boolean(),
20
+ z.array(z.string()),
21
+ ])
22
+
16
23
  export const instanceStatusFieldSchema = z.object({
17
24
  name: z.string(),
18
- value: z.string().optional(),
25
+ value: instanceStatusFieldValueSchema.optional(),
19
26
  displayName: z.string().optional(),
20
27
  sensitive: z.boolean().optional(),
21
28
  url: z.string().optional(),
@@ -138,6 +145,7 @@ export const instanceStateUpdateSchema = z
138
145
  })
139
146
  .partial()
140
147
 
148
+ export type InstanceStatusFieldValue = z.infer<typeof instanceStatusFieldValueSchema>
141
149
  export type InstanceStatusField = z.infer<typeof instanceStatusFieldSchema>
142
150
  export type InstanceTerminal = z.infer<typeof instanceTerminalSchema>
143
151