@highstate/contract 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +682 -426
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/component.spec.ts +912 -3
- package/src/component.ts +524 -89
- package/src/cuidv2d.spec.ts +39 -0
- package/src/cuidv2d.ts +54 -0
- package/src/entity.ts +321 -81
- package/src/evaluation.ts +62 -12
- package/src/index.ts +9 -1
- package/src/instance-input.ts +219 -0
- package/src/instance.ts +312 -63
- package/src/json-schema.ts +115 -0
- package/src/pulumi.ts +45 -16
- package/src/unit.ts +26 -10
- package/src/compaction.spec.ts +0 -215
- package/src/compaction.ts +0 -381
package/src/component.spec.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it } from "vitest"
|
|
2
2
|
import { z } from "zod"
|
|
3
|
-
import { defineComponent } from "./component"
|
|
3
|
+
import { defineComponent, kind } from "./component"
|
|
4
4
|
import { defineEntity } from "./entity"
|
|
5
|
-
import { resetEvaluation } from "./evaluation"
|
|
5
|
+
import { boundaryInput, getRuntimeInstances, resetEvaluation } from "./evaluation"
|
|
6
|
+
import { type EntityInput, type InstanceInput, selectInput } from "./instance"
|
|
6
7
|
|
|
7
8
|
describe("defineComponent", () => {
|
|
8
9
|
beforeEach(resetEvaluation)
|
|
@@ -77,8 +78,16 @@ describe("defineComponent", () => {
|
|
|
77
78
|
])
|
|
78
79
|
|
|
79
80
|
const embeddedResult = server.schema.safeParse({
|
|
81
|
+
$meta: {
|
|
82
|
+
type: "common.server.v1",
|
|
83
|
+
identity: "server-1",
|
|
84
|
+
},
|
|
80
85
|
endpoint: "127.0.0.1",
|
|
81
86
|
resource: {
|
|
87
|
+
$meta: {
|
|
88
|
+
type: "common.resource.v1",
|
|
89
|
+
identity: "resource-1",
|
|
90
|
+
},
|
|
82
91
|
id: "test",
|
|
83
92
|
},
|
|
84
93
|
})
|
|
@@ -182,9 +191,21 @@ describe("defineComponent", () => {
|
|
|
182
191
|
|
|
183
192
|
expect(
|
|
184
193
|
server.schema.safeParse({
|
|
194
|
+
$meta: {
|
|
195
|
+
type: "common.server.v1",
|
|
196
|
+
identity: "server-1",
|
|
197
|
+
},
|
|
185
198
|
tag: "prod",
|
|
186
199
|
endpoint: "127.0.0.1",
|
|
187
|
-
"common.resource.v1": [
|
|
200
|
+
"common.resource.v1": [
|
|
201
|
+
{
|
|
202
|
+
$meta: {
|
|
203
|
+
type: "common.resource.v1",
|
|
204
|
+
identity: "resource-1",
|
|
205
|
+
},
|
|
206
|
+
id: "test",
|
|
207
|
+
},
|
|
208
|
+
],
|
|
188
209
|
}).success,
|
|
189
210
|
).toBe(true)
|
|
190
211
|
})
|
|
@@ -242,4 +263,892 @@ describe("defineComponent", () => {
|
|
|
242
263
|
|
|
243
264
|
void servers
|
|
244
265
|
})
|
|
266
|
+
|
|
267
|
+
it("should create deep output accessors with dynamic paths", () => {
|
|
268
|
+
const subnet = defineEntity({
|
|
269
|
+
type: "network.subnet.v1",
|
|
270
|
+
schema: z.object({ cidr: z.string() }),
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
const endpoint = defineEntity({
|
|
274
|
+
type: "network.l4-endpoint.v1",
|
|
275
|
+
includes: {
|
|
276
|
+
subnet,
|
|
277
|
+
},
|
|
278
|
+
schema: z.object({ host: z.string() }),
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
const peer = defineEntity({
|
|
282
|
+
type: "wireguard.peer.v1",
|
|
283
|
+
includes: {
|
|
284
|
+
endpoints: {
|
|
285
|
+
entity: endpoint,
|
|
286
|
+
multiple: true,
|
|
287
|
+
required: false,
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
schema: z.object({ name: z.string() }),
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
const producer = defineComponent({
|
|
294
|
+
type: "test.producer.v1",
|
|
295
|
+
outputs: {
|
|
296
|
+
peer,
|
|
297
|
+
},
|
|
298
|
+
create: ({ id }) => ({
|
|
299
|
+
peer: {
|
|
300
|
+
instanceId: id,
|
|
301
|
+
output: "peer",
|
|
302
|
+
} as never,
|
|
303
|
+
}),
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
const { peer: peerInput } = producer({ name: "test" })
|
|
307
|
+
|
|
308
|
+
expect(Array.isArray(peerInput.endpoints)).toBe(false)
|
|
309
|
+
expect(peerInput.endpoints.path).toBe("endpoints")
|
|
310
|
+
expect(peerInput.endpoints.subnet.path).toBe("endpoints.subnet")
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it("should preserve parent boundary for selected provided input", () => {
|
|
314
|
+
const cluster = defineEntity({
|
|
315
|
+
type: "example.cluster.v1",
|
|
316
|
+
schema: z.object({ id: z.string() }),
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
const source = defineComponent({
|
|
320
|
+
type: "example.source.v1",
|
|
321
|
+
outputs: { k8sCluster: cluster },
|
|
322
|
+
create: ({ id }) => ({
|
|
323
|
+
k8sCluster: { instanceId: id, output: "k8sCluster" },
|
|
324
|
+
}),
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
const child = defineComponent({
|
|
328
|
+
type: "example.child.v1",
|
|
329
|
+
inputs: { k8sCluster: cluster },
|
|
330
|
+
create: () => ({}),
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
const parent = defineComponent({
|
|
334
|
+
type: "example.parent.v1",
|
|
335
|
+
inputs: {
|
|
336
|
+
k8sClusters: {
|
|
337
|
+
entity: cluster,
|
|
338
|
+
multiple: true,
|
|
339
|
+
required: false,
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
create: ({ name, inputs }) => {
|
|
343
|
+
child({
|
|
344
|
+
name: `${name}.selected-child`,
|
|
345
|
+
inputs: {
|
|
346
|
+
k8sCluster: selectInput(inputs.k8sClusters, "cluster-1"),
|
|
347
|
+
},
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
return {}
|
|
351
|
+
},
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
const { k8sCluster } = source({ name: "cluster-1" })
|
|
355
|
+
|
|
356
|
+
parent({
|
|
357
|
+
name: "parent-1",
|
|
358
|
+
inputs: { k8sClusters: [k8sCluster] },
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
const childInstance = getRuntimeInstances().find(
|
|
362
|
+
runtime => runtime.instance.id === "example.child.v1:parent-1.selected-child",
|
|
363
|
+
)?.instance
|
|
364
|
+
|
|
365
|
+
expect(childInstance).toBeDefined()
|
|
366
|
+
expect(childInstance?.inputs?.k8sCluster).toEqual([
|
|
367
|
+
{
|
|
368
|
+
instanceId: "example.parent.v1:parent-1",
|
|
369
|
+
output: "k8sClusters",
|
|
370
|
+
},
|
|
371
|
+
])
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
it("should preserve nested dynamic accessors for selected provided input", () => {
|
|
375
|
+
const subnet = defineEntity({
|
|
376
|
+
type: "network.subnet.v1",
|
|
377
|
+
schema: z.object({ cidr: z.string() }),
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
const endpoint = defineEntity({
|
|
381
|
+
type: "network.l4-endpoint.v1",
|
|
382
|
+
includes: {
|
|
383
|
+
subnet,
|
|
384
|
+
},
|
|
385
|
+
schema: z.object({ host: z.string() }),
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
const peer = defineEntity({
|
|
389
|
+
type: "wireguard.peer.v1",
|
|
390
|
+
includes: {
|
|
391
|
+
endpoints: {
|
|
392
|
+
entity: endpoint,
|
|
393
|
+
multiple: true,
|
|
394
|
+
required: false,
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
schema: z.object({ name: z.string() }),
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
let selectedSubnetPath: string | undefined
|
|
401
|
+
|
|
402
|
+
const producer = defineComponent({
|
|
403
|
+
type: "example.peer-source.v1",
|
|
404
|
+
outputs: {
|
|
405
|
+
peer,
|
|
406
|
+
},
|
|
407
|
+
create: ({ id }) => ({
|
|
408
|
+
peer: {
|
|
409
|
+
instanceId: id,
|
|
410
|
+
output: "peer",
|
|
411
|
+
} as never,
|
|
412
|
+
}),
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
const selector = defineComponent({
|
|
416
|
+
type: "example.peer-selector.v1",
|
|
417
|
+
inputs: {
|
|
418
|
+
peers: {
|
|
419
|
+
entity: peer,
|
|
420
|
+
multiple: true,
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
create: ({ inputs }) => {
|
|
424
|
+
const selectedPeer = selectInput(inputs.peers, "peer-1")
|
|
425
|
+
selectedSubnetPath = selectedPeer.endpoints.subnet.path
|
|
426
|
+
|
|
427
|
+
return {}
|
|
428
|
+
},
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
const { peer: peerInput } = producer({ name: "peer-1" })
|
|
432
|
+
|
|
433
|
+
selector({
|
|
434
|
+
name: "selector-1",
|
|
435
|
+
inputs: {
|
|
436
|
+
peers: [peerInput],
|
|
437
|
+
},
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
expect(selectedSubnetPath).toBe("endpoints.subnet")
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
it("should not mutate original boundary when selecting provided input", () => {
|
|
444
|
+
const peer = defineEntity({
|
|
445
|
+
type: "wireguard.peer.v1",
|
|
446
|
+
schema: z.object({ name: z.string() }),
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
const source = defineComponent({
|
|
450
|
+
type: "example.peer-source.v1",
|
|
451
|
+
outputs: { peer },
|
|
452
|
+
create: ({ id }) => ({
|
|
453
|
+
peer: { instanceId: id, output: "peer" },
|
|
454
|
+
}),
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
const { peer: peerInput } = source({ name: "peer-1" })
|
|
458
|
+
const originalBoundary = peerInput[boundaryInput]
|
|
459
|
+
|
|
460
|
+
const selectedGroup = [peerInput] as (typeof peerInput)[] & { [boundaryInput]: InstanceInput }
|
|
461
|
+
selectedGroup[boundaryInput] = {
|
|
462
|
+
instanceId: "example.parent.v1:parent-1",
|
|
463
|
+
output: "peers",
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const selected = selectInput(selectedGroup, "peer-1")
|
|
467
|
+
|
|
468
|
+
expect(peerInput[boundaryInput]).toEqual(originalBoundary)
|
|
469
|
+
expect(selected[boundaryInput]).toEqual({
|
|
470
|
+
instanceId: "example.parent.v1:parent-1",
|
|
471
|
+
output: "peers",
|
|
472
|
+
})
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
it("should return chained missing proxy when selected input is absent", () => {
|
|
476
|
+
const peer = defineEntity({
|
|
477
|
+
type: "wireguard.peer.v1",
|
|
478
|
+
schema: z.object({ name: z.string() }),
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
const source = defineComponent({
|
|
482
|
+
type: "example.peer-source.v1",
|
|
483
|
+
outputs: { peer },
|
|
484
|
+
create: ({ id }) => ({
|
|
485
|
+
peer: { instanceId: id, output: "peer" },
|
|
486
|
+
}),
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
const { peer: peerInput } = source({ name: "peer-1" })
|
|
490
|
+
|
|
491
|
+
const selectedGroup = [peerInput] as (typeof peerInput)[] & { [boundaryInput]: InstanceInput }
|
|
492
|
+
selectedGroup[boundaryInput] = {
|
|
493
|
+
instanceId: "example.parent.v1:parent-1",
|
|
494
|
+
output: "peers",
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const missingPeer = selectInput(selectedGroup, "missing-peer")
|
|
498
|
+
|
|
499
|
+
expect(missingPeer.provided).toBe(false)
|
|
500
|
+
expect(missingPeer[boundaryInput]).toEqual({
|
|
501
|
+
instanceId: "example.parent.v1:parent-1",
|
|
502
|
+
output: "peers",
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
const nestedMissing = (missingPeer as Record<string, unknown>).network as Record<
|
|
506
|
+
string,
|
|
507
|
+
unknown
|
|
508
|
+
>
|
|
509
|
+
expect((nestedMissing.provided as boolean) ?? false).toBe(false)
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
it("should preserve parent boundary for indexed multiple input item", () => {
|
|
513
|
+
const cluster = defineEntity({
|
|
514
|
+
type: "example.cluster.v1",
|
|
515
|
+
schema: z.object({ id: z.string() }),
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
const source = defineComponent({
|
|
519
|
+
type: "example.source.v1",
|
|
520
|
+
outputs: { k8sCluster: cluster },
|
|
521
|
+
create: ({ id }) => ({
|
|
522
|
+
k8sCluster: { instanceId: id, output: "k8sCluster" },
|
|
523
|
+
}),
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
const child = defineComponent({
|
|
527
|
+
type: "example.child.v1",
|
|
528
|
+
inputs: { k8sCluster: cluster },
|
|
529
|
+
create: () => ({}),
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
const parent = defineComponent({
|
|
533
|
+
type: "example.parent.v1",
|
|
534
|
+
inputs: {
|
|
535
|
+
k8sClusters: {
|
|
536
|
+
entity: cluster,
|
|
537
|
+
multiple: true,
|
|
538
|
+
required: false,
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
create: ({ name, inputs }) => {
|
|
542
|
+
child({
|
|
543
|
+
name: `${name}.indexed-child`,
|
|
544
|
+
inputs: {
|
|
545
|
+
k8sCluster: inputs.k8sClusters[0],
|
|
546
|
+
},
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
return {}
|
|
550
|
+
},
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
const { k8sCluster } = source({ name: "cluster-1" })
|
|
554
|
+
|
|
555
|
+
parent({
|
|
556
|
+
name: "parent-2",
|
|
557
|
+
inputs: { k8sClusters: [k8sCluster] },
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
const childInstance = getRuntimeInstances().find(
|
|
561
|
+
runtime => runtime.instance.id === "example.child.v1:parent-2.indexed-child",
|
|
562
|
+
)?.instance
|
|
563
|
+
|
|
564
|
+
expect(childInstance).toBeDefined()
|
|
565
|
+
expect(childInstance?.inputs?.k8sCluster).toEqual([
|
|
566
|
+
{
|
|
567
|
+
instanceId: "example.parent.v1:parent-2",
|
|
568
|
+
output: "k8sClusters",
|
|
569
|
+
},
|
|
570
|
+
])
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
it("should preserve nested composite output boundary when passing to another component", () => {
|
|
574
|
+
const peer = defineEntity({
|
|
575
|
+
type: "example.peer.v1",
|
|
576
|
+
schema: z.object({ name: z.string() }),
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
const producer = defineComponent({
|
|
580
|
+
type: "example.peer-source.v1",
|
|
581
|
+
outputs: { peer },
|
|
582
|
+
create: ({ id }) => ({
|
|
583
|
+
peer: {
|
|
584
|
+
instanceId: id,
|
|
585
|
+
output: "peer",
|
|
586
|
+
},
|
|
587
|
+
}),
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
const location = defineComponent({
|
|
591
|
+
type: "example.location.v1",
|
|
592
|
+
outputs: { peer },
|
|
593
|
+
create: ({ name }) => {
|
|
594
|
+
const { peer: producedPeer } = producer({ name })
|
|
595
|
+
|
|
596
|
+
return {
|
|
597
|
+
peer: producedPeer,
|
|
598
|
+
}
|
|
599
|
+
},
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
const consumer = defineComponent({
|
|
603
|
+
type: "example.peer-consumer.v1",
|
|
604
|
+
inputs: {
|
|
605
|
+
peers: {
|
|
606
|
+
entity: peer,
|
|
607
|
+
multiple: true,
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
create: () => ({}),
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
const locationSet = defineComponent({
|
|
614
|
+
type: "example.location-set.v1",
|
|
615
|
+
create: () => {
|
|
616
|
+
const { peer: locationPeer } = location({ name: "amsterdam" })
|
|
617
|
+
|
|
618
|
+
consumer({
|
|
619
|
+
name: "identity",
|
|
620
|
+
inputs: {
|
|
621
|
+
peers: [locationPeer],
|
|
622
|
+
},
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
return {}
|
|
626
|
+
},
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
locationSet({ name: "test" })
|
|
630
|
+
|
|
631
|
+
const consumerInstance = getRuntimeInstances().find(
|
|
632
|
+
runtime => runtime.instance.id === "example.peer-consumer.v1:identity",
|
|
633
|
+
)?.instance
|
|
634
|
+
|
|
635
|
+
expect(consumerInstance?.inputs?.peers).toEqual([
|
|
636
|
+
{
|
|
637
|
+
instanceId: "example.location.v1:amsterdam",
|
|
638
|
+
output: "peer",
|
|
639
|
+
},
|
|
640
|
+
])
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
it("should preserve boundaries from empty nested input groups", () => {
|
|
644
|
+
const cluster = defineEntity({
|
|
645
|
+
type: "example.cluster.v1",
|
|
646
|
+
schema: z.object({ id: z.string() }),
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
const component = defineComponent({
|
|
650
|
+
type: "example.boundary-preserving.v1",
|
|
651
|
+
inputs: {
|
|
652
|
+
k8sClusters: {
|
|
653
|
+
entity: cluster,
|
|
654
|
+
multiple: true,
|
|
655
|
+
required: false,
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
create: () => ({}),
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
const emptyGroup = [] as unknown as InstanceInput[] & { [boundaryInput]: InstanceInput }
|
|
662
|
+
|
|
663
|
+
emptyGroup[boundaryInput] = {
|
|
664
|
+
instanceId: "example.source.v1:source",
|
|
665
|
+
output: "k8sClusters",
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
component({
|
|
669
|
+
name: "test",
|
|
670
|
+
inputs: {
|
|
671
|
+
k8sClusters: [emptyGroup],
|
|
672
|
+
},
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
const instance = getRuntimeInstances().find(
|
|
676
|
+
runtime => runtime.instance.id === "example.boundary-preserving.v1:test",
|
|
677
|
+
)?.instance
|
|
678
|
+
|
|
679
|
+
expect(instance?.inputs?.k8sClusters).toEqual([
|
|
680
|
+
{
|
|
681
|
+
instanceId: "example.source.v1:source",
|
|
682
|
+
output: "k8sClusters",
|
|
683
|
+
},
|
|
684
|
+
])
|
|
685
|
+
})
|
|
686
|
+
|
|
687
|
+
it("should throw when selecting from empty input array without boundary", () => {
|
|
688
|
+
expect(() => selectInput([], "missing")).toThrow(
|
|
689
|
+
'Cannot select input "missing": empty input group has no boundary metadata to build a missing input reference.',
|
|
690
|
+
)
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
it("should not leak optional metadata into resolved outputs", () => {
|
|
694
|
+
const cluster = defineEntity({
|
|
695
|
+
type: "example.cluster.v1",
|
|
696
|
+
schema: z.object({ id: z.string() }),
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
const passthrough = defineComponent({
|
|
700
|
+
type: "example.passthrough.v1",
|
|
701
|
+
inputs: {
|
|
702
|
+
k8sCluster: {
|
|
703
|
+
entity: cluster,
|
|
704
|
+
required: false,
|
|
705
|
+
},
|
|
706
|
+
},
|
|
707
|
+
outputs: {
|
|
708
|
+
k8sCluster: {
|
|
709
|
+
entity: cluster,
|
|
710
|
+
required: false,
|
|
711
|
+
},
|
|
712
|
+
},
|
|
713
|
+
create: ({ inputs }) => ({
|
|
714
|
+
k8sCluster: inputs.k8sCluster,
|
|
715
|
+
}),
|
|
716
|
+
})
|
|
717
|
+
|
|
718
|
+
passthrough({ name: "test" })
|
|
719
|
+
|
|
720
|
+
const instance = getRuntimeInstances().find(
|
|
721
|
+
runtime => runtime.instance.id === "example.passthrough.v1:test",
|
|
722
|
+
)?.instance
|
|
723
|
+
|
|
724
|
+
expect(instance).toBeDefined()
|
|
725
|
+
expect(instance?.resolvedOutputs?.k8sCluster).toEqual([])
|
|
726
|
+
expect(instance?.outputs?.k8sCluster).toEqual([
|
|
727
|
+
{
|
|
728
|
+
instanceId: "example.passthrough.v1:test",
|
|
729
|
+
output: "k8sCluster",
|
|
730
|
+
},
|
|
731
|
+
])
|
|
732
|
+
})
|
|
733
|
+
|
|
734
|
+
it("should keep output path as plain string metadata", () => {
|
|
735
|
+
const peer = defineEntity({
|
|
736
|
+
type: "example.peer.v1",
|
|
737
|
+
schema: z.object({ id: z.string() }),
|
|
738
|
+
})
|
|
739
|
+
|
|
740
|
+
const source = defineComponent({
|
|
741
|
+
type: "example.path-source.v1",
|
|
742
|
+
outputs: { peer },
|
|
743
|
+
create: ({ id }) => ({
|
|
744
|
+
peer: {
|
|
745
|
+
instanceId: id,
|
|
746
|
+
output: "peer",
|
|
747
|
+
},
|
|
748
|
+
}),
|
|
749
|
+
})
|
|
750
|
+
|
|
751
|
+
const passthrough = defineComponent({
|
|
752
|
+
type: "example.path-passthrough.v1",
|
|
753
|
+
inputs: {
|
|
754
|
+
peer: {
|
|
755
|
+
entity: peer,
|
|
756
|
+
required: false,
|
|
757
|
+
},
|
|
758
|
+
},
|
|
759
|
+
outputs: {
|
|
760
|
+
peer: {
|
|
761
|
+
entity: peer,
|
|
762
|
+
required: false,
|
|
763
|
+
},
|
|
764
|
+
},
|
|
765
|
+
create: ({ inputs }) => ({
|
|
766
|
+
peer: inputs.peer.provided
|
|
767
|
+
? {
|
|
768
|
+
instanceId: inputs.peer.instanceId,
|
|
769
|
+
output: inputs.peer.output,
|
|
770
|
+
path: "id",
|
|
771
|
+
}
|
|
772
|
+
: undefined,
|
|
773
|
+
}),
|
|
774
|
+
})
|
|
775
|
+
|
|
776
|
+
const { peer: sourcePeer } = source({ name: "source" })
|
|
777
|
+
|
|
778
|
+
passthrough({
|
|
779
|
+
name: "test",
|
|
780
|
+
inputs: {
|
|
781
|
+
peer: sourcePeer,
|
|
782
|
+
},
|
|
783
|
+
})
|
|
784
|
+
|
|
785
|
+
const instance = getRuntimeInstances().find(
|
|
786
|
+
runtime => runtime.instance.id === "example.path-passthrough.v1:test",
|
|
787
|
+
)?.instance
|
|
788
|
+
|
|
789
|
+
expect(instance?.resolvedOutputs?.peer).toEqual([
|
|
790
|
+
{
|
|
791
|
+
instanceId: "example.path-source.v1:source",
|
|
792
|
+
output: "peer",
|
|
793
|
+
path: "id",
|
|
794
|
+
},
|
|
795
|
+
])
|
|
796
|
+
})
|
|
797
|
+
|
|
798
|
+
it("should track missing input in model and pass provided-false placeholder with parent boundary", () => {
|
|
799
|
+
const cluster = defineEntity({
|
|
800
|
+
type: "example.cluster.v1",
|
|
801
|
+
schema: z.object({ id: z.string() }),
|
|
802
|
+
})
|
|
803
|
+
|
|
804
|
+
let createInputs: Record<string, unknown> | undefined
|
|
805
|
+
|
|
806
|
+
const component = defineComponent({
|
|
807
|
+
type: "example.missing-input-tracking.v1",
|
|
808
|
+
inputs: {
|
|
809
|
+
k8sCluster: {
|
|
810
|
+
entity: cluster,
|
|
811
|
+
required: false,
|
|
812
|
+
},
|
|
813
|
+
},
|
|
814
|
+
create: ({ inputs }) => {
|
|
815
|
+
createInputs = inputs as Record<string, unknown>
|
|
816
|
+
return {}
|
|
817
|
+
},
|
|
818
|
+
})
|
|
819
|
+
|
|
820
|
+
component({
|
|
821
|
+
name: "test",
|
|
822
|
+
inputs: {
|
|
823
|
+
k8sCluster: {
|
|
824
|
+
provided: false,
|
|
825
|
+
[boundaryInput]: {
|
|
826
|
+
instanceId: "example.source.v1:source",
|
|
827
|
+
output: "k8sCluster",
|
|
828
|
+
},
|
|
829
|
+
} as never,
|
|
830
|
+
},
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
const instance = getRuntimeInstances().find(
|
|
834
|
+
runtime => runtime.instance.id === "example.missing-input-tracking.v1:test",
|
|
835
|
+
)?.instance
|
|
836
|
+
|
|
837
|
+
expect(instance?.inputs?.k8sCluster).toEqual([
|
|
838
|
+
{
|
|
839
|
+
instanceId: "example.source.v1:source",
|
|
840
|
+
output: "k8sCluster",
|
|
841
|
+
},
|
|
842
|
+
])
|
|
843
|
+
expect(createInputs).toEqual({
|
|
844
|
+
k8sCluster: {
|
|
845
|
+
provided: false,
|
|
846
|
+
[boundaryInput]: {
|
|
847
|
+
instanceId: "example.missing-input-tracking.v1:test",
|
|
848
|
+
output: "k8sCluster",
|
|
849
|
+
},
|
|
850
|
+
},
|
|
851
|
+
})
|
|
852
|
+
})
|
|
853
|
+
|
|
854
|
+
it("should pass missing multiple input with parent boundary", () => {
|
|
855
|
+
const endpoint = defineEntity({
|
|
856
|
+
type: "example.endpoint.v1",
|
|
857
|
+
schema: z.object({ host: z.string() }),
|
|
858
|
+
})
|
|
859
|
+
|
|
860
|
+
let receivedBoundary: InstanceInput | undefined
|
|
861
|
+
|
|
862
|
+
const component = defineComponent({
|
|
863
|
+
type: "example.missing-multiple-boundary.v1",
|
|
864
|
+
inputs: {
|
|
865
|
+
endpoints: {
|
|
866
|
+
entity: endpoint,
|
|
867
|
+
multiple: true,
|
|
868
|
+
required: false,
|
|
869
|
+
},
|
|
870
|
+
},
|
|
871
|
+
create: ({ inputs }) => {
|
|
872
|
+
receivedBoundary = (inputs.endpoints as unknown as { [boundaryInput]: InstanceInput })[
|
|
873
|
+
boundaryInput
|
|
874
|
+
]
|
|
875
|
+
return {}
|
|
876
|
+
},
|
|
877
|
+
})
|
|
878
|
+
|
|
879
|
+
const upstreamGroup = [] as unknown as EntityInput<typeof endpoint>[] & {
|
|
880
|
+
[boundaryInput]: InstanceInput
|
|
881
|
+
}
|
|
882
|
+
upstreamGroup[boundaryInput] = {
|
|
883
|
+
instanceId: "example.upstream.v1:source",
|
|
884
|
+
output: "endpoints",
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
component({
|
|
888
|
+
name: "test",
|
|
889
|
+
inputs: {
|
|
890
|
+
endpoints: upstreamGroup,
|
|
891
|
+
},
|
|
892
|
+
})
|
|
893
|
+
|
|
894
|
+
const instance = getRuntimeInstances().find(
|
|
895
|
+
runtime => runtime.instance.id === "example.missing-multiple-boundary.v1:test",
|
|
896
|
+
)?.instance
|
|
897
|
+
|
|
898
|
+
expect(instance?.inputs?.endpoints).toEqual([
|
|
899
|
+
{
|
|
900
|
+
instanceId: "example.upstream.v1:source",
|
|
901
|
+
output: "endpoints",
|
|
902
|
+
},
|
|
903
|
+
])
|
|
904
|
+
|
|
905
|
+
expect(receivedBoundary).toEqual({
|
|
906
|
+
instanceId: "example.missing-multiple-boundary.v1:test",
|
|
907
|
+
output: "endpoints",
|
|
908
|
+
})
|
|
909
|
+
})
|
|
910
|
+
|
|
911
|
+
it("should pass only provided items for multiple input", () => {
|
|
912
|
+
const cluster = defineEntity({
|
|
913
|
+
type: "example.cluster.v1",
|
|
914
|
+
schema: z.object({ id: z.string() }),
|
|
915
|
+
})
|
|
916
|
+
|
|
917
|
+
const source = defineComponent({
|
|
918
|
+
type: "example.source.v1",
|
|
919
|
+
outputs: { k8sCluster: cluster },
|
|
920
|
+
create: ({ id }) => ({
|
|
921
|
+
k8sCluster: { instanceId: id, output: "k8sCluster" },
|
|
922
|
+
}),
|
|
923
|
+
})
|
|
924
|
+
|
|
925
|
+
let receivedCount = -1
|
|
926
|
+
|
|
927
|
+
const component = defineComponent({
|
|
928
|
+
type: "example.multiple-filtering.v1",
|
|
929
|
+
inputs: {
|
|
930
|
+
k8sClusters: {
|
|
931
|
+
entity: cluster,
|
|
932
|
+
multiple: true,
|
|
933
|
+
required: false,
|
|
934
|
+
},
|
|
935
|
+
},
|
|
936
|
+
create: ({ inputs }) => {
|
|
937
|
+
receivedCount = inputs.k8sClusters?.length ?? 0
|
|
938
|
+
return {}
|
|
939
|
+
},
|
|
940
|
+
})
|
|
941
|
+
|
|
942
|
+
const { k8sCluster } = source({ name: "cluster-1" })
|
|
943
|
+
|
|
944
|
+
component({
|
|
945
|
+
name: "test",
|
|
946
|
+
inputs: {
|
|
947
|
+
k8sClusters: [
|
|
948
|
+
k8sCluster,
|
|
949
|
+
{
|
|
950
|
+
provided: false,
|
|
951
|
+
[boundaryInput]: {
|
|
952
|
+
instanceId: "example.source.v1:missing",
|
|
953
|
+
output: "k8sCluster",
|
|
954
|
+
},
|
|
955
|
+
} as never,
|
|
956
|
+
],
|
|
957
|
+
},
|
|
958
|
+
})
|
|
959
|
+
|
|
960
|
+
expect(receivedCount).toBe(1)
|
|
961
|
+
})
|
|
962
|
+
|
|
963
|
+
it("should keep empty multiple input iterable", () => {
|
|
964
|
+
const cluster = defineEntity({
|
|
965
|
+
type: "example.cluster.v1",
|
|
966
|
+
schema: z.object({ id: z.string() }),
|
|
967
|
+
})
|
|
968
|
+
|
|
969
|
+
let receivedCount = -1
|
|
970
|
+
|
|
971
|
+
const component = defineComponent({
|
|
972
|
+
type: "example.empty-multiple.v1",
|
|
973
|
+
inputs: {
|
|
974
|
+
k8sClusters: {
|
|
975
|
+
entity: cluster,
|
|
976
|
+
multiple: true,
|
|
977
|
+
required: false,
|
|
978
|
+
},
|
|
979
|
+
},
|
|
980
|
+
create: ({ inputs }) => {
|
|
981
|
+
receivedCount = [...(inputs.k8sClusters ?? [])].length
|
|
982
|
+
return {}
|
|
983
|
+
},
|
|
984
|
+
})
|
|
985
|
+
|
|
986
|
+
component({
|
|
987
|
+
name: "test",
|
|
988
|
+
inputs: {
|
|
989
|
+
k8sClusters: [
|
|
990
|
+
{
|
|
991
|
+
provided: false,
|
|
992
|
+
[boundaryInput]: {
|
|
993
|
+
instanceId: "example.source.v1:missing",
|
|
994
|
+
output: "k8sCluster",
|
|
995
|
+
},
|
|
996
|
+
} as never,
|
|
997
|
+
],
|
|
998
|
+
},
|
|
999
|
+
})
|
|
1000
|
+
|
|
1001
|
+
expect(receivedCount).toBe(0)
|
|
1002
|
+
})
|
|
1003
|
+
|
|
1004
|
+
it("should keep declared multiple output defined when omitted", () => {
|
|
1005
|
+
const endpoint = defineEntity({
|
|
1006
|
+
type: "example.endpoint.v1",
|
|
1007
|
+
schema: z.object({ id: z.string() }),
|
|
1008
|
+
})
|
|
1009
|
+
|
|
1010
|
+
const component = defineComponent({
|
|
1011
|
+
type: "example.missing-multiple-output.v1",
|
|
1012
|
+
outputs: {
|
|
1013
|
+
endpoints: {
|
|
1014
|
+
entity: endpoint,
|
|
1015
|
+
multiple: true,
|
|
1016
|
+
},
|
|
1017
|
+
},
|
|
1018
|
+
create: () => ({}),
|
|
1019
|
+
})
|
|
1020
|
+
|
|
1021
|
+
const outputs = component({ name: "test" }) as Record<string, unknown>
|
|
1022
|
+
const endpoints = outputs.endpoints as Array<Record<string, unknown>> & {
|
|
1023
|
+
[boundaryInput]?: InstanceInput
|
|
1024
|
+
address: {
|
|
1025
|
+
asSubnet: Array<Record<string, unknown>>
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
expect(Array.isArray(endpoints)).toBe(true)
|
|
1030
|
+
expect(endpoints.length).toBe(0)
|
|
1031
|
+
expect(endpoints.address.asSubnet.length).toBe(0)
|
|
1032
|
+
expect(endpoints[boundaryInput]).toEqual({
|
|
1033
|
+
instanceId: "example.missing-multiple-output.v1:test",
|
|
1034
|
+
output: "endpoints",
|
|
1035
|
+
})
|
|
1036
|
+
})
|
|
1037
|
+
|
|
1038
|
+
it("should keep declared single output defined when omitted", () => {
|
|
1039
|
+
const endpoint = defineEntity({
|
|
1040
|
+
type: "example.endpoint.v1",
|
|
1041
|
+
schema: z.object({ id: z.string() }),
|
|
1042
|
+
})
|
|
1043
|
+
|
|
1044
|
+
const component = defineComponent({
|
|
1045
|
+
type: "example.missing-single-output.v1",
|
|
1046
|
+
outputs: {
|
|
1047
|
+
endpoint,
|
|
1048
|
+
},
|
|
1049
|
+
create: () => ({}),
|
|
1050
|
+
})
|
|
1051
|
+
|
|
1052
|
+
const outputs = component({ name: "test" }) as Record<string, unknown>
|
|
1053
|
+
const output = outputs.endpoint as Record<string | symbol, unknown>
|
|
1054
|
+
|
|
1055
|
+
expect(output).toBeDefined()
|
|
1056
|
+
expect(output.provided).toBe(false)
|
|
1057
|
+
expect(output[boundaryInput]).toEqual({
|
|
1058
|
+
instanceId: "example.missing-single-output.v1:test",
|
|
1059
|
+
output: "endpoint",
|
|
1060
|
+
})
|
|
1061
|
+
})
|
|
1062
|
+
|
|
1063
|
+
it("should allow non-provided single output in composite component", () => {
|
|
1064
|
+
const endpoint = defineEntity({
|
|
1065
|
+
type: "example.endpoint.v1",
|
|
1066
|
+
schema: z.object({ id: z.string() }),
|
|
1067
|
+
})
|
|
1068
|
+
|
|
1069
|
+
const component = defineComponent({
|
|
1070
|
+
type: "example.non-provided-single-output.v1",
|
|
1071
|
+
outputs: {
|
|
1072
|
+
endpoint,
|
|
1073
|
+
},
|
|
1074
|
+
create: ({ id }) => ({
|
|
1075
|
+
endpoint: {
|
|
1076
|
+
provided: false as never,
|
|
1077
|
+
[boundaryInput]: {
|
|
1078
|
+
instanceId: id,
|
|
1079
|
+
output: "endpoint",
|
|
1080
|
+
},
|
|
1081
|
+
},
|
|
1082
|
+
}),
|
|
1083
|
+
})
|
|
1084
|
+
|
|
1085
|
+
const outputs = component({ name: "test" }) as Record<string, unknown>
|
|
1086
|
+
const output = outputs.endpoint as Record<string | symbol, unknown>
|
|
1087
|
+
|
|
1088
|
+
expect(output.provided).toBe(false)
|
|
1089
|
+
expect(output[boundaryInput]).toEqual({
|
|
1090
|
+
instanceId: "example.non-provided-single-output.v1:test",
|
|
1091
|
+
output: "endpoint",
|
|
1092
|
+
})
|
|
1093
|
+
})
|
|
1094
|
+
|
|
1095
|
+
it("should reject non-provided items in multiple outputs", () => {
|
|
1096
|
+
const endpoint = defineEntity({
|
|
1097
|
+
type: "example.endpoint.v1",
|
|
1098
|
+
schema: z.object({ id: z.string() }),
|
|
1099
|
+
})
|
|
1100
|
+
|
|
1101
|
+
const component = defineComponent({
|
|
1102
|
+
type: "example.invalid-multiple-output.v1",
|
|
1103
|
+
outputs: {
|
|
1104
|
+
endpoints: {
|
|
1105
|
+
entity: endpoint,
|
|
1106
|
+
multiple: true,
|
|
1107
|
+
},
|
|
1108
|
+
},
|
|
1109
|
+
create: ({ id }) => ({
|
|
1110
|
+
endpoints: [
|
|
1111
|
+
{
|
|
1112
|
+
provided: false,
|
|
1113
|
+
[boundaryInput]: {
|
|
1114
|
+
instanceId: id,
|
|
1115
|
+
output: "endpoints",
|
|
1116
|
+
},
|
|
1117
|
+
} as never,
|
|
1118
|
+
],
|
|
1119
|
+
}),
|
|
1120
|
+
})
|
|
1121
|
+
|
|
1122
|
+
expect(() => component({ name: "test" })).toThrow(
|
|
1123
|
+
'Multiple output "endpoints" in instance "example.invalid-multiple-output.v1:test" cannot contain non-provided items',
|
|
1124
|
+
)
|
|
1125
|
+
})
|
|
1126
|
+
|
|
1127
|
+
it("should require provided outputs for unit components", () => {
|
|
1128
|
+
const endpoint = defineEntity({
|
|
1129
|
+
type: "example.endpoint.v1",
|
|
1130
|
+
schema: z.object({ id: z.string() }),
|
|
1131
|
+
})
|
|
1132
|
+
|
|
1133
|
+
const unitLike = defineComponent({
|
|
1134
|
+
type: "example.invalid-unit-output.v1",
|
|
1135
|
+
[kind]: "unit",
|
|
1136
|
+
outputs: {
|
|
1137
|
+
endpoint,
|
|
1138
|
+
},
|
|
1139
|
+
create: ({ id }) => ({
|
|
1140
|
+
endpoint: {
|
|
1141
|
+
provided: false,
|
|
1142
|
+
[boundaryInput]: {
|
|
1143
|
+
instanceId: id,
|
|
1144
|
+
output: "endpoint",
|
|
1145
|
+
},
|
|
1146
|
+
},
|
|
1147
|
+
}),
|
|
1148
|
+
})
|
|
1149
|
+
|
|
1150
|
+
expect(() => unitLike({ name: "test" })).toThrow(
|
|
1151
|
+
'Unit output "endpoint" in instance "example.invalid-unit-output.v1:test" must be provided',
|
|
1152
|
+
)
|
|
1153
|
+
})
|
|
245
1154
|
})
|