@highstate/pulumi 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/src/entity.ts ADDED
@@ -0,0 +1,108 @@
1
+ import {
2
+ type Entity,
3
+ type EntityMeta,
4
+ type EntityValue,
5
+ type EntityValueInput,
6
+ type EntityWithMeta,
7
+ getEntityId,
8
+ HighstateSignature,
9
+ type Secret,
10
+ } from "@highstate/contract"
11
+ import { type Input, type Output, output, type Unwrap } from "@pulumi/pulumi"
12
+ import { type DeepInput, type InputArray, toPromise } from "./utils"
13
+
14
+ export type MakeEntityOptions<TEntity extends Entity> = {
15
+ entity: TEntity
16
+ identity: string
17
+ meta?: Omit<EntityMeta, "type" | "identity">
18
+ value: Omit<EntityValueInput<TEntity>, "$meta">
19
+ }
20
+
21
+ export function makeEntity<TEntity extends Entity>({
22
+ entity,
23
+ identity,
24
+ meta,
25
+ value,
26
+ }: MakeEntityOptions<TEntity>): EntityValue<TEntity> {
27
+ const built = {
28
+ ...(value as Record<string, unknown>),
29
+ $meta: {
30
+ type: entity.type,
31
+ identity,
32
+ ...meta,
33
+ },
34
+ }
35
+
36
+ return entity.schema.parse(built) as EntityValue<TEntity>
37
+ }
38
+
39
+ type CommonEntityMeta = Omit<EntityMeta, "type" | "identity">
40
+
41
+ export type MakeEntityAsyncOptions<TEntity extends Entity> = {
42
+ entity: TEntity
43
+ identity: Input<string>
44
+ meta?: { [K in keyof CommonEntityMeta]?: Input<CommonEntityMeta[K]> }
45
+ value: {
46
+ [K in keyof Omit<EntityValueInput<TEntity>, "$meta">]: DeepInput<EntityValueInput<TEntity>[K]>
47
+ }
48
+ }
49
+
50
+ export function makeSecret<TValue>(value: TValue): Secret<TValue> {
51
+ return {
52
+ [HighstateSignature.Secret]: true,
53
+ value,
54
+ }
55
+ }
56
+
57
+ export function makeSecretOutput<TValue>(value: Input<TValue>): Output<Secret<Unwrap<TValue>>> {
58
+ return output(value).apply(makeSecret)
59
+ }
60
+
61
+ export function makeSecretAsync<TValue>(value: Input<TValue>): Promise<Secret<Unwrap<TValue>>> {
62
+ return toPromise(makeSecretOutput(value)) as Promise<Secret<Unwrap<TValue>>>
63
+ }
64
+
65
+ export function makeEntityOutput<TEntity extends Entity>({
66
+ entity,
67
+ identity,
68
+ meta,
69
+ value,
70
+ }: MakeEntityAsyncOptions<TEntity>): Output<EntityValue<TEntity>> {
71
+ return output({
72
+ ...value,
73
+ $meta: {
74
+ type: entity.type,
75
+ identity,
76
+ ...meta,
77
+ },
78
+ }).apply(built => entity.schema.parse(built)) as Output<EntityValue<TEntity>>
79
+ }
80
+
81
+ export function makeEntityAsync<TEntity extends Entity>(
82
+ options: MakeEntityAsyncOptions<TEntity>,
83
+ ): Promise<EntityValue<TEntity>> {
84
+ return toPromise(makeEntityOutput(options)) as Promise<EntityValue<TEntity>>
85
+ }
86
+
87
+ export type IdentitySource = EntityWithMeta | string
88
+
89
+ /**
90
+ * Get the combined identity based on the ids of the given entities.
91
+ *
92
+ * This function can be used for entities that do not have their own identity but are defined by the combination of other entities (e.g. a server defined by its network endpoints).
93
+ */
94
+ export function getCombinedIdentity(entities: IdentitySource[]): string {
95
+ const sortedIds = entities
96
+ .map(source => (typeof source === "string" ? source : getEntityId(source)))
97
+ .sort() // sort to ensure consistent identity regardless of the order of entities
98
+
99
+ return sortedIds.join(":")
100
+ }
101
+
102
+ export function getCombinedIdentityOutput(entities: InputArray<IdentitySource>): Output<string> {
103
+ return output(entities).apply(getCombinedIdentity)
104
+ }
105
+
106
+ export function getCombinedIdentityAsync(entities: IdentitySource[]): Promise<string> {
107
+ return toPromise(getCombinedIdentityOutput(entities))
108
+ }
package/src/file.ts CHANGED
@@ -1,68 +1,106 @@
1
- import type { File } from "@highstate/contract"
2
- import { type Input, type Output, output, secret } from "@pulumi/pulumi"
1
+ import type { EntityWithMeta, FileContent, FileMeta } from "@highstate/contract"
2
+ import { crc32 } from "node:zlib"
3
+ import { type Input, type Output, output, type Unwrap } from "@pulumi/pulumi"
4
+ import { makeSecret } from "./entity"
5
+ import { toPromise } from "./utils"
6
+
7
+ /**
8
+ * The BaseFile is type that compatible with both the contract's File type and the file entity used in the library.
9
+ *
10
+ * It should be used in most places instead of the contract's File type to avoid unnecessary conversions, and it can be safely cast to the contract's File type when needed.
11
+ */
12
+ export type BaseFile = EntityWithMeta & {
13
+ meta: FileMeta
14
+ content: FileContent
15
+ }
3
16
 
