@highstate/backend 0.19.1 → 0.21.1
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-b05q6fm2.js +37 -0
- package/dist/{chunk-V2NILDHS.js → chunk-gxjwa93h.js} +704 -604
- package/dist/{chunk-X2WG3WGL.js → chunk-vzdz6chj.js} +18 -15
- package/dist/highstate.manifest.json +4 -4
- package/dist/index.js +7350 -3514
- package/dist/library/package-resolution-worker.js +121 -10
- package/dist/library/worker/main.js +31 -17
- package/dist/shared/index.js +254 -4
- package/package.json +19 -20
- 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/artifact/factory.ts +3 -2
- 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/find-package-json.test.ts +77 -0
- package/src/library/find-package-json.ts +149 -0
- package/src/library/package-resolution-worker.ts +7 -3
- 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 +230 -68
- package/src/orchestrator/operation.update.failure.test.ts +4 -0
- package/src/orchestrator/operation.update.skip.test.ts +196 -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/factory.ts +3 -3
- package/src/runner/force-abort.ts +7 -2
- package/src/runner/local.ts +116 -87
- package/src/runner/pulumi.ts +3 -5
- package/src/services.ts +53 -2
- 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 +59 -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/terminal/run.sh.ts +9 -4
- package/src/worker/manager.ts +97 -1
- package/LICENSE +0 -21
- package/dist/chunk-I7BWSAN6.js +0 -49
- package/dist/chunk-I7BWSAN6.js.map +0 -1
- package/dist/chunk-V2NILDHS.js.map +0 -1
- package/dist/chunk-X2WG3WGL.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/library/package-resolution-worker.js.map +0 -1
- package/dist/library/worker/main.js.map +0 -1
- package/dist/shared/index.js.map +0 -1
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ComponentModel,
|
|
3
|
+
EntityModel,
|
|
4
|
+
HubInput,
|
|
5
|
+
InstanceInput,
|
|
6
|
+
InstanceModel,
|
|
7
|
+
} from "@highstate/contract"
|
|
8
|
+
import { describe, expect, test } from "vitest"
|
|
9
|
+
import { type InstanceTypeContext, resolveEffectiveOutputType } from "./effective-output-type"
|
|
10
|
+
|
|
11
|
+
function createEntity(
|
|
12
|
+
type: EntityModel["type"],
|
|
13
|
+
inclusions?: EntityModel["inclusions"],
|
|
14
|
+
): EntityModel {
|
|
15
|
+
return {
|
|
16
|
+
type,
|
|
17
|
+
inclusions,
|
|
18
|
+
schema: {},
|
|
19
|
+
meta: {
|
|
20
|
+
title: type,
|
|
21
|
+
},
|
|
22
|
+
definitionHash: 1,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createInputSpec(
|
|
27
|
+
type: EntityModel["type"],
|
|
28
|
+
options?: { fromInput?: string; multiple?: boolean },
|
|
29
|
+
) {
|
|
30
|
+
return {
|
|
31
|
+
type,
|
|
32
|
+
fromInput: options?.fromInput,
|
|
33
|
+
required: true,
|
|
34
|
+
multiple: options?.multiple ?? false,
|
|
35
|
+
meta: {
|
|
36
|
+
title: type,
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function createComponent(options: {
|
|
42
|
+
type: ComponentModel["type"]
|
|
43
|
+
inputs?: Record<string, ReturnType<typeof createInputSpec>>
|
|
44
|
+
outputs?: Record<string, ReturnType<typeof createInputSpec>>
|
|
45
|
+
}): ComponentModel {
|
|
46
|
+
return {
|
|
47
|
+
type: options.type,
|
|
48
|
+
kind: "unit",
|
|
49
|
+
args: {},
|
|
50
|
+
inputs: options.inputs ?? {},
|
|
51
|
+
outputs: options.outputs ?? {},
|
|
52
|
+
meta: {
|
|
53
|
+
title: options.type,
|
|
54
|
+
defaultNamePrefix: "test",
|
|
55
|
+
},
|
|
56
|
+
definitionHash: 1,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function createInstance(options: {
|
|
61
|
+
id: InstanceModel["id"]
|
|
62
|
+
type: InstanceModel["type"]
|
|
63
|
+
inputs?: Record<string, InstanceInput[]>
|
|
64
|
+
hubInputs?: Record<string, HubInput[]>
|
|
65
|
+
injectionInputs?: HubInput[]
|
|
66
|
+
}): InstanceModel {
|
|
67
|
+
return {
|
|
68
|
+
id: options.id,
|
|
69
|
+
name: "instance",
|
|
70
|
+
type: options.type,
|
|
71
|
+
kind: "unit",
|
|
72
|
+
inputs: options.inputs,
|
|
73
|
+
hubInputs: options.hubInputs,
|
|
74
|
+
injectionInputs: options.injectionInputs,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function createResolver(contexts: Record<string, InstanceTypeContext>) {
|
|
79
|
+
return (instanceId: string) => contexts[instanceId]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe("resolveEffectiveOutputType", () => {
|
|
83
|
+
test.concurrent("returns declared output type when no fromInput is used", () => {
|
|
84
|
+
const entities = {
|
|
85
|
+
"example.base.v1": createEntity("example.base.v1"),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const instance = createInstance({
|
|
89
|
+
id: "example.writer.v1:writer-1",
|
|
90
|
+
type: "example.writer.v1",
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const component = createComponent({
|
|
94
|
+
type: "example.writer.v1",
|
|
95
|
+
outputs: {
|
|
96
|
+
value: createInputSpec("example.base.v1"),
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const resolved = resolveEffectiveOutputType({
|
|
101
|
+
input: { instanceId: instance.id, output: "value" },
|
|
102
|
+
fallbackType: "example.fallback.v1",
|
|
103
|
+
getInstanceContext: createResolver({
|
|
104
|
+
[instance.id]: {
|
|
105
|
+
instance,
|
|
106
|
+
component,
|
|
107
|
+
entities,
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
expect(resolved).toBe("example.base.v1")
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test.concurrent("forwards output type from a single direct input", () => {
|
|
116
|
+
const entities = {
|
|
117
|
+
"example.base.v1": createEntity("example.base.v1"),
|
|
118
|
+
"example.child.v1": createEntity("example.child.v1"),
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const source = createInstance({
|
|
122
|
+
id: "example.source.v1:source-1",
|
|
123
|
+
type: "example.source.v1",
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const sourceComponent = createComponent({
|
|
127
|
+
type: "example.source.v1",
|
|
128
|
+
outputs: {
|
|
129
|
+
value: createInputSpec("example.child.v1"),
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
const forwarder = createInstance({
|
|
134
|
+
id: "example.forwarder.v1:forwarder-1",
|
|
135
|
+
type: "example.forwarder.v1",
|
|
136
|
+
inputs: {
|
|
137
|
+
source: [{ instanceId: source.id, output: "value" }],
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const forwarderComponent = createComponent({
|
|
142
|
+
type: "example.forwarder.v1",
|
|
143
|
+
inputs: {
|
|
144
|
+
source: createInputSpec("example.base.v1"),
|
|
145
|
+
},
|
|
146
|
+
outputs: {
|
|
147
|
+
value: createInputSpec("example.base.v1", { fromInput: "source" }),
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
const resolved = resolveEffectiveOutputType({
|
|
152
|
+
input: { instanceId: forwarder.id, output: "value" },
|
|
153
|
+
fallbackType: "example.base.v1",
|
|
154
|
+
getInstanceContext: createResolver({
|
|
155
|
+
[source.id]: {
|
|
156
|
+
instance: source,
|
|
157
|
+
component: sourceComponent,
|
|
158
|
+
entities,
|
|
159
|
+
},
|
|
160
|
+
[forwarder.id]: {
|
|
161
|
+
instance: forwarder,
|
|
162
|
+
component: forwarderComponent,
|
|
163
|
+
entities,
|
|
164
|
+
},
|
|
165
|
+
}),
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
expect(resolved).toBe("example.child.v1")
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test.concurrent("falls back when forwarded source input has hub inputs", () => {
|
|
172
|
+
const entities = {
|
|
173
|
+
"example.base.v1": createEntity("example.base.v1"),
|
|
174
|
+
"example.child.v1": createEntity("example.child.v1"),
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const source = createInstance({
|
|
178
|
+
id: "example.source.v1:source-1",
|
|
179
|
+
type: "example.source.v1",
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
const sourceComponent = createComponent({
|
|
183
|
+
type: "example.source.v1",
|
|
184
|
+
outputs: {
|
|
185
|
+
value: createInputSpec("example.child.v1"),
|
|
186
|
+
},
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const forwarder = createInstance({
|
|
190
|
+
id: "example.forwarder.v1:forwarder-1",
|
|
191
|
+
type: "example.forwarder.v1",
|
|
192
|
+
inputs: {
|
|
193
|
+
source: [{ instanceId: source.id, output: "value" }],
|
|
194
|
+
},
|
|
195
|
+
hubInputs: {
|
|
196
|
+
source: [{ hubId: "hub1" }],
|
|
197
|
+
},
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
const forwarderComponent = createComponent({
|
|
201
|
+
type: "example.forwarder.v1",
|
|
202
|
+
inputs: {
|
|
203
|
+
source: createInputSpec("example.base.v1"),
|
|
204
|
+
},
|
|
205
|
+
outputs: {
|
|
206
|
+
value: createInputSpec("example.base.v1", { fromInput: "source" }),
|
|
207
|
+
},
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
const resolved = resolveEffectiveOutputType({
|
|
211
|
+
input: { instanceId: forwarder.id, output: "value" },
|
|
212
|
+
fallbackType: "example.fallback.v1",
|
|
213
|
+
getInstanceContext: createResolver({
|
|
214
|
+
[source.id]: {
|
|
215
|
+
instance: source,
|
|
216
|
+
component: sourceComponent,
|
|
217
|
+
entities,
|
|
218
|
+
},
|
|
219
|
+
[forwarder.id]: {
|
|
220
|
+
instance: forwarder,
|
|
221
|
+
component: forwarderComponent,
|
|
222
|
+
entities,
|
|
223
|
+
},
|
|
224
|
+
}),
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
expect(resolved).toBe("example.base.v1")
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
test.concurrent("falls back when forwarded source input has multiple direct inputs", () => {
|
|
231
|
+
const entities = {
|
|
232
|
+
"example.base.v1": createEntity("example.base.v1"),
|
|
233
|
+
"example.child.v1": createEntity("example.child.v1"),
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const source = createInstance({
|
|
237
|
+
id: "example.source.v1:source-1",
|
|
238
|
+
type: "example.source.v1",
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
const sourceComponent = createComponent({
|
|
242
|
+
type: "example.source.v1",
|
|
243
|
+
outputs: {
|
|
244
|
+
value: createInputSpec("example.child.v1"),
|
|
245
|
+
},
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
const forwarder = createInstance({
|
|
249
|
+
id: "example.forwarder.v1:forwarder-1",
|
|
250
|
+
type: "example.forwarder.v1",
|
|
251
|
+
inputs: {
|
|
252
|
+
source: [
|
|
253
|
+
{ instanceId: source.id, output: "value" },
|
|
254
|
+
{ instanceId: source.id, output: "value" },
|
|
255
|
+
],
|
|
256
|
+
},
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
const forwarderComponent = createComponent({
|
|
260
|
+
type: "example.forwarder.v1",
|
|
261
|
+
inputs: {
|
|
262
|
+
source: createInputSpec("example.base.v1"),
|
|
263
|
+
},
|
|
264
|
+
outputs: {
|
|
265
|
+
value: createInputSpec("example.base.v1", { fromInput: "source" }),
|
|
266
|
+
},
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
const resolved = resolveEffectiveOutputType({
|
|
270
|
+
input: { instanceId: forwarder.id, output: "value" },
|
|
271
|
+
fallbackType: "example.fallback.v1",
|
|
272
|
+
getInstanceContext: createResolver({
|
|
273
|
+
[source.id]: {
|
|
274
|
+
instance: source,
|
|
275
|
+
component: sourceComponent,
|
|
276
|
+
entities,
|
|
277
|
+
},
|
|
278
|
+
[forwarder.id]: {
|
|
279
|
+
instance: forwarder,
|
|
280
|
+
component: forwarderComponent,
|
|
281
|
+
entities,
|
|
282
|
+
},
|
|
283
|
+
}),
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
expect(resolved).toBe("example.base.v1")
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
test.concurrent("applies path traversal to the resolved type", () => {
|
|
290
|
+
const entities = {
|
|
291
|
+
"example.base.v1": createEntity("example.base.v1", [
|
|
292
|
+
{
|
|
293
|
+
type: "example.child.v1",
|
|
294
|
+
field: "child",
|
|
295
|
+
required: true,
|
|
296
|
+
multiple: false,
|
|
297
|
+
},
|
|
298
|
+
]),
|
|
299
|
+
"example.child.v1": createEntity("example.child.v1", [
|
|
300
|
+
{
|
|
301
|
+
type: "example.leaf.v1",
|
|
302
|
+
field: "leaf",
|
|
303
|
+
required: true,
|
|
304
|
+
multiple: false,
|
|
305
|
+
},
|
|
306
|
+
]),
|
|
307
|
+
"example.leaf.v1": createEntity("example.leaf.v1"),
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const instance = createInstance({
|
|
311
|
+
id: "example.writer.v1:writer-1",
|
|
312
|
+
type: "example.writer.v1",
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
const component = createComponent({
|
|
316
|
+
type: "example.writer.v1",
|
|
317
|
+
outputs: {
|
|
318
|
+
value: createInputSpec("example.base.v1"),
|
|
319
|
+
},
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
const resolved = resolveEffectiveOutputType({
|
|
323
|
+
input: {
|
|
324
|
+
instanceId: instance.id,
|
|
325
|
+
output: "value",
|
|
326
|
+
path: "child.leaf",
|
|
327
|
+
},
|
|
328
|
+
fallbackType: "example.fallback.v1",
|
|
329
|
+
getInstanceContext: createResolver({
|
|
330
|
+
[instance.id]: {
|
|
331
|
+
instance,
|
|
332
|
+
component,
|
|
333
|
+
entities,
|
|
334
|
+
},
|
|
335
|
+
}),
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
expect(resolved).toBe("example.leaf.v1")
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
test.concurrent("returns root type when path is invalid", () => {
|
|
342
|
+
const entities = {
|
|
343
|
+
"example.base.v1": createEntity("example.base.v1", [
|
|
344
|
+
{
|
|
345
|
+
type: "example.child.v1",
|
|
346
|
+
field: "child",
|
|
347
|
+
required: true,
|
|
348
|
+
multiple: false,
|
|
349
|
+
},
|
|
350
|
+
]),
|
|
351
|
+
"example.child.v1": createEntity("example.child.v1"),
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const instance = createInstance({
|
|
355
|
+
id: "example.writer.v1:writer-1",
|
|
356
|
+
type: "example.writer.v1",
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
const component = createComponent({
|
|
360
|
+
type: "example.writer.v1",
|
|
361
|
+
outputs: {
|
|
362
|
+
value: createInputSpec("example.base.v1"),
|
|
363
|
+
},
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
const resolved = resolveEffectiveOutputType({
|
|
367
|
+
input: {
|
|
368
|
+
instanceId: instance.id,
|
|
369
|
+
output: "value",
|
|
370
|
+
path: "missing.segment",
|
|
371
|
+
},
|
|
372
|
+
fallbackType: "example.fallback.v1",
|
|
373
|
+
getInstanceContext: createResolver({
|
|
374
|
+
[instance.id]: {
|
|
375
|
+
instance,
|
|
376
|
+
component,
|
|
377
|
+
entities,
|
|
378
|
+
},
|
|
379
|
+
}),
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
expect(resolved).toBe("example.base.v1")
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
test.concurrent(
|
|
386
|
+
"uses direct inclusion when no path is provided and fallback matches inclusion type",
|
|
387
|
+
() => {
|
|
388
|
+
const entities = {
|
|
389
|
+
"example.identity.v1": createEntity("example.identity.v1", [
|
|
390
|
+
{
|
|
391
|
+
type: "example.peer.v1",
|
|
392
|
+
field: "peer",
|
|
393
|
+
required: true,
|
|
394
|
+
multiple: false,
|
|
395
|
+
},
|
|
396
|
+
]),
|
|
397
|
+
"example.peer.v1": createEntity("example.peer.v1"),
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const identity = createInstance({
|
|
401
|
+
id: "example.identity.v1:identity-1",
|
|
402
|
+
type: "example.identity.v1",
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
const component = createComponent({
|
|
406
|
+
type: "example.identity.v1",
|
|
407
|
+
outputs: {
|
|
408
|
+
identity: createInputSpec("example.identity.v1"),
|
|
409
|
+
},
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
const resolved = resolveEffectiveOutputType({
|
|
413
|
+
input: {
|
|
414
|
+
instanceId: identity.id,
|
|
415
|
+
output: "identity",
|
|
416
|
+
},
|
|
417
|
+
fallbackType: "example.peer.v1",
|
|
418
|
+
getInstanceContext: createResolver({
|
|
419
|
+
[identity.id]: {
|
|
420
|
+
instance: identity,
|
|
421
|
+
component,
|
|
422
|
+
entities,
|
|
423
|
+
},
|
|
424
|
+
}),
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
expect(resolved).toBe("example.peer.v1")
|
|
428
|
+
},
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
test.concurrent("handles forwarding cycles without recursion errors", () => {
|
|
432
|
+
const entities = {
|
|
433
|
+
"example.base.v1": createEntity("example.base.v1"),
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const instanceA = createInstance({
|
|
437
|
+
id: "example.a.v1:a-1",
|
|
438
|
+
type: "example.a.v1",
|
|
439
|
+
inputs: {
|
|
440
|
+
source: [{ instanceId: "example.b.v1:b-1", output: "value" }],
|
|
441
|
+
},
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
const instanceB = createInstance({
|
|
445
|
+
id: "example.b.v1:b-1",
|
|
446
|
+
type: "example.b.v1",
|
|
447
|
+
inputs: {
|
|
448
|
+
source: [{ instanceId: "example.a.v1:a-1", output: "value" }],
|
|
449
|
+
},
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
const componentA = createComponent({
|
|
453
|
+
type: "example.a.v1",
|
|
454
|
+
inputs: {
|
|
455
|
+
source: createInputSpec("example.base.v1"),
|
|
456
|
+
},
|
|
457
|
+
outputs: {
|
|
458
|
+
value: createInputSpec("example.base.v1", { fromInput: "source" }),
|
|
459
|
+
},
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
const componentB = createComponent({
|
|
463
|
+
type: "example.b.v1",
|
|
464
|
+
inputs: {
|
|
465
|
+
source: createInputSpec("example.base.v1"),
|
|
466
|
+
},
|
|
467
|
+
outputs: {
|
|
468
|
+
value: createInputSpec("example.base.v1", { fromInput: "source" }),
|
|
469
|
+
},
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
const resolved = resolveEffectiveOutputType({
|
|
473
|
+
input: {
|
|
474
|
+
instanceId: instanceA.id,
|
|
475
|
+
output: "value",
|
|
476
|
+
},
|
|
477
|
+
fallbackType: "example.fallback.v1",
|
|
478
|
+
getInstanceContext: createResolver({
|
|
479
|
+
[instanceA.id]: {
|
|
480
|
+
instance: instanceA,
|
|
481
|
+
component: componentA,
|
|
482
|
+
entities,
|
|
483
|
+
},
|
|
484
|
+
[instanceB.id]: {
|
|
485
|
+
instance: instanceB,
|
|
486
|
+
component: componentB,
|
|
487
|
+
entities,
|
|
488
|
+
},
|
|
489
|
+
}),
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
expect(resolved).toBe("example.base.v1")
|
|
493
|
+
})
|
|
494
|
+
})
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { ComponentModel, EntityModel, InstanceInput, InstanceModel } from "@highstate/contract"
|
|
2
|
+
|
|
3
|
+
export type InstanceTypeContext = {
|
|
4
|
+
instance: InstanceModel
|
|
5
|
+
component: ComponentModel
|
|
6
|
+
entities: Readonly<Record<string, EntityModel>>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type ResolveEffectiveOutputTypeOptions = {
|
|
10
|
+
input: InstanceInput
|
|
11
|
+
fallbackType: string
|
|
12
|
+
getInstanceContext: (instanceId: string) => InstanceTypeContext | undefined
|
|
13
|
+
visited?: Set<string>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolves the effective entity type of a connected output.
|
|
18
|
+
*
|
|
19
|
+
* The function starts from the connected output declaration and then applies
|
|
20
|
+
* two additional transformations used by the platform:
|
|
21
|
+
*
|
|
22
|
+
* 1. `fromInput` forwarding.
|
|
23
|
+
* If the output references `fromInput`, it follows that input edge and reuses
|
|
24
|
+
* the upstream effective type when the forwarding source is deterministic.
|
|
25
|
+
*
|
|
26
|
+
* 2. Input `path` traversal.
|
|
27
|
+
* If the edge provides a `path`, the type is narrowed by following entity
|
|
28
|
+
* inclusions segment-by-segment.
|
|
29
|
+
*
|
|
30
|
+
* The function is intentionally conservative.
|
|
31
|
+
* If a forwarding source is ambiguous (missing, multiple, hub/injection-backed,
|
|
32
|
+
* unknown node/entity, or cyclic), it falls back to the declared type.
|
|
33
|
+
*
|
|
34
|
+
* @param options.input The connected instance output reference being consumed.
|
|
35
|
+
* @param options.fallbackType The type to use when resolution cannot continue safely.
|
|
36
|
+
* @param options.getInstanceContext A lookup that returns instance/component/entities for an instance ID.
|
|
37
|
+
* @param options.visited A recursion guard set used to prevent forwarding cycles.
|
|
38
|
+
* @returns The effective resolved output entity type.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveEffectiveOutputType(options: ResolveEffectiveOutputTypeOptions): string {
|
|
41
|
+
const visited = options.visited ?? new Set<string>()
|
|
42
|
+
|
|
43
|
+
const producer = options.getInstanceContext(options.input.instanceId)
|
|
44
|
+
if (!producer) {
|
|
45
|
+
return resolveTypeByPathOrFallbackInclusion({
|
|
46
|
+
rootType: options.fallbackType,
|
|
47
|
+
path: options.input.path,
|
|
48
|
+
entities: undefined,
|
|
49
|
+
fallbackType: options.fallbackType,
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const outputSpec = producer.component.outputs[options.input.output]
|
|
54
|
+
if (!outputSpec) {
|
|
55
|
+
return resolveTypeByPathOrFallbackInclusion({
|
|
56
|
+
rootType: options.fallbackType,
|
|
57
|
+
path: options.input.path,
|
|
58
|
+
entities: producer.entities,
|
|
59
|
+
fallbackType: options.fallbackType,
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let effectiveType: string = outputSpec.type
|
|
64
|
+
|
|
65
|
+
if (outputSpec.fromInput) {
|
|
66
|
+
const visitedKey = `${producer.instance.id}:${options.input.output}`
|
|
67
|
+
if (!visited.has(visitedKey)) {
|
|
68
|
+
visited.add(visitedKey)
|
|
69
|
+
|
|
70
|
+
const forwardedInputName = outputSpec.fromInput
|
|
71
|
+
const forwardedFallback = producer.component.inputs[forwardedInputName]?.type ?? effectiveType
|
|
72
|
+
const hasHubInputs = (producer.instance.hubInputs?.[forwardedInputName]?.length ?? 0) > 0
|
|
73
|
+
const hasInjectionInputs = (producer.instance.injectionInputs?.length ?? 0) > 0
|
|
74
|
+
const directInputs = producer.instance.inputs?.[forwardedInputName] ?? []
|
|
75
|
+
|
|
76
|
+
if (!hasHubInputs && !hasInjectionInputs && directInputs.length === 1) {
|
|
77
|
+
const forwardedInput = directInputs[0]
|
|
78
|
+
if (forwardedInput) {
|
|
79
|
+
effectiveType = resolveEffectiveOutputType({
|
|
80
|
+
input: forwardedInput,
|
|
81
|
+
fallbackType: forwardedFallback,
|
|
82
|
+
getInstanceContext: options.getInstanceContext,
|
|
83
|
+
visited,
|
|
84
|
+
})
|
|
85
|
+
} else {
|
|
86
|
+
effectiveType = forwardedFallback
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
effectiveType = forwardedFallback
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return resolveTypeByPathOrFallbackInclusion({
|
|
95
|
+
rootType: effectiveType,
|
|
96
|
+
path: options.input.path,
|
|
97
|
+
entities: producer.entities,
|
|
98
|
+
fallbackType: options.fallbackType,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function resolveTypeByPathOrFallbackInclusion(options: {
|
|
103
|
+
rootType: string
|
|
104
|
+
path: string | undefined
|
|
105
|
+
entities: Readonly<Record<string, EntityModel>> | undefined
|
|
106
|
+
fallbackType: string
|
|
107
|
+
}): string {
|
|
108
|
+
const { rootType, path, entities, fallbackType } = options
|
|
109
|
+
|
|
110
|
+
if (!path || !entities) {
|
|
111
|
+
return resolveTypeByImplicitInclusion(rootType, fallbackType, entities)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let currentType = rootType
|
|
115
|
+
const segments = path.split(".")
|
|
116
|
+
|
|
117
|
+
for (const segment of segments) {
|
|
118
|
+
const entity = entities[currentType]
|
|
119
|
+
if (!entity) {
|
|
120
|
+
return rootType
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const inclusion = entity.inclusions?.find(inc => inc.field === segment)
|
|
124
|
+
if (!inclusion) {
|
|
125
|
+
return rootType
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
currentType = inclusion.type
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return currentType
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function resolveTypeByImplicitInclusion(
|
|
135
|
+
rootType: string,
|
|
136
|
+
fallbackType: string,
|
|
137
|
+
entities: Readonly<Record<string, EntityModel>> | undefined,
|
|
138
|
+
): string {
|
|
139
|
+
if (!entities) {
|
|
140
|
+
return rootType
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (rootType === fallbackType) {
|
|
144
|
+
return rootType
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const rootEntity = entities[rootType]
|
|
148
|
+
if (!rootEntity) {
|
|
149
|
+
return rootType
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (rootEntity.extensions?.includes(fallbackType)) {
|
|
153
|
+
return rootType
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const inclusion = rootEntity.inclusions?.find(inc => inc.type === fallbackType)
|
|
157
|
+
if (inclusion) {
|
|
158
|
+
return fallbackType
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return rootType
|
|
162
|
+
}
|