@highstate/contract 0.9.15 → 0.9.18

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/src/evaluation.ts CHANGED
@@ -1,41 +1,87 @@
1
- import type { ComponentModel } from "./component"
2
1
  import { mapValues } from "remeda"
3
- import { boundaryInput, type InstanceInput, type InstanceModel } from "./instance"
4
- import { isUnitModel } from "./unit"
2
+ import { boundaryInput, type Component } from "./component"
3
+ import { type InstanceInput, type InstanceModel } from "./instance"
4
+ import { isUnitModel } from "./component"
5
5
 
6
- export type CompositeInstance = {
6
+ export type RuntimeInstance = {
7
7
  instance: InstanceModel
8
- children: InstanceModel[]
8
+ component: Component
9
9
  }
10
10
 
11
- const compositeInstances: Map<string, CompositeInstance> = new Map()
12
- let currentCompositeInstance: CompositeInstance | null = null
11
+ function formatInstancePath(instance: InstanceModel): string {
12
+ let result = instance.id
13
+ while (instance.parentId) {
14
+ const parent = runtimeInstances.get(instance.parentId)?.instance
15
+ if (!parent) {
16
+ break
17
+ }
18
+
19
+ result = `${parent.id} -> ${result}`
20
+ instance = parent
21
+ }
22
+
23
+ return result
24
+ }
13
25
 
26
+ export class InstanceNameConflictError extends Error {
27
+ constructor(
28
+ readonly instanceId: string,
29
+ readonly firstPath: string,
30
+ readonly secondPath: string,
31
+ ) {
32
+ super(
33
+ `Multiple instances produced with the same instance ID "${instanceId}":\n` +
34
+ `1. ${firstPath}\n` +
35
+ `2. ${secondPath}`,
36
+ )
37
+
38
+ this.name = "InstanceNameConflictError"
39
+ }
40
+ }
41
+
42
+ let currentInstance: InstanceModel | null = null
43
+
44
+ const runtimeInstances: Map<string, RuntimeInstance> = new Map()
45
+
46
+ /**
47
+ * Resets the evaluation state, clearing all collected composite instances and runtime instances.
48
+ */
14
49
  export function resetEvaluation(): void {
15
- compositeInstances.clear()
16
- currentCompositeInstance = null
50
+ runtimeInstances.clear()
51
+ currentInstance = null
17
52
  }
18
53
 
19
- export function getCompositeInstances(): CompositeInstance[] {
20
- return Array.from(compositeInstances.values())
54
+ /**
55
+ * Returns all runtime instances collected during the evaluation.
56
+ *
57
+ * Note that these instances are not serializable.
58
+ */
59
+ export function getRuntimeInstances(): RuntimeInstance[] {
60
+ return Array.from(runtimeInstances.values())
21
61
  }
22
62
 
23
- export function registerInstance<T>(
24
- component: ComponentModel,
25
- instance: InstanceModel,
26
- fn: () => T,
27
- ): T {
28
- let previousParentInstance: CompositeInstance | null = null
63
+ export function registerInstance<T>(component: Component, instance: InstanceModel, fn: () => T): T {
64
+ if (runtimeInstances.has(instance.id)) {
65
+ const existing = runtimeInstances.get(instance.id)!
66
+
67
+ throw new InstanceNameConflictError(
68
+ instance.id,
69
+ formatInstancePath(existing.instance),
70
+ formatInstancePath(instance),
71
+ )
72
+ }
73
+
74
+ runtimeInstances.set(instance.id, { instance, component })
75
+
76
+ let previousParentInstance: InstanceModel | null = null
29
77
 
30
- if (currentCompositeInstance) {
31
- instance.parentId = currentCompositeInstance.instance.id
32
- currentCompositeInstance.children.push(instance)
78
+ if (currentInstance) {
79
+ instance.parentId = currentInstance.id
33
80
  }
34
81
 
35
- if (!isUnitModel(component)) {
36
- previousParentInstance = currentCompositeInstance
37
- currentCompositeInstance = { instance, children: [] }
38
- compositeInstances.set(currentCompositeInstance.instance.id, currentCompositeInstance)
82
+ if (!isUnitModel(component.model)) {
83
+ previousParentInstance = currentInstance
84
+ currentInstance = instance
39
85
  }
40
86
 
41
87
  try {
@@ -55,7 +101,7 @@ export function registerInstance<T>(
55
101
  ) as T
56
102
  } finally {
57
103
  if (previousParentInstance) {
58
- currentCompositeInstance = previousParentInstance
104
+ currentInstance = previousParentInstance
59
105
  }
60
106
  }
61
107
  }
package/src/i18n.ts ADDED
@@ -0,0 +1,25 @@
1
+ const knownAbbreviationsMap = new Map<string, string>()
2
+
3
+ export function registerKnownAbbreviations(abbreviations: string[]) {
4
+ for (const abbr of abbreviations) {
5
+ const lower = abbr.toLowerCase()
6
+ if (!knownAbbreviationsMap.has(lower)) {
7
+ knownAbbreviationsMap.set(lower, abbr)
8
+ }
9
+ }
10
+ }
11
+
12
+ export function camelCaseToHumanReadable(text: string) {
13
+ const words = text.split(/(?=[A-Z])|_|-|\./)
14
+
15
+ return words
16
+ .map(word => {
17
+ const lower = word.toLowerCase()
18
+ if (knownAbbreviationsMap.has(lower)) {
19
+ return knownAbbreviationsMap.get(lower)
20
+ }
21
+
22
+ return word.charAt(0).toUpperCase() + word.slice(1)
23
+ })
24
+ .join(" ")
25
+ }
package/src/index.ts CHANGED
@@ -1,69 +1,52 @@
1
+ export * from "./entity"
2
+ export * from "./instance"
3
+ export * from "./unit"
4
+ export * from "./i18n"
5
+ export * from "./meta"
6
+
1
7
  export {
2
- type InstanceInput,
3
- type HubInput,
4
- type InstanceModel,
5
- type Position,
6
- getInstanceId,
7
- parseInstanceId,
8
- findInput,
9
- findRequiredInput,
10
- findInputs,
11
- findRequiredInputs,
12
- } from "./instance"
13
- export { getCompositeInstances, resetEvaluation, type CompositeInstance } from "./evaluation"
14
- export { type Entity, defineEntity, isEntity } from "./entity"
8
+ // common utilities
9
+ bytesToHumanReadable,
10
+ trimIndentation,
11
+ text,
12
+
13
+ // type utilities
14
+ type PartialKeys,
15
+ type RequiredKeys,
16
+ } from "./utils"
17
+
15
18
  export {
16
- type Component,
17
- type ComponentModel,
18
- type ComponentMeta,
19
- type ComponentArgument,
20
- type ComponentInput,
21
- type ComponentInputSpec,
22
- type ComponentArgumentSpec,
23
- type ComponentArgumentSpecToStatic,
19
+ // user API
24
20
  defineComponent,
21
+ $args,
22
+ $inputs,
23
+ $outputs,
24
+
25
+ // extra helpers
25
26
  isComponent,
26
- originalCreate,
27
- } from "./component"
28
- export { type Unit, type UnitModel, type UnitSource, defineUnit, isUnitModel } from "./unit"
29
- export {
30
- type RequiredKeys,
31
- type PartialKeys,
32
- text,
33
- trimIndentation,
34
- bytesToHumanReadable,
35
- } from "./utils"
36
- export { type ArgumentValue, type ArgumentValueSchema } from "./types"
37
- export * from "@sinclair/typebox"
27
+ setValidationEnabled,
28
+ isUnitModel,
38
29
 
39
- import {
40
- Type as BaseType,
41
- type Static,
42
- type TLiteral,
43
- type TSchema,
44
- type TUnion,
45
- } from "@sinclair/typebox"
30
+ // types
31
+ type Component,
32
+ type ComponentModel,
33
+ type ComponentInput,
34
+ type ComponentArgument,
46
35
 
47
- type MapToLiteral<T extends readonly string[]> = {
48
- [K in keyof T]: T[K] extends string ? TLiteral<T[K]> : never
49
- }
36
+ // schemas
37
+ componentModelSchema,
38
+ componentInputSchema,
39
+ componentArgumentSchema,
50
40
 
51
- function StringEnum<T extends string[]>(values: [...T]): TUnion<MapToLiteral<T>> {
52
- return Type.Union(values.map(value => Type.Literal(value))) as TUnion<MapToLiteral<T>>
53
- }
41
+ // for evaluation
42
+ originalCreate,
43
+ } from "./component"
54
44
 
55
- function Default<T extends TSchema>(
56
- schema: T,
57
- defaultValue: Static<T>,
58
- ): T & { default: Static<T> } {
59
- return { ...schema, default: defaultValue }
60
- }
45
+ export {
46
+ type RuntimeInstance,
47
+ InstanceNameConflictError,
48
+ getRuntimeInstances,
49
+ resetEvaluation,
50
+ } from "./evaluation"
61
51
 
62
- export const Type = {
63
- ...BaseType,
64
- StringEnum,
65
- Default,
66
- } as typeof BaseType & {
67
- StringEnum: typeof StringEnum
68
- Default: typeof Default
69
- }
52
+ export { z } from "zod"
package/src/instance.ts CHANGED
@@ -1,66 +1,55 @@
1
+ import type { boundaryInput } from "./component"
2
+ import { z } from "zod"
3
+ import { genericNameSchema } from "./meta"
4
+
1
5
  declare const type: unique symbol
2
6
 
3
7
  export type InstanceInput<TType extends string = string> = {
4
8
  [type]?: TType
5
9
  [boundaryInput]?: InstanceInput
6
- instanceId: string
10
+ instanceId: InstanceId
7
11
  output: string
8
12
  }
9
13
 
10
- export const boundaryInput = Symbol("boundaryInput")
14
+ export const positionSchema = z.object({
15
+ x: z.number(),
16
+ y: z.number(),
17
+ })
11
18
 
12
- export type HubInput = {
13
- hubId: string
14
- }
19
+ export type Position = z.infer<typeof positionSchema>
15
20
 
16
- export type Position = {
17
- x: number
18
- y: number
19
- }
21
+ export const instanceIdSchema = z.templateLiteral([genericNameSchema, ":", genericNameSchema])
20
22
 
21
- export type InstanceModel = {
22
- /**
23
- * The id of the instance unique within the project.
24
- *
25
- * The format is `${instanceType}:${instanceName}`.
26
- */
27
- id: string
23
+ export type InstanceId = z.infer<typeof instanceIdSchema>
28
24
 
29
- /**
30
- * The type of the instance.
31
- */
32
- type: string
25
+ export const instanceInputSchema = z.object({
26
+ instanceId: instanceIdSchema,
27
+ output: z.string(),
28
+ })
33
29
 
34
- /**
35
- * The name of the instance.
36
- *
37
- * Must be unique within instances of the same type in the project.
38
- */
39
- name: string
30
+ export const hubInputSchema = z.object({
31
+ hubId: z.string(),
32
+ })
33
+
34
+ export type HubInput = z.infer<typeof hubInputSchema>
40
35
 
36
+ export const instanceModelPatchSchema = z.object({
41
37
  /**
42
38
  * The static arguments passed to the instance.
43
39
  */
44
- args?: Record<string, unknown>
40
+ args: z.record(z.string(), z.unknown()).optional(),
45
41
 
46
42
  /**
47
43
  * The direct instances passed as inputs to the instance.
48
44
  */
49
- inputs?: Record<string, InstanceInput[]>
45
+ inputs: z.record(z.string(), z.array(instanceInputSchema)).optional(),
50
46
 
51
47
  /**
52
48
  * The resolved unit inputs for the instance.
53
49
  *
54
50
  * Only for computed composite instances.
55
51
  */
56
- resolvedInputs?: Record<string, InstanceInput[]>
57
-
58
- /**
59
- * The inputs passed to the instance from the hubs.
60
- *
61
- * Only for designer-first instances.
62
- */
63
- hubInputs?: Record<string, HubInput[]>
52
+ hubInputs: z.record(z.string(), z.array(hubInputSchema)).optional(),
64
53
 
65
54
  /**
66
55
  * The inputs injected to the instance from the hubs.
@@ -70,36 +59,101 @@ export type InstanceModel = {
70
59
  *
71
60
  * Only for designer-first instances.
72
61
  */
73
- injectionInputs?: HubInput[]
62
+ injectionInputs: z.array(hubInputSchema).optional(),
74
63
 
75
64
  /**
76
65
  * The position of the instance on the canvas.
77
66
  *
78
67
  * Only for designer-first instances.
79
68
  */
80
- position?: Position
69
+ position: positionSchema.optional(),
70
+ })
71
+
72
+ export const instanceModelSchema = z.object({
73
+ /**
74
+ * The id of the instance unique within the project.
75
+ *
76
+ * The format is `${instanceType}:${instanceName}`.
77
+ */
78
+ id: instanceIdSchema,
79
+
80
+ /**
81
+ * The type of the instance.
82
+ */
83
+ type: genericNameSchema,
84
+
85
+ /**
86
+ * The name of the instance.
87
+ *
88
+ * Must be unique within instances of the same type in the project.
89
+ */
90
+ name: genericNameSchema,
91
+
92
+ ...instanceModelPatchSchema.shape,
81
93
 
82
94
  /**
83
95
  * The id of the top level parent instance.
84
96
  *
85
97
  * Only for child instances of the composite instances.
86
98
  */
87
- parentId?: string
99
+ resolvedInputs: z.record(z.string(), z.array(instanceInputSchema)).optional(),
100
+
101
+ /**
102
+ * The position of the instance on the canvas.
103
+ *
104
+ * Only for designer-first instances.
105
+ */
106
+ parentId: z.string().optional(),
88
107
 
89
108
  /**
90
- * The direct instance inputs and same-level children outputs returned by the instance as outputs.
109
+ * The direct instance outputs returned by the instance as outputs.
91
110
  *
92
111
  * Only for computed composite instances.
93
112
  */
94
- outputs?: Record<string, InstanceInput[]>
113
+ outputs: z.record(z.string(), z.array(instanceInputSchema)).optional(),
95
114
 
96
115
  /**
97
116
  * The resolved unit outputs for the instance.
98
117
  *
99
118
  * Only for computed composite instances.
100
119
  */
101
- resolvedOutputs?: Record<string, InstanceInput[]>
102
- }
120
+ resolvedOutputs: z.record(z.string(), z.array(instanceInputSchema)).optional(),
121
+ })
122
+
123
+ export type InstanceModel = z.infer<typeof instanceModelSchema>
124
+
125
+ export const hubModelPatchSchema = z.object({
126
+ /**
127
+ * The position of the hub on the canvas.
128
+ */
129
+ position: positionSchema.optional(),
130
+
131
+ /**
132
+ * The inputs of the hub.
133
+ */
134
+ inputs: z.array(instanceInputSchema).optional(),
135
+
136
+ /**
137
+ * The inputs injected to the hub from the hubs.
138
+ *
139
+ * While `inputs` allows to pass hubs to distinct inputs,
140
+ * `injectionInputs` allows to pass hubs to the hub as a whole filling all inputs with matching types.
141
+ */
142
+ injectionInputs: z.array(hubInputSchema).optional(),
143
+ })
144
+
145
+ export const hubModelSchema = z.object({
146
+ /**
147
+ * The id of the hub unique within the project.
148
+ */
149
+ id: z.nanoid(),
150
+
151
+ ...hubModelPatchSchema.shape,
152
+ })
153
+
154
+ export type InstanceModelPatch = z.infer<typeof instanceModelPatchSchema>
155
+ export type HubModel = z.infer<typeof hubModelSchema>
156
+ export type HubModelPatch = z.infer<typeof hubModelPatchSchema>
103
157
 
104
158
  /**
105
159
  * Formats the instance id from the instance type and instance name.
@@ -109,7 +163,7 @@ export type InstanceModel = {
109
163
  *
110
164
  * @returns The formatted instance id.
111
165
  */
112
- export function getInstanceId(instanceType: string, instanceName: string): string {
166
+ export function getInstanceId(instanceType: string, instanceName: string): InstanceId {
113
167
  return `${instanceType}:${instanceName}`
114
168
  }
115
169
 
@@ -124,7 +178,7 @@ export function parseInstanceId(instanceId: string): [instanceType: string, inst
124
178
  const parts = instanceId.split(":")
125
179
 
126
180
  if (parts.length !== 2) {
127
- throw new Error(`Invalid instance key: ${instanceId}`)
181
+ throw new Error(`Invalid instance ID: ${instanceId}`)
128
182
  }
129
183
 
130
184
  return parts as [string, string]
@@ -177,3 +231,70 @@ export function findRequiredInputs<T extends string>(
177
231
  ): InstanceInput<T>[] {
178
232
  return names.map(name => findRequiredInput(inputs, name))
179
233
  }
234
+
235
+ /**
236
+ * The field names that indicate special objects which Highstate understands regardless of the context.
237
+ *
238
+ * UUIDs are used to prevent conflicts with user-defined fields.
239
+ */
240
+ export enum HighstateSignature {
241
+ Artifact = "d55c63ac-3174-4756-808f-f778e99af0d1",
242
+ Secret = "56ebf97b-57de-4985-8c86-bc1bc5871e6e",
243
+ Yaml = "c857cac5-caa6-4421-b82c-e561fbce6367",
244
+ }
245
+
246
+ export const yamlValueSchema = z.object({
247
+ [HighstateSignature.Yaml]: z.literal(true),
248
+ value: z.string(),
249
+ })
250
+
251
+ export type YamlValue = z.infer<typeof yamlValueSchema>
252
+
253
+ export const fileMetaSchema = z.object({
254
+ name: z.string(),
255
+ contentType: z.string().optional(),
256
+ size: z.number().optional(),
257
+ mode: z.number().optional(),
258
+ isBinary: z.boolean().optional(),
259
+ })
260
+
261
+ export const unitObjectMetaSchema = z.object({
262
+ displayName: z.string().optional(),
263
+ description: z.string().optional(),
264
+ primaryIcon: z.string().optional(),
265
+ })
266
+
267
+ export const unitSecretSchema = z.object({
268
+ [HighstateSignature.Secret]: z.literal(true),
269
+ id: z.uuidv7(),
270
+ value: z.unknown(),
271
+ })
272
+
273
+ export const unitArtifactSchema = z.object({
274
+ [HighstateSignature.Artifact]: z.literal(true),
275
+ hash: z.string(),
276
+ meta: unitObjectMetaSchema.optional(),
277
+ })
278
+
279
+ export const fileContentSchema = z.union([
280
+ z.object({
281
+ type: z.literal("embedded"),
282
+ value: z.string(),
283
+ }),
284
+ z.object({
285
+ type: z.literal("artifact"),
286
+ ...unitArtifactSchema.shape,
287
+ }),
288
+ ])
289
+
290
+ export const fileSchema = z.object({
291
+ meta: fileMetaSchema,
292
+ content: fileContentSchema,
293
+ })
294
+
295
+ export type FileMeta = z.infer<typeof fileMetaSchema>
296
+ export type FileContent = z.infer<typeof fileContentSchema>
297
+ export type UnitObjectMeta = z.infer<typeof unitObjectMetaSchema>
298
+ export type UnitArtifact = z.infer<typeof unitArtifactSchema>
299
+ export type File = z.infer<typeof fileSchema>
300
+ export type UnitSecretModel = z.infer<typeof unitSecretSchema>
package/src/meta.ts ADDED
@@ -0,0 +1,120 @@
1
+ import { z } from "zod"
2
+
3
+ export const userObjectMetaSchema = z.object({
4
+ /**
5
+ * Human-readable name of the object.
6
+ *
7
+ * Used in UI components for better user experience.
8
+ */
9
+ title: z.string().optional(),
10
+
11
+ /**
12
+ * The title used globally for the object.
13
+ *
14
+ * For example, the title of an instance secret is "Password" which is okay
15
+ * to display in the instance secret list, but when the secret is displayed in a
16
+ * global secret list the name should be more descriptive, like "Proxmox Password".
17
+ */
18
+ globalTitle: z.string().optional(),
19
+
20
+ /**
21
+ * Description of the object.
22
+ *
23
+ * Provides additional context for users and developers.
24
+ */
25
+ description: z.string().optional(),
26
+
27
+ /**
28
+ * The color of the object.
29
+ *
30
+ * Used in UI components to visually distinguish objects.
31
+ */
32
+ color: z.string().optional(),
33
+
34
+ /**
35
+ * Primary icon identifier.
36
+ *
37
+ * Should reference a iconify icon name, like "mdi:server" or "gg:remote".
38
+ */
39
+ icon: z.string().optional(),
40
+
41
+ /**
42
+ * The color of the primary icon.
43
+ */
44
+ iconColor: z.string().optional(),
45
+
46
+ /**
47
+ * The secondary icon identifier.
48
+ *
49
+ * Used to provide additional context or actions related to the object.
50
+ *
51
+ * Should reference a iconify icon name, like "mdi:edit" or "mdi:delete".
52
+ */
53
+ secondaryIcon: z.string().optional(),
54
+
55
+ /**
56
+ * The color of the secondary icon.
57
+ */
58
+ secondaryIconColor: z.string().optional(),
59
+ })
60
+
61
+ export const objectMetaSchema = userObjectMetaSchema.extend({
62
+ /**
63
+ * Creation timestamp in milliseconds.
64
+ *
65
+ * Managed automatically by the system.
66
+ */
67
+ createdAt: z.number().optional(),
68
+
69
+ /**
70
+ * Last update timestamp in milliseconds.
71
+ *
72
+ * Managed automatically by the system.
73
+ */
74
+ updatedAt: z.number().optional(),
75
+
76
+ /**
77
+ * The optional version of the document to support optimistic concurrency control.
78
+ *
79
+ * Managed automatically by the system.
80
+ */
81
+ version: z.number().optional(),
82
+ })
83
+
84
+ export type UserObjectMeta = z.infer<typeof userObjectMetaSchema>
85
+ export type ObjectMeta = z.infer<typeof objectMetaSchema>
86
+
87
+ /**
88
+ * The schema for strings that represent names in Highstate.
89
+ *
90
+ * The name:
91
+ * - must be alphanumeric;
92
+ * - can include dashes (`-`) as word separators and dots (`.`) as namespace separators;
93
+ * - must begin with a letter;
94
+ * - must be lowercase;
95
+ * - must include from 2 to 64 characters.
96
+ */
97
+ export const genericNameSchema = z
98
+ .string()
99
+ .regex(/^[a-z][a-z0-9-_.]+$/)
100
+ .min(2)
101
+ .max(64)
102
+
103
+ export type GenericName = z.infer<typeof genericNameSchema>
104
+
105
+ /**
106
+ * The schema for field names in Highstate.
107
+ *
108
+ * The field name:
109
+ * - must be alphanumeric;
110
+ * - must be in camelCase;
111
+ * - must begin with a letter;
112
+ * - must include from 2 to 64 characters.
113
+ */
114
+ export const fieldNameSchema = z
115
+ .string()
116
+ .regex(/^[a-z][a-zA-Z0-9]+$/)
117
+ .min(2)
118
+ .max(64)
119
+
120
+ export type FieldName = z.infer<typeof fieldNameSchema>