@highstate/contract 0.7.2 → 0.7.4
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 +240 -0
- package/package.json +8 -8
- package/src/component.spec.ts +166 -0
- package/src/component.ts +361 -0
- package/src/entity.ts +54 -0
- package/src/evaluation.ts +60 -0
- package/src/index.ts +42 -0
- package/src/instance.ts +165 -0
- package/src/types.ts +45 -0
- package/src/unit.ts +102 -0
- package/src/utils.ts +89 -0
- package/dist/index.d.ts +0 -326
- package/dist/index.mjs +0 -192
package/dist/index.js
ADDED
@@ -0,0 +1,240 @@
|
|
1
|
+
import { mapValues, pickBy, isNonNullish } from 'remeda';
|
2
|
+
import { OptionalKind, Type as Type$1 } from '@sinclair/typebox';
|
3
|
+
import { Ajv } from 'ajv';
|
4
|
+
|
5
|
+
const boundaryInput = Symbol("boundaryInput");
|
6
|
+
function getInstanceId(instanceType, instanceName) {
|
7
|
+
return `${instanceType}:${instanceName}`;
|
8
|
+
}
|
9
|
+
function parseInstanceId(instanceId) {
|
10
|
+
const parts = instanceId.split(":");
|
11
|
+
if (parts.length !== 2) {
|
12
|
+
throw new Error(`Invalid instance key: ${instanceId}`);
|
13
|
+
}
|
14
|
+
return parts;
|
15
|
+
}
|
16
|
+
function findInput(inputs, name) {
|
17
|
+
const matchedInputs = inputs.filter(
|
18
|
+
(input) => parseInstanceId(input.instanceId)[1] === name || input.instanceId === name
|
19
|
+
);
|
20
|
+
if (matchedInputs.length === 0) {
|
21
|
+
return null;
|
22
|
+
}
|
23
|
+
if (1 < matchedInputs.length) {
|
24
|
+
throw new Error(
|
25
|
+
`Multiple inputs found for "${name}": ${matchedInputs.map((input) => input.instanceId).join(", ")}. Specify the full instance id to disambiguate.`
|
26
|
+
);
|
27
|
+
}
|
28
|
+
return matchedInputs[0];
|
29
|
+
}
|
30
|
+
function findRequiredInput(inputs, name) {
|
31
|
+
const input = findInput(inputs, name);
|
32
|
+
if (input === null) {
|
33
|
+
throw new Error(`Required input "${name}" not found.`);
|
34
|
+
}
|
35
|
+
return input;
|
36
|
+
}
|
37
|
+
|
38
|
+
function text(array, ...values) {
|
39
|
+
const str = array.reduce(
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
41
|
+
(result, part, i) => result + part + (values[i] ? String(values[i]) : ""),
|
42
|
+
""
|
43
|
+
);
|
44
|
+
return trimIndentation(str);
|
45
|
+
}
|
46
|
+
function trimIndentation(text2) {
|
47
|
+
const lines = text2.split("\n");
|
48
|
+
const indent = lines.filter((line) => line.trim() !== "").map((line) => line.match(/^\s*/)?.[0].length ?? 0).reduce((min, indent2) => Math.min(min, indent2), Infinity);
|
49
|
+
return lines.map((line) => line.slice(indent)).join("\n").trim();
|
50
|
+
}
|
51
|
+
|
52
|
+
const ajv = new Ajv();
|
53
|
+
const originalCreate = Symbol("originalCreate");
|
54
|
+
function defineComponent(options) {
|
55
|
+
function create(params) {
|
56
|
+
const { name, args, inputs } = params;
|
57
|
+
const id = `${options.type}:${name}`;
|
58
|
+
validateArgs(id, create.model, args ?? {});
|
59
|
+
const flatInputs = mapValues(pickBy(inputs ?? {}, isNonNullish), (inputs2) => [inputs2].flat(2));
|
60
|
+
return registerInstance(
|
61
|
+
create.model,
|
62
|
+
{
|
63
|
+
id,
|
64
|
+
type: options.type,
|
65
|
+
name,
|
66
|
+
args: args ?? {},
|
67
|
+
inputs: mapValues(flatInputs, (inputs2) => inputs2.map((input) => input[boundaryInput] ?? input)),
|
68
|
+
resolvedInputs: flatInputs
|
69
|
+
},
|
70
|
+
() => {
|
71
|
+
const markedInputs = mapValues(flatInputs, (inputs2, key) => {
|
72
|
+
const result = inputs2.map((input) => ({
|
73
|
+
...input,
|
74
|
+
[boundaryInput]: { instanceId: id, output: key }
|
75
|
+
}));
|
76
|
+
return create.model.inputs?.[key]?.multiple === false ? result[0] : result;
|
77
|
+
});
|
78
|
+
const outputs = options.create({
|
79
|
+
id,
|
80
|
+
name,
|
81
|
+
args: args ?? {},
|
82
|
+
inputs: markedInputs
|
83
|
+
}) ?? {};
|
84
|
+
return mapValues(pickBy(outputs, isNonNullish), (outputs2) => [outputs2].flat(2));
|
85
|
+
}
|
86
|
+
);
|
87
|
+
}
|
88
|
+
create.entities = /* @__PURE__ */ new Map();
|
89
|
+
const mapInput = createInputMapper(create.entities);
|
90
|
+
create.model = {
|
91
|
+
type: options.type,
|
92
|
+
args: mapValues(options.args ?? {}, mapArgument),
|
93
|
+
inputs: mapValues(options.inputs ?? {}, mapInput),
|
94
|
+
outputs: mapValues(options.outputs ?? {}, mapInput),
|
95
|
+
meta: options.meta ?? {}
|
96
|
+
};
|
97
|
+
create[originalCreate] = options.create;
|
98
|
+
return create;
|
99
|
+
}
|
100
|
+
function isComponent(value) {
|
101
|
+
return typeof value === "function" && "model" in value;
|
102
|
+
}
|
103
|
+
function mapArgument(value) {
|
104
|
+
if ("schema" in value) {
|
105
|
+
return {
|
106
|
+
schema: value.schema,
|
107
|
+
required: value.required ?? (!value.schema[OptionalKind] && !value.schema.default),
|
108
|
+
meta: {
|
109
|
+
displayName: value.displayName,
|
110
|
+
description: value.description,
|
111
|
+
color: value.color
|
112
|
+
}
|
113
|
+
};
|
114
|
+
}
|
115
|
+
return {
|
116
|
+
schema: value,
|
117
|
+
required: !value[OptionalKind] && !value.default,
|
118
|
+
meta: {}
|
119
|
+
};
|
120
|
+
}
|
121
|
+
function createInputMapper(entities) {
|
122
|
+
return (value) => {
|
123
|
+
if ("entity" in value) {
|
124
|
+
entities.set(value.entity.type, value.entity);
|
125
|
+
return {
|
126
|
+
type: value.entity.type,
|
127
|
+
required: value.required ?? true,
|
128
|
+
multiple: value.multiple ?? false,
|
129
|
+
meta: {
|
130
|
+
displayName: value.displayName,
|
131
|
+
description: value.description,
|
132
|
+
color: value.color
|
133
|
+
}
|
134
|
+
};
|
135
|
+
}
|
136
|
+
entities.set(value.type, value);
|
137
|
+
return {
|
138
|
+
type: value.type,
|
139
|
+
required: true,
|
140
|
+
multiple: false,
|
141
|
+
meta: {}
|
142
|
+
};
|
143
|
+
};
|
144
|
+
}
|
145
|
+
function validateArgs(instanceId, model, args) {
|
146
|
+
for (const [key, argModel] of Object.entries(model.args)) {
|
147
|
+
const value = args[key];
|
148
|
+
if (!value && argModel.required) {
|
149
|
+
throw new Error(`Missing required argument "${key}" for instance "${instanceId}"`);
|
150
|
+
}
|
151
|
+
if (value && !ajv.validate(argModel.schema, value)) {
|
152
|
+
throw new Error(`Invalid argument "${key}" for instance "${instanceId}": ${ajv.errorsText()}`);
|
153
|
+
}
|
154
|
+
}
|
155
|
+
}
|
156
|
+
|
157
|
+
function isUnitModel(model) {
|
158
|
+
return "source" in model;
|
159
|
+
}
|
160
|
+
function defineUnit(options) {
|
161
|
+
const component = defineComponent({
|
162
|
+
...options,
|
163
|
+
create({ id }) {
|
164
|
+
const outputs = {};
|
165
|
+
for (const key in options.outputs ?? {}) {
|
166
|
+
outputs[key] = [
|
167
|
+
{
|
168
|
+
instanceId: id,
|
169
|
+
output: key
|
170
|
+
}
|
171
|
+
];
|
172
|
+
}
|
173
|
+
return outputs;
|
174
|
+
}
|
175
|
+
});
|
176
|
+
component.model.source = options.source ?? {};
|
177
|
+
component.model.secrets = mapValues(options.secrets ?? {}, mapArgument);
|
178
|
+
return component;
|
179
|
+
}
|
180
|
+
|
181
|
+
const compositeInstances = /* @__PURE__ */ new Map();
|
182
|
+
let currentCompositeInstance = null;
|
183
|
+
function resetEvaluation() {
|
184
|
+
compositeInstances.clear();
|
185
|
+
currentCompositeInstance = null;
|
186
|
+
}
|
187
|
+
function getCompositeInstances() {
|
188
|
+
return Array.from(compositeInstances.values());
|
189
|
+
}
|
190
|
+
function registerInstance(component, instance, fn) {
|
191
|
+
if (currentCompositeInstance) {
|
192
|
+
instance.parentId = currentCompositeInstance.instance.id;
|
193
|
+
currentCompositeInstance.children.push(instance);
|
194
|
+
}
|
195
|
+
let previousParentInstance = null;
|
196
|
+
if (!isUnitModel(component)) {
|
197
|
+
previousParentInstance = currentCompositeInstance;
|
198
|
+
currentCompositeInstance = { instance, children: [] };
|
199
|
+
compositeInstances.set(currentCompositeInstance.instance.id, currentCompositeInstance);
|
200
|
+
}
|
201
|
+
try {
|
202
|
+
const outputs = fn();
|
203
|
+
instance.resolvedOutputs = outputs;
|
204
|
+
instance.outputs = mapValues(
|
205
|
+
outputs ?? {},
|
206
|
+
(outputs2) => outputs2.map((output) => output[boundaryInput] ?? output)
|
207
|
+
);
|
208
|
+
return mapValues(
|
209
|
+
outputs,
|
210
|
+
(outputs2, outputKey) => outputs2.map((output) => ({
|
211
|
+
...output,
|
212
|
+
[boundaryInput]: { instanceId: instance.id, output: outputKey }
|
213
|
+
}))
|
214
|
+
);
|
215
|
+
} finally {
|
216
|
+
if (previousParentInstance) {
|
217
|
+
currentCompositeInstance = previousParentInstance;
|
218
|
+
}
|
219
|
+
}
|
220
|
+
}
|
221
|
+
|
222
|
+
function defineEntity(options) {
|
223
|
+
return {
|
224
|
+
meta: {},
|
225
|
+
...options
|
226
|
+
};
|
227
|
+
}
|
228
|
+
function isEntity(value) {
|
229
|
+
return typeof value === "object" && value !== null && "type" in value && "schema" in value && "meta" in value;
|
230
|
+
}
|
231
|
+
|
232
|
+
function StringEnum(values) {
|
233
|
+
return Type.Union(values.map((value) => Type.Literal(value)));
|
234
|
+
}
|
235
|
+
const Type = {
|
236
|
+
...Type$1,
|
237
|
+
StringEnum
|
238
|
+
};
|
239
|
+
|
240
|
+
export { Type, defineComponent, defineEntity, defineUnit, findInput, findRequiredInput, getCompositeInstances, getInstanceId, isComponent, isEntity, isUnitModel, originalCreate, parseInstanceId, resetEvaluation, text, trimIndentation };
|
package/package.json
CHANGED
@@ -1,27 +1,27 @@
|
|
1
1
|
{
|
2
2
|
"name": "@highstate/contract",
|
3
|
-
"version": "0.7.
|
3
|
+
"version": "0.7.4",
|
4
4
|
"type": "module",
|
5
|
-
"module": "dist/index.mjs",
|
6
|
-
"types": "dist/index.d.ts",
|
7
5
|
"files": [
|
8
|
-
"dist"
|
6
|
+
"dist",
|
7
|
+
"src"
|
9
8
|
],
|
10
9
|
"exports": {
|
11
10
|
".": {
|
12
|
-
"types": "./
|
13
|
-
"default": "./dist/index.
|
11
|
+
"types": "./src/index.ts",
|
12
|
+
"default": "./dist/index.js"
|
14
13
|
}
|
15
14
|
},
|
16
15
|
"publishConfig": {
|
17
16
|
"access": "public"
|
18
17
|
},
|
19
18
|
"scripts": {
|
20
|
-
"build": "pkgroll --tsconfig=tsconfig.build.json",
|
19
|
+
"build": "pkgroll --clean --tsconfig=tsconfig.build.json",
|
21
20
|
"test": "vitest run --coverage"
|
22
21
|
},
|
23
22
|
"dependencies": {
|
24
23
|
"@sinclair/typebox": "^0.34.11",
|
24
|
+
"ajv": "^8.17.1",
|
25
25
|
"remeda": "^2.21.0"
|
26
26
|
},
|
27
27
|
"devDependencies": {
|
@@ -29,5 +29,5 @@
|
|
29
29
|
"pkgroll": "^2.5.1",
|
30
30
|
"vitest": "^2.1.8"
|
31
31
|
},
|
32
|
-
"gitHead": "
|
32
|
+
"gitHead": "c482cdf650746f6814122602d65bf5b842a2bc2c"
|
33
33
|
}
|
@@ -0,0 +1,166 @@
|
|
1
|
+
import { Type } from "@sinclair/typebox"
|
2
|
+
import { describe, it } from "vitest"
|
3
|
+
import { defineComponent } from "./component"
|
4
|
+
import { defineEntity } from "./entity"
|
5
|
+
|
6
|
+
describe("defineComponent", () => {
|
7
|
+
it("should return a component with no args", () => {
|
8
|
+
const virtualMachine = defineComponent({
|
9
|
+
type: "proxmox.virtual_machine",
|
10
|
+
create: () => ({}),
|
11
|
+
})
|
12
|
+
|
13
|
+
virtualMachine({
|
14
|
+
name: "test",
|
15
|
+
})
|
16
|
+
|
17
|
+
virtualMachine({
|
18
|
+
name: "test",
|
19
|
+
args: {},
|
20
|
+
})
|
21
|
+
})
|
22
|
+
|
23
|
+
it("should return a component with args", () => {
|
24
|
+
const virtualMachine = defineComponent({
|
25
|
+
type: "proxmox.virtual_machine",
|
26
|
+
args: {
|
27
|
+
cores: Type.Optional(Type.Number()),
|
28
|
+
},
|
29
|
+
create: () => ({}),
|
30
|
+
})
|
31
|
+
|
32
|
+
virtualMachine({
|
33
|
+
name: "test",
|
34
|
+
args: {
|
35
|
+
cores: 2,
|
36
|
+
},
|
37
|
+
})
|
38
|
+
})
|
39
|
+
|
40
|
+
it("should return a component with inputs", () => {
|
41
|
+
const server = defineEntity({
|
42
|
+
type: "common.server",
|
43
|
+
|
44
|
+
schema: Type.Object({
|
45
|
+
endpoint: Type.String(),
|
46
|
+
}),
|
47
|
+
|
48
|
+
meta: {
|
49
|
+
displayName: "Server",
|
50
|
+
description: "A server entity",
|
51
|
+
color: "#ff0000",
|
52
|
+
},
|
53
|
+
})
|
54
|
+
|
55
|
+
const cluster = defineComponent({
|
56
|
+
type: "talos.cluster",
|
57
|
+
inputs: {
|
58
|
+
servers: {
|
59
|
+
entity: server,
|
60
|
+
multiple: true,
|
61
|
+
},
|
62
|
+
},
|
63
|
+
create: () => ({}),
|
64
|
+
})
|
65
|
+
|
66
|
+
cluster({
|
67
|
+
name: "test",
|
68
|
+
inputs: {
|
69
|
+
servers: [
|
70
|
+
{
|
71
|
+
instanceId: "test",
|
72
|
+
output: "test",
|
73
|
+
},
|
74
|
+
],
|
75
|
+
},
|
76
|
+
})
|
77
|
+
})
|
78
|
+
|
79
|
+
it("should return a component with outputs", () => {
|
80
|
+
const server = defineEntity({
|
81
|
+
type: "common.server",
|
82
|
+
|
83
|
+
schema: Type.Object({
|
84
|
+
endpoint: Type.String(),
|
85
|
+
}),
|
86
|
+
|
87
|
+
meta: {
|
88
|
+
displayName: "Server",
|
89
|
+
description: "A server entity",
|
90
|
+
color: "#ff0000",
|
91
|
+
},
|
92
|
+
})
|
93
|
+
|
94
|
+
const cluster = defineComponent({
|
95
|
+
type: "talos.cluster",
|
96
|
+
outputs: {
|
97
|
+
servers: {
|
98
|
+
entity: server,
|
99
|
+
multiple: true,
|
100
|
+
required: false,
|
101
|
+
},
|
102
|
+
},
|
103
|
+
create: () => ({
|
104
|
+
servers: [],
|
105
|
+
}),
|
106
|
+
})
|
107
|
+
|
108
|
+
cluster({
|
109
|
+
name: "test",
|
110
|
+
})
|
111
|
+
})
|
112
|
+
|
113
|
+
it("should return a component with args, inputs and outputs", () => {
|
114
|
+
const server = defineEntity({
|
115
|
+
type: "common.server",
|
116
|
+
|
117
|
+
schema: Type.Object({
|
118
|
+
endpoint: Type.String(),
|
119
|
+
}),
|
120
|
+
|
121
|
+
meta: {
|
122
|
+
displayName: "Server",
|
123
|
+
description: "A server entity",
|
124
|
+
color: "#ff0000",
|
125
|
+
},
|
126
|
+
})
|
127
|
+
|
128
|
+
const cluster = defineComponent({
|
129
|
+
type: "talos.cluster",
|
130
|
+
args: {
|
131
|
+
name: Type.String(),
|
132
|
+
},
|
133
|
+
inputs: {
|
134
|
+
servers: {
|
135
|
+
entity: server,
|
136
|
+
multiple: true,
|
137
|
+
required: false,
|
138
|
+
},
|
139
|
+
},
|
140
|
+
outputs: {
|
141
|
+
servers: {
|
142
|
+
entity: server,
|
143
|
+
multiple: true,
|
144
|
+
},
|
145
|
+
},
|
146
|
+
create: ({ inputs }) => ({ servers: inputs.servers ?? [] }),
|
147
|
+
})
|
148
|
+
|
149
|
+
const { servers } = cluster({
|
150
|
+
name: "test",
|
151
|
+
args: {
|
152
|
+
name: "test",
|
153
|
+
},
|
154
|
+
inputs: {
|
155
|
+
servers: [
|
156
|
+
{
|
157
|
+
instanceId: "test",
|
158
|
+
output: "test",
|
159
|
+
},
|
160
|
+
],
|
161
|
+
},
|
162
|
+
})
|
163
|
+
|
164
|
+
void servers
|
165
|
+
})
|
166
|
+
})
|