4
17
  export type FileOptions = {
5
- isSecret?: boolean
18
+ /**
19
+ * The name of the file.
20
+ */
21
+ name: Input<string>
22
+
23
+ /**
24
+ * The content of the file as a string or a buffer.
25
+ */
26
+ content: Input<string | Buffer>
27
+
28
+ /**
29
+ * The identity to use for the file entity.
30
+ *
31
+ * If not provided, defaults to the hash of the content.
32
+ */
33
+ identity?: Input<string>
34
+
35
+ /**
36
+ * Whether the content should be treated as a secret or not.
37
+ */
38
+ isSecret?: Input<boolean>
39
+
40
+ /**
41
+ * The content type of the file.
42
+ *
43
+ * Defaults to "text/plain" if the content is a string and "application/octet-stream" if the content is a buffer.
44
+ */
6
45
  contentType?: Input<string>
46
+
47
+ /**
48
+ * The file mode (permissions) to set on the file when materialized.
49
+ *
50
+ * Defaults to 0o644.
51
+ */
7
52
  mode?: Input<number>
8
53
  }
9
54
 
10
55
  /**
11
- * Creates a file from a string input.
12
- * This file can then be passed to terminals/pages or other components.
56
+ * Creates a file entity from the given options.
13
57
  *
14
- * @param name The name of the file.
15
- * @param content The content of the file.
16
- * @param options Additional options for the file.
58
+ * This file can also be used for both:
59
+ * - core Highstate capabilities like terminals and pages;
60
+ * - as inputs for other units requiring files.
17
61
  */
18
- export function fileFromString(
19
- name: Input<string>,
20
- content: Input<string>,
21
- { contentType = "text/plain", isSecret = false, mode }: FileOptions = {},
22
- ): Output<File> {
23
- return output({
62
+ export function makeFile({
63
+ name,
64
+ content,
65
+ contentType,
66
+ identity,
67
+ isSecret,
68
+ mode,
69
+ }: Unwrap<FileOptions>): BaseFile {
70
+ const stringContent = typeof content === "string" ? content : content.toString("base64")
71
+ const isBinary = typeof content !== "string"
72
+ const inferredContentType =
73
+ contentType ?? (typeof content === "string" ? "text/plain" : "application/octet-stream")
74
+ const size =
75
+ typeof content === "string" ? Buffer.byteLength(content, "utf-8") : content.byteLength
76
+
77
+ return {
78
+ $meta: {
79
+ type: "common.file.v1",
80
+ identity: identity ?? crc32(stringContent).toString(16), // use crc32 hash of the content as the default identity
81
+ },
24
82
  meta: {
25
83
  name,
26
- contentType,
27
- size: output(content).apply(content => Buffer.byteLength(content, "utf8")),
84
+ contentType: inferredContentType,
85
+ size,
28
86
  mode,
29
87
  },
30
- content: {
31
- type: "embedded",
32
- value: isSecret ? secret(content) : content,
33
- },
34
- })
88
+ content: isSecret
89
+ ? { type: "embedded-secret", value: makeSecret(stringContent), isBinary }
90
+ : { type: "embedded", value: stringContent, isBinary },
91
+ }
35
92
  }
36
93
 
37
94
  /**
38
- * Creates a file from a buffer input.
39
- * This file can then be passed to terminals/pages or other components.
40
- *
41
- * @param name The name of the file.
42
- * @param content The content of the file as a Buffer.
43
- * @param options Additional options for the file.
95
+ * Similar to `makeFile`, but returns a Pulumi Output that resolves to a file entity.
44
96
  */
45
- export function fileFromBuffer(
46
- name: Input<string>,
47
- content: Buffer,
48
- { contentType = "application/octet-stream", isSecret = false, mode }: FileOptions = {},
49
- ): Output<File> {
50
- // const base64Content = output(content).apply(
51
- // c => (console.log("fileFromBuffer", c), c.toString("base64")),
52
- // )
53
- const base64Content = content.toString("base64")
97
+ export function makeFileOutput(options: FileOptions): Output<BaseFile> {
98
+ return output(options).apply(opts => makeFile(opts))
99
+ }
54
100
 
55
- return output({
56
- meta: {
57
- name,
58
- contentType,
59
- size: output(content).apply(content => content.byteLength),
60
- mode,
61
- },
62
- content: {
63
- type: "embedded",
64
- isBinary: true,
65
- value: isSecret ? secret(base64Content) : base64Content,
66
- },
67
- })
101
+ /**
102
+ * Similar to `makeFile`, but returns a Promise that resolves to a file entity.
103
+ */
104
+ export function makeFileAsync(options: FileOptions): Promise<BaseFile> {
105
+ return toPromise(makeFileOutput(options))
68
106
  }
package/src/index.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export * from "@pulumi/pulumi"
2
+ export * from "./entity"
2
3
  export * from "./file"
4
+ export * from "./resource-hooks"
3
5
  export * from "./unit"
4
6
  export * from "./utils"
@@ -0,0 +1,14 @@
1
+ let hasResourceHooks = false
2
+
3
+ /**
4
+ * Marks the current Pulumi program as using resource hooks.
5
+ *
6
+ * The backend uses `$hasResourceHooks` output to decide whether it should run the Pulumi program on destroy.
7
+ */
8
+ export function setResourceHooks(): void {
9
+ hasResourceHooks = true
10
+ }
11
+
12
+ export function getHasResourceHooks(): boolean {
13
+ return hasResourceHooks
14
+ }