@highstate/backend-api 0.27.0 → 0.29.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.
@@ -0,0 +1,593 @@
1
+ import type {
2
+ InstanceState as BackendInstanceState,
3
+ Operation as BackendOperation,
4
+ OperationPhase as BackendOperationPhase,
5
+ FullProjectModel as BackendProjectModel,
6
+ ProjectOutput,
7
+ } from "@highstate/backend/shared"
8
+ import { create, fromJson, type JsonValue, toJson } from "@bufbuild/protobuf"
9
+ import { type Timestamp, timestampFromDate, ValueSchema } from "@bufbuild/protobuf/wkt"
10
+ import {
11
+ type Component,
12
+ ComponentKind,
13
+ ComponentSchema,
14
+ type Entity,
15
+ EntitySchema,
16
+ EvaluationStatus,
17
+ type Hub,
18
+ HubSchema,
19
+ type Instance,
20
+ type InstanceCustomStatus,
21
+ InstanceCustomStatusSchema,
22
+ InstanceOperationStatus,
23
+ InstanceSchema,
24
+ InstanceSource,
25
+ type InstanceState,
26
+ InstanceStateSchema,
27
+ InstanceStatus,
28
+ type Library,
29
+ LibrarySchema,
30
+ type Operation,
31
+ type OperationLog,
32
+ OperationLogSchema,
33
+ type OperationPhase,
34
+ OperationPhaseSchema,
35
+ OperationPhaseType,
36
+ OperationSchema,
37
+ OperationStatus,
38
+ OperationType,
39
+ type Project,
40
+ type ProjectModel,
41
+ ProjectModelSchema,
42
+ ProjectSchema,
43
+ } from "@highstate/api/v1"
44
+ import {
45
+ instanceCustomStatusInputSchema,
46
+ operationMetaSchema,
47
+ operationOptionsSchema,
48
+ operationPhaseSchema,
49
+ } from "@highstate/backend/shared"
50
+ import {
51
+ type ComponentModel,
52
+ type EntityModel,
53
+ type HubModel,
54
+ type HubModelPatch,
55
+ type InstanceModel,
56
+ type InstanceModelPatch,
57
+ instanceInputSchema,
58
+ instanceModelSchema,
59
+ z,
60
+ } from "@highstate/contract"
61
+
62
+ const jsonValueSchema: z.ZodType<JsonValue> = z.lazy(() =>
63
+ z.union([
64
+ z.string(),
65
+ z.number(),
66
+ z.boolean(),
67
+ z.null(),
68
+ z.array(jsonValueSchema),
69
+ z.record(z.string(), jsonValueSchema),
70
+ ]),
71
+ )
72
+ const jsonObjectSchema = z.record(z.string(), jsonValueSchema)
73
+
74
+ export function toProject(project: ProjectOutput): Project {
75
+ return create(ProjectSchema, {
76
+ id: project.id,
77
+ name: project.name,
78
+ meta: project.meta,
79
+ spaceId: project.spaceId,
80
+ modelStorageId: project.modelStorageId,
81
+ libraryId: project.libraryId,
82
+ pulumiBackendId: project.pulumiBackendId,
83
+ createdAt: toTimestamp(project.createdAt),
84
+ updatedAt: toTimestamp(project.updatedAt),
85
+ })
86
+ }
87
+
88
+ export function toProjectModel(model: BackendProjectModel): ProjectModel {
89
+ return create(ProjectModelSchema, {
90
+ instances: [...model.instances, ...model.virtualInstances, ...model.ghostInstances].map(
91
+ toInstance,
92
+ ),
93
+ hubs: model.hubs.map(toHub),
94
+ })
95
+ }
96
+
97
+ export function toInstance(instance: InstanceModel): Instance {
98
+ return create(InstanceSchema, {
99
+ id: instance.id,
100
+ kind: toComponentKind(instance.kind),
101
+ type: instance.type,
102
+ name: instance.name,
103
+ arguments: Object.entries(instance.args ?? {}).map(([key, value]) => ({
104
+ key,
105
+ value: fromJson(ValueSchema, value as JsonValue),
106
+ })),
107
+ inputs: toInstanceReferenceMap(instance.inputs),
108
+ hubInputs: Object.fromEntries(
109
+ Object.entries(instance.hubInputs ?? {}).map(([key, values]) => [
110
+ key,
111
+ { values: values.map(value => ({ hubId: value.hubId })) },
112
+ ]),
113
+ ),
114
+ injectionInputs: (instance.injectionInputs ?? []).map(value => ({ hubId: value.hubId })),
115
+ position: instance.position ?? undefined,
116
+ parentId: instance.parentId,
117
+ outputs: toInstanceReferenceMap(instance.outputs),
118
+ resolvedOutputs: toInstanceReferenceMap(instance.resolvedOutputs),
119
+ })
120
+ }
121
+
122
+ export function fromInstance(instance: Instance): InstanceModel {
123
+ return instanceModelSchema.parse({
124
+ id: instance.id,
125
+ kind: fromComponentKind(instance.kind),
126
+ type: instance.type,
127
+ name: instance.name,
128
+ args: Object.fromEntries(
129
+ instance.arguments.map(argument => [
130
+ argument.key,
131
+ argument.value ? toJson(ValueSchema, argument.value) : null,
132
+ ]),
133
+ ),
134
+ inputs: fromInstanceReferenceMap(instance.inputs),
135
+ hubInputs: Object.fromEntries(
136
+ Object.entries(instance.hubInputs).map(([key, list]) => [
137
+ key,
138
+ list.values.map(value => ({ hubId: value.hubId })),
139
+ ]),
140
+ ),
141
+ injectionInputs: instance.injectionInputs.map(value => ({ hubId: value.hubId })),
142
+ position: instance.position,
143
+ })
144
+ }
145
+
146
+ export function toHub(hub: HubModel): Hub {
147
+ return create(HubSchema, {
148
+ id: hub.id,
149
+ position: hub.position ?? undefined,
150
+ inputs: hub.inputs ?? [],
151
+ injectionInputs: (hub.injectionInputs ?? []).map(value => ({ hubId: value.hubId })),
152
+ })
153
+ }
154
+
155
+ export function fromHub(hub: Hub): HubModel {
156
+ return {
157
+ id: hub.id,
158
+ position: hub.position,
159
+ inputs: hub.inputs.map(fromInstanceReference),
160
+ injectionInputs: hub.injectionInputs.map(value => ({ hubId: value.hubId })),
161
+ }
162
+ }
163
+
164
+ export function toInstancePatch(instance: Instance, paths: readonly string[]): InstanceModelPatch {
165
+ const patch: InstanceModelPatch = {}
166
+
167
+ for (const path of paths) {
168
+ switch (path) {
169
+ case "arguments":
170
+ patch.args = Object.fromEntries(
171
+ instance.arguments.map(argument => [
172
+ argument.key,
173
+ argument.value ? toJson(ValueSchema, argument.value) : null,
174
+ ]),
175
+ )
176
+ break
177
+ case "inputs":
178
+ patch.inputs = fromInstanceReferenceMap(instance.inputs)
179
+ break
180
+ case "hub_inputs":
181
+ patch.hubInputs = Object.fromEntries(
182
+ Object.entries(instance.hubInputs).map(([key, list]) => [
183
+ key,
184
+ list.values.map(value => ({ hubId: value.hubId })),
185
+ ]),
186
+ )
187
+ break
188
+ case "injection_inputs":
189
+ patch.injectionInputs = instance.injectionInputs.map(value => ({ hubId: value.hubId }))
190
+ break
191
+ case "position":
192
+ patch.position = instance.position
193
+ ? { x: instance.position.x, y: instance.position.y }
194
+ : null
195
+ break
196
+ case "position.x":
197
+ patch.position = { x: instance.position?.x ?? 0 }
198
+ break
199
+ case "position.y":
200
+ patch.position = { y: instance.position?.y ?? 0 }
201
+ break
202
+ }
203
+ }
204
+
205
+ return patch
206
+ }
207
+
208
+ export function toHubPatch(hub: Hub, paths: readonly string[]): HubModelPatch {
209
+ const patch: HubModelPatch = {}
210
+
211
+ for (const path of paths) {
212
+ switch (path) {
213
+ case "position":
214
+ patch.position = hub.position ? { x: hub.position.x, y: hub.position.y } : null
215
+ break
216
+ case "position.x":
217
+ patch.position = { x: hub.position?.x ?? 0 }
218
+ break
219
+ case "position.y":
220
+ patch.position = { y: hub.position?.y ?? 0 }
221
+ break
222
+ case "inputs":
223
+ patch.inputs = hub.inputs.map(fromInstanceReference)
224
+ break
225
+ case "injection_inputs":
226
+ patch.injectionInputs = hub.injectionInputs.map(value => ({ hubId: value.hubId }))
227
+ break
228
+ }
229
+ }
230
+
231
+ return patch
232
+ }
233
+
234
+ export function toInstanceState(state: BackendInstanceState): InstanceState {
235
+ const model = state.model ? instanceModelSchema.parse(state.model) : undefined
236
+
237
+ return create(InstanceStateSchema, {
238
+ id: state.id,
239
+ instanceId: state.instanceId,
240
+ status: toInstanceStatus(state.status),
241
+ source: toInstanceSource(state.source),
242
+ kind: toComponentKind(state.kind),
243
+ parentInstanceId: state.parentInstanceId ?? undefined,
244
+ evaluationState: state.evaluationState
245
+ ? {
246
+ status: toEvaluationStatus(state.evaluationState.status),
247
+ message: state.evaluationState.message ?? undefined,
248
+ model: state.evaluationState.model
249
+ ? toInstance(instanceModelSchema.parse(state.evaluationState.model))
250
+ : undefined,
251
+ evaluatedAt: toTimestamp(state.evaluationState.evaluatedAt),
252
+ }
253
+ : undefined,
254
+ lastOperationState: state.lastOperationState
255
+ ? {
256
+ operationId: state.lastOperationState.operationId,
257
+ stateId: state.lastOperationState.stateId,
258
+ status: toInstanceOperationStatus(state.lastOperationState.status),
259
+ currentResourceCount: state.lastOperationState.currentResourceCount ?? undefined,
260
+ totalResourceCount: state.lastOperationState.totalResourceCount ?? undefined,
261
+ model: toInstance(instanceModelSchema.parse(state.lastOperationState.model)),
262
+ startedAt: toNullableTimestamp(state.lastOperationState.startedAt),
263
+ finishedAt: toNullableTimestamp(state.lastOperationState.finishedAt),
264
+ }
265
+ : undefined,
266
+ terminalIds: state.terminalIds ?? [],
267
+ pageIds: state.pageIds ?? [],
268
+ panelIds: state.panelIds ?? [],
269
+ secretNames: state.secretNames ?? [],
270
+ customStatuses: (state.customStatuses ?? []).map(toInstanceCustomStatus),
271
+ currentResourceCount: state.currentResourceCount ?? undefined,
272
+ hasResourceHooks: state.hasResourceHooks,
273
+ statusFields: state.statusFields
274
+ ? fromJson(ValueSchema, jsonValueSchema.parse(state.statusFields))
275
+ : undefined,
276
+ model: model ? toInstance(model) : undefined,
277
+ })
278
+ }
279
+
280
+ export function toInstanceCustomStatus(
281
+ status: NonNullable<BackendInstanceState["customStatuses"]>[number],
282
+ ): InstanceCustomStatus {
283
+ const meta = instanceCustomStatusInputSchema.shape.meta.parse(status.meta)
284
+
285
+ return create(InstanceCustomStatusSchema, {
286
+ name: status.name,
287
+ meta: {
288
+ title: meta.title ?? status.name,
289
+ description: meta.description,
290
+ icon: meta.icon,
291
+ iconColor: meta.iconColor,
292
+ },
293
+ value: status.value,
294
+ message: status.message ?? undefined,
295
+ order: status.order,
296
+ serviceAccountId: status.serviceAccountId,
297
+ createdAt: toTimestamp(status.createdAt),
298
+ updatedAt: toTimestamp(status.updatedAt),
299
+ })
300
+ }
301
+
302
+ export function toOperation(operation: BackendOperation): Operation {
303
+ const meta = operationMetaSchema.parse(operation.meta)
304
+ const options = operationOptionsSchema.partial().parse(operation.options)
305
+ const phases = operation.phases ? operationPhaseSchema.array().parse(operation.phases) : []
306
+
307
+ return create(OperationSchema, {
308
+ id: operation.id,
309
+ meta,
310
+ type: toOperationType(operation.type),
311
+ status: toOperationStatus(operation.status),
312
+ options: {
313
+ forceUpdateDependencies: options.forceUpdateDependencies ?? false,
314
+ ignoreChangedDependencies: options.ignoreChangedDependencies ?? false,
315
+ ignoreDependencies: options.ignoreDependencies ?? false,
316
+ forceUpdateChildren: options.forceUpdateChildren ?? false,
317
+ onlyDestroyGhosts: options.onlyDestroyGhosts ?? false,
318
+ firstDestroyGhosts: options.firstDestroyGhosts ?? false,
319
+ ignoreGhosts: options.ignoreGhosts ?? false,
320
+ destroyDependentInstances: options.destroyDependentInstances ?? false,
321
+ invokeDestroyTriggers: options.invokeDestroyTriggers ?? false,
322
+ deleteUnreachableResources: options.deleteUnreachableResources ?? false,
323
+ forceDeleteState: options.forceDeleteState ?? false,
324
+ allowPartialCompositeInstanceUpdate: options.allowPartialCompositeInstanceUpdate ?? false,
325
+ allowPartialCompositeInstanceDestruction:
326
+ options.allowPartialCompositeInstanceDestruction ?? false,
327
+ refresh: options.refresh ?? false,
328
+ debug: options.debug ?? false,
329
+ },
330
+ requestedInstanceIds: z.string().array().parse(operation.requestedInstanceIds),
331
+ phases: phases.map(toOperationPhase),
332
+ startedAt: toTimestamp(operation.startedAt),
333
+ updatedAt: toTimestamp(operation.updatedAt),
334
+ finishedAt: toNullableTimestamp(operation.finishedAt),
335
+ })
336
+ }
337
+
338
+ export function toOperationPhase(phase: BackendOperationPhase): OperationPhase {
339
+ return create(OperationPhaseSchema, {
340
+ type: toOperationPhaseType(phase.type),
341
+ instances: phase.instances,
342
+ })
343
+ }
344
+
345
+ export function toOperationLog(
346
+ operationId: string,
347
+ log: { id: string; stateId: string | null; content: string; isSystem?: boolean },
348
+ ): OperationLog {
349
+ return create(OperationLogSchema, {
350
+ id: log.id,
351
+ operationId,
352
+ stateId: log.stateId ?? undefined,
353
+ isSystem: log.isSystem ?? false,
354
+ content: log.content,
355
+ })
356
+ }
357
+
358
+ export function toLibrary(library: {
359
+ components: Record<string, ComponentModel>
360
+ entities: Record<string, EntityModel>
361
+ }): Library {
362
+ return create(LibrarySchema, {
363
+ components: Object.fromEntries(
364
+ Object.entries(library.components).map(([type, component]) => [type, toComponent(component)]),
365
+ ),
366
+ entities: Object.fromEntries(
367
+ Object.entries(library.entities).map(([type, entity]) => [type, toEntity(entity)]),
368
+ ),
369
+ })
370
+ }
371
+
372
+ export function toComponent(component: ComponentModel): Component {
373
+ try {
374
+ return create(ComponentSchema, {
375
+ type: component.type,
376
+ kind: toComponentKind(component.kind),
377
+ arguments: Object.fromEntries(
378
+ Object.entries(component.args).map(([name, argument]) => [
379
+ name,
380
+ {
381
+ schema: jsonObjectSchema.parse(argument.schema),
382
+ required: argument.required,
383
+ meta: argument.meta,
384
+ },
385
+ ]),
386
+ ),
387
+ inputs: Object.fromEntries(
388
+ Object.entries(component.inputs).map(([name, port]) => [name, toComponentPort(port)]),
389
+ ),
390
+ outputs: Object.fromEntries(
391
+ Object.entries(component.outputs).map(([name, port]) => [name, toComponentPort(port)]),
392
+ ),
393
+ meta: component.meta,
394
+ definitionHash: component.definitionHash,
395
+ })
396
+ } catch (error) {
397
+ throw new Error(`Failed to convert component "${component.type}" to an API message`, {
398
+ cause: error,
399
+ })
400
+ }
401
+ }
402
+
403
+ export function toEntity(entity: EntityModel): Entity {
404
+ return create(EntitySchema, {
405
+ type: entity.type,
406
+ extensions: entity.extensions ?? [],
407
+ directExtensions: entity.directExtensions ?? [],
408
+ inclusions: entity.inclusions ?? [],
409
+ directInclusions: entity.directInclusions ?? [],
410
+ meta: entity.meta,
411
+ definitionHash: entity.definitionHash,
412
+ })
413
+ }
414
+
415
+ export function toTimestamp(value: Date): Timestamp {
416
+ if (!Number.isFinite(value.getTime())) {
417
+ throw new Error("Cannot convert invalid date to timestamp")
418
+ }
419
+
420
+ return timestampFromDate(value)
421
+ }
422
+
423
+ export function toNullableTimestamp(value: Date | null | undefined): Timestamp | undefined {
424
+ return value ? toTimestamp(value) : undefined
425
+ }
426
+
427
+ function toComponentPort(port: ComponentModel["inputs"][string]) {
428
+ return {
429
+ entityType: port.type,
430
+ fromInput: port.fromInput,
431
+ required: port.required,
432
+ multiple: port.multiple,
433
+ meta: port.meta,
434
+ }
435
+ }
436
+
437
+ function toInstanceReferenceMap(values?: InstanceModel["inputs"]) {
438
+ return Object.fromEntries(
439
+ Object.entries(values ?? {}).map(([key, references]) => [
440
+ key,
441
+ {
442
+ values: references,
443
+ },
444
+ ]),
445
+ )
446
+ }
447
+
448
+ function fromInstanceReferenceMap(
449
+ values: Instance["inputs"],
450
+ ): NonNullable<InstanceModel["inputs"]> {
451
+ return Object.fromEntries(
452
+ Object.entries(values).map(([key, list]) => [key, list.values.map(fromInstanceReference)]),
453
+ )
454
+ }
455
+
456
+ function fromInstanceReference(value: Instance["inputs"][string]["values"][number]) {
457
+ return instanceInputSchema.parse({
458
+ instanceId: value.instanceId,
459
+ output: value.output,
460
+ path: value.path,
461
+ })
462
+ }
463
+
464
+ function toComponentKind(value: string): ComponentKind {
465
+ if (value === "unit") return ComponentKind.UNIT
466
+ if (value === "composite") return ComponentKind.COMPOSITE
467
+ throw new Error(`Unknown component kind "${value}"`)
468
+ }
469
+
470
+ function fromComponentKind(value: ComponentKind): "unit" | "composite" {
471
+ if (value === ComponentKind.UNIT) return "unit"
472
+ if (value === ComponentKind.COMPOSITE) return "composite"
473
+ throw new Error("Component kind must be specified")
474
+ }
475
+
476
+ function toInstanceStatus(value: string): InstanceStatus {
477
+ const statuses = {
478
+ undeployed: InstanceStatus.UNDEPLOYED,
479
+ attempted: InstanceStatus.ATTEMPTED,
480
+ deployed: InstanceStatus.DEPLOYED,
481
+ failed: InstanceStatus.FAILED,
482
+ } as const
483
+ const status = statuses[value as keyof typeof statuses]
484
+ if (status === undefined) throw new Error(`Unknown instance status "${value}"`)
485
+ return status
486
+ }
487
+
488
+ function toInstanceSource(value: string): InstanceSource {
489
+ if (value === "resident") return InstanceSource.RESIDENT
490
+ if (value === "virtual") return InstanceSource.VIRTUAL
491
+ throw new Error(`Unknown instance source "${value}"`)
492
+ }
493
+
494
+ function toEvaluationStatus(value: string): EvaluationStatus {
495
+ if (value === "evaluating") return EvaluationStatus.EVALUATING
496
+ if (value === "evaluated") return EvaluationStatus.EVALUATED
497
+ if (value === "error") return EvaluationStatus.ERROR
498
+ throw new Error(`Unknown evaluation status "${value}"`)
499
+ }
500
+
501
+ function toInstanceOperationStatus(value: string): InstanceOperationStatus {
502
+ const statuses = {
503
+ updating: InstanceOperationStatus.UPDATING,
504
+ processing_triggers: InstanceOperationStatus.PROCESSING_TRIGGERS,
505
+ previewing: InstanceOperationStatus.PREVIEWING,
506
+ destroying: InstanceOperationStatus.DESTROYING,
507
+ refreshing: InstanceOperationStatus.REFRESHING,
508
+ pending: InstanceOperationStatus.PENDING,
509
+ cancelling: InstanceOperationStatus.CANCELLING,
510
+ updated: InstanceOperationStatus.UPDATED,
511
+ previewed: InstanceOperationStatus.PREVIEWED,
512
+ skipped: InstanceOperationStatus.SKIPPED,
513
+ destroyed: InstanceOperationStatus.DESTROYED,
514
+ refreshed: InstanceOperationStatus.REFRESHED,
515
+ cancelled: InstanceOperationStatus.CANCELLED,
516
+ failed: InstanceOperationStatus.FAILED,
517
+ } as const
518
+ const status = statuses[value as keyof typeof statuses]
519
+ if (status === undefined) throw new Error(`Unknown instance operation status "${value}"`)
520
+ return status
521
+ }
522
+
523
+ export function fromOperationType(value: OperationType) {
524
+ if (value === OperationType.UPDATE) return "update"
525
+ if (value === OperationType.PREVIEW) return "preview"
526
+ if (value === OperationType.DESTROY) return "destroy"
527
+ if (value === OperationType.RECREATE) return "recreate"
528
+ if (value === OperationType.REFRESH) return "refresh"
529
+ throw new Error("Operation type must be specified")
530
+ }
531
+
532
+ export function fromOperationPhase(phase: OperationPhase): BackendOperationPhase {
533
+ return operationPhaseSchema.parse({
534
+ type: fromOperationPhaseType(phase.type),
535
+ instances: phase.instances.map(instance => ({
536
+ id: instance.id,
537
+ parentId: instance.parentId,
538
+ message: instance.message,
539
+ })),
540
+ })
541
+ }
542
+
543
+ function toOperationType(value: string): OperationType {
544
+ const types = {
545
+ update: OperationType.UPDATE,
546
+ preview: OperationType.PREVIEW,
547
+ destroy: OperationType.DESTROY,
548
+ recreate: OperationType.RECREATE,
549
+ refresh: OperationType.REFRESH,
550
+ } as const
551
+ const type = types[value as keyof typeof types]
552
+ if (type === undefined) throw new Error(`Unknown operation type "${value}"`)
553
+ return type
554
+ }
555
+
556
+ function toOperationStatus(value: string): OperationStatus {
557
+ const statuses = {
558
+ pending: OperationStatus.PENDING,
559
+ running: OperationStatus.RUNNING,
560
+ failing: OperationStatus.FAILING,
561
+ cancelling: OperationStatus.CANCELLING,
562
+ completed: OperationStatus.COMPLETED,
563
+ failed: OperationStatus.FAILED,
564
+ cancelled: OperationStatus.CANCELLED,
565
+ } as const
566
+ const status = statuses[value as keyof typeof statuses]
567
+ if (status === undefined) throw new Error(`Unknown operation status "${value}"`)
568
+ return status
569
+ }
570
+
571
+ function toOperationPhaseType(value: string): OperationPhaseType {
572
+ const types = {
573
+ destroy: OperationPhaseType.DESTROY,
574
+ preview: OperationPhaseType.PREVIEW,
575
+ update: OperationPhaseType.UPDATE,
576
+ refresh: OperationPhaseType.REFRESH,
577
+ } as const
578
+ const type = types[value as keyof typeof types]
579
+ if (type === undefined) throw new Error(`Unknown operation phase type "${value}"`)
580
+ return type
581
+ }
582
+
583
+ function fromOperationPhaseType(value: OperationPhaseType): BackendOperationPhase["type"] {
584
+ const types = {
585
+ [OperationPhaseType.DESTROY]: "destroy",
586
+ [OperationPhaseType.PREVIEW]: "preview",
587
+ [OperationPhaseType.UPDATE]: "update",
588
+ [OperationPhaseType.REFRESH]: "refresh",
589
+ } as const
590
+ const type = types[value as keyof typeof types]
591
+ if (!type) throw new Error("Operation phase type must be specified")
592
+ return type
593
+ }