@highstate/pulumi 0.9.18 → 0.9.20

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/unit.ts CHANGED
@@ -1,152 +1,73 @@
1
- /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2
- /* eslint-disable @typescript-eslint/no-unsafe-member-access */
3
- /* eslint-disable @typescript-eslint/no-unsafe-argument */
4
- /* eslint-disable @typescript-eslint/no-unsafe-return */
5
- /* eslint-disable @typescript-eslint/no-explicit-any */
6
-
7
- import type { DeepInput, InputArray, InputMap } from "./utils"
8
- import type { ComponentSecret } from "../../contract/src/unit"
1
+ /** biome-ignore-all lint/suspicious/noExplicitAny: здесь орать запрещено */
9
2
  import {
10
- type ComponentInputSpec,
11
- type EntityModel,
12
- type Unit,
13
3
  type ComponentInput,
4
+ type ComponentInputSpec,
5
+ camelCaseToHumanReadable,
6
+ HighstateConfigKey,
14
7
  type InstanceInput,
8
+ type InstanceStatusField,
9
+ type InstanceStatusFieldValue,
10
+ type PartialKeys,
11
+ parseArgumentValue,
15
12
  parseInstanceId,
16
- getInstanceId,
17
- type ComponentArgumentSpec,
18
- type ComponentArgumentSpecToStatic,
19
- HighstateSignature,
20
- camelCaseToHumanReadable,
21
- z,
22
- type UnitSecretModel,
23
- unitSecretSchema,
13
+ runtimeSchema,
14
+ type TriggerInvocation,
15
+ type Unit,
16
+ type UnitArtifact,
17
+ type UnitConfig,
18
+ type UnitPage,
19
+ type UnitTerminal,
20
+ type UnitTrigger,
21
+ type UnitWorker,
24
22
  unitArtifactSchema,
23
+ unitConfigSchema,
24
+ z,
25
25
  } from "@highstate/contract"
26
- import { mapValues, pickBy, pipe } from "remeda"
27
26
  import {
28
27
  Config,
29
- getStack,
30
- Output,
28
+ type Input,
29
+ type Output,
31
30
  output,
32
- secret,
31
+ secret as pulumiSecret,
33
32
  StackReference,
34
- type Input,
35
33
  type Unwrap,
36
34
  } from "@pulumi/pulumi"
37
- import { Ajv } from "ajv"
38
- import { updatedSecretValues, type UnitSecret } from "./secret"
39
-
40
- const ajv = new Ajv({ strict: false })
41
-
42
- export type ObjectMeta = {
43
- title?: Input<string | undefined>
44
- description?: Input<string | undefined>
45
- icon?: Input<string | undefined>
46
- iconColor?: Input<string | undefined>
47
- }
48
-
49
- export type InstanceFileMeta = {
50
- name: Input<string>
51
- contentType?: Input<string>
52
- size?: Input<number>
53
- isBinary?: Input<boolean>
54
- mode?: Input<number>
55
- }
56
-
57
- export type UnitArtifact = {
58
- hash: Input<string>
59
- meta?: Input<ObjectMeta>
60
- }
61
-
62
- export type InstanceFile = {
63
- meta: Input<InstanceFileMeta>
64
- content:
65
- | { type: "embedded"; value: Input<string> }
66
- | {
67
- type: "artifact"
68
- [HighstateSignature.Artifact]: UnitArtifact
69
- }
70
- }
71
-
72
- export type InstanceTerminalSpec = {
73
- image: Input<string>
74
- command: InputArray<string>
75
- cwd?: Input<string | undefined>
76
- env?: InputMap<string | undefined>
77
- files?: InputMap<InstanceFile | string | undefined>
78
- }
79
-
80
- export type InstanceTerminal = {
81
- name: Input<string>
82
- meta: Input<ObjectMeta>
83
- spec: Input<InstanceTerminalSpec>
84
- }
85
-
86
- export type StatusFieldValue = string | number | boolean | string[]
87
-
88
- export type StatusField<TArgName extends string = string> = {
89
- name: Input<string>
90
- meta?: Input<ObjectMeta>
91
- complementaryTo?: Input<TArgName | undefined>
92
- value?: Input<StatusFieldValue | undefined>
93
- }
94
-
95
- export type InstancePageBlock =
96
- | { type: "markdown"; content: Input<string> }
97
- | { type: "qr"; content: Input<string>; showContent?: boolean; language?: string }
98
- | ({ type: "file" } & InstanceFile)
99
-
100
- export type InstancePage = {
101
- name: Input<string>
102
- meta: Input<ObjectMeta>
103
- content: InputArray<InstancePageBlock>
104
- }
105
-
106
- export type InstanceTriggerSpec =
107
- | {
108
- type: "before-destroy"
109
- }
110
- | {
111
- type: "schedule"
112
- schedule: string
113
- }
114
-
115
- export type InstanceTrigger = {
116
- name: Input<string>
117
- title: Input<string>
118
- description?: Input<string>
119
- spec: Input<InstanceTriggerSpec>
120
- }
35
+ import { mapValues } from "remeda"
36
+ import { type DeepInput, toPromise } from "./utils"
121
37
 
