alchemy 0.1.17 → 0.2.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.
Files changed (301) hide show
  1. package/lib/alchemy.d.ts +67 -0
  2. package/lib/alchemy.js +40 -0
  3. package/lib/apply.d.ts +3 -20
  4. package/lib/apply.js +86 -78
  5. package/lib/aws/account-id.d.ts +0 -1
  6. package/lib/aws/account-id.js +0 -1
  7. package/lib/aws/bucket.d.ts +4 -7
  8. package/lib/aws/bucket.js +8 -12
  9. package/lib/aws/function.d.ts +4 -7
  10. package/lib/aws/function.js +9 -30
  11. package/lib/aws/index.d.ts +1 -1
  12. package/lib/aws/index.js +1 -1
  13. package/lib/aws/oidc/github-oidc-provider.d.ts +2 -6
  14. package/lib/aws/oidc/github-oidc-provider.js +10 -15
  15. package/lib/aws/oidc/index.d.ts +0 -1
  16. package/lib/aws/oidc/index.js +0 -1
  17. package/lib/aws/oidc/oidc-provider.d.ts +4 -6
  18. package/lib/aws/oidc/oidc-provider.js +9 -11
  19. package/lib/aws/policy-attachment.d.ts +9 -0
  20. package/lib/aws/policy-attachment.js +20 -0
  21. package/lib/aws/policy.d.ts +4 -16
  22. package/lib/aws/policy.js +9 -40
  23. package/lib/aws/queue.d.ts +4 -34
  24. package/lib/aws/queue.js +10 -20
  25. package/lib/aws/role.d.ts +4 -7
  26. package/lib/aws/role.js +17 -26
  27. package/lib/aws/ses.d.ts +4 -10
  28. package/lib/aws/ses.js +118 -131
  29. package/lib/aws/table.d.ts +4 -7
  30. package/lib/aws/table.js +8 -11
  31. package/lib/cloudflare/api.d.ts +0 -1
  32. package/lib/cloudflare/api.js +0 -1
  33. package/lib/cloudflare/asset-manifest.d.ts +0 -1
  34. package/lib/cloudflare/asset-manifest.js +0 -1
  35. package/lib/cloudflare/auth.d.ts +0 -1
  36. package/lib/cloudflare/auth.js +0 -1
  37. package/lib/cloudflare/bindings.d.ts +4 -3
  38. package/lib/cloudflare/bindings.js +3 -2
  39. package/lib/cloudflare/bound.d.ts +7 -0
  40. package/lib/cloudflare/bound.js +1 -0
  41. package/lib/cloudflare/bucket.d.ts +7 -15
  42. package/lib/cloudflare/bucket.js +12 -19
  43. package/lib/cloudflare/durable-object-namespace.d.ts +0 -3
  44. package/lib/cloudflare/durable-object-namespace.js +0 -4
  45. package/lib/cloudflare/generate-asset-manifest.d.ts +0 -1
  46. package/lib/cloudflare/generate-asset-manifest.js +1 -2
  47. package/lib/cloudflare/index.d.ts +0 -1
  48. package/lib/cloudflare/index.js +0 -1
  49. package/lib/cloudflare/kv-namespace.d.ts +6 -9
  50. package/lib/cloudflare/kv-namespace.js +16 -20
  51. package/lib/cloudflare/state.d.ts +0 -1
  52. package/lib/cloudflare/state.js +0 -1
  53. package/lib/cloudflare/static-site-router.d.ts +0 -1
  54. package/lib/cloudflare/static-site-router.js +0 -4
  55. package/lib/cloudflare/static-site.d.ts +5 -11
  56. package/lib/cloudflare/static-site.js +16 -28
  57. package/lib/cloudflare/types.d.ts +0 -1
  58. package/lib/cloudflare/types.js +0 -1
  59. package/lib/cloudflare/upload-asset-manifest.d.ts +0 -1
  60. package/lib/cloudflare/upload-asset-manifest.js +0 -1
  61. package/lib/cloudflare/worker-metadata.d.ts +0 -1
  62. package/lib/cloudflare/worker-metadata.js +0 -1
  63. package/lib/cloudflare/worker-migration.d.ts +0 -1
  64. package/lib/cloudflare/worker-migration.js +0 -1
  65. package/lib/cloudflare/worker.d.ts +13 -13
  66. package/lib/cloudflare/worker.js +43 -43
  67. package/lib/context.d.ts +58 -0
  68. package/lib/context.js +36 -0
  69. package/lib/destroy.d.ts +10 -13
  70. package/lib/destroy.js +104 -43
  71. package/lib/{esbuild.d.ts → esbuild/bundle.d.ts} +4 -18
  72. package/lib/{esbuild.js → esbuild/bundle.js} +10 -24
  73. package/lib/esbuild/index.d.ts +1 -0
  74. package/lib/esbuild/index.js +1 -0
  75. package/lib/fs/file.d.ts +13 -0
  76. package/lib/fs/file.js +20 -0
  77. package/lib/fs/folder.d.ts +10 -0
  78. package/lib/fs/folder.js +16 -0
  79. package/lib/fs/index.d.ts +2 -0
  80. package/lib/fs/index.js +2 -0
  81. package/lib/github/client.d.ts +0 -1
  82. package/lib/github/client.js +0 -1
  83. package/lib/github/index.d.ts +0 -1
  84. package/lib/github/index.js +0 -1
  85. package/lib/github/secret.d.ts +6 -7
  86. package/lib/github/secret.js +30 -36
  87. package/lib/index.d.ts +7 -10
  88. package/lib/index.js +6 -9
  89. package/lib/resource.d.ts +34 -62
  90. package/lib/resource.js +29 -270
  91. package/lib/scope.d.ts +35 -14
  92. package/lib/scope.js +78 -23
  93. package/lib/secret.d.ts +7 -0
  94. package/lib/secret.js +17 -0
  95. package/lib/state.d.ts +14 -10
  96. package/lib/state.js +15 -13
  97. package/lib/stripe/index.d.ts +0 -1
  98. package/lib/stripe/index.js +0 -1
  99. package/lib/stripe/price.d.ts +4 -5
  100. package/lib/stripe/price.js +10 -20
  101. package/lib/stripe/product.d.ts +4 -5
  102. package/lib/stripe/product.js +10 -20
  103. package/lib/stripe/webhook.d.ts +4 -6
  104. package/lib/stripe/webhook.js +12 -25
  105. package/lib/test/bun.d.ts +19 -0
  106. package/lib/test/bun.js +55 -0
  107. package/lib/{utils → util}/content-type.d.ts +0 -1
  108. package/lib/{utils → util}/content-type.js +0 -1
  109. package/lib/util/encrypt.d.ts +16 -0
  110. package/lib/util/encrypt.js +44 -0
  111. package/lib/{error.d.ts → util/ignore.d.ts} +0 -1
  112. package/lib/{error.js → util/ignore.js} +0 -1
  113. package/lib/{utils → util}/retry.d.ts +0 -1
  114. package/lib/{utils → util}/retry.js +0 -1
  115. package/lib/util/serde.d.ts +3 -0
  116. package/lib/util/serde.js +46 -0
  117. package/lib/util/slugify.d.ts +1 -0
  118. package/lib/util/slugify.js +3 -0
  119. package/package.json +5 -7
  120. package/src/alchemy.ts +120 -0
  121. package/src/apply.ts +109 -111
  122. package/src/aws/bucket.ts +11 -14
  123. package/src/aws/function.ts +13 -33
  124. package/src/aws/index.ts +1 -0
  125. package/src/aws/oidc/github-oidc-provider.ts +14 -19
  126. package/src/aws/oidc/oidc-provider.ts +19 -12
  127. package/src/aws/policy-attachment.ts +51 -0
  128. package/src/aws/policy.ts +15 -60
  129. package/src/aws/queue.ts +17 -21
  130. package/src/aws/role.ts +25 -28
  131. package/src/aws/ses.ts +150 -164
  132. package/src/aws/table.ts +15 -15
  133. package/src/cloudflare/bindings.ts +12 -2
  134. package/src/cloudflare/bound.ts +18 -0
  135. package/src/cloudflare/bucket.ts +40 -48
  136. package/src/cloudflare/durable-object-namespace.ts +0 -10
  137. package/src/cloudflare/generate-asset-manifest.ts +1 -1
  138. package/src/cloudflare/kv-namespace.ts +35 -33
  139. package/src/cloudflare/static-site-router.ts +0 -3
  140. package/src/cloudflare/static-site.ts +43 -59
  141. package/src/cloudflare/worker.ts +76 -64
  142. package/src/context.ts +122 -0
  143. package/src/destroy.ts +124 -73
  144. package/src/{esbuild.ts → esbuild/bundle.ts} +16 -28
  145. package/src/esbuild/index.ts +1 -0
  146. package/src/fs/file.ts +39 -0
  147. package/src/fs/folder.ts +32 -0
  148. package/src/fs/index.ts +2 -0
  149. package/src/github/secret.ts +53 -49
  150. package/src/index.ts +8 -15
  151. package/src/resource.ts +101 -462
  152. package/src/scope.ts +101 -36
  153. package/src/secret.ts +18 -0
  154. package/src/state.ts +30 -20
  155. package/src/stripe/price.ts +17 -22
  156. package/src/stripe/product.ts +17 -22
  157. package/src/stripe/webhook.ts +21 -27
  158. package/src/test/bun.ts +105 -0
  159. package/src/util/encrypt.ts +79 -0
  160. package/src/util/serde.ts +53 -0
  161. package/src/util/slugify.ts +3 -0
  162. package/lib/$.d.ts +0 -12
  163. package/lib/$.d.ts.map +0 -1
  164. package/lib/$.js +0 -42
  165. package/lib/$.js.map +0 -1
  166. package/lib/alchemize.d.ts +0 -43
  167. package/lib/alchemize.d.ts.map +0 -1
  168. package/lib/alchemize.js +0 -112
  169. package/lib/alchemize.js.map +0 -1
  170. package/lib/apply.d.ts.map +0 -1
  171. package/lib/apply.js.map +0 -1
  172. package/lib/aws/account-id.d.ts.map +0 -1
  173. package/lib/aws/account-id.js.map +0 -1
  174. package/lib/aws/bucket.d.ts.map +0 -1
  175. package/lib/aws/bucket.js.map +0 -1
  176. package/lib/aws/function.d.ts.map +0 -1
  177. package/lib/aws/function.js.map +0 -1
  178. package/lib/aws/index.d.ts.map +0 -1
  179. package/lib/aws/index.js.map +0 -1
  180. package/lib/aws/oidc/github-oidc-provider.d.ts.map +0 -1
  181. package/lib/aws/oidc/github-oidc-provider.js.map +0 -1
  182. package/lib/aws/oidc/index.d.ts.map +0 -1
  183. package/lib/aws/oidc/index.js.map +0 -1
  184. package/lib/aws/oidc/oidc-provider.d.ts.map +0 -1
  185. package/lib/aws/oidc/oidc-provider.js.map +0 -1
  186. package/lib/aws/policy.d.ts.map +0 -1
  187. package/lib/aws/policy.js.map +0 -1
  188. package/lib/aws/queue.d.ts.map +0 -1
  189. package/lib/aws/queue.js.map +0 -1
  190. package/lib/aws/role.d.ts.map +0 -1
  191. package/lib/aws/role.js.map +0 -1
  192. package/lib/aws/ses.d.ts.map +0 -1
  193. package/lib/aws/ses.js.map +0 -1
  194. package/lib/aws/table.d.ts.map +0 -1
  195. package/lib/aws/table.js.map +0 -1
  196. package/lib/cloudflare/api.d.ts.map +0 -1
  197. package/lib/cloudflare/api.js.map +0 -1
  198. package/lib/cloudflare/asset-manifest.d.ts.map +0 -1
  199. package/lib/cloudflare/asset-manifest.js.map +0 -1
  200. package/lib/cloudflare/auth.d.ts.map +0 -1
  201. package/lib/cloudflare/auth.js.map +0 -1
  202. package/lib/cloudflare/bindings.d.ts.map +0 -1
  203. package/lib/cloudflare/bindings.js.map +0 -1
  204. package/lib/cloudflare/bucket.d.ts.map +0 -1
  205. package/lib/cloudflare/bucket.js.map +0 -1
  206. package/lib/cloudflare/durable-object-namespace.d.ts.map +0 -1
  207. package/lib/cloudflare/durable-object-namespace.js.map +0 -1
  208. package/lib/cloudflare/generate-asset-manifest.d.ts.map +0 -1
  209. package/lib/cloudflare/generate-asset-manifest.js.map +0 -1
  210. package/lib/cloudflare/index.d.ts.map +0 -1
  211. package/lib/cloudflare/index.js.map +0 -1
  212. package/lib/cloudflare/kv-namespace.d.ts.map +0 -1
  213. package/lib/cloudflare/kv-namespace.js.map +0 -1
  214. package/lib/cloudflare/state.d.ts.map +0 -1
  215. package/lib/cloudflare/state.js.map +0 -1
  216. package/lib/cloudflare/static-site-router.d.ts.map +0 -1
  217. package/lib/cloudflare/static-site-router.js.map +0 -1
  218. package/lib/cloudflare/static-site.d.ts.map +0 -1
  219. package/lib/cloudflare/static-site.js.map +0 -1
  220. package/lib/cloudflare/types.d.ts.map +0 -1
  221. package/lib/cloudflare/types.js.map +0 -1
  222. package/lib/cloudflare/upload-asset-manifest.d.ts.map +0 -1
  223. package/lib/cloudflare/upload-asset-manifest.js.map +0 -1
  224. package/lib/cloudflare/worker-metadata.d.ts.map +0 -1
  225. package/lib/cloudflare/worker-metadata.js.map +0 -1
  226. package/lib/cloudflare/worker-migration.d.ts.map +0 -1
  227. package/lib/cloudflare/worker-migration.js.map +0 -1
  228. package/lib/cloudflare/worker.d.ts.map +0 -1
  229. package/lib/cloudflare/worker.js.map +0 -1
  230. package/lib/destroy.d.ts.map +0 -1
  231. package/lib/destroy.js.map +0 -1
  232. package/lib/error.d.ts.map +0 -1
  233. package/lib/error.js.map +0 -1
  234. package/lib/esbuild.d.ts.map +0 -1
  235. package/lib/esbuild.js.map +0 -1
  236. package/lib/fs.d.ts +0 -12
  237. package/lib/fs.d.ts.map +0 -1
  238. package/lib/fs.js +0 -41
  239. package/lib/fs.js.map +0 -1
  240. package/lib/github/client.d.ts.map +0 -1
  241. package/lib/github/client.js.map +0 -1
  242. package/lib/github/index.d.ts.map +0 -1
  243. package/lib/github/index.js.map +0 -1
  244. package/lib/github/secret.d.ts.map +0 -1
  245. package/lib/github/secret.js.map +0 -1
  246. package/lib/global.d.ts +0 -16
  247. package/lib/global.d.ts.map +0 -1
  248. package/lib/global.js +0 -42
  249. package/lib/global.js.map +0 -1
  250. package/lib/index.d.ts.map +0 -1
  251. package/lib/index.js.map +0 -1
  252. package/lib/input.d.ts +0 -6
  253. package/lib/input.d.ts.map +0 -1
  254. package/lib/input.js +0 -2
  255. package/lib/input.js.map +0 -1
  256. package/lib/main.d.ts +0 -3
  257. package/lib/main.d.ts.map +0 -1
  258. package/lib/main.js +0 -32
  259. package/lib/main.js.map +0 -1
  260. package/lib/output.d.ts +0 -24
  261. package/lib/output.d.ts.map +0 -1
  262. package/lib/output.js +0 -23
  263. package/lib/output.js.map +0 -1
  264. package/lib/print.d.ts +0 -7
  265. package/lib/print.d.ts.map +0 -1
  266. package/lib/print.js +0 -10
  267. package/lib/print.js.map +0 -1
  268. package/lib/resource.d.ts.map +0 -1
  269. package/lib/resource.js.map +0 -1
  270. package/lib/scope.d.ts.map +0 -1
  271. package/lib/scope.js.map +0 -1
  272. package/lib/slug.d.ts +0 -2
  273. package/lib/slug.d.ts.map +0 -1
  274. package/lib/slug.js +0 -4
  275. package/lib/slug.js.map +0 -1
  276. package/lib/state.d.ts.map +0 -1
  277. package/lib/state.js.map +0 -1
  278. package/lib/stripe/index.d.ts.map +0 -1
  279. package/lib/stripe/index.js.map +0 -1
  280. package/lib/stripe/price.d.ts.map +0 -1
  281. package/lib/stripe/price.js.map +0 -1
  282. package/lib/stripe/product.d.ts.map +0 -1
  283. package/lib/stripe/product.js.map +0 -1
  284. package/lib/stripe/webhook.d.ts.map +0 -1
  285. package/lib/stripe/webhook.js.map +0 -1
  286. package/lib/utils/content-type.d.ts.map +0 -1
  287. package/lib/utils/content-type.js.map +0 -1
  288. package/lib/utils/retry.d.ts.map +0 -1
  289. package/lib/utils/retry.js.map +0 -1
  290. package/src/$.ts +0 -69
  291. package/src/alchemize.ts +0 -186
  292. package/src/fs.ts +0 -47
  293. package/src/global.ts +0 -63
  294. package/src/input.ts +0 -16
  295. package/src/main.ts +0 -34
  296. package/src/output.ts +0 -71
  297. package/src/print.ts +0 -18
  298. package/src/slug.ts +0 -3
  299. /package/src/{utils → util}/content-type.ts +0 -0
  300. /package/src/{error.ts → util/ignore.ts} +0 -0
  301. /package/src/{utils → util}/retry.ts +0 -0
