alchemy 0.0.8 → 0.1.2

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 (105) hide show
  1. package/README.md +355 -0
  2. package/package.json +24 -20
  3. package/src/alchemize.ts +151 -0
  4. package/src/apply.ts +117 -0
  5. package/src/components/agent/agent.ts +45 -0
  6. package/src/components/agent/anthropic.ts +27 -0
  7. package/src/components/agent/index.ts +4 -0
  8. package/src/components/agent/model.ts +22 -0
  9. package/src/components/agent/openai.ts +44 -0
  10. package/src/components/agent/package.ts +126 -0
  11. package/src/components/agent/requirements.ts +111 -0
  12. package/src/components/agent/scrape.ts +47 -0
  13. package/src/components/agent/tsconfig.ts +150 -0
  14. package/src/components/agent/typescript.ts +177 -0
  15. package/src/components/aws/bucket.ts +168 -0
  16. package/src/components/aws/function.ts +302 -0
  17. package/src/components/aws/index.ts +6 -0
  18. package/src/components/aws/policy.ts +245 -0
  19. package/src/components/aws/queue.ts +222 -0
  20. package/src/components/aws/role.ts +245 -0
  21. package/src/components/aws/table.ts +172 -0
  22. package/src/components/esbuild.ts +128 -0
  23. package/src/components/fs.ts +45 -0
  24. package/src/destroy.ts +63 -0
  25. package/src/error.ts +16 -0
  26. package/src/global.ts +72 -0
  27. package/src/index.ts +6 -0
  28. package/src/input.ts +16 -0
  29. package/src/main.ts +34 -0
  30. package/src/output.ts +49 -0
  31. package/src/resource.ts +313 -0
  32. package/src/state.ts +100 -0
  33. package/test/aws/function.test.ts +144 -0
  34. package/test/aws/queue.test.ts +85 -0
  35. package/test/aws/role.test.ts +153 -0
  36. package/test/aws/table.test.ts +73 -0
  37. package/test/esbuild.test.ts +41 -0
  38. package/test/handler.ts +9 -0
  39. package/tsconfig.json +7 -0
  40. package/Column.js +0 -1
  41. package/Create.js +0 -1
  42. package/Delete.js +0 -1
  43. package/Drop.js +0 -1
  44. package/Insert.js +0 -1
  45. package/Makefile +0 -8
  46. package/Select.js +0 -1
  47. package/Table.js +0 -1
  48. package/Transaction.js +0 -1
  49. package/Types.js +0 -1
  50. package/Update.js +0 -1
  51. package/alchemy.js +0 -57
  52. package/alchemy.sublime-project +0 -9
  53. package/alchemy.sublime-workspace +0 -2304
  54. package/config/app.js +0 -8
  55. package/config/db.js +0 -20
  56. package/copy.js +0 -1
  57. package/defaultEngine.js +0 -8
  58. package/engine.js +0 -282
  59. package/getInstantiatorFunction.js +0 -28
  60. package/index.js +0 -52
  61. package/jstest.js +0 -3
  62. package/loggerFactory.js +0 -27
  63. package/logs/alchemy.log +0 -46
  64. package/logs/sql.log +0 -41
  65. package/sql/Column.js +0 -158
  66. package/sql/Columnable.js +0 -21
  67. package/sql/Create.js +0 -15
  68. package/sql/Delete.js +0 -34
  69. package/sql/Drop.js +0 -36
  70. package/sql/Insert.js +0 -96
  71. package/sql/SchemaElement.js +0 -11
  72. package/sql/Select.js +0 -192
  73. package/sql/SqlBase.js +0 -134
  74. package/sql/SqlStatement.js +0 -80
  75. package/sql/Table.js +0 -96
  76. package/sql/Transaction.js +0 -45
  77. package/sql/Update.js +0 -105
  78. package/sql/defineElement.js +0 -26
  79. package/sql/defineStatement.js +0 -26
  80. package/sql/types/BaseType.js +0 -43
  81. package/sql/types/BigSerialType.js +0 -5
  82. package/sql/types/IntegerType.js +0 -5
  83. package/sql/types/JsonType.js +0 -5
  84. package/sql/types/SerialType.js +0 -5
  85. package/sql/types/StringType.js +0 -31
  86. package/sql/types/TimestampType.js +0 -5
  87. package/sql/types/UUIDType.js +0 -5
  88. package/sql/types/defineType.js +0 -25
  89. package/sql/types/getType.js +0 -12
  90. package/sql/types/index.js +0 -11
  91. package/sql/where.js +0 -31
  92. package/sql/whereToString.js +0 -29
  93. package/test/ColumnTests.js +0 -80
  94. package/test/CreateTable.js +0 -54
  95. package/test/DeleteTests.js +0 -28
  96. package/test/DropTests.js +0 -23
  97. package/test/GetTypeTests.js +0 -56
  98. package/test/InsertTests.js +0 -86
  99. package/test/SelectExistsTests.js +0 -26
  100. package/test/SelectTests.js +0 -130
  101. package/test/TableTests.js +0 -46
  102. package/test.js +0 -8
  103. package/test.ls +0 -4
  104. package/validate.js +0 -26
  105. package/x.js +0 -5
