@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 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.2",
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": "./dist/index.d.ts",
13
- "default": "./dist/index.mjs"
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": "e177535015e0fa3c74ae8ddc0bc6d31b191d2c54"
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
+ })