@highstate/backend 0.7.1 → 0.7.3
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/{index.mjs → index.js} +1255 -916
- package/dist/library/source-resolution-worker.js +55 -0
- package/dist/library/worker/main.js +207 -0
- package/dist/{terminal-CqIsctlZ.mjs → library-BW5oPM7V.js} +210 -87
- package/dist/shared/index.js +6 -0
- package/dist/utils-ByadNcv4.js +102 -0
- package/package.json +14 -18
- package/src/common/index.ts +3 -0
- package/src/common/local.ts +22 -0
- package/src/common/pulumi.ts +230 -0
- package/src/common/utils.ts +137 -0
- package/src/config.ts +40 -0
- package/src/index.ts +6 -0
- package/src/library/abstractions.ts +83 -0
- package/src/library/factory.ts +20 -0
- package/src/library/index.ts +2 -0
- package/src/library/local.ts +404 -0
- package/src/library/source-resolution-worker.ts +96 -0
- package/src/library/worker/evaluator.ts +119 -0
- package/src/library/worker/loader.ts +93 -0
- package/src/library/worker/main.ts +82 -0
- package/src/library/worker/protocol.ts +38 -0
- package/src/orchestrator/index.ts +1 -0
- package/src/orchestrator/manager.ts +165 -0
- package/src/orchestrator/operation-workset.ts +483 -0
- package/src/orchestrator/operation.ts +647 -0
- package/src/preferences/shared.ts +1 -0
- package/src/project/abstractions.ts +89 -0
- package/src/project/factory.ts +11 -0
- package/src/project/index.ts +4 -0
- package/src/project/local.ts +412 -0
- package/src/project/lock.ts +39 -0
- package/src/project/manager.ts +374 -0
- package/src/runner/abstractions.ts +146 -0
- package/src/runner/factory.ts +22 -0
- package/src/runner/index.ts +2 -0
- package/src/runner/local.ts +698 -0
- package/src/secret/abstractions.ts +59 -0
- package/src/secret/factory.ts +22 -0
- package/src/secret/index.ts +2 -0
- package/src/secret/local.ts +152 -0
- package/src/services.ts +133 -0
- package/src/shared/index.ts +10 -0
- package/src/shared/library.ts +77 -0
- package/src/shared/operation.ts +85 -0
- package/src/shared/project.ts +62 -0
- package/src/shared/resolvers/graph-resolver.ts +111 -0
- package/src/shared/resolvers/input-hash.ts +77 -0
- package/src/shared/resolvers/input.ts +314 -0
- package/src/shared/resolvers/registry.ts +10 -0
- package/src/shared/resolvers/validation.ts +94 -0
- package/src/shared/state.ts +262 -0
- package/src/shared/terminal.ts +13 -0
- package/src/state/abstractions.ts +222 -0
- package/src/state/factory.ts +22 -0
- package/src/state/index.ts +3 -0
- package/src/state/local.ts +605 -0
- package/src/state/manager.ts +33 -0
- package/src/terminal/docker.ts +90 -0
- package/src/terminal/factory.ts +20 -0
- package/src/terminal/index.ts +3 -0
- package/src/terminal/manager.ts +330 -0
- package/src/terminal/run.sh.ts +37 -0
- package/src/terminal/shared.ts +50 -0
- package/src/workspace/abstractions.ts +41 -0
- package/src/workspace/factory.ts +14 -0
- package/src/workspace/index.ts +2 -0
- package/src/workspace/local.ts +54 -0
- package/dist/index.d.ts +0 -760
- package/dist/library/worker/main.mjs +0 -164
- package/dist/runner/source-resolution-worker.mjs +0 -22
- package/dist/shared/index.d.ts +0 -85
- package/dist/shared/index.mjs +0 -54
- package/dist/terminal-Cm2WqcyB.d.ts +0 -1589
@@ -0,0 +1,111 @@
|
|
1
|
+
import type { Logger } from "pino"
|
2
|
+
import { unique } from "remeda"
|
3
|
+
|
4
|
+
export type GraphResolverOptions<TNode, TOutput> = {
|
5
|
+
name: string
|
6
|
+
|
7
|
+
getNodeId(input: TNode): string
|
8
|
+
|
9
|
+
getNodeDependencies(input: TNode): string[]
|
10
|
+
|
11
|
+
process(
|
12
|
+
node: TNode,
|
13
|
+
dependencies: ReadonlyMap<string, TOutput>,
|
14
|
+
logger: Logger,
|
15
|
+
): TOutput | Promise<TOutput>
|
16
|
+
}
|
17
|
+
|
18
|
+
export interface GraphResolverBackend<TOutput> {
|
19
|
+
promiseCache: Map<string, Promise<TOutput>>
|
20
|
+
|
21
|
+
setOutput?(id: string, value: TOutput): void
|
22
|
+
setDependencies?(id: string, dependencies: string[]): void
|
23
|
+
}
|
24
|
+
|
25
|
+
export type GraphResolverFactory<TNode, TOutput> = {
|
26
|
+
factoryName: string
|
27
|
+
} & ((
|
28
|
+
nodes: ReadonlyMap<string, TNode>,
|
29
|
+
logger: Logger,
|
30
|
+
backend?: GraphResolverBackend<TOutput>,
|
31
|
+
) => GraphResolver<TOutput>)
|
32
|
+
|
33
|
+
export type GraphResolver<TOutput> = (id: string) => Promise<TOutput>
|
34
|
+
|
35
|
+
export class CircularDependencyError extends Error {
|
36
|
+
constructor(path: string[]) {
|
37
|
+
super(`Circular dependency detected: ${path.join(" -> ")}`)
|
38
|
+
this.name = "CircularDependencyError"
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
export function createDefaultGraphResolverBackend<TOutput>(): GraphResolverBackend<TOutput> {
|
43
|
+
const promiseCache = new Map<string, Promise<TOutput>>()
|
44
|
+
|
45
|
+
return {
|
46
|
+
promiseCache,
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
export function defineGraphResolver<TInput, TOutput>(
|
51
|
+
options: GraphResolverOptions<TInput, TOutput>,
|
52
|
+
): GraphResolverFactory<TInput, TOutput> {
|
53
|
+
const factory: GraphResolverFactory<TInput, TOutput> = (nodes, logger, backend) => {
|
54
|
+
backend ??= createDefaultGraphResolverBackend<TOutput>()
|
55
|
+
logger = logger.child({ resolver: options.name })
|
56
|
+
|
57
|
+
const outputs = new Map<string, TOutput>()
|
58
|
+
|
59
|
+
const resolver = (itemId: string, dependencyChain: string[]) => {
|
60
|
+
logger.trace({ itemId }, "resolving item")
|
61
|
+
|
62
|
+
const existingPromise = backend.promiseCache.get(itemId)
|
63
|
+
if (existingPromise) {
|
64
|
+
return existingPromise
|
65
|
+
}
|
66
|
+
|
67
|
+
if (dependencyChain.includes(itemId)) {
|
68
|
+
throw new CircularDependencyError([...dependencyChain, itemId])
|
69
|
+
}
|
70
|
+
|
71
|
+
const item = nodes.get(itemId)
|
72
|
+
if (!item) {
|
73
|
+
return Promise.resolve(undefined!)
|
74
|
+
}
|
75
|
+
|
76
|
+
const resolve = async () => {
|
77
|
+
const dependencies = unique(options.getNodeDependencies(item))
|
78
|
+
|
79
|
+
backend.setDependencies?.(itemId, dependencies)
|
80
|
+
logger.trace({ itemId, dependencies }, "resolving item dependencies")
|
81
|
+
|
82
|
+
const newChain = [...dependencyChain, itemId]
|
83
|
+
|
84
|
+
for (const depId of dependencies) {
|
85
|
+
await resolver(depId, newChain)
|
86
|
+
}
|
87
|
+
|
88
|
+
return await options.process(item, outputs, logger)
|
89
|
+
}
|
90
|
+
|
91
|
+
const promise = resolve().then(result => {
|
92
|
+
if (backend.promiseCache.get(itemId) === promise) {
|
93
|
+
// persist the result to the state if provided
|
94
|
+
// ignore if the promise has been replaced (which means this calculation was invalidated)
|
95
|
+
backend.setOutput?.(itemId, result)
|
96
|
+
outputs.set(itemId, result)
|
97
|
+
}
|
98
|
+
|
99
|
+
return result
|
100
|
+
})
|
101
|
+
|
102
|
+
backend.promiseCache.set(itemId, promise)
|
103
|
+
return promise
|
104
|
+
}
|
105
|
+
|
106
|
+
return id => resolver(id, [])
|
107
|
+
}
|
108
|
+
|
109
|
+
factory.factoryName = options.name
|
110
|
+
return factory
|
111
|
+
}
|
@@ -0,0 +1,77 @@
|
|
1
|
+
import type { ComponentModel, InstanceModel } from "@highstate/contract"
|
2
|
+
import type { InstanceState } from "../state"
|
3
|
+
import type { ResolvedInstanceInput } from "./input"
|
4
|
+
import { sha256 } from "crypto-hash"
|
5
|
+
import { defineGraphResolver } from "./graph-resolver"
|
6
|
+
|
7
|
+
export type InputHashResolverInput = {
|
8
|
+
instance: InstanceModel
|
9
|
+
component: ComponentModel
|
10
|
+
resolvedInputs: Record<string, ResolvedInstanceInput[]>
|
11
|
+
state: InstanceState | undefined
|
12
|
+
sourceHash: string | undefined
|
13
|
+
}
|
14
|
+
|
15
|
+
export type InputHashResolverOutput = {
|
16
|
+
inputHash: string
|
17
|
+
outputHash: string
|
18
|
+
}
|
19
|
+
|
20
|
+
/**
|
21
|
+
* Resolves the hash of the instance based on its args, resolved input hashes, source hash, and the output hash.
|
22
|
+
*/
|
23
|
+
export const createInputHashResolver = defineGraphResolver<
|
24
|
+
InputHashResolverInput,
|
25
|
+
InputHashResolverOutput
|
26
|
+
>({
|
27
|
+
name: "input-hash-resolver",
|
28
|
+
getNodeId: node => node.instance.id,
|
29
|
+
|
30
|
+
getNodeDependencies({ resolvedInputs }) {
|
31
|
+
const dependencies: string[] = []
|
32
|
+
|
33
|
+
for (const inputs of Object.values(resolvedInputs ?? {})) {
|
34
|
+
for (const input of inputs) {
|
35
|
+
dependencies.push(input.input.instanceId)
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
return dependencies
|
40
|
+
},
|
41
|
+
|
42
|
+
async process({ instance, component, resolvedInputs, sourceHash, state }, dependencies) {
|
43
|
+
let sink = component.definitionHash + JSON.stringify(instance.args ?? {})
|
44
|
+
|
45
|
+
if (sourceHash) {
|
46
|
+
sink += sourceHash
|
47
|
+
}
|
48
|
+
|
49
|
+
const sortedInputs = Object.entries(resolvedInputs)
|
50
|
+
//
|
51
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
52
|
+
|
53
|
+
for (const [inputKey, inputs] of sortedInputs) {
|
54
|
+
if (Object.keys(inputs).length === 0) {
|
55
|
+
continue
|
56
|
+
}
|
57
|
+
|
58
|
+
sink += inputKey
|
59
|
+
|
60
|
+
const instanceIds = inputs.map(input => input.input.instanceId)
|
61
|
+
instanceIds.sort()
|
62
|
+
|
63
|
+
for (const instanceId of instanceIds) {
|
64
|
+
const dependency = dependencies.get(instanceId)
|
65
|
+
if (!dependency) continue
|
66
|
+
|
67
|
+
sink += dependency.inputHash
|
68
|
+
sink += dependency.outputHash
|
69
|
+
}
|
70
|
+
}
|
71
|
+
|
72
|
+
return {
|
73
|
+
inputHash: await sha256(sink),
|
74
|
+
outputHash: state?.outputHash ?? "",
|
75
|
+
}
|
76
|
+
},
|
77
|
+
})
|
@@ -0,0 +1,314 @@
|
|
1
|
+
import type { HubModel } from "../project"
|
2
|
+
import {
|
3
|
+
isUnitModel,
|
4
|
+
type ComponentModel,
|
5
|
+
type HubInput,
|
6
|
+
type InstanceInput,
|
7
|
+
type InstanceModel,
|
8
|
+
} from "@highstate/contract"
|
9
|
+
import { fromEntries, mapValues } from "remeda"
|
10
|
+
import { defineGraphResolver } from "./graph-resolver"
|
11
|
+
|
12
|
+
export type InputResolverInput =
|
13
|
+
| {
|
14
|
+
kind: "instance"
|
15
|
+
instance: InstanceModel
|
16
|
+
component: ComponentModel
|
17
|
+
}
|
18
|
+
| {
|
19
|
+
kind: "hub"
|
20
|
+
hub: HubModel
|
21
|
+
}
|
22
|
+
|
23
|
+
export type ResolvedInstanceInput = {
|
24
|
+
input: InstanceInput
|
25
|
+
type: string
|
26
|
+
}
|
27
|
+
|
28
|
+
export type InputResolverOutput =
|
29
|
+
| {
|
30
|
+
kind: "instance"
|
31
|
+
component: ComponentModel
|
32
|
+
resolvedInputs: Record<string, ResolvedInstanceInput[]>
|
33
|
+
resolvedOutputs: Record<string, InstanceInput[]>
|
34
|
+
resolvedInjectionInputs: ResolvedInstanceInput[]
|
35
|
+
matchedInjectionInputs: ResolvedInstanceInput[]
|
36
|
+
}
|
37
|
+
| {
|
38
|
+
kind: "hub"
|
39
|
+
resolvedInputs: ResolvedInstanceInput[]
|
40
|
+
}
|
41
|
+
|
42
|
+
/**
|
43
|
+
* Resolves the all recursive instance and hub inputs based on its direct inputs and injected inputs.
|
44
|
+
*/
|
45
|
+
export const createInputResolver = defineGraphResolver<InputResolverInput, InputResolverOutput>({
|
46
|
+
name: "input-resolver",
|
47
|
+
getNodeId(node) {
|
48
|
+
if (node.kind === "hub") {
|
49
|
+
return `hub:${node.hub.id}`
|
50
|
+
}
|
51
|
+
|
52
|
+
return `instance:${node.instance.id}`
|
53
|
+
},
|
54
|
+
|
55
|
+
getNodeDependencies(node) {
|
56
|
+
const dependencies: string[] = []
|
57
|
+
|
58
|
+
if (node.kind === "hub") {
|
59
|
+
for (const input of node.hub.inputs ?? []) {
|
60
|
+
dependencies.push(`instance:${input.instanceId}`)
|
61
|
+
}
|
62
|
+
|
63
|
+
for (const input of node.hub.injectionInputs ?? []) {
|
64
|
+
dependencies.push(`hub:${input.hubId}`)
|
65
|
+
}
|
66
|
+
|
67
|
+
return dependencies
|
68
|
+
}
|
69
|
+
|
70
|
+
for (const inputs of Object.values(node.instance.inputs ?? {})) {
|
71
|
+
for (const input of inputs) {
|
72
|
+
dependencies.push(`instance:${input.instanceId}`)
|
73
|
+
}
|
74
|
+
}
|
75
|
+
|
76
|
+
for (const inputs of Object.values(node.instance.hubInputs ?? {})) {
|
77
|
+
for (const input of inputs) {
|
78
|
+
dependencies.push(`hub:${input.hubId}`)
|
79
|
+
}
|
80
|
+
}
|
81
|
+
|
82
|
+
for (const input of node.instance.injectionInputs ?? []) {
|
83
|
+
dependencies.push(`hub:${input.hubId}`)
|
84
|
+
}
|
85
|
+
|
86
|
+
return dependencies
|
87
|
+
},
|
88
|
+
|
89
|
+
process(node, dependencies, logger) {
|
90
|
+
const getHubOutput = (input: HubInput) => {
|
91
|
+
const output = dependencies.get(`hub:${input.hubId}`)
|
92
|
+
if (!output) {
|
93
|
+
return { resolvedInputs: [] }
|
94
|
+
}
|
95
|
+
|
96
|
+
if (output.kind !== "hub") {
|
97
|
+
throw new Error("Expected hub node")
|
98
|
+
}
|
99
|
+
|
100
|
+
return output
|
101
|
+
}
|
102
|
+
|
103
|
+
const getInstanceOutput = (input: InstanceInput) => {
|
104
|
+
const output = dependencies.get(`instance:${input.instanceId}`)
|
105
|
+
if (!output) {
|
106
|
+
return {
|
107
|
+
component: null,
|
108
|
+
resolvedInputs: [] as ResolvedInstanceInput[],
|
109
|
+
resolvedOutputs: [] as InstanceInput[],
|
110
|
+
}
|
111
|
+
}
|
112
|
+
|
113
|
+
if (output.kind !== "instance") {
|
114
|
+
throw new Error("Expected instance node")
|
115
|
+
}
|
116
|
+
|
117
|
+
return {
|
118
|
+
component: output.component,
|
119
|
+
resolvedInputs: output.resolvedInputs[input.output] ?? [],
|
120
|
+
resolvedOutputs: output.resolvedOutputs[input.output] ?? [],
|
121
|
+
}
|
122
|
+
}
|
123
|
+
|
124
|
+
// resolve inputs for hub
|
125
|
+
|
126
|
+
if (node.kind === "hub") {
|
127
|
+
const hubResult: Map<string, ResolvedInstanceInput> = new Map()
|
128
|
+
|
129
|
+
const addHubResult = (input: ResolvedInstanceInput) => {
|
130
|
+
hubResult.set(`${input.input.instanceId}:${input.input.output}`, input)
|
131
|
+
}
|
132
|
+
|
133
|
+
for (const input of node.hub.inputs ?? []) {
|
134
|
+
const { component } = getInstanceOutput(input)
|
135
|
+
const componentInput = component?.outputs[input.output]
|
136
|
+
|
137
|
+
if (!componentInput) {
|
138
|
+
logger.warn({ msg: "output not found in the component", input, component })
|
139
|
+
continue
|
140
|
+
}
|
141
|
+
|
142
|
+
addHubResult({ input, type: componentInput.type })
|
143
|
+
}
|
144
|
+
|
145
|
+
for (const injectionInput of node.hub.injectionInputs ?? []) {
|
146
|
+
const { resolvedInputs } = getHubOutput(injectionInput)
|
147
|
+
|
148
|
+
for (const input of resolvedInputs) {
|
149
|
+
addHubResult(input)
|
150
|
+
}
|
151
|
+
}
|
152
|
+
|
153
|
+
return {
|
154
|
+
kind: "hub",
|
155
|
+
resolvedInputs: Array.from(hubResult.values()),
|
156
|
+
}
|
157
|
+
}
|
158
|
+
|
159
|
+
// хуяк (which translates as "lets reuse resolved inputs of level 2+ composite instances provided by the evaluator")
|
160
|
+
if (node.instance.resolvedInputs) {
|
161
|
+
return {
|
162
|
+
kind: "instance",
|
163
|
+
component: node.component,
|
164
|
+
resolvedInputs: mapValues(node.instance.resolvedInputs, (inputs, inputName) => {
|
165
|
+
const componentInput = node.component.inputs[inputName]
|
166
|
+
if (!componentInput) {
|
167
|
+
logger.warn({
|
168
|
+
msg: "input not found in the component",
|
169
|
+
inputName,
|
170
|
+
component: node.component,
|
171
|
+
})
|
172
|
+
return []
|
173
|
+
}
|
174
|
+
|
175
|
+
return inputs.map(input => ({ input, type: componentInput.type }))
|
176
|
+
}),
|
177
|
+
resolvedOutputs: node.instance.resolvedOutputs ?? {},
|
178
|
+
resolvedInjectionInputs: [],
|
179
|
+
matchedInjectionInputs: [],
|
180
|
+
}
|
181
|
+
}
|
182
|
+
|
183
|
+
// resolve inputs for instance
|
184
|
+
|
185
|
+
const resolvedInputsMap: Map<string, Map<string, ResolvedInstanceInput>> = new Map()
|
186
|
+
|
187
|
+
const addInstanceResult = (inputName: string, input: ResolvedInstanceInput) => {
|
188
|
+
let inputs = resolvedInputsMap.get(inputName)
|
189
|
+
if (!inputs) {
|
190
|
+
inputs = new Map()
|
191
|
+
resolvedInputsMap.set(inputName, inputs)
|
192
|
+
}
|
193
|
+
|
194
|
+
inputs.set(`${input.input.instanceId}:${input.input.output}`, input)
|
195
|
+
}
|
196
|
+
|
197
|
+
const addInstanceInput = (inputName: string, input: InstanceInput) => {
|
198
|
+
const componentInput = node.component.inputs[inputName]
|
199
|
+
if (!componentInput) {
|
200
|
+
logger.warn({ msg: "input not found in the component", input, component: node.component })
|
201
|
+
return
|
202
|
+
}
|
203
|
+
|
204
|
+
const { component, resolvedOutputs } = getInstanceOutput(input)
|
205
|
+
|
206
|
+
if (!component) {
|
207
|
+
logger.warn({ instanceId: node.instance.id, input }, "no output found for the input")
|
208
|
+
return
|
209
|
+
}
|
210
|
+
|
211
|
+
if (isUnitModel(component)) {
|
212
|
+
addInstanceResult(inputName, { input, type: componentInput.type })
|
213
|
+
return
|
214
|
+
}
|
215
|
+
|
216
|
+
for (const output of resolvedOutputs) {
|
217
|
+
addInstanceResult(inputName, { input: output, type: componentInput.type })
|
218
|
+
}
|
219
|
+
}
|
220
|
+
|
221
|
+
for (const [inputName, inputs] of Object.entries(node.instance.inputs ?? {})) {
|
222
|
+
for (const input of inputs) {
|
223
|
+
addInstanceInput(inputName, input)
|
224
|
+
}
|
225
|
+
}
|
226
|
+
|
227
|
+
const injectionInputs: Map<string, ResolvedInstanceInput> = new Map()
|
228
|
+
const matchedInjectionInputs: Map<string, ResolvedInstanceInput> = new Map()
|
229
|
+
|
230
|
+
for (const injectionInput of node.instance.injectionInputs ?? []) {
|
231
|
+
const { resolvedInputs } = getHubOutput(injectionInput)
|
232
|
+
for (const input of resolvedInputs) {
|
233
|
+
injectionInputs.set(`${input.input.instanceId}:${input.input.output}`, input)
|
234
|
+
}
|
235
|
+
}
|
236
|
+
|
237
|
+
for (const [inputName, componentInput] of Object.entries(node.component.inputs ?? {})) {
|
238
|
+
const allInputs = new Map<string, ResolvedInstanceInput>(injectionInputs)
|
239
|
+
const hubInputs = node.instance.hubInputs?.[inputName] ?? []
|
240
|
+
|
241
|
+
for (const hubInput of hubInputs) {
|
242
|
+
const { resolvedInputs } = getHubOutput(hubInput)
|
243
|
+
for (const input of resolvedInputs) {
|
244
|
+
allInputs.set(`${input.input.instanceId}:${input.input.output}`, input)
|
245
|
+
}
|
246
|
+
}
|
247
|
+
|
248
|
+
for (const input of allInputs.values()) {
|
249
|
+
if (input.type === componentInput.type) {
|
250
|
+
addInstanceInput(inputName, input.input)
|
251
|
+
|
252
|
+
const key = `${input.input.instanceId}:${input.input.output}`
|
253
|
+
if (injectionInputs.has(key)) {
|
254
|
+
matchedInjectionInputs.set(key, input)
|
255
|
+
}
|
256
|
+
}
|
257
|
+
}
|
258
|
+
}
|
259
|
+
|
260
|
+
const resolvedInputs = fromEntries(
|
261
|
+
Array.from(resolvedInputsMap.entries()).map(([inputName, inputs]) => [
|
262
|
+
inputName,
|
263
|
+
Array.from(inputs.values()),
|
264
|
+
]),
|
265
|
+
)
|
266
|
+
|
267
|
+
return {
|
268
|
+
kind: "instance",
|
269
|
+
component: node.component,
|
270
|
+
resolvedInputs,
|
271
|
+
resolvedOutputs: node.instance.resolvedOutputs ?? {},
|
272
|
+
resolvedInjectionInputs: Array.from(injectionInputs.values()),
|
273
|
+
matchedInjectionInputs: Array.from(matchedInjectionInputs.values()),
|
274
|
+
}
|
275
|
+
},
|
276
|
+
})
|
277
|
+
|
278
|
+
export function getResolvedHubInputs(output: InputResolverOutput): ResolvedInstanceInput[] {
|
279
|
+
if (output.kind !== "hub") {
|
280
|
+
throw new Error("Expected hub node")
|
281
|
+
}
|
282
|
+
|
283
|
+
return output.resolvedInputs
|
284
|
+
}
|
285
|
+
|
286
|
+
export function getResolvedInstanceInputs(
|
287
|
+
output: InputResolverOutput,
|
288
|
+
): Record<string, ResolvedInstanceInput[]> {
|
289
|
+
if (output.kind !== "instance") {
|
290
|
+
throw new Error("Expected instance node")
|
291
|
+
}
|
292
|
+
|
293
|
+
return output.resolvedInputs
|
294
|
+
}
|
295
|
+
|
296
|
+
export function getResolvedInjectionInstanceInputs(
|
297
|
+
output: InputResolverOutput,
|
298
|
+
): ResolvedInstanceInput[] {
|
299
|
+
if (output.kind !== "instance") {
|
300
|
+
throw new Error("Expected instance node")
|
301
|
+
}
|
302
|
+
|
303
|
+
return output.resolvedInjectionInputs
|
304
|
+
}
|
305
|
+
|
306
|
+
export function getMatchedInjectionInstanceInputs(
|
307
|
+
output: InputResolverOutput,
|
308
|
+
): ResolvedInstanceInput[] {
|
309
|
+
if (output.kind !== "instance") {
|
310
|
+
throw new Error("Expected instance node")
|
311
|
+
}
|
312
|
+
|
313
|
+
return output.matchedInjectionInputs
|
314
|
+
}
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import type { GraphResolverFactory } from "./graph-resolver"
|
2
|
+
import { createInputResolver } from "./input"
|
3
|
+
import { createInputHashResolver } from "./input-hash"
|
4
|
+
import { createValidationResolver } from "./validation"
|
5
|
+
|
6
|
+
export const resolverFactories: Record<string, GraphResolverFactory<any, any>> = {
|
7
|
+
[createInputResolver.factoryName]: createInputResolver,
|
8
|
+
[createInputHashResolver.factoryName]: createInputHashResolver,
|
9
|
+
[createValidationResolver.factoryName]: createValidationResolver,
|
10
|
+
}
|
@@ -0,0 +1,94 @@
|
|
1
|
+
import type { ComponentModel, InstanceModel } from "@highstate/contract"
|
2
|
+
import type { ResolvedInstanceInput } from "./input"
|
3
|
+
import { Ajv } from "ajv"
|
4
|
+
import { defineGraphResolver } from "./graph-resolver"
|
5
|
+
|
6
|
+
export type ValidationResolverInput = {
|
7
|
+
instance: InstanceModel
|
8
|
+
component: ComponentModel
|
9
|
+
resolvedInputs: Record<string, ResolvedInstanceInput[]>
|
10
|
+
}
|
11
|
+
|
12
|
+
export type ValidationResolverOutput =
|
13
|
+
| {
|
14
|
+
status: "ok"
|
15
|
+
}
|
16
|
+
| {
|
17
|
+
status: "invalid-args"
|
18
|
+
errorText: string
|
19
|
+
}
|
20
|
+
| {
|
21
|
+
status: "invalid-inputs" | "missing-inputs"
|
22
|
+
errorText: string
|
23
|
+
}
|
24
|
+
|
25
|
+
/**
|
26
|
+
* Validates the instance based on its arguments and inputs.
|
27
|
+
*/
|
28
|
+
export const createValidationResolver = defineGraphResolver<
|
29
|
+
ValidationResolverInput,
|
30
|
+
ValidationResolverOutput
|
31
|
+
>({
|
32
|
+
name: "validation-resolver",
|
33
|
+
getNodeId: node => node.instance.id,
|
34
|
+
|
35
|
+
getNodeDependencies({ resolvedInputs }) {
|
36
|
+
const dependencies: string[] = []
|
37
|
+
|
38
|
+
for (const inputs of Object.values(resolvedInputs)) {
|
39
|
+
for (const input of inputs) {
|
40
|
+
dependencies.push(input.input.instanceId)
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
return dependencies
|
45
|
+
},
|
46
|
+
|
47
|
+
process({ instance, component, resolvedInputs }, dependencies, logger) {
|
48
|
+
const ajv = new Ajv()
|
49
|
+
|
50
|
+
logger.debug({ instanceId: instance.id }, "validating instance")
|
51
|
+
|
52
|
+
for (const [name, argument] of Object.entries(component.args)) {
|
53
|
+
if (!argument.required && !instance.args?.[name]) {
|
54
|
+
continue
|
55
|
+
}
|
56
|
+
|
57
|
+
if (!ajv.validate(argument.schema, instance.args?.[name])) {
|
58
|
+
logger.debug({ instanceId: instance.id, argumentName: name }, "invalid argument")
|
59
|
+
|
60
|
+
return {
|
61
|
+
status: "invalid-args",
|
62
|
+
errorText: ajv.errorsText(),
|
63
|
+
}
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
for (const inputs of Object.values(resolvedInputs)) {
|
68
|
+
for (const input of inputs) {
|
69
|
+
const inputInstance = dependencies.get(input.input.instanceId)
|
70
|
+
if (inputInstance?.status !== "ok") {
|
71
|
+
return {
|
72
|
+
status: "invalid-inputs",
|
73
|
+
errorText: `instance "${input.input.instanceId}" is invalid`,
|
74
|
+
}
|
75
|
+
}
|
76
|
+
}
|
77
|
+
}
|
78
|
+
|
79
|
+
for (const [name, input] of Object.entries(component.inputs)) {
|
80
|
+
if (!input.required || input.multiple) {
|
81
|
+
continue
|
82
|
+
}
|
83
|
+
|
84
|
+
if (!resolvedInputs[name] || !resolvedInputs[name].length) {
|
85
|
+
return {
|
86
|
+
status: "missing-inputs",
|
87
|
+
errorText: `input "${name}" is missing`,
|
88
|
+
}
|
89
|
+
}
|
90
|
+
}
|
91
|
+
|
92
|
+
return { status: "ok" }
|
93
|
+
},
|
94
|
+
})
|