package/README.md CHANGED
@@ -0,0 +1,355 @@
1
+ # Alchemy
2
+
3
+ Alchemy is a JS-native, embeddable Infrastructure as Code (IaC) library designed to run in any JavaScript runtime, including the browser.
4
+
5
+ Alchemy fully embraces AI code generation at its core, even going so far as to encourage you to fork this repo and include it in your project as a Git submodule (instead of installing as a dependency) so that you can modify the core resources to fit your needs. Contribute 'em back if you want, or not - that's fine too!
6
+
7
+ AI is so damn good at CRUD, so good that we no longer need to shackle ourselves to heavy toolchains like Pulumi and Terraform. All of the pre-built components in [src/components](./alchemy/src/components) were generated in less than a few minutes. Use them if you want, or create your own. It's that easy.
8
+
9
+ [![Demo](./alchemy.gif)](./alchemy.gif)
10
+
11
+ # Features
12
+
13
+ - **JS-native** - no second language, toolchains, dependencies, processes, services, etc. to lug around.
14
+ - **ESM-native** - built exclusively on ESM, with a slight preference for modern JS runtimes like Bun.
15
+ - **Embeddable** - runs in any JavaScript/TypeScript environment, including the browser!
16
+ - **Extensible** - implement your own resources with a simple function.
17
+ - **AI-first** - alchemy actively encourages you to use LLMs to create/copy/fork/modify resources to fit your needs. No more waiting around for a provider to be implemented, just do it yourself in a few minutes.
18
+ - **No dependencies** - the `alchemy` core package has 0 required dependencies.
19
+ - **No service** - state files are stored locally in your project and can be easily inspected, modified, checked into your repo, etc.
20
+ - **No strong opinions** - structure your codebase however you want, store state anywhere - we don't care!
21
+
22
+ # Getting Started
23
+
24
+ > [!NOTE]
25
+ > You can see a comprehensive example involving a DynamoDB Table, IAM Role, Bundling and AWS Lambda [here](./examples/app.ts).
26
+
27
+ An alchemy "app" (if you want to call it that) is just an ordinary TypeScript or JavaScript script. Once you've installed the `alchemy` package, you can start using it however you want.
28
+
29
+ ```bash
30
+ # I recommend bun, but you can use any JavaScript runtime.
31
+ bun add alchemy
32
+ ```
33
+
34
+ Usually, you'll want to create a script where you'll define your resources.
35
+
36
+ ```ts
37
+ import { Role } from "alchemy/aws";
38
+
39
+ const role = new Role("my-role", {
40
+ name: "my-role",
41
+ //..
42
+ });
43
+ ```
44
+
45
+ Then, call `alchemize` at the end to trigger the Create/Update/Delete lifecycle.
46
+
47
+ ```ts
48
+ await alchemize();
49
+ ```
50
+
51
+ Finally, run your script.
52
+
53
+ ```sh
54
+ bun ./my-app.ts
55
+ ```
56
+
57
+ You'll notice some files show up in your repo:
58
+
59
+ ```
60
+ .alchemy/
61
+ - sam/
62
+ - my-role.json
63
+ ```
64
+
65
+ Go ahead, click on one and take a look. Here's how my role looks:
66
+
67
+ ```jsonc
68
+ {
69
+ "provider": "iam::Role",
70
+ "data": {},
71
+ "deps": [],
72
+ "status": "updated",
73
+ "output": {
74
+ "roleName": "alchemy-api-lambda-role"
75
+ // ..
76
+ },
77
+ "inputs": [
78
+ {
79
+ "roleName": "alchemy-api-lambda-role",
80
+ "assumeRolePolicy": {
81
+ "Version": "2012-10-17"
82
+ // ..
83
+ }
84
+ }
85
+ ]
86
+ }
87
+ ```
88
+
89
+ > [!TIP]
90
+ > Alchemy goes to great effort to be fully transparent. State is just a JSON file, nothing more. You can inspect it, modify it, commit it to your repo, etc.
91
+
92
+ ## `alchemize`
93
+
94
+ Calling `alchemize` will create new resources, update modified resources and then delete "orphaned" resources at the end.
95
+
96
+ This is equivalent to `terraform apply`, `pulumi up`, `cdk deploy`, etc. except really fast and in your control.
97
+
98
+ ## Creating a Resource Provider
99
+
100
+ Adding new resources is the whole point of Alchemy, and is therefore very simple.
101
+
102
+ A Resource provider is just a function with a globally unique name, e.g. `dynamo::Table`, and an implementation of the Create, Update, Delete lifecycle operations.
103
+
104
+ E.g. below we show what a simple `dynamo::Table` provider might look like.
105
+
106
+ > [!NOTE]
107
+ > See [table.ts](./alchemy/src/components/aws/table.ts) for the full implementation.
108
+
109
+ ```ts
110
+ interface TableInputs {
111
+ name: string;
112
+ //..
113
+ }
114
+ interface TableOutput {
115
+ tableArn: string;
116
+ }
117
+ class Table extends Resource(
118
+ "dynamo::Table",
119
+ async (ctx: Context<TableOutput>, inputs: TableInputs) => {
120
+ if (ctx.event === "create") {
121
+ // create logic
122
+ } else if (ctx.event === "update") {
123
+ // update logic
124
+ } else if (ctx.event === "delete") {
125
+ // delete logic
126
+ }
127
+ // ..
128
+ return output;
129
+ }
130
+ ) {}
131
+ ```
132
+
133
+ > [!TIP]
134
+ > Use Cursor or an LLM like Claude/OpenAI to generate the implementation of your resource. I think you'll be pleasantly surprised at how well it works, especially if you provide the API reference docs in your context.
135
+
136
+ That's it! Now you can instantiate tables in your app.
137
+
138
+ ```ts
139
+ const table = new Table("items", {
140
+ name: "items",
141
+ //..
142
+ });
143
+
144
+ table.tableArn; // Output<string>
145
+ ```
146
+
147
+ You may have noticed the odd pattern of extending the result of a function call, `extends Resource(..)`. This is called the "mix-in" pattern and is optional. You are free to just use a `const` instead:
148
+
149
+ ```ts
150
+ const Table = Resource("dynamo::Table", async (ctx, inputs) => {
151
+ //..
152
+ });
153
+
154
+ const table = new Table("items", {
155
+ name: "items",
156
+ //..
157
+ });
158
+ ```
159
+
160
+ > [!TIP]
161
+ > I recommend using a class so each resource has a type, e.g. `Table`, and a place to add helper/utility methods. Totally optional, though. Knock yourself out.
162
+
163
+ ## Lazy Outputs
164
+
165
+ A Resource is evaluated lazily (not when you call `new`), so you can't immediately access the value of a property like `tableArn` or `functionArn`. Instead you reference properties using the `Output<T>` interface.
166
+
167
+ ```ts
168
+ const table = new Table("my-table", { .. });
169
+
170
+ const tableArn = table.tableArn; // Output<string>
171
+ ```
172
+
173
+ Outputs can be chained explicitly and implicitly:
174
+
175
+ 1. _Explicitly_ with `.apply`
176
+
177
+ ```ts
178
+ const tableArn: Output<string> = table.tableArn.apply((arn: string) =>
179
+ // do something with the arn string value
180
+ arn.replace("table", "arn:aws:dynamodb:")
181
+ );
182
+ ```
183
+
184
+ 2. _Implicitly_ chained
185
+
186
+ ```ts
187
+ const tableArn = table.attributeDefinitions[0].attributeName; // Output<string>
188
+ // equivalent to:
189
+ // table.apply(t => t.attributeDefinitions[0].attributeName);
190
+ ```
191
+
192
+ > [!NOTE]
193
+ > Output is inspired by Pulumi, with a little extra added sugar.
194
+
195
+ ## `apply` and `destroy`
196
+
197
+ Calling `alchemize` is optional. Any object in your graph (`Resource` or `Output<T>`) can be "applied" or "destroyed" individually and programmatically.
198
+
199
+ Say, you've got some two resources, a `Role` and a `Function`.
200
+
201
+ ```ts
202
+ const role = new Role("my-role", {
203
+ name: "my-role",
204
+ //..
205
+ });
206
+
207
+ const func = new Function("my-function", {
208
+ name: "my-function",
209
+ role: role.roleArn,
210
+ //..
211
+ });
212
+ ```
213
+
214
+ Each of these Resources is known as a "sub-graph".
215
+
216
+ In this case we have `Role` (a 1-node graph, `Role`), and `Function` (a 2-node graph, `Role → Function`).
217
+
218
+ Each sub-graph can be "applied" or "destroyed" individually using the `apply` and `destroy` functions:
219
+
220
+ ```ts
221
+ import { apply, destroy } from "alchemy";
222
+
223
+ // will create Role and then Function (in that order)
224
+ const { functionArn } = await apply(func);
225
+
226
+ // you can destroy it right after if you want ☠️
227
+ await destroy(func); // will delete just the Function
228
+
229
+ // destroy deletes the resource and any downstream dependencies
230
+ // so, if you want to delete Role AND Function, you should call destroy(role)
231
+ await destroy(role); // will delete Role and then Function
232
+ ```
233
+
234
+ ## Destroying the app
235
+
236
+ To destroy the whole app (aka. the whole graph), you can call `alchemize` with the `mode: "destroy"` option. This will delete all resources in the specified or default stage.
237
+
238
+ ```ts
239
+ await alchemize({ mode: "destroy", stage: <optional> });
240
+ ```
241
+
242
+ > [!TIP]
243
+ > Alchemy is designed to have the minimum number of opinions as possible. This "embeddable" design is so that you can implement your own tools around Alchemy, e.g. a CLI or UI, instead of being stuck with a specific tool.
244
+ >
245
+ > ```ts
246
+ > await alchemize({
247
+ > // decide the mode/stage however you want, e.g. a CLI parser
248
+ > mode: process.argv[2] === "destroy" ? "destroy" : "up",
249
+ > stage: process.argv[3],
250
+ > });
251
+ > ```
252
+
253
+ ## "Stage" and State
254
+
255
+ > [!NOTE]
256
+ > Stage is inspired by [SST](https://sst.dev)'s stage concept.
257
+
258
+ Alchemy supports a "stage" concept to help isolate different environments from each other. E.g. a `"user"` or `"dev"` or `"prod"` stage.
259
+
260
+ By default, the stage is assumed to be your user name (a sensible default for local development).
261
+
262
+ To override the stage, you have three options:
263
+
264
+ 1. Pass the `stage` option to `alchemize`/`apply`/`destroy` (recommended)
265
+
266
+ ```ts
267
+ // alchemize the entire app
268
+ await alchemize({ stage: "production" });
269
+
270
+ // apply a single resource
271
+ await apply(func, { stage: "production" });
272
+ ```
273
+
274
+ 2. Config in `./alchemy.ts` (up to you if you want to have a global config). See the [alchemy.ts section](#global-values-and-the-alchemy-ts-config-file) for more details.
275
+
276
+ ```ts
277
+ export default {
278
+ defaultStage: "production",
279
+ };
280
+ ```
281
+
282
+ 3. Set the `ALCHEMY_STAGE` environment variable (not recommended, but available as an escape hatch)
283
+
284
+ ```sh
285
+ ALCHEMY_STAGE=production bun ./my-app.ts
286
+ ```
287
+
288
+ Each Resource "provider" can access the stage it's being deployed to via the `ctx.stage` property.
289
+
290
+ ```ts
291
+ class Table extends Resource("dynamo::Table", async (ctx, inputs) => {
292
+ ctx.stage; // "production"
293
+ });
294
+ ```
295
+
296
+ > [!CAUTION]
297
+ > It is up to you to ensure that the physical names of resources don't conflict - alchemy does not (yet) offer any help or opinions here. You must decide on physical names, but you're free to add name generation logic to your resources if you so desire.
298
+ >
299
+ > ```ts
300
+ > class Table extends Resource("dynamo::Table", async (ctx, inputs) => {
301
+ > const tableName = `${ctx.stage}-${inputs.tableName}`;
302
+ >
303
+ > // ..
304
+ > });
305
+ > ```
306
+
307
+ ## Global values and the `alchemy.ts` config file.
308
+
309
+ Alchemy looks for a `${cwd}/alchemy.ts` file and imports it if it finds it. This can be useful for emulating SST's `sst.config.ts` file as a convention for global configuration.
310
+
311
+ It supports overriding the `defaultStage` (instead of defaulting to your username) and providing a custom `stateStore` (instead of writing to the local file system).
312
+
313
+ ```ts
314
+ import type { Config } from "alchemy";
315
+
316
+ export default {
317
+ defaultStage: "dev",
318
+ stateStore: myCustomStateStore,
319
+ } satisfies Config;
320
+ ```
321
+
322
+ > [!NOTE]
323
+ > See [global.ts](./alchemy/src/global.ts).
324
+
325
+ ## Philosophy and comparison with existing IaC frameworks
326
+
327
+ I built alchemy after years of working with every other option. IaC is non-negotiable in my opinion, and has been one of my favorite technologies as a developer.
328
+
329
+ I started with CloudFormation (since I worked at Amazon) and hated that. Fuck deploying JSON to a slow service, am I right? The CDK was a huge upgrade on that (yay for TypeScript) but it is limited by the CloudFormation service, which is slow to _change_ and slow to _use_, and has very strong opinions on how and where it should be used.
330
+
331
+ For example, the CDK is written in a "meta" language called JSII. Which is a subset of TypeScript designed to be compiled to Java, Python, etc. This means you're not free to use all TypeScript features (which are ideal for configuration as code). It's also coupled to synchronous I/O. It is damn near impossible to do anything async, making it slow and clunky. Finally, the CDK is still CJS and doesn't emit ESM. Taking a dependency on the CDK will 10x the size of your bundle and destroy your DX.
332
+
333
+ Later, I moved on to Pulumi, which was a nice change of pace since it runs locally and is much faster. You can cancel a deployment by hitting Ctrl+C (hallelujah!). It also supported more than just AWS, which is useful for managing all your resources, such as Stripe, GitHub or another provider like Cloudflare.
334
+
335
+ However, Pulumi is largely a wrapper around Terraform and relies on complicated "providers" implemented with Go, running in a separate process. It can't run anywhere (like the browser). Different languages work better/worse than others and always have "sharp edge" limitations and gotchas. Custom Resources are possible but more of an afterthought and a PITA to implement.
336
+
337
+ I've used Terraform when I've been forced into it. It's ... ok ... but I don't love it. It's a custom DSL and a heavy toolchain for what, calling a few CRUD APIs? Way overkill. Every time I think about implementing a custom resource for Terraform, I just can't bring myself to do it. Let me just write a function, please!
338
+
339
+ Lately, I've been using SST because I've been doing a ton of web development. At first, I really liked SST, especially because of its local development experience. `sst dev` gives you live deploy, a TUI multiplexer and a proxy from the cloud to your local code. This is great for building web apps in AWS.
340
+
341
+ But, as my app grew, SST's bugs ate away at me. I got blocked by broken resources that have race conditions and it was impossible to workaround. I also wanted to deploy a nested app (another `sst.config.ts`) which didn't gel well with the generated `sst-env.d.ts` files.
342
+
343
+ Again, I was let down by the complexity and opinions of my chosen IaC framework. And for what? To help me call a few CRUD APIs? Honestly, it just seems insane how far we've drifted away from simplicity in this area. This became even more apparent as I started using Cursor more and more to write code.
344
+
345
+ You see, I've found myself doing more frontend than I've ever done before, and I'm not a good frontend developer. So, I relied on Cursor to write most of the code for me and (as many others have experienced) it totally blew my mind 🤯. Cursor is just really, really, really good at TypeScript, React and Tailwind. I've built a functioning and (if i don't say so myself) good looking SPA. It's been a blast.
346
+
347
+ But, this got me thinking ... you know what else Cursor is really great at? Perhaps even better at? Interacting with CRUD APIs, that's what! While there's a ton of frontend training data for LLMs, there's just as much (if not more) training data for CRUD lifecycle operations. There's also all of Terraform, Pulumi, SST and the AWS CDK's training data. All. Of. It.
348
+
349
+ Long story short, I discovered that Cursor can pretty much one-shot the implementation of Resources. All of the resources in [src/components](./alchemy/src/components) are entirely generated on-demand (1 minute time investment, tops). When I run into a bug, I just explained it to Cursor who fixed it immediately.
350
+
351
+ This is a game changer. It means we don't need tools like Terraform to build suites of "provider" libraries for us. We just need the engine - the bit that tracks state and decides what to create/update/delete. The rest can be generated at near zero cost.
352
+
353
+ This story is ultimately why I built Alchemy - I just wanted total control and freedom over how I deploy my resources. And, I wanted it to be in TypeScript, so I can use its amazing type system. Annnnd, I wanted it to be embeddable, so I can use it anywhere, maybe even in a React app.
354
+
355
+ "Reactive IaC", anyone?
package/package.json CHANGED
@@ -1,28 +1,32 @@
1
1
  {
2
2
  "name": "alchemy",
3
- "version": "0.0.8",
4
- "description": "sql alchemy",
5
- "main": "alchemy.js",
3
+ "version": "0.1.2",
4
+ "module": "index.ts",
5
+ "type": "module",
6
6
  "scripts": {
7
- "test": "make test",
8
- "test-w": "make test-w",
9
- "test-debug": "mocha --debug-brk --require should"
7
+ "publish": "cp ../README.md ./README.md && npm publish && rm ./README.md"
10
8
  },
11
- "author": "Radu Brehar",
12
- "license": "MIT",
13
- "dependencies": {
14
- "pg": "~3.4.0",
15
- "expect.js": "^0.3.0",
16
- "functionally": "^0.2.1",
17
- "classy": "^1.4.0",
18
- "bluebird": "~2.2.2",
19
- "bunyan": "^0.23.1",
20
- "jsesc": "~0.4.3",
21
- "newify": "~1.0.0",
22
- "ustring": "^1.1.1"
9
+ "exports": {
10
+ ".": "./src/index.ts",
11
+ "./agent": "./src/components/agent/index.ts",
12
+ "./fs": "./src/components/fs/index.ts",
13
+ "./aws": "./src/components/aws/index.ts",
14
+ "./esbuild": "./src/components/esbuild.ts"
23
15
  },
24
16
  "devDependencies": {
25
- "should": "~4.0.4",
26
- "mocha": "~1.20.1"
17
+ "@types/bun": "latest"
18
+ },
19
+ "peerDependencies": {
20
+ "@ai-sdk/openai": "^1.0.0",
21
+ "@aws-sdk/client-dynamodb": "^3.0.0",
22
+ "@aws-sdk/client-iam": "^3.0.0",
23
+ "@aws-sdk/client-lambda": "^3.0.0",
24
+ "@aws-sdk/client-s3": "^3.0.0",
25
+ "@aws-sdk/client-sqs": "^3.0.1",
26
+ "ai": "^4.0.0",
27
+ "jszip": "^3.0.0",
28
+ "esbuild": "^0.24.2",
29
+ "turndown": "^7.2.0",
30
+ "zod": "^3.24.1"
27
31
  }
28
32
  }
@@ -0,0 +1,151 @@
1
+ import { evaluate } from "./apply";
2
+ import { destroy } from "./destroy";
3
+ import { defaultStage, nodes, providers, stateStore } from "./global";
4
+ import type { StateStore } from "./state";
5
+
6
+ let finalized = false;
7
+
8
+ export interface AlchemizeOptions {
9
+ /**
10
+ * Determines whether the resources will be created/updated or deleted.
11
+ *
12
+ * @default "up"
13
+ */
14
+ mode?: "up" | "destroy";
15
+ /**
16
+ * Name to scope the resource state under (e.g. `.alchemy/{stage}/..`).
17
+ *
18
+ * @default - your POSIX username
19
+ */
20
+ stage?: string;
21
+ /**
22
+ * If true, will not prune resources that were dropped from the root stack.
23
+ *
24
+ * @default true
25
+ */
26
+ destroyOrphans?: boolean;
27
+ /**
28
+ * A custom state store to use instead of the default file system store.
29
+ */
30
+ stateStore?: StateStore;
31
+ }
32
+
33
+ /**
34
+ * Explicitly finalize the program by deleting any resources that were dropped from the root stack.
35
+ *
36
+ * By default, this will be called when
37
+ */
38
+ export async function alchemize(options?: AlchemizeOptions) {
39
+ if (finalized) {
40
+ return;
41
+ }
42
+ finalized = true;
43
+
44
+ const stage = options?.stage ?? defaultStage;
45
+ const state = options?.stateStore ?? stateStore;
46
+
47
+ await state.init?.();
48
+
49
+ // Track in-progress deletions to avoid duplicate work
50
+ const deletionPromises = new Map<string, Promise<void>>();
51
+
52
+ const priorStates = await state.all(stage);
53
+ const priorIDs = Object.keys(priorStates);
54
+
55
+ const mode = options?.mode ?? "up";
56
+
57
+ if (mode === "up") {
58
+ await Promise.allSettled(
59
+ Array.from(nodes.values())
60
+ .reverse()
61
+ .map((node) =>
62
+ evaluate(node.resource, {
63
+ stage,
64
+ }),
65
+ ),
66
+ );
67
+
68
+ if (options?.destroyOrphans === false) {
69
+ return;
70
+ }
71
+ }
72
+ const aliveIDs = new Set(
73
+ mode === "up"
74
+ ? nodes.keys()
75
+ : // There are no alive resources in destroy mode
76
+ [],
77
+ );
78
+
79
+ const orphanIDs = Array.from(priorIDs).filter((id) => !aliveIDs.has(id));
80
+
81
+ const orphanStates = Object.fromEntries(
82
+ orphanIDs.map((id) => [id, priorStates[id]] as const),
83
+ );
84
+ // compute a map of resourceID -> upstream dependencies (resources that depend on it)
85
+ const orphanGraph: Record<string, Set<string>> = {};
86
+ for (const [orphanID, orphanState] of Object.entries(orphanStates)) {
87
+ orphanGraph[orphanID] ??= new Set();
88
+ for (const dep of orphanState.deps) {
89
+ (orphanGraph[dep] ??= new Set()).add(orphanID);
90
+ }
91
+ }
92
+
93
+ // Start deletion from each orphan that has no dependents (nothing depends on it)
94
+ await Promise.all(
95
+ orphanIDs
96
+ .filter((id) => orphanGraph[id].size === 0)
97
+ .map((id) => deleteOrphan(id)),
98
+ );
99
+
100
+ // Recursively delete orphans in dependency order
101
+ async function deleteOrphan(orphanID: string): Promise<void> {
102
+ // Return existing deletion promise if this orphan is already being deleted
103
+ const existing = deletionPromises.get(orphanID);
104
+ if (existing) {
105
+ return existing;
106
+ }
107
+
108
+ // Create promise immediately to prevent race conditions
109
+ let resolve: () => void;
110
+ let reject: (error: any) => void;
111
+ const promise = new Promise<void>((res, rej) => {
112
+ resolve = res;
113
+ reject = rej;
114
+ });
115
+ deletionPromises.set(orphanID, promise);
116
+
117
+ try {
118
+ const orphanState = orphanStates[orphanID];
119
+
120
+ // First delete this resource
121
+ const providerType = orphanState.provider;
122
+ const provider = providers.get(providerType);
123
+ if (!provider) {
124
+ throw new Error(
125
+ `No provider found for ${providerType}. Did you forget to import it?`,
126
+ );
127
+ }
128
+ await destroy(stage, orphanID, orphanState, provider);
129
+
130
+ // After this resource is deleted, we can delete its dependencies if they're orphans
131
+ if (orphanState.deps.length > 0) {
132
+ await Promise.all(
133
+ orphanState.deps
134
+ .filter((dep) => orphanIDs.includes(dep)) // Only delete if it's an orphan
135
+ .map((dep) => deleteOrphan(dep)),
136
+ );
137
+ }
138
+
139
+ resolve!();
140
+ } catch (error) {
141
+ reject!(error);
142
+ }
143
+
144
+ return promise;
145
+ }
146
+ }
147
+
148
+ if (process.env.ALCHEMY_NO_DEPLOY !== "true") {
149
+ // Listen for signals or events
150
+ process.on("exit", () => alchemize());
151
+ }
package/src/apply.ts ADDED
@@ -0,0 +1,117 @@
1
+ import { defaultStage, stateStore } from "./global";
2
+ import { Output } from "./output";
3
+ import {
4
+ Input,
5
+ Provider,
6
+ type Resource,
7
+ ResourceID,
8
+ isResource,
9
+ } from "./resource";
10
+ import type { StateStore } from "./state";
11
+
12
+ interface ApplyOptions {
13
+ stage?: string;
14
+ stateStore?: StateStore;
15
+ }
16
+
17
+ /**
18
+ * Apply a sub-graph to produce a resource.
19
+ * @param output A sub-graph that produces a resource.
20
+ * @returns The resource properties.
21
+ */
22
+ export async function apply<T>(
23
+ output: T | Output<T>,
24
+ options?: ApplyOptions,
25
+ ): Promise<T> {
26
+ return (await evaluate(output, options)).value;
27
+ }
28
+
29
+ class Evaluated<T> {
30
+ constructor(
31
+ public readonly value: T,
32
+ public readonly deps: string[] = [],
33
+ ) {}
34
+ }
35
+
36
+ const cache = new WeakMap<Resource, Promise<Evaluated<any>>>();
37
+
38
+ export async function evaluate<T>(
39
+ output: T | Output<T>,
40
+ options: ApplyOptions = {},
41
+ ): Promise<Evaluated<T>> {
42
+ const state = options.stateStore ?? stateStore;
43
+ if (isResource(output)) {
44
+ const resource = output;
45
+ const resourceID = resource[ResourceID];
46
+ const evaluated = await Promise.all(
47
+ resource[Input].map((r) => evaluate<any>(r, options)),
48
+ );
49
+
50
+ if (cache.has(resource)) {
51
+ return await cache.get(resource)!;
52
+ }
53
+ let resolve: (value: Evaluated<any>) => void;
54
+ let reject: (reason?: any) => void;
55
+ const promise = new Promise<Evaluated<any>>((res, rej) => {
56
+ resolve = res;
57
+ reject = rej;
58
+ });
59
+ // set this eagerly to avoid double-apply (caused by recursive applies to inputs)
60
+ cache.set(resource, promise);
61
+
62
+ const deps = new Set(evaluated.flatMap((input) => input.deps));
63
+ const inputs = evaluated.map((input) => input.value);
64
+ try {
65
+ const result: T = await resource[Provider].update(
66
+ options.stage ?? defaultStage,
67
+ resource,
68
+ deps,
69
+ inputs as [],
70
+ state,
71
+ );
72
+ resolve!(new Evaluated(result, [resourceID, ...deps]));
73
+ } catch (error) {
74
+ reject!(error);
75
+ }
76
+ return promise;
77
+ } else if (output instanceof Output) {
78
+ const inside = output as unknown as {
79
+ parent: Output<any>;
80
+ fn: (value: any) => T;
81
+ };
82
+ const parent = await evaluate(inside.parent, options);
83
+ const ret = inside.fn(parent.value);
84
+ // the ret may be an Output (e.g. in the flatMap case), so we need to evaluate it and include its deps
85
+ const evaluated = await evaluate(ret, options);
86
+ return new Evaluated<T>(evaluated.value, [
87
+ ...parent.deps,
88
+ ...evaluated.deps,
89
+ ]);
90
+ } else if (Array.isArray(output)) {
91
+ const evaluatedItems = await Promise.all(
92
+ output.map((item) => evaluate(item, options)),
93
+ );
94
+ return new Evaluated(
95
+ evaluatedItems.map((e) => e.value) as unknown as T,
96
+ evaluatedItems.flatMap((e) => e.deps),
97
+ );
98
+ } else if (output && typeof output === "object") {
99
+ const entries = Object.entries(output);
100
+ const evaluatedEntries = await Promise.all(
101
+ entries.map(
102
+ async ([key, value]) => [key, await evaluate(value, options)] as const,
103
+ ),
104
+ );
105
+
106
+ const result = Object.fromEntries(
107
+ evaluatedEntries.map(([key, evaluated]) => [key, evaluated.value]),
108
+ ) as unknown as T;
109
+
110
+ const deps = evaluatedEntries.flatMap(([_, evaluated]) => evaluated.deps);
111
+
112
+ return new Evaluated(result, deps);
113
+ }
114
+
115
+ // Base case: primitive value
116
+ return new Evaluated<T>(output as T);
117
+ }