package/src/alchemy.ts ADDED
@@ -0,0 +1,120 @@
1
+ import { destroy } from "./destroy";
2
+ import { Scope } from "./scope";
3
+ import { secret } from "./secret";
4
+ import type { StateStoreType } from "./state";
5
+
6
+ // TODO: support browser
7
+ const DEFAULT_STAGE = process.env.ALCHEMY_STAGE ?? process.env.USER ?? "dev";
8
+
9
+ function _alchemy(appName: string, options: Omit<AlchemyOptions, "appName">) {
10
+ return scope(undefined, {
11
+ ...options,
12
+ appName,
13
+ stage: options.stage,
14
+ });
15
+ }
16
+ _alchemy.destroy = destroy;
17
+ _alchemy.run = run;
18
+ _alchemy.scope = scope;
19
+ _alchemy.secret = secret;
20
+
21
+ // alchemy type is to semantically highlight `alchemy` as a type (keyword)
22
+ export type alchemy = Alchemy;
23
+
24
+ export const alchemy: Alchemy = _alchemy as any;
25
+
26
+ // Alchemy is for module augmentation
27
+ export interface Alchemy {
28
+ scope: typeof scope;
29
+ run: typeof run;
30
+ destroy: typeof destroy;
31
+ secret: typeof secret;
32
+ (...parameters: Parameters<typeof scope>): ReturnType<typeof scope>;
33
+ }
34
+
35
+ export interface AlchemyOptions {
36
+ /**
37
+ * The name of the application.
38
+ */
39
+ appName?: string;
40
+ /**
41
+ * Determines whether the resources will be created/updated or deleted.
42
+ *
43
+ * @default "up"
44
+ */
45
+ phase?: "up" | "destroy";
46
+ /**
47
+ * Name to scope the resource state under (e.g. `.alchemy/{stage}/..`).
48
+ *
49
+ * @default - your POSIX username
50
+ */
51
+ stage?: string;
52
+ /**
53
+ * If true, will not prune resources that were dropped from the root stack.
54
+ *
55
+ * @default true
56
+ */
57
+ destroyOrphans?: boolean;
58
+ /**
59
+ * A custom state store to use instead of the default file system store.
60
+ */
61
+ stateStore?: StateStoreType;
62
+ /**
63
+ * A custom scope to use as a parent.
64
+ */
65
+ parent?: Scope;
66
+ /**
67
+ * If true, will not print any Create/Update/Delete messages.
68
+ *
69
+ * @default false
70
+ */
71
+ quiet?: boolean;
72
+
73
+ /**
74
+ * A passphrase to use to encrypt/decrypt secrets.
75
+ */
76
+ password?: string;
77
+ }
78
+
79
+ /**
80
+ * Enter a new scope synchronously.
81
+ * @param options
82
+ * @returns
83
+ */
84
+ function scope(
85
+ id: string | undefined,
86
+ options?: AlchemyOptions,
87
+ // TODO: maybe we want to allow using _ = await alchemy.scope(import.meta)
88
+ // | [meta: ImportMeta]
89
+ ): Scope {
90
+ const scope = new Scope({
91
+ ...options,
92
+ appName: options?.appName,
93
+ stage: options?.stage ?? DEFAULT_STAGE,
94
+ scopeName: id,
95
+ parent: options?.parent ?? Scope.get(),
96
+ });
97
+ scope.enter();
98
+ return scope;
99
+ }
100
+
101
+ async function run<T>(
102
+ ...args:
103
+ | [id: string, fn: (this: Scope, scope: Scope) => Promise<T>]
104
+ | [
105
+ id: string,
106
+ options: AlchemyOptions,
107
+ fn: (this: Scope, scope: Scope) => Promise<T>,
108
+ ]
109
+ ): Promise<T> {
110
+ const [id, options, fn] =
111
+ typeof args[1] === "function"
112
+ ? [args[0], undefined, args[1]]
113
+ : (args as [
114
+ string,
115
+ AlchemyOptions | undefined,
116
+ (this: Scope, scope: Scope) => Promise<T>,
117
+ ]);
118
+ await using scope = alchemy.scope(id, options);
119
+ return await fn.bind(scope)(scope);
120
+ }
package/src/apply.ts CHANGED
@@ -1,132 +1,130 @@
1
- import { defaultStage, defaultStateStore } from "./global";
2
- import { type Output, OutputChain, type Resolved } from "./output";
1
+ import { alchemy } from "./alchemy";
2
+ import { context } from "./context";
3
3
  import {
4
- Input,
5
- Provider,
4
+ PROVIDERS,
5
+ type PendingResource,
6
+ type Provider,
6
7
  type Resource,
7
- ResourceID,
8
- isResource,
8
+ type ResourceProps,
9
9
  } from "./resource";