122
- export type InstanceWorker = {
123
- name: Input<string>
124
- image: Input<string>
125
- params?: InputMap<unknown>
38
+ type StatusField<TArgName extends string = string> = Omit<
39
+ InstanceStatusField,
40
+ "complementaryTo" | "meta"
41
+ > & {
42
+ meta?: PartialKeys<InstanceStatusField["meta"], "title">
43
+ complementaryTo?: TArgName
126
44
  }
127
45
 
128
- export type ExtraOutputs<TArgName extends string = string> = {
46
+ type ExtraOutputs<TArgName extends string = string> = {
129
47
  $statusFields?:
130
- | InputMap<Omit<StatusField<TArgName>, "name"> | StatusFieldValue | undefined>
131
- | InputArray<StatusField<TArgName> | undefined>
48
+ | Input<
49
+ Record<
50
+ string,
51
+ DeepInput<Omit<StatusField<TArgName>, "name"> | InstanceStatusFieldValue | undefined>
52
+ >
53
+ >
54
+ | Input<DeepInput<StatusField<TArgName> | undefined>[]>
132
55
 
133
56
  $terminals?:
134
- | InputMap<Omit<InstanceTerminal, "name"> | undefined>
135
- | InputArray<InstanceTerminal | undefined>
57
+ | Input<Record<string, DeepInput<Omit<UnitTerminal, "name"> | undefined>>>
58
+ | Input<DeepInput<UnitTerminal | undefined>[]>
136
59
 
137
- $pages?: InputMap<Omit<InstancePage, "name"> | undefined> | InputArray<InstancePage | undefined>
60
+ $pages?:
61
+ | Input<Record<string, DeepInput<Omit<UnitPage, "name"> | undefined>>>
62
+ | Input<DeepInput<UnitPage | undefined>[]>
138
63
 
139
64
  $triggers?:
140
- | InputMap<Omit<InstanceTrigger, "name"> | undefined>
141
- | InputArray<InstanceTrigger | undefined>
65
+ | Input<Record<string, DeepInput<Omit<UnitTrigger, "name"> | undefined>>>
66
+ | Input<DeepInput<UnitTrigger | undefined>[]>
142
67
 
143
68
  $workers?:
144
- | InputMap<Omit<InstanceWorker, "name"> | undefined>
145
- | InputArray<InstanceWorker | undefined>
146
- }
147
-
148
- export type InstanceTriggerInvocation = {
149
- name: string
69
+ | Input<Record<string, DeepInput<Omit<UnitWorker, "name"> | undefined>>>
70
+ | Input<DeepInput<UnitWorker | undefined>[]>
150
71
  }
151
72
 
152
73
  type OutputMapToDeepInputMap<T extends Record<string, unknown>, TArgName extends string> =
@@ -154,11 +75,7 @@ type OutputMapToDeepInputMap<T extends Record<string, unknown>, TArgName extends
154
75
  ? ExtraOutputs
155
76
  : { [K in keyof T]: DeepInput<T[K]> } & ExtraOutputs<TArgName>
156
77
 
157
- export type SecretValueMapToSecretMap<T extends Record<string, unknown>> = {
158
- [K in keyof T]-?: UnitSecret<T[K]>
159
- }
160
-
161
- export interface UnitContext<
78
+ interface UnitContext<
162
79
  TArgs extends Record<string, unknown>,
163
80
  TInputs extends Record<string, unknown>,
164
81
  TOutputs extends Record<string, unknown>,
@@ -168,17 +85,26 @@ export interface UnitContext<
168
85
  instanceId: string
169
86
  type: string
170
87
  name: string
171
- secrets: SecretValueMapToSecretMap<TSecrets>
172
-
173
- inputs: TInputs extends Record<string, never>
174
- ? never
175
- : {
176
- [K in keyof TInputs]: undefined extends TInputs[K]
177
- ? Output<NonNullable<TInputs[K]>> | undefined
178
- : Output<TInputs[K]>
179
- }
180
88
 
181
- invokedTriggers: InstanceTriggerInvocation[]
89
+ secrets: {
90
+ [K in keyof TSecrets]: undefined extends TSecrets[K]
91
+ ? Output<NonNullable<TSecrets[K]>> | undefined
92
+ : Output<TSecrets[K]>
93
+ }
94
+
95
+ getSecret<K extends keyof TSecrets>(
96
+ this: void,
97
+ name: K,
98
+ ): Output<NonNullable<TSecrets[K]> | undefined>
99
+
100
+ getSecret<K extends keyof TSecrets>(
101
+ this: void,
102
+ name: K,
103
+ factory: () => Input<NonNullable<TSecrets[K]>>,
104
+ ): Output<NonNullable<TSecrets[K]>>
105
+
106
+ inputs: TInputs
107
+ invokedTriggers: TriggerInvocation[]
182
108
 
183
109
  outputs(
184
110
  this: void,
@@ -186,68 +112,102 @@ export interface UnitContext<
186
112
  ): Promise<unknown>
187
113
  }
188
114
 
189
- type InputSpecToValue<T extends ComponentInputSpec> = T[2] extends true
190
- ? z.infer<T[0]["schema"]>[]
115
+ // z.output since the values are validated/transformed and passed to the user
116
+ type InputSpecToWrappedValue<T extends ComponentInputSpec> = T[2] extends true
117
+ ? // we have to wrap the array in Output since we don't know how many items will be returned by each multiple input
118
+ Output<NonNullable<z.output<T[0]["schema"]>>[]>
191
119
  : T[1] extends true
192
- ? z.infer<T[0]["schema"]>
193
- : z.infer<T[0]["schema"]> | undefined
194
-
195
- type InputSpecMapToValueMap<T extends Record<string, ComponentInputSpec>> =
196
- T extends Record<string, never>
197
- ? Record<string, never>
198
- : { [K in keyof T]: InputSpecToValue<T[K]> }
120
+ ? Output<NonNullable<z.output<T[0]["schema"]>>>
121
+ : Output<NonNullable<z.output<T[0]["schema"]>>> | undefined
122
+
123
+ // z.input since the values are passed from the user and should be validated/transformed before returning from the unit
124
+ type OutputSpecToValue<T extends ComponentInputSpec> = T[2] extends true
125
+ ? T[1] extends true
126
+ ? NonNullable<z.input<T[0]["schema"]>>[]
127
+ : NonNullable<z.input<T[0]["schema"]>>[] | undefined
128
+ : T[1] extends true
129
+ ? NonNullable<z.input<T[0]["schema"]>>
130
+ : NonNullable<z.input<T[0]["schema"]>> | undefined
199
131
 
200
132
  const stackRefMap = new Map<string, StackReference>()
201
- const [projectId, instanceName] = getStack().split("_")
202
133
 
203
134
  let instanceId: string | undefined
135
+ let instanceName: string | undefined
204
136
 
137
+ /**
138
+ * Returns the current unit instance id.
139
+ *
140
+ * Only available after calling `forUnit` function.
141
+ */
205
142
  export function getUnitInstanceId(): string {
206
143
  if (!instanceId) {
207
- throw new Error("Instance id is not set. Did you call 'forUnit' function?")
144
+ throw new Error(`Instance id is not set. Did you call "forUnit" function?`)
208
145
  }
209
146
 
210
147
  return instanceId
211
148
  }
212
149
 
213
- export function getResourceComment(): string {
214
- return `Managed by Highstate Unit (${getUnitInstanceId()})`
215
- }
216
-
150
+ /**
151
+ * Returns the current unit instance name.
152
+ */
217
153
  export function getUnitInstanceName(): string {
154
+ if (!instanceName) {
155
+ throw new Error(`Instance name is not set. Did you call "forUnit" function?`)
156
+ }
157
+
218
158
  return instanceName
219
159
  }
220
160
 
221
- function getStackRef(input: InstanceInput) {
222
- const [instanceType, instanceName] = parseInstanceId(input.instanceId)
223
- const key = `organization/${instanceType}/${projectId}_${instanceName}`
161
+ /**
162
+ * Returns a comment that can be used in resources to indicate that they are managed by Highstate.
163
+ */
164
+ export function getResourceComment(): string {
165
+ return `Managed by Highstate (${getUnitInstanceId()})`
166
+ }
224
167
 
225
- if (!stackRefMap.has(key)) {
226
- stackRefMap.set(key, new StackReference(key))
168
+ function getStackRef(config: UnitConfig, input: InstanceInput) {
169
+ const [instanceType] = parseInstanceId(input.instanceId)
170
+ const stateId = config.stateIdMap[input.instanceId]
171
+ if (!stateId) {
172
+ throw new Error(`State ID for instance "${input.instanceId}" not found in the unit config.`)
227
173
  }
228
174
 
229
- return stackRefMap.get(key)!
175
+ const key = `organization/${instanceType}/${stateId}`
176
+ let stackRef = stackRefMap.get(key)
177
+
178
+ if (!stackRef) {
179
+ stackRef = new StackReference(key)
180
+ stackRefMap.set(key, stackRef)
181
+ }
182
+
183
+ return stackRef
230
184
  }
231
185
 
232
- function getOutput(unit: Unit, input: ComponentInput, refs: InstanceInput[]) {
186
+ function getOutput(config: UnitConfig, unit: Unit, input: ComponentInput, refs: InstanceInput[]) {
233
187
  const entity = unit.entities.get(input.type)
234
188
  if (!entity) {
235
- throw new Error(`Entity '${input.type}' not found in the unit '${unit.model.type}'.`)
189
+ throw new Error(`Entity "${input.type}" not found in the unit "${unit.model.type}".`)
236
190
  }
237
191
 
238
192
  const _getOutput = (ref: InstanceInput) => {
239
- const value = getStackRef(ref).requireOutput(ref.output)
193
+ const value = getStackRef(config, ref).requireOutput(ref.output)
240
194
 
241
195
  return value.apply(value => {
242
196
  if (Array.isArray(value)) {
243
197
  for (const [index, item] of value.entries()) {
244
- if (!ajv.validate(entity.schema, item)) {
245
- throw new Error(`Invalid output for '${input.type}[${index}]': ${ajv.errorsText()}`)
198
+ const result = entity.schema.safeParse(item)
199
+
200
+ if (!result.success) {
201
+ throw new Error(
202
+ `Invalid output for "${input.type}[${index}]": ${z.prettifyError(result.error)}`,
203
+ )
246
204
  }
247
205
  }
248
206
  } else {
249
- if (!ajv.validate(entity.schema, value)) {
250
- throw new Error(`Invalid output for '${input.type}': ${ajv.errorsText()}`)
207
+ const result = entity.schema.safeParse(value)
208
+
209
+ if (!result.success) {
210
+ throw new Error(`Invalid output for "${input.type}": ${z.prettifyError(result.error)}`)
251
211
  }
252
212
  }
253
213
 
@@ -268,199 +228,105 @@ function getOutput(unit: Unit, input: ComponentInput, refs: InstanceInput[]) {
268
228
  return values
269
229
  }
270
230
 
271
- function isAnyOfSchema(schema: z.core.JSONSchema.BaseSchema, itemType: string): boolean {
272
- if (schema.anyOf) {
273
- return Object.values(schema.anyOf).every(schema => isAnyOfSchema(schema, itemType))
274
- }
275
-
276
- return schema.type === itemType
277
- }
278
-
279
- function isStringSchema(schema: z.core.JSONSchema.BaseSchema): boolean {
280
- if (schema.type === "string") {
281
- return true
282
- }
283
-
284
- if (isAnyOfSchema(schema, "string")) {
285
- return true
286
- }
287
-
288
- return false
289
- }
290
-
291
- function isNumberSchema(schema: z.core.JSONSchema.BaseSchema): boolean {
292
- if (schema.type === "number") {
293
- return true
294
- }
295
-
296
- if (isAnyOfSchema(schema, "number")) {
297
- return true
298
- }
299
-
300
- return false
301
- }
302
-
303
- function isBooleanSchema(schema: z.core.JSONSchema.BaseSchema): boolean {
304
- if (schema.type === "boolean") {
305
- return true
306
- }
307
-
308
- if (isAnyOfSchema(schema, "boolean")) {
309
- return true
310
- }
311
-
312
- return false
313
- }
314
-
315
231
  export function forUnit<
316
- TArgs extends Record<string, ComponentArgumentSpec>,
232
+ TArgs extends Record<string, z.ZodType>,
317
233
  TInputs extends Record<string, ComponentInputSpec>,
318
234
  TOutputs extends Record<string, ComponentInputSpec>,
319
- TSecrets extends Record<string, ComponentArgumentSpec>,
235
+ TSecrets extends Record<string, z.ZodType>,
320
236
  >(
321
237
  unit: Unit<TArgs, TInputs, TOutputs, TSecrets>,
322
238
  ): UnitContext<
323
- //
324
- ComponentArgumentSpecToStatic<TArgs>,
325
- InputSpecMapToValueMap<TInputs>,
326
- InputSpecMapToValueMap<TOutputs>,
327
- ComponentArgumentSpecToStatic<TSecrets>
239
+ { [K in keyof TArgs]: z.output<TArgs[K]> },
240
+ { [K in keyof TInputs]: InputSpecToWrappedValue<TInputs[K]> },
241
+ { [K in keyof TOutputs]: OutputSpecToValue<TOutputs[K]> },
242
+ { [K in keyof TSecrets]: z.output<TSecrets[K]> }
328
243
  > {
329
244
  const config = new Config()
245
+ const rawHSConfig = config.requireObject(HighstateConfigKey.Config)
246
+ const hsConfig = unitConfigSchema.parse(rawHSConfig)
330
247
 
331
- const args = mapValues(unit.model.args, (arg, argName) => {
332
- switch (true) {
333
- case isStringSchema(arg.schema): {
334
- if (arg.required) {
335
- return config.require(argName)
336
- }
337
-
338
- // handle empty strings as undefined
339
- return config.get(argName) || arg.schema.default
340
- }
341
- case isNumberSchema(arg.schema): {
342
- if (arg.required) {
343
- return config.requireNumber(argName)
344
- }
345
-
346
- // handle empty strings as undefined
347
- const value = config.get(argName)
348
- if (!value) {
349
- return arg.schema.default
350
- }
248
+ const rawHsSecrets = config
249
+ .requireSecretObject(HighstateConfigKey.Secrets)
250
+ .apply(secrets => z.record(z.string(), z.unknown()).parse(secrets))
351
251
 
352
- return config.getNumber(argName) ?? arg.schema.default
353
- }
354
- case isBooleanSchema(arg.schema): {
355
- if (arg.required) {
356
- return config.requireBoolean(argName)
357
- }
358
-
359
- // handle empty strings as undefined
360
- const value = config.get(argName)
361
- if (!value) {
362
- return arg.schema.default
363
- }
252
+ const args = mapValues(unit.model.args, (arg, argName) => {
253
+ const value = parseArgumentValue(hsConfig.args[argName])
254
+ // biome-ignore lint/style/noNonNullAssertion: runtime schema is there in runtime
255
+ const result = arg[runtimeSchema]!.safeParse(value)
364
256
 
365
- return config.getBoolean(argName) ?? arg.schema.default
366
- }
367
- default: {
368
- if (!arg.required) {
369
- const value = config.get(argName)
370
- // handle empty strings as undefined
371
- if (!value) {
372
- return arg.schema.default
373
- }
374
- }
257
+ if (!result.success) {
258
+ throw new Error(`Invalid argument "${argName}": ${z.prettifyError(result.error)}`)
259
+ }
375
260
 
376
- const value = arg.required ? config.requireObject(argName) : config.getObject(argName)
377
- if (value === undefined) return arg.schema.default
261
+ return result.data
262
+ })
378
263
 
379
- if (!ajv.validate(arg.schema, value)) {
380
- throw new Error(`Invalid config for '${argName}': ${ajv.errorsText()}`)
381
- }
264
+ const secrets = mapValues(unit.model.secrets, (secret, secretName) => {
265
+ const hasValue = hsConfig.secretNames.includes(secretName)
382
266
 
383
- return value
384
- }
267
+ if (!hasValue && !secret.required) {
268
+ return secret.schema.default ? pulumiSecret(secret.schema.default) : undefined
385
269
  }
386
- }) as ComponentArgumentSpecToStatic<TArgs>
387
-
388
- const secretIds = config.requireObject<Record<string, string>>("$secretIds")
389
270
 
390
- const getSecretValue = (
391
- secretName: string,
392
- secret: ComponentSecret,
393
- ): Output<unknown> | undefined => {
394
- switch (true) {
395
- case isStringSchema(secret.schema): {
396
- return secret.required ? config.requireSecret(secretName) : config.getSecret(secretName)
397
- }
398
- case isNumberSchema(secret.schema): {
399
- return secret.required
400
- ? config.requireSecretNumber(secretName)
401
- : config.getSecretNumber(secretName)
402
- }
403
- case isBooleanSchema(secret.schema): {
404
- return secret.required
405
- ? config.requireSecretBoolean(secretName)
406
- : config.getSecretBoolean(secretName)
407
- }
408
- default: {
409
- const value = secret.required
410
- ? config.requireSecretObject(secretName)
411
- : config.getSecretObject(secretName)
271
+ if (!hasValue && secret.required) {
272
+ throw new Error(`Secret "${secretName}" is required but not provided.`)
273
+ }
412
274
 
413
- if (!ajv.validate(secret.schema, value)) {
414
- throw new Error(`Invalid secret for '${secretName}': ${ajv.errorsText()}`)
415
- }
275
+ return rawHsSecrets[secretName].apply(rawValue => {
276
+ const value = parseArgumentValue(rawValue)
277
+ // biome-ignore lint/style/noNonNullAssertion: runtime schema is there in runtime
278
+ const result = secret[runtimeSchema]!.safeParse(value)
416
279
 
417
- return value
280
+ if (!result.success) {
281
+ throw new Error(`Invalid secret "${secretName}": ${z.prettifyError(result.error)}`)
418
282
  }
419
- }
420
- }
421
283
 
422
- const secrets = mapValues(unit.model.secrets, (secret, secretName): UnitSecret<unknown> => {
423
- const secretId = secretIds[secretName]
424
- if (!secretId) {
425
- throw new Error(`Secret '${secretName}' not found in the config.`)
426
- }
427
-
428
- return {
429
- [HighstateSignature.Secret]: true,
430
- id: secretId,
431
- value: secret.required
432
- ? config.requireSecret(secretName)
433
- : output(getSecretValue(secretName, secret)),
434
- }
284
+ return pulumiSecret(result.data)
285
+ })
435
286
  })
436
287
 
437
288
  const inputs = mapValues(unit.model.inputs, (input, inputName) => {
438
- const value = input.required
439
- ? config.requireObject<InstanceInput[]>(`input.${inputName}`)
440
- : config.getObject<InstanceInput[]>(`input.${inputName}`)
289
+ const value = hsConfig.inputs[inputName]
441
290
 
442
291
  if (!value) {
443
292
  if (input.multiple) {
444
- return output([])
293
+ return []
445
294
  }
446
295
 
447
296
  return undefined
448
297
  }
449
298
 
450
- return getOutput(unit as unknown as Unit, input, value)
299
+ return getOutput(hsConfig, unit as unknown as Unit, input, value)
451
300
  })
452
301
 
453
- const type = unit.model.type
454
- instanceId = getInstanceId(type, instanceName)
302
+ const [type, name] = parseInstanceId(hsConfig.instanceId)
303
+
304
+ instanceId = hsConfig.instanceId
305
+ instanceName = name
455
306
 
456
307
  return {
457
- args,
458
- instanceId,
308
+ instanceId: hsConfig.instanceId,
459
309
  type,
460
- name: instanceName,
461
- secrets: secrets as SecretValueMapToSecretMap<ComponentArgumentSpecToStatic<TSecrets>>,
310
+ name,
311
+
312
+ args: args as any,
313
+ secrets: secrets as any,
462
314
  inputs: inputs as any,
463
- invokedTriggers: config.getObject<InstanceTriggerInvocation[]>("$invokedTriggers") ?? [],
315
+ invokedTriggers: hsConfig.invokedTriggers,
316
+
317
+ getSecret: (<K extends keyof TSecrets>(
318
+ name: K,
319
+ factory?: () => Input<NonNullable<TSecrets[K]>>,
320
+ ) => {
321
+ if (!factory) {
322
+ return secrets[name as string]
323
+ }
324
+
325
+ const value = secrets[name as string] ?? pulumiSecret(factory())
326
+ secrets[name as string] = value
327
+
328
+ return value
329
+ }) as any,
464
330
 
465
331
  outputs: async (outputs: any = {}) => {
466
332
  const result: any = mapValues(outputs, (outputValue, outputName) => {
@@ -480,75 +346,59 @@ export function forUnit<
480
346
  return output(outputValue).apply(mapTriggers)
481
347
  }
482
348
 
349
+ if (outputName === "$workers") {
350
+ return output(outputValue).apply(mapWorkers)
351
+ }
352
+
483
353
  if (outputName.startsWith("$")) {
484
- throw new Error(`Unknown extra output '${outputName}'.`)
354
+ throw new Error(`Unknown extra output "${outputName}".`)
485
355
  }
486
356
 
487
357
  const outputModel = unit.model.outputs[outputName]
488
358
  if (!outputModel) {
489
- throw new Error(`Output '${outputName}' not found in the unit '${unit.model.type}'.`)
359
+ throw new Error(
360
+ `Output "${outputName}" not found in the unit "${unit.model.type}", but was passed to outputs(...).`,
361
+ )
490
362
  }
491
363
 
492
364
  const entity = unit.entities.get(outputModel.type)
493
365
  if (!entity) {
494
366
  throw new Error(
495
- `Entity '${outputModel.type}' not found in the unit '${unit.model.type}'.`,
367
+ `Entity "${outputModel.type}" not found in the unit "${unit.model.type}". It looks like a bug in the unit definition.`,
496
368
  )
497
369
  }
498
370
 
499
371
  return output(outputValue).apply(value => {
500
- if (value === undefined) {
501
- if (outputModel.required) {
502
- throw new Error(`Output '${outputName}' is required.`)
503
- }
504
-
505
- return undefined
506
- }
507
-
508
372
  const schema = outputModel.multiple ? entity.schema.array() : entity.schema
373
+ const result = schema.safeParse(value)
509
374
 
510
- if (!ajv.validate(schema, value)) {
511
- throw new Error(`Invalid output for '${outputName}': ${ajv.errorsText()}`)
375
+ if (!result.success) {
376
+ throw new Error(
377
+ `Invalid output "${outputName}" of type "${outputModel.type}": ${z.prettifyError(result.error)}`,
378
+ )
512
379
  }
513
380
 
514
- return value
381
+ return result.data
515
382
  })
516
- }) as Record<string, Output<unknown>> & ExtraOutputs
517
-
518
- await Promise.all(Object.values(result).map(o => outputToPromise(o)))
383
+ })
519
384
 
520
- result.$secrets = updatedSecretValues
385
+ // wait for all outputs to resolve before collecting secrets and artifacts
386
+ await Promise.all(Object.values(result).map(o => toPromise(o)))
521
387
 
522
- // collect secrets from all outputs
523
- const secretsMap: Record<string, UnitSecretModel[]> = {}
524
- for (const [outputName, outputValue] of Object.entries(outputs)) {
525
- if (!outputName.startsWith("$")) {
526
- const resolvedValue = await outputToPromise(outputValue)
527
- const secrets = extractObjectsFromValue(unitSecretSchema, resolvedValue)
528
- if (secrets.length > 0) {
529
- secretsMap[outputName] = secrets
530
- }
531
- }
532
- }
388
+ result.$secrets = secrets
533
389
 
534
390
  // collect artifacts from all outputs
535
391
  const artifactsMap: Record<string, UnitArtifact[]> = {}
536
392
  for (const [outputName, outputValue] of Object.entries(outputs)) {
537
- if (!outputName.startsWith("$")) {
538
- const resolvedValue = await outputToPromise(outputValue)
539
- const artifacts = extractObjectsFromValue(unitArtifactSchema, resolvedValue)
540
- if (artifacts.length > 0) {
541
- artifactsMap[outputName] = artifacts
542
- }
393
+ const resolvedValue = await toPromise(outputValue)
394
+ const artifacts = extractObjectsFromValue(unitArtifactSchema, resolvedValue)
395
+ if (artifacts.length > 0) {
396
+ artifactsMap[outputName] = artifacts
543
397
  }
544
398
  }
545
399
 
546
400
  if (Object.keys(artifactsMap).length > 0) {
547
- result.$exportedArtifacts = artifactsMap
548
- }
549
-
550
- if (Object.keys(secretsMap).length > 0) {
551
- result.$exportedSecretIds = mapValues(secretsMap, v => v.map(secret => secret.id))
401
+ result.$artifacts = artifactsMap
552
402
  }
553
403
 
554
404
  return result
@@ -556,29 +406,22 @@ export function forUnit<
556
406
  }
557
407
  }
558
408
 
559
- export type EntityValue<T extends EntityModel> = z.infer<T["schema"]>
560
- export type EntityInput<T extends EntityModel> = Output<EntityValue<T>>
561
-
562
- function outputToPromise(o: unknown): Promise<unknown> {
563
- return new Promise(resolve => (output(o) as Output<unknown>).apply(resolve))
564
- }
565
-
566
- function mapStatusFields(status: Unwrap<ExtraOutputs["$statusFields"]>): StatusField[] {
409
+ function mapStatusFields(status: Unwrap<ExtraOutputs["$statusFields"]>): InstanceStatusField[] {
567
410
  if (!status) {
568
411
  return []
569
412
  }
570
413
 
571
414
  if (Array.isArray(status)) {
572
415
  return status
573
- .filter(field => !!field?.value)
416
+ .filter((field): field is NonNullable<StatusField> => field?.value !== undefined)
574
417
  .map(field => {
575
418
  return {
576
- name: field!.name,
419
+ name: field.name,
577
420
  meta: {
578
- title: field!.meta?.title ?? camelCaseToHumanReadable(field!.name),
421
+ title: field.meta?.title ?? camelCaseToHumanReadable(field.name),
579
422
  },
580
- value: field!.value,
581
- } as StatusField
423
+ value: field.value,
424
+ }
582
425
  })
583
426
  }
584
427
 
@@ -604,7 +447,7 @@ function mapStatusFields(status: Unwrap<ExtraOutputs["$statusFields"]>): StatusF
604
447
  }
605
448
 
606
449
  return {
607
- ...(field as StatusField),
450
+ ...field,
608
451
  meta: {
609
452
  ...field.meta,
610
453
  title: field.meta?.title ?? camelCaseToHumanReadable(name),
@@ -612,65 +455,30 @@ function mapStatusFields(status: Unwrap<ExtraOutputs["$statusFields"]>): StatusF
612
455
  name,
613
456
  }
614
457
  })
615
- .filter(field => !!field?.value) as StatusField[]
458
+ .filter((field): field is InstanceStatusField => field?.value !== undefined)
616
459
  }
617
460
 
618
- function mapPages(pages: Unwrap<ExtraOutputs["$pages"]>): InstancePage[] {
461
+ function mapPages(pages: Unwrap<ExtraOutputs["$pages"]>): Output<UnitPage[]> {
619
462
  if (!pages) {
620
- return []
621
- }
622
-
623
- if (Array.isArray(pages)) {
624
- return pages.filter(page => !!page)
463
+ return output([])
625
464
  }
626
465
 
627
- return Object.entries(pages)
628
- .filter(([, page]) => !!page)
629
- .map(([name, page]) => ({ ...page!, name }))
630
- }
466
+ if (!Array.isArray(pages)) {
467
+ pages = Object.entries(pages).map(([name, page]) => {
468
+ if (!page) {
469
+ return undefined
470
+ }
631
471
 
632
- export function fileFromString(
633
- name: string,
634
- content: string,
635
- contentType = "text/plain",
636
- isSecret = false,
637
- ): InstanceFile {
638
- return {
639
- meta: {
640
- name,
641
- contentType,
642
- size: Buffer.byteLength(content, "utf8"),
643
- },
644
- content: {
645
- type: "embedded",
646
- value: isSecret ? secret(content) : content,
647
- },
472
+ return { ...page, name }
473
+ })
648
474
  }
649
- }
650
475
 
651
- export function fileFromBuffer(
652
- name: string,
653
- content: Buffer,
654
- contentType = "application/octet-stream",
655
- isSecret = false,
656
- ): InstanceFile {
657
- return {
658
- meta: {
659
- name,
660
- contentType,
661
- size: content.byteLength,
662
- isBinary: true,
663
- },
664
- content: {
665
- type: "embedded",
666
- value: isSecret ? secret(content.toString("base64")) : content.toString("base64"),
667
- },
668
- }
476
+ return output(pages.filter((page): page is NonNullable<UnitPage> => !!page))
669
477
  }
670
478
 
671
- function mapTerminals(terminals: Unwrap<ExtraOutputs["$terminals"]>): InstanceTerminal[] {
479
+ function mapTerminals(terminals: Unwrap<ExtraOutputs["$terminals"]>): Output<UnitTerminal[]> {
672
480
  if (!terminals) {
673
- return []
481
+ return output([])
674
482
  }
675
483
 
676
484
  if (!Array.isArray(terminals)) {
@@ -683,57 +491,43 @@ function mapTerminals(terminals: Unwrap<ExtraOutputs["$terminals"]>): InstanceTe
683
491
  })
684
492
  }
685
493
 
686
- return terminals
687
- .filter(terminal => !!terminal)
688
- .map(terminal => {
689
- if (!terminal.spec.files) {
690
- return terminal
691
- }
494
+ return output(terminals.filter((terminal): terminal is NonNullable<UnitTerminal> => !!terminal))
495
+ }
692
496
 
693
- return {
694
- ...terminal,
695
-
696
- spec: {
697
- ...terminal.spec,
698
-
699
- files: pipe(
700
- terminal.spec.files,
701
- mapValues(file => {
702
- if (typeof file === "string") {
703
- return {
704
- meta: {
705
- name: "content",
706
- contentType: "text/plain",
707
- size: Buffer.byteLength(file, "utf8"),
708
- },
709
- content: {
710
- type: "embedded" as const,
711
- value: file,
712
- },
713
- }
714
- }
715
-
716
- return file
717
- }),
718
- pickBy(value => !!value),
719
- ),
720
- },
497
+ function mapTriggers(triggers: Unwrap<ExtraOutputs["$triggers"]>): Output<UnitTrigger[]> {
498
+ if (!triggers) {
499
+ return output([])
500
+ }
501
+
502
+ if (!Array.isArray(triggers)) {
503
+ triggers = Object.entries(triggers).map(([name, trigger]) => {
504
+ if (!trigger) {
505
+ return undefined
721
506
  }
507
+
508
+ return { ...trigger, name }
722
509
  })
510
+ }
511
+
512
+ return output(triggers.filter((trigger): trigger is NonNullable<UnitTrigger> => !!trigger))
723
513
  }
724
514
 
725
- function mapTriggers(triggers: Unwrap<ExtraOutputs["$triggers"]>): InstanceTrigger[] {
726
- if (!triggers) {
727
- return []
515
+ function mapWorkers(workers: Unwrap<ExtraOutputs["$workers"]>): Output<Unwrap<UnitWorker>[]> {
516
+ if (!workers) {
517
+ return output([])
728
518
  }
729
519
 
730
- if (Array.isArray(triggers)) {
731
- return triggers.filter(trigger => !!trigger)
520
+ if (!Array.isArray(workers)) {
521
+ workers = Object.entries(workers).map(([name, worker]) => {
522
+ if (!worker) {
523
+ return undefined
524
+ }
525
+
526
+ return { ...worker, name }
527
+ })
732
528
  }
733
529
 
734
- return Object.entries(triggers)
735
- .filter(([, trigger]) => !!trigger)
736
- .map(([name, trigger]) => ({ ...(trigger as InstanceTrigger), name }))
530
+ return output(workers.filter((worker): worker is NonNullable<Unwrap<UnitWorker>> => !!worker))
737
531
  }
738
532
 
739
533
  /**