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/scope.ts CHANGED
@@ -1,51 +1,116 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
- import path from "node:path";
3
- import type { Output } from "./output";
4
- import type { Provider, Resource, ResourceID } from "./resource";
2
+ import type { PendingResource, ResourceID } from "./resource";
3
+ import {
4
+ FileSystemStateStore,
5
+ type StateStore,
6
+ type StateStoreType,
7
+ } from "./state";
5
8
 
6
9
  const scopeStorage = new AsyncLocalStorage<Scope>();
7
10
 
11
+ export type ScopeOptions = {
12
+ appName?: string;
13
+ stage: string;
14
+ parent?: Scope;
15
+ scopeName?: string;
16
+ password?: string;
17
+ stateStore?: StateStoreType;
18
+ quiet?: boolean;
19
+ };
20
+
8
21
  export class Scope {
9
- public readonly nodes = new Map<
10
- ResourceID,
11
- {
12
- provider: Provider<any, any[], any>;
13
- resource: Resource<any, any>;
22
+ public static get(): Scope | undefined {
23
+ return scopeStorage.getStore();
24
+ }
25
+
26
+ public static get current(): Scope {
27
+ const scope = Scope.get();
28
+ if (!scope) {
29
+ throw new Error("Not running within an Alchemy Scope");
14
30
  }
15
- >();
31
+ return scope;
32
+ }
16
33
 
17
- public readonly callbacks: Output<void>[] = [];
34
+ public readonly resources = new Map<ResourceID, PendingResource>();
35
+ public readonly appName: string | undefined;
36
+ public readonly stage: string;
37
+ public readonly scopeName: string | null;
38
+ public readonly parent: Scope | undefined;
39
+ public readonly password: string | undefined;
40
+ public readonly state: StateStore;
41
+ public readonly quiet: boolean;
18
42
 
19
- constructor(
20
- public readonly scopeName: string | null = null,
21
- public readonly parent: Scope | undefined = undefined,
22
- ) {}
43
+ constructor(options: ScopeOptions) {
44
+ this.appName = options.appName;
45
+ this.stage = options.stage;
46
+ this.scopeName = options.scopeName ?? null;
47
+ this.parent = options.parent ?? Scope.get();
48
+ this.quiet = options.quiet ?? this.parent?.quiet ?? false;
49
+ if (this.parent && !this.scopeName) {
50
+ throw new Error("Scope name is required when creating a child scope");
51
+ }
52
+ this.password = options.password;
53
+ this.state = new (options.stateStore ?? FileSystemStateStore)(this);
54
+ }
23
55
 
24
- public getScopePath(root: string): string {
25
- // First, compute the parent's scope path
26
- const parentPath = this.parent ? this.parent.getScopePath(root) : root;
27
- // Then join the current scope name (if any) onto the parent's path
28
- return this.scopeName ? path.join(parentPath, this.scopeName) : parentPath;
56
+ public async delete(resourceID: ResourceID) {
57
+ await this.state.delete(resourceID);
58
+ this.resources.delete(resourceID);
29
59
  }
30
- }
31
60
 
32
- export const rootScope = new Scope(null);
61
+ private _seq = 0;
33
62
 
34
- export function getScope(): Scope {
35
- return scopeStorage.getStore() ?? rootScope;
36
- }
63
+ public seq() {
64
+ return this._seq++;
65
+ }
37
66
 
38
- export function withScope<T>(
39
- scope: Scope,
40
- fn: () => T | Promise<T>,
41
- ): Promise<T> {
42
- return scopeStorage.run(scope, async () => fn());
43
- }
67
+ public get chain(): string[] {
68
+ const thisScope = this.scopeName ? [this.scopeName] : [];
69
+ const app = this.appName ? [this.appName] : [];
70
+ if (this.parent) {
71
+ return [...this.parent.chain, ...thisScope];
72
+ } else {
73
+ return [...app, this.stage, ...thisScope];
74
+ }
75
+ }
76
+
77
+ public enter() {
78
+ scopeStorage.enterWith(this);
79
+ }
44
80
 
45
- export function pushScope<T>(
46
- parent: Scope,
47
- scopeName: string,
48
- fn: () => T | Promise<T>,
49
- ): Promise<T> {
50
- return scopeStorage.run(new Scope(scopeName, parent), async () => fn());
81
+ public async init() {
82
+ await this.state.init?.();
83
+ }
84
+
85
+ public async deinit() {
86
+ await this.state.deinit?.();
87
+ }
88
+
89
+ public fqn(resourceID: ResourceID): string {
90
+ return [...this.chain, resourceID].join("/");
91
+ }
92
+
93
+ public async finalize() {
94
+ // TODO
95
+ }
96
+
97
+ public async run<T>(fn: (scope: Scope) => Promise<T>): Promise<T> {
98
+ return scopeStorage.run(this, () => fn(this));
99
+ }
100
+
101
+ [Symbol.asyncDispose]() {
102
+ return this.finalize();
103
+ }
104
+
105
+ /**
106
+ * Returns a string representation of the scope.
107
+ */
108
+ toString() {
109
+ return `Scope(
110
+ chain=${this.chain.join("/")},
111
+ resources=[${Array.from(this.resources.values())
112
+ .map((r) => r.ID)
113
+ .join(",\n ")}]
114
+ )`;
115
+ }
51
116
  }
package/src/secret.ts ADDED
@@ -0,0 +1,18 @@
1
+ export class Secret {
2
+ public readonly type = "secret";
3
+ constructor(readonly unencrypted: string) {}
4
+ }
5
+
6
+ export function isSecret(binding: any): binding is Secret {
7
+ return (
8
+ binding instanceof Secret ||
9
+ (typeof binding === "object" && binding.type === "secret_text")
10
+ );
11
+ }
12
+
13
+ export function secret<S extends string | undefined>(unencrypted: S): Secret {
14
+ if (unencrypted === undefined) {
15
+ throw new Error("Secret cannot be undefined");
16
+ }
17
+ return new Secret(unencrypted);
18
+ }
package/src/state.ts CHANGED
@@ -1,8 +1,15 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { ignore } from "./error";
3
+ import type { Resource, ResourceProps } from "./resource";
4
+ import type { Scope } from "./scope";
5
+ import { ignore } from "./util/ignore";
6
+ import { deserialize, serialize } from "./util/serde";
4
7
 
5
- export interface State {
8
+ export interface State<
9
+ Kind extends string = string,
10
+ Props extends ResourceProps = ResourceProps,
11
+ Out extends Resource = Resource,
12
+ > {
6
13
  status:
7
14
  | `creating`
8
15
  | `created`
@@ -10,14 +17,19 @@ export interface State {
10
17
  | `updated`
11
18
  | `deleting`
12
19
  | `deleted`;
13
- provider: string;
20
+ kind: Kind;
21
+ id: string;
22
+ fqn: string;
23
+ seq: number;
14
24
  data: Record<string, any>;
15
- deps: string[];
16
- inputs: any[];
17
- oldInputs?: any[];
18
- output: any;
25
+ // deps: string[];
26
+ props: Props;
27
+ oldProps?: Props;
28
+ output: Out;
19
29
  }
20
30
 
31
+ export type StateStoreType = new (scope: Scope) => StateStore;
32
+
21
33
  export interface StateStore {
22
34
  /** Initialize the state container if one is required */
23
35
  init?(): Promise<void>;
@@ -34,23 +46,21 @@ export interface StateStore {
34
46
  delete(key: string): Promise<void>;
35
47
  }
36
48
 
37
- const stateFile = path.join(process.cwd(), ".alchemy");
38
-
39
- export type StateStoreType = new (dir: string) => StateStore;
49
+ const stateRootDir = path.join(process.cwd(), ".alchemy");
40
50
 
41
51
  export class FileSystemStateStore implements StateStore {
42
- public readonly path: string;
43
- constructor(dir: string) {
44
- this.path = path.join(stateFile, dir);
52
+ public readonly dir: string;
53
+ constructor(public readonly scope: Scope) {
54
+ this.dir = path.join(stateRootDir, ...scope.chain);
45
55
  }
46
56
 
47
57
  async init(): Promise<void> {
48
- await fs.promises.mkdir(stateFile, { recursive: true });
49
- await fs.promises.mkdir(this.path, { recursive: true });
58
+ await fs.promises.mkdir(stateRootDir, { recursive: true });
59
+ await fs.promises.mkdir(this.dir, { recursive: true });
50
60
  }
51
61
 
52
62
  async deinit(): Promise<void> {
53
- await ignore("ENOENT", () => fs.promises.rmdir(this.path));
63
+ await ignore("ENOENT", () => fs.promises.rmdir(this.dir));
54
64
  }
55
65
 
56
66
  async count(): Promise<number> {
@@ -59,7 +69,7 @@ export class FileSystemStateStore implements StateStore {
59
69
 
60
70
  async list(): Promise<string[]> {
61
71
  try {
62
- const files = await fs.promises.readdir(this.path, {
72
+ const files = await fs.promises.readdir(this.dir, {
63
73
  withFileTypes: true,
64
74
  });
65
75
  return files
@@ -79,7 +89,7 @@ export class FileSystemStateStore implements StateStore {
79
89
  await this.getPath(key),
80
90
  "utf8",
81
91
  );
82
- return JSON.parse(content) as State;
92
+ return (await deserialize(this.scope, JSON.parse(content))) as State;
83
93
  } catch (error: any) {
84
94
  if (error.code === "ENOENT") {
85
95
  return undefined;
@@ -91,7 +101,7 @@ export class FileSystemStateStore implements StateStore {
91
101
  async set(key: string, value: State): Promise<void> {
92
102
  return fs.promises.writeFile(
93
103
  await this.getPath(key),
94
- JSON.stringify(value, null, 2),
104
+ JSON.stringify(await serialize(this.scope, value), null, 2),
95
105
  );
96
106
  }
97
107
 
@@ -120,7 +130,7 @@ export class FileSystemStateStore implements StateStore {
120
130
  }
121
131
 
122
132
  private async getPath(key: string): Promise<string> {
123
- const file = path.join(this.path, `${key}.json`);
133
+ const file = path.join(this.dir, `${key}.json`);
124
134
  const dir = path.dirname(file);
125
135
  await fs.promises.mkdir(dir, { recursive: true });
126
136
  return file;
@@ -1,5 +1,5 @@
1
1
  import Stripe from "stripe";
2
- import type { Context } from "../resource";
2
+ import type { Context } from "../context";
3
3
  import { Resource } from "../resource";
4
4
 
5
5
  /**
@@ -98,7 +98,7 @@ export interface PriceProps {
98
98
  /**
99
99
  * Output from the Stripe price
100
100
  */
101
- export interface PriceOutput extends PriceProps {
101
+ export interface Price extends Resource<"stripe::Price">, PriceProps {
102
102
  /**
103
103
  * The ID of the price
104
104
  */
@@ -125,9 +125,13 @@ export interface PriceOutput extends PriceProps {
125
125
  lookupKey?: string;
126
126
  }
127
127
 
128
- export class Price extends Resource(
128
+ export const Price = Resource(
129
129
  "stripe::Price",
130
- async (ctx: Context<PriceOutput>, props: PriceProps) => {
130
+ async function (
131
+ this: Context<Price>,
132
+ id: string,
133
+ props: PriceProps,
134
+ ): Promise<Price> {
131
135
  // Get Stripe API key from context or environment
132
136
  const apiKey = process.env.STRIPE_API_KEY;
133
137
  if (!apiKey) {
@@ -137,32 +141,25 @@ export class Price extends Resource(
137
141
  // Initialize Stripe client
138
142
  const stripe = new Stripe(apiKey);
139
143
 
140
- if (ctx.event === "delete") {
144
+ if (this.phase === "delete") {
141
145
  try {
142
- if (ctx.event === "delete" && ctx.output?.id) {
146
+ if (this.phase === "delete" && this.output?.id) {
143
147
  // Prices can't be deleted, only deactivated
144
- await stripe.prices.update(ctx.output.id, { active: false });
148
+ await stripe.prices.update(this.output.id, { active: false });
145
149
  }
146
150
  } catch (error) {
147
151
  // Ignore if the price doesn't exist
148
152
  console.error("Error deactivating price:", error);
149
153
  }
150
154
 
151
- // Return a minimal output for deleted state
152
- return {
153
- ...props,
154
- id: "",
155
- createdAt: 0,
156
- livemode: false,
157
- type: (props.recurring ? "recurring" : "one_time") as Stripe.Price.Type,
158
- };
155
+ return this.destroy();
159
156
  } else {
160
157
  try {
161
158
  let price: Stripe.Price;
162
159
 
163
- if (ctx.event === "update" && ctx.output?.id) {
160
+ if (this.phase === "update" && this.output?.id) {
164
161
  // Update existing price (limited properties can be updated)
165
- price = await stripe.prices.update(ctx.output.id, {
162
+ price = await stripe.prices.update(this.output.id, {
166
163
  active: props.active,
167
164
  metadata: props.metadata,
168
165
  nickname: props.nickname,
@@ -213,7 +210,7 @@ export class Price extends Resource(
213
210
  : undefined;
214
211
 
215
212
  // Map Stripe API response to our output format
216
- const output: PriceOutput = {
213
+ return this({
217
214
  id: price.id,
218
215
  product:
219
216
  typeof price.product === "string"
@@ -232,13 +229,11 @@ export class Price extends Resource(
232
229
  livemode: price.livemode,
233
230
  type: price.type as Stripe.Price.Type,
234
231
  lookupKey: price.lookup_key || undefined,
235
- };
236
-
237
- return output;
232
+ });
238
233
  } catch (error) {
239
234
  console.error("Error creating/updating price:", error);
240
235
  throw error;
241
236
  }
242
237
  }
243
238
  },
244
- ) {}
239
+ );
@@ -1,5 +1,5 @@
1
1
  import Stripe from "stripe";
2
- import type { Context } from "../resource";
2
+ import type { Context } from "../context";
3
3
  import { Resource } from "../resource";
4
4
 
5
5
  type ProductType = Stripe.Product.Type;
@@ -67,7 +67,7 @@ export interface ProductProps {
67
67
  /**
68
68
  * Output from the Stripe product
69
69
  */
70
- export interface ProductOutput extends ProductProps {
70
+ export interface Product extends Resource<"stripe::Product">, ProductProps {
71
71
  /**
72
72
  * The ID of the product
73
73
  */
@@ -99,9 +99,13 @@ export interface ProductOutput extends ProductProps {
99
99
  };
100
100
  }
101
101
 
102
- export class Product extends Resource(
102
+ export const Product = Resource(
103
103
  "stripe::Product",
104
- async (ctx: Context<ProductOutput>, props: ProductProps) => {
104
+ async function (
105
+ this: Context<Product>,
106
+ id: string,
107
+ props: ProductProps,
108
+ ): Promise<Product> {
105
109
  // Get Stripe API key from context or environment
106
110
  const apiKey = process.env.STRIPE_API_KEY;
107
111
  if (!apiKey) {
@@ -111,10 +115,10 @@ export class Product extends Resource(
111
115
  // Initialize Stripe client
112
116
  const stripe = new Stripe(apiKey);
113
117
 
114
- if (ctx.event === "delete") {
118
+ if (this.phase === "delete") {
115
119
  try {
116
- if (ctx.event === "delete" && ctx.output?.id) {
117
- await stripe.products.update(ctx.output.id, { active: false });
120
+ if (this.phase === "delete" && this.output?.id) {
121
+ await stripe.products.update(this.output.id, { active: false });
118
122
  }
119
123
  } catch (error) {
120
124
  // Ignore if the product doesn't exist
@@ -122,20 +126,14 @@ export class Product extends Resource(
122
126
  }
123
127
 
124
128
  // Return a minimal output for deleted state
125
- return {
126
- ...props,
127
- id: "",
128
- createdAt: 0,
129
- updatedAt: 0,
130
- livemode: false,
131
- };
129
+ return this.destroy();
132
130
  } else {
133
131
  try {
134
132
  let product: Stripe.Product;
135
133
 
136
- if (ctx.event === "update" && ctx.output?.id) {
134
+ if (this.phase === "update" && this.output?.id) {
137
135
  // Update existing product
138
- product = await stripe.products.update(ctx.output.id, {
136
+ product = await stripe.products.update(this.output.id, {
139
137
  name: props.name,
140
138
  description: props.description,
141
139
  active: props.active,
@@ -162,8 +160,7 @@ export class Product extends Resource(
162
160
  });
163
161
  }
164
162
 
165
- // Map Stripe API response to our output format
166
- const output: ProductOutput = {
163
+ return this({
167
164
  id: product.id,
168
165
  name: product.name,
169
166
  description: product.description || undefined,
@@ -181,13 +178,11 @@ export class Product extends Resource(
181
178
  livemode: product.livemode,
182
179
  updatedAt: product.updated,
183
180
  packageDimensions: product.package_dimensions || undefined,
184
- };
185
-
186
- return output;
181
+ });
187
182
  } catch (error) {
188
183
  console.error("Error creating/updating product:", error);
189
184
  throw error;
190
185
  }
191
186
  }
192
187
  },
193
- ) {}
188
+ );
@@ -1,5 +1,5 @@
1
1
  import Stripe from "stripe";
2
- import type { Context } from "../resource";
2
+ import type { Context } from "../context";
3
3
  import { Resource } from "../resource";
4
4
 
5
5
  export type EnabledEvent = Stripe.WebhookEndpointUpdateParams.EnabledEvent;
@@ -47,7 +47,9 @@ export interface WebhookEndpointProps {
47
47
  /**
48
48
  * Output from the Stripe webhook endpoint
49
49
  */
50
- export interface WebhookEndpointOutput extends WebhookEndpointProps {
50
+ export interface WebhookEndpoint
51
+ extends Resource<"stripe::WebhookEndpoint">,
52
+ WebhookEndpointProps {
51
53
  /**
52
54
  * The ID of the webhook
53
55
  */
@@ -84,9 +86,13 @@ export interface WebhookEndpointOutput extends WebhookEndpointProps {
84
86
  status: string;
85
87
  }
86
88
 
87
- export class WebhookEndpoint extends Resource(
89
+ export const WebhookEndpoint = Resource(
88
90
  "stripe::WebhookEndpoint",
89
- async (ctx: Context<WebhookEndpointOutput>, props: WebhookEndpointProps) => {
91
+ async function (
92
+ this: Context<WebhookEndpoint>,
93
+ id: string,
94
+ props: WebhookEndpointProps,
95
+ ) {
90
96
  // Get Stripe API key from context or environment
91
97
  const apiKey = process.env.STRIPE_API_KEY;
92
98
  if (!apiKey) {
@@ -96,34 +102,25 @@ export class WebhookEndpoint extends Resource(
96
102
  // Initialize Stripe client
97
103
  const stripe = new Stripe(apiKey);
98
104
 
99
- if (ctx.event === "delete") {
105
+ if (this.phase === "delete") {
100
106
  try {
101
107
  // Get the webhook ID from the stored output
102
- if (ctx.event === "delete" && ctx.output?.id) {
103
- await stripe.webhookEndpoints.del(ctx.output.id);
108
+ if (this.phase === "delete" && this.output?.id) {
109
+ await stripe.webhookEndpoints.del(this.output.id);
104
110
  }
105
111
  } catch (error) {
106
112
  // Ignore if the webhook doesn't exist
107
113
  console.error("Error deleting webhook:", error);
108
114
  }
109
115
 
110
- // Return a minimal output for deleted state
111
- return {
112
- ...props,
113
- id: "",
114
- secret: "",
115
- createdAt: 0,
116
- updatedAt: 0,
117
- livemode: false,
118
- status: "deleted",
119
- };
116
+ return this.destroy();
120
117
  } else {
121
118
  try {
122
119
  let webhook: Stripe.WebhookEndpoint;
123
120
 
124
- if (ctx.event === "update" && ctx.output?.id) {
121
+ if (this.phase === "update" && this.output?.id) {
125
122
  // Update existing webhook
126
- webhook = await stripe.webhookEndpoints.update(ctx.output.id, {
123
+ webhook = await stripe.webhookEndpoints.update(this.output.id, {
127
124
  url: props.url,
128
125
  enabled_events: props.enabledEvents,
129
126
  description: props.description,
@@ -152,12 +149,11 @@ export class WebhookEndpoint extends Resource(
152
149
  let secret = "";
153
150
  if (webhook.secret) {
154
151
  secret = webhook.secret;
155
- } else if (ctx.event === "update" && ctx.output?.secret) {
156
- secret = ctx.output.secret;
152
+ } else if (this.phase === "update" && this.output?.secret) {
153
+ secret = this.output.secret;
157
154
  }
158
155
 
159
- // Map Stripe API response to our output format
160
- const output: WebhookEndpointOutput = {
156
+ return this({
161
157
  id: webhook.id,
162
158
  url: webhook.url,
163
159
  enabledEvents: webhook.enabled_events as EnabledEvent[],
@@ -172,13 +168,11 @@ export class WebhookEndpoint extends Resource(
172
168
  livemode: webhook.livemode,
173
169
  updatedAt: webhook.created, // Using created timestamp as updated
174
170
  status: webhook.status,
175
- };
176
-
177
- return output;
171
+ });
178
172
  } catch (error) {
179
173
  console.error("Error creating/updating webhook:", error);
180
174
  throw error;
181
175
  }
182
176
  }
183
177
  },
184
- ) {}
178
+ );
@@ -0,0 +1,105 @@
1
+ /// <reference types="bun" />
2
+
3
+ import { afterAll, it } from "bun:test";
4
+ import path from "node:path";
5
+ import { alchemy } from "../alchemy";
6
+ import { destroy } from "../destroy";
7
+ import type { Scope } from "../scope";
8
+
9
+ declare module "../alchemy" {
10
+ interface Alchemy {
11
+ test: typeof test;
12
+ }
13
+ }
14
+
15
+ alchemy.test = test;
16
+
17
+ export interface TestOptions {
18
+ destroy?: boolean;
19
+ quiet?: boolean;
20
+ password?: string;
21
+ }
22
+
23
+ const globalTestScope = alchemy.scope("test");
24
+
25
+ type test = {
26
+ (name: string, fn: (scope: Scope) => Promise<any>, timeout?: number): void;
27
+ (
28
+ name: string,
29
+ options: TestOptions,
30
+ fn: (scope: Scope) => Promise<any>,
31
+ timeout?: number,
32
+ ): void;
33
+ skipIf(condition: boolean): test;
34
+ scope: Scope;
35
+ };
36
+
37
+ export function test(meta: ImportMeta, defaultOptions?: TestOptions): test {
38
+ test.skipIf = (condition: boolean) => {
39
+ if (condition) {
40
+ // TODO: proxy through to bun:test.skipIf
41
+ return (...args: any[]) => {};
42
+ }
43
+ return test;
44
+ };
45
+
46
+ const localTestScope = alchemy.scope(path.basename(meta.filename), {
47
+ parent: globalTestScope,
48
+ });
49
+ test.scope = localTestScope;
50
+
51
+ afterAll(() => alchemy.destroy(test.scope));
52
+
53
+ return test as any;
54
+
55
+ function test(
56
+ ...args:
57
+ | [
58
+ name: string,
59
+ options: TestOptions,
60
+ fn: (scope: Scope) => Promise<void>,
61
+ ]
62
+ | [name: string, fn: (scope: Scope) => Promise<void>]
63
+ ) {
64
+ const testName = args[0];
65
+ const _options = typeof args[1] === "object" ? args[1] : undefined;
66
+ const spread = (obj: any) =>
67
+ obj && typeof obj === "object"
68
+ ? Object.fromEntries(
69
+ Object.entries(obj).flatMap(([k, v]) =>
70
+ v !== undefined ? [[k, v]] : [],
71
+ ),
72
+ )
73
+ : {};
74
+ const options = {
75
+ destroy: false,
76
+ quiet: false,
77
+ password: "test-password",
78
+ ...spread(defaultOptions),
79
+ ...spread(_options),
80
+ };
81
+ const fn = typeof args[1] === "function" ? args[1] : args[2]!;
82
+
83
+ return alchemy.run(
84
+ testName,
85
+ {
86
+ ...options,
87
+ parent: localTestScope,
88
+ },
89
+ async (scope) => {
90
+ return it(testName, async () => {
91
+ // bun will call from a different scope, so we need to enter the test's scope manually
92
+ scope.enter();
93
+ try {
94
+ await fn(scope);
95
+ } finally {
96
+ if (options.destroy !== false) {
97
+ // TODO: auto-destroy resources
98
+ await destroy(scope);
99
+ }
100
+ }
101
+ });
102
+ },
103
+ );
104
+ }
105
+ }