10
- import { type Scope as IScope, getScope } from "./scope";
11
- import type { StateStore } from "./state";
10
+ import type { State } from "./state";
11
+ import { serialize } from "./util/serde";
12
12
 
13
13
  export interface ApplyOptions {
14
- stage: string;
15
- scope: IScope;
16
- stateStore: StateStore;
17
14
  quiet?: boolean;
15
+ alwaysUpdate?: boolean;
18
16
  }
19
17
 
20
- /**
21
- * Apply a sub-graph to produce a resource.
22
- * @param output A sub-graph that produces a resource.
23
- * @returns The resource properties.
24
- */
25
- export async function apply<T>(
26
- output: T,
27
- options?: Partial<ApplyOptions>,
28
- ): Promise<Resolved<T>> {
29
- const stage = options?.stage ?? defaultStage;
30
- const scope = options?.scope ?? getScope();
31
- const statePath = scope.getScopePath(stage);
32
- const stateStore = options?.stateStore ?? new defaultStateStore(statePath);
18
+ export async function apply<Out extends Resource>(
19
+ resource: PendingResource<Out>,
20
+ props: ResourceProps,
21
+ options?: ApplyOptions,
22
+ ): Promise<Awaited<Out>> {
23
+ const scope = resource.Scope;
24
+ const quiet = props.quiet ?? scope.quiet;
25
+ await scope.init();
26
+ let state: State | undefined = (await scope.state.get(resource.ID))!;
27
+ const provider: Provider = PROVIDERS.get(resource.Kind);
28
+ if (provider === undefined) {
29
+ throw new Error(`Provider "${resource.Kind}" not found`);
30
+ }
31
+ if (state === undefined) {
32
+ state = {
33
+ kind: resource.Kind,
34
+ id: resource.ID,
35
+ fqn: resource.FQN,
36
+ seq: resource.Seq,
37
+ status: "creating",
38
+ data: {},
39
+ output: {
40
+ ID: resource.ID,
41
+ FQN: resource.FQN,
42
+ Kind: resource.Kind,
43
+ Scope: scope,
44
+ Seq: resource.Seq,
45
+ },
46
+ // deps: [...deps],
47
+ props,
48
+ };
49
+ await scope.state.set(resource.ID, state);
50
+ }
33
51
 
34
- return (
35
- await evaluate(output, { stage, scope, stateStore, quiet: options?.quiet })
36
- ).value as Resolved<T>;
37
- }
52
+ const alwaysUpdate =
53
+ options?.alwaysUpdate ?? provider.options?.alwaysUpdate ?? false;
38
54
 
39
- class Evaluated<T> {
40
- constructor(
41
- public readonly value: T,
42
- public readonly deps: string[] = [],
43
- ) {}
44
- }
55
+ // Skip update if inputs haven't changed and resource is in a stable state
56
+ if (state.status === "created" || state.status === "updated") {
57
+ if (
58
+ JSON.stringify(state.props) === JSON.stringify(serialize(scope, props)) &&
59
+ alwaysUpdate !== true
60
+ ) {
61
+ if (!quiet) {
62
+ console.log(`Skip: "${resource.FQN}" (no changes)`);
63
+ }
64
+ // if (resourceState.output !== undefined) {
65
+ // resource[Provide](resourceState.output);
66
+ // }
67
+ return state.output as Awaited<Out>;
68
+ }
69
+ }
45
70
 
46
- const cache = new WeakMap<Resource, Promise<Evaluated<any>>>();
71
+ const phase = state.status === "creating" ? "create" : "update";
72
+ state.status = phase === "create" ? "creating" : "updating";
73
+ state.oldProps = state.props;
74
+ state.props = props;
47
75
 
48
- export async function evaluate<T>(
49
- output: T | Output<T>,
50
- options: ApplyOptions,
51
- ): Promise<Evaluated<T>> {
52
- const stage = options.stage;
53
- const stateStore = options.stateStore;
54
- if (isResource(output)) {
55
- const resource = output;
56
- const resourceID = resource[ResourceID];
57
- const evaluated = await Promise.all(
58
- resource[Input].map((r) => evaluate<any>(r, options)),
76
+ if (!quiet) {
77
+ console.log(
78
+ `${phase === "create" ? "Create" : "Update"}: "${resource.FQN}"`,
59
79
  );
80
+ }
60
81
 
61
- if (cache.has(resource)) {
62
- return await cache.get(resource)!;
63
- }
64
- let resolve: (value: Evaluated<any>) => void;
65
- let reject: (reason?: any) => void;
66
- const promise = new Promise<Evaluated<any>>((res, rej) => {
67
- resolve = res;
68
- reject = rej;
69
- });
70
- // set this eagerly to avoid double-apply (caused by recursive applies to inputs)
71
- cache.set(resource, promise);
82
+ await scope.state.set(resource.ID, state);
72
83
 
73
- const deps = new Set(evaluated.flatMap((input) => input.deps));
74
- const inputs = evaluated.map((input) => input.value);
75
- try {
76
- const result: T = await resource[Provider].update(
77
- stage,
78
- resource,
79
- deps,
80
- inputs as [],
81
- stateStore,
82
- options,
83
- );
84
- resolve!(new Evaluated(result, [resourceID, ...deps]));
85
- } catch (error) {
86
- console.error(error);
87
- reject!(error);
88
- }
89
- return promise;
90
- } else if (output instanceof OutputChain) {
91
- const inside = output as unknown as {
92
- parent: Output<any>;
93
- fn: (value: any) => T;
94
- };
95
- const parent = await evaluate(inside.parent, options);
96
- const ret = inside.fn(parent.value);
97
- // the ret may be an Output (e.g. in the flatMap case), so we need to evaluate it and include its deps
98
- const evaluated = await evaluate(ret, options);
99
- return new Evaluated<T>(evaluated.value, [
100
- ...parent.deps,
101
- ...evaluated.deps,
102
- ]);
103
- } else if (Array.isArray(output)) {
104
- const evaluatedItems = await Promise.all(
105
- output.map((item) => evaluate(item, options)),
106
- );
107
- return new Evaluated(
108
- evaluatedItems.map((e) => e.value) as unknown as T,
109
- evaluatedItems.flatMap((e) => e.deps),
110
- );
111
- } else if (output instanceof Date) {
112
- return new Evaluated(output as T);
113
- } else if (output && typeof output === "object") {
114
- const entries = Object.entries(output);
115
- const evaluatedEntries = await Promise.all(
116
- entries.map(
117
- async ([key, value]) => [key, await evaluate(value, options)] as const,
118
- ),
119
- );
84
+ let isReplaced = false;
120
85
 
121
- const result = Object.fromEntries(
122
- evaluatedEntries.map(([key, evaluated]) => [key, evaluated.value]),
123
- ) as unknown as T;
86
+ const ctx = context({
87
+ scope,
88
+ phase,
89
+ kind: resource.Kind,
90
+ id: resource.ID,
91
+ fqn: resource.FQN,
92
+ seq: resource.Seq,
93
+ state,
94
+ replace: () => {
95
+ if (isReplaced) {
96
+ console.warn(
97
+ `Resource ${resource.Kind} ${resource.FQN} is already marked as REPLACE`,
98
+ );
99
+ return;
100
+ }
101
+ isReplaced = true;
102
+ },
103
+ });
124
104
 
125
- const deps = evaluatedEntries.flatMap(([_, evaluated]) => evaluated.deps);
105
+ const output = await alchemy.run(resource.ID, async () =>
106
+ provider.handler.bind(ctx)(resource.ID, props),
107
+ );
126
108
 
127
- return new Evaluated(result, deps);
109
+ if (!quiet) {
110
+ console.log(
111
+ `${phase === "create" ? "Created" : "Updated"}: "${resource.FQN}"`,
112
+ );
128
113
  }
129
114
 
130
- // Base case: primitive value
131
- return new Evaluated<T>(output as T);
115
+ await scope.state.set(resource.ID, {
116
+ kind: resource.Kind,
117
+ id: resource.ID,
118
+ fqn: resource.FQN,
119
+ seq: resource.Seq,
120
+ data: state.data,
121
+ status: phase === "create" ? "created" : "updated",
122
+ output,
123
+ props,
124
+ // deps: [...deps],
125
+ });
126
+ // if (output !== undefined) {
127
+ // resource[Provide](output as Out);
128
+ // }
129
+ return output as any;
132
130
  }
package/src/aws/bucket.ts CHANGED
@@ -10,16 +10,16 @@ import {
10
10
  PutBucketTaggingCommand,
11
11
  S3Client,
12
12
  } from "@aws-sdk/client-s3";
13
- import { ignore } from "../error";
13
+ import type { Context } from "../context";
14
14
  import { Resource } from "../resource";
15
+ import { ignore } from "../util/ignore";
15
16
 
16
17
  export interface BucketProps {
17
18
  bucketName: string;
18
19
  tags?: Record<string, string>;
19
20
  }
20
21
 
21
- export interface BucketOutput extends BucketProps {
22
- id: string; // Same as bucketName for AWS
22
+ export interface Bucket extends Resource<"s3::Bucket">, BucketProps {
23
23
  arn: string;
24
24
  bucketDomainName: string; // Format: bucket-name.s3.amazonaws.com
25
25
  bucketRegionalDomainName?: string; // Format: bucket-name.s3.region.amazonaws.com
@@ -31,12 +31,12 @@ export interface BucketOutput extends BucketProps {
31
31
  acl?: string;
32
32
  }
33
33
 
34
- export class Bucket extends Resource(
34
+ export const Bucket = Resource(
35
35
  "s3::Bucket",
36
- async (ctx, props: BucketProps) => {
36
+ async function (this: Context<Bucket>, id: string, props: BucketProps) {
37
37
  const client = new S3Client({});
38
38
 
39
- if (ctx.event === "delete") {
39
+ if (this.phase === "delete") {
40
40
  await ignore(NoSuchBucket.name, () =>
41
41
  client.send(
42
42
  new DeleteBucketCommand({
@@ -44,7 +44,7 @@ export class Bucket extends Resource(
44
44
  }),
45
45
  ),
46
46
  );
47
- return props;
47
+ return this.destroy();
48
48
  } else {
49
49
  try {
50
50
  // Check if bucket exists
@@ -55,7 +55,7 @@ export class Bucket extends Resource(
55
55
  );
56
56
 
57
57
  // Update tags if they changed and bucket exists
58
- if (ctx.event === "update" && props.tags) {
58
+ if (this.phase === "update" && props.tags) {
59
59
  await client.send(
60
60
  new PutBucketTaggingCommand({
61
61
  Bucket: props.bucketName,
@@ -121,9 +121,8 @@ export class Bucket extends Resource(
121
121
  }
122
122
  }
123
123
 
124
- const output: BucketOutput = {
124
+ return this({
125
125
  bucketName: props.bucketName,
126
- id: props.bucketName,
127
126
  arn: `arn:aws:s3:::${props.bucketName}`,
128
127
  bucketDomainName: `${props.bucketName}.s3.amazonaws.com`,
129
128
  bucketRegionalDomainName: `${props.bucketName}.s3.${region}.amazonaws.com`,
@@ -132,12 +131,10 @@ export class Bucket extends Resource(
132
131
  versioningEnabled: versioningResponse.Status === "Enabled",
133
132
  acl: aclResponse.Grants?.[0]?.Permission?.toLowerCase(),
134
133
  ...(tags && { tags }),
135
- };
136
-
137
- return output;
134
+ });
138
135
  }
139
136
  },
140
- ) {}
137
+ );
141
138
 
142
139
  // Helper function to get S3 hosted zone IDs by region
143
140
  function getHostedZoneId(region: string): string {
@@ -12,8 +12,9 @@ import {
12
12
  } from "@aws-sdk/client-lambda";
13
13
  import fs from "node:fs";
14
14
  import path from "node:path";
15
- import { ignore } from "../error";
16
- import { type Context, Resource } from "../resource";
15
+ import type { Context } from "../context";
16
+ import { Resource } from "../resource";
17
+ import { ignore } from "../util/ignore";
17
18
 
18
19
  async function resolveRegion(client: LambdaClient): Promise<string> {
19
20
  const region = client.config.region;
@@ -47,8 +48,7 @@ export interface FunctionProps {
47
48
  };
48
49
  }
49
50
 
50
- export interface FunctionOutput extends FunctionProps {
51
- id: string; // Same as functionName
51
+ export interface Function extends Resource<"lambda::Function">, FunctionProps {
52
52
  arn: string;
53
53
  lastModified: string;
54
54
  version: string;
@@ -71,15 +71,15 @@ export interface FunctionOutput extends FunctionProps {
71
71
  signingJobArn?: string;
72
72
  }
73
73
 
74
- export class Function extends Resource(
74
+ export const Function = Resource(
75
75
  "lambda::Function",
76
- async (ctx: Context<FunctionOutput>, props: FunctionProps) => {
76
+ async function (this: Context<Function>, id: string, props: FunctionProps) {
77
77
  const client = new LambdaClient({});
78
78
  const region = await resolveRegion(client);
79
79
 
80
80
  const code = await zipCode(props.zipPath);
81
81
 
82
- if (ctx.event === "delete") {
82
+ if (this.phase === "delete") {
83
83
  await ignore(ResourceNotFoundException.name, () =>
84
84
  client.send(
85
85
  new DeleteFunctionCommand({
@@ -88,34 +88,17 @@ export class Function extends Resource(
88
88
  ),
89
89
  );
90
90
 
91
- // Return a minimal FunctionOutput for deleted state
92
- return {
93
- ...props,
94
- id: props.functionName,
95
- arn: `arn:aws:lambda:${region}:${process.env.AWS_ACCOUNT_ID}:function:${props.functionName}`,
96
- lastModified: new Date().toISOString(),
97
- version: "$LATEST",
98
- qualifiedArn: `arn:aws:lambda:${region}:${process.env.AWS_ACCOUNT_ID}:function:${props.functionName}:$LATEST`,
99
- invokeArn: `arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${region}:${process.env.AWS_ACCOUNT_ID}:function:${props.functionName}/invocations`,
100
- sourceCodeHash: "",
101
- sourceCodeSize: 0,
102
- architectures: props.architecture
103
- ? [props.architecture]
104
- : [Architecture.x86_64],
105
- revisionId: "",
106
- state: "Inactive",
107
- packageType: "Zip",
108
- };
91
+ return this.destroy();
109
92
  } else {
110
93
  try {
111
94
  // Check if function exists
112
- const func = await client.send(
95
+ await client.send(
113
96
  new GetFunctionCommand({
114
97
  FunctionName: props.functionName,
115
98
  }),
116
99
  );
117
100
 
118
- if (ctx.event === "update") {
101
+ if (this.phase === "update") {
119
102
  // Wait for function to stabilize
120
103
  await waitForFunctionStabilization(client, props.functionName);
121
104
 
@@ -228,9 +211,8 @@ export class Function extends Resource(
228
211
  ),
229
212
  ]);
230
213
 
231
- const output: FunctionOutput = {
214
+ return this({
232
215
  ...props,
233
- id: props.functionName,
234
216
  arn: config.FunctionArn!,
235
217
  lastModified: config.LastModified!,
236
218
  version: config.Version!,
@@ -251,12 +233,10 @@ export class Function extends Resource(
251
233
  packageType: config.PackageType!,
252
234
  signingProfileVersionArn: config.SigningProfileVersionArn,
253
235
  signingJobArn: config.SigningJobArn,
254
- };
255
-
256
- return output;
236
+ });
257
237
  }
258
238
  },
259
- ) {}
239
+ );
260
240
 
261
241
  // Helper to wait for function to stabilize
262
242
  async function waitForFunctionStabilization(
package/src/aws/index.ts CHANGED
@@ -2,6 +2,7 @@ export * from "./account-id";
2
2
  export * from "./bucket";
3
3
  export * from "./function";
4
4
  export * from "./policy";
5
+ export * from "./policy-attachment";
5
6
  export * from "./queue";
6
7
  export * from "./role";
7
8
  export * from "./ses";
@@ -1,5 +1,3 @@
1
- import type { Input } from "../../input";
2
- import { isOutput } from "../../output";
3
1
  import { OIDCProvider, type OIDCProviderProps } from "./oidc-provider";
4
2
 
5
3
  /**
@@ -13,22 +11,19 @@ export interface GitHubOIDCProviderProps
13
11
  extends Omit<OIDCProviderProps, "thumbprint"> {
14
12
  owner: string;
15
13
  repository: string;
16
- thumbprint?: string;
17
14
  }
15
+ export type GitHubOIDCProvider = ReturnType<typeof GitHubOIDCProvider>;
18
16
 
19
- export class GitHubOIDCProvider extends OIDCProvider {
20
- constructor(id: string, props: Input<GitHubOIDCProviderProps>) {
21
- super(
22
- id,
23
- isOutput<GitHubOIDCProviderProps>(props)
24
- ? props.apply((props) => ({
25
- ...props,
26
- thumbprint: props.thumbprint ?? DEFAULT_GITHUB_THUMBPRINT,
27
- }))
28
- : ({
29
- ...props,
30
- thumbprint: props.thumbprint ?? DEFAULT_GITHUB_THUMBPRINT,
31
- } as Input<OIDCProviderProps>),
32
- );
33
- }
34
- }
17
+ export const GitHubOIDCProvider = async (
18
+ id: string,
19
+ props: GitHubOIDCProviderProps,
20
+ ) => {
21
+ return OIDCProvider(id, {
22
+ owner: props.owner,
23
+ repository: props.repository,
24
+ roleArn: props.roleArn,
25
+ branches: props.branches,
26
+ environments: props.environments,
27
+ thumbprint: DEFAULT_GITHUB_THUMBPRINT,
28
+ });
29
+ };
@@ -7,7 +7,8 @@ import {
7
7
  type Tag,
8
8
  UpdateAssumeRolePolicyCommand,
9
9
  } from "@aws-sdk/client-iam";
10
- import { type Context, Resource } from "../../resource";
10
+ import type { Context } from "../../context";
11
+ import { Resource } from "../../resource";
11
12
  import { getAccountId } from "../account-id";
12
13
 
13
14
  /**
@@ -60,7 +61,9 @@ export interface OIDCProviderProps {
60
61
  /**
61
62
  * Output returned after OIDC provider configuration
62
63
  */
63
- export interface OIDCProviderOutput extends OIDCProviderProps {
64
+ export interface OIDCProvider
65
+ extends Resource<"aws::OIDCProvider">,
66
+ OIDCProviderProps {
64
67
  /**
65
68
  * The ARN of the OIDC provider
66
69
  */
@@ -81,16 +84,20 @@ const TRUST_POLICY_SID = "GitHubOIDCTrust";
81
84
  /**
82
85
  * Resource for configuring AWS OIDC provider for GitHub Actions
83
86
  */
84
- export class OIDCProvider extends Resource(
87
+ export const OIDCProvider = Resource(
85
88
  "aws::OIDCProvider",
86
- async (ctx: Context<OIDCProviderOutput>, props: OIDCProviderProps) => {
89
+ async function (
90
+ this: Context<OIDCProvider>,
91
+ id: string,
92
+ props: OIDCProviderProps,
93
+ ) {
87
94
  // Initialize AWS SDK client
88
95
  const client = new IAMClient({
89
96
  region: props.region,
90
97
  });
91
98
 
92
- if (ctx.event === "delete") {
93
- if (ctx.output?.providerArn) {
99
+ if (this.phase === "delete") {
100
+ if (this.output?.providerArn) {
94
101
  try {
95
102
  // First, remove our trust policy statement from the role
96
103
  const getRole = await client.send(
@@ -120,7 +127,7 @@ export class OIDCProvider extends Resource(
120
127
  // Then delete the OIDC provider if we're the last user
121
128
  const provider = await client.send(
122
129
  new GetOpenIDConnectProviderCommand({
123
- OpenIDConnectProviderArn: ctx.output.providerArn,
130
+ OpenIDConnectProviderArn: this.output.providerArn,
124
131
  }),
125
132
  );
126
133
 
@@ -128,7 +135,7 @@ export class OIDCProvider extends Resource(
128
135
  if (provider && (!provider.Tags || provider.Tags.length === 0)) {
129
136
  await client.send(
130
137
  new DeleteOpenIDConnectProviderCommand({
131
- OpenIDConnectProviderArn: ctx.output.providerArn,
138
+ OpenIDConnectProviderArn: this.output.providerArn,
132
139
  }),
133
140
  );
134
141
  }
@@ -137,7 +144,7 @@ export class OIDCProvider extends Resource(
137
144
  console.error("Error during cleanup:", error);
138
145
  }
139
146
  }
140
- return;
147
+ return this.destroy();
141
148
  }
142
149
 
143
150
  try {
@@ -240,14 +247,14 @@ export class OIDCProvider extends Resource(
240
247
  }),
241
248
  );
242
249
 
243
- return {
250
+ return this({
244
251
  ...props,
245
252
  providerArn,
246
253
  createdAt: Date.now(),
247
- };
254
+ });
248
255
  } catch (error) {
249
256
  console.error("Error configuring OIDC provider:", error);
250
257
  throw error;
251
258
  }
252
259
  },
253
- ) {}
260
+ );