alchemy 0.22.3 → 0.23.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 (178) hide show
  1. package/lib/aws/bucket.d.ts.map +1 -1
  2. package/lib/aws/bucket.js +13 -12
  3. package/lib/aws/bucket.js.map +1 -1
  4. package/lib/aws/control/client.d.ts.map +1 -1
  5. package/lib/aws/control/client.js +5 -3
  6. package/lib/aws/control/client.js.map +1 -1
  7. package/lib/aws/control/resource.d.ts.map +1 -1
  8. package/lib/aws/control/resource.js +13 -4
  9. package/lib/aws/control/resource.js.map +1 -1
  10. package/lib/aws/control/types.d.ts +85129 -0
  11. package/lib/aws/function.d.ts.map +1 -1
  12. package/lib/aws/function.js +51 -42
  13. package/lib/aws/function.js.map +1 -1
  14. package/lib/aws/index.d.ts +1 -0
  15. package/lib/aws/index.d.ts.map +1 -1
  16. package/lib/aws/index.js +1 -0
  17. package/lib/aws/index.js.map +1 -1
  18. package/lib/aws/policy-attachment.d.ts.map +1 -1
  19. package/lib/aws/policy-attachment.js +5 -4
  20. package/lib/aws/policy-attachment.js.map +1 -1
  21. package/lib/aws/policy.d.ts.map +1 -1
  22. package/lib/aws/policy.js +21 -20
  23. package/lib/aws/policy.js.map +1 -1
  24. package/lib/aws/queue.d.ts.map +1 -1
  25. package/lib/aws/queue.js +19 -15
  26. package/lib/aws/queue.js.map +1 -1
  27. package/lib/aws/retry.d.ts +5 -0
  28. package/lib/aws/retry.d.ts.map +1 -0
  29. package/lib/aws/retry.js +27 -0
  30. package/lib/aws/retry.js.map +1 -0
  31. package/lib/aws/role.d.ts.map +1 -1
  32. package/lib/aws/role.js +31 -30
  33. package/lib/aws/role.js.map +1 -1
  34. package/lib/aws/ses.d.ts.map +1 -1
  35. package/lib/aws/ses.js +27 -26
  36. package/lib/aws/ses.js.map +1 -1
  37. package/lib/aws/ssm-parameter.d.ts +150 -0
  38. package/lib/aws/ssm-parameter.d.ts.map +1 -0
  39. package/lib/aws/ssm-parameter.js +184 -0
  40. package/lib/aws/ssm-parameter.js.map +1 -0
  41. package/lib/aws/table.d.ts.map +1 -1
  42. package/lib/aws/table.js +9 -27
  43. package/lib/aws/table.js.map +1 -1
  44. package/lib/cloudflare/account-id.d.ts +1 -0
  45. package/lib/cloudflare/account-id.d.ts.map +1 -1
  46. package/lib/cloudflare/account-id.js.map +1 -1
  47. package/lib/cloudflare/api.d.ts.map +1 -1
  48. package/lib/cloudflare/api.js +22 -2
  49. package/lib/cloudflare/api.js.map +1 -1
  50. package/lib/cloudflare/bindings.d.ts +2 -1
  51. package/lib/cloudflare/bindings.d.ts.map +1 -1
  52. package/lib/cloudflare/bindings.js.map +1 -1
  53. package/lib/cloudflare/bound.d.ts +2 -1
  54. package/lib/cloudflare/bound.d.ts.map +1 -1
  55. package/lib/cloudflare/index.d.ts +2 -0
  56. package/lib/cloudflare/index.d.ts.map +1 -1
  57. package/lib/cloudflare/index.js +2 -0
  58. package/lib/cloudflare/index.js.map +1 -1
  59. package/lib/cloudflare/queue.d.ts.map +1 -1
  60. package/lib/cloudflare/queue.js +1 -0
  61. package/lib/cloudflare/queue.js.map +1 -1
  62. package/lib/cloudflare/route.d.ts +7 -0
  63. package/lib/cloudflare/route.d.ts.map +1 -1
  64. package/lib/cloudflare/route.js +44 -2
  65. package/lib/cloudflare/route.js.map +1 -1
  66. package/lib/cloudflare/version-metadata.d.ts +7 -0
  67. package/lib/cloudflare/version-metadata.d.ts.map +1 -0
  68. package/lib/cloudflare/version-metadata.js +7 -0
  69. package/lib/cloudflare/version-metadata.js.map +1 -0
  70. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  71. package/lib/cloudflare/worker-metadata.js +7 -2
  72. package/lib/cloudflare/worker-metadata.js.map +1 -1
  73. package/lib/cloudflare/worker.d.ts +2 -1
  74. package/lib/cloudflare/worker.d.ts.map +1 -1
  75. package/lib/cloudflare/worker.js +17 -8
  76. package/lib/cloudflare/worker.js.map +1 -1
  77. package/lib/cloudflare/workflow.d.ts +7 -0
  78. package/lib/cloudflare/workflow.d.ts.map +1 -1
  79. package/lib/cloudflare/workflow.js +2 -0
  80. package/lib/cloudflare/workflow.js.map +1 -1
  81. package/lib/cloudflare/wrangler.json.d.ts +7 -0
  82. package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
  83. package/lib/cloudflare/wrangler.json.js +14 -2
  84. package/lib/cloudflare/wrangler.json.js.map +1 -1
  85. package/lib/destroy.js +1 -1
  86. package/lib/destroy.js.map +1 -1
  87. package/lib/dns/godaddy.d.ts.map +1 -1
  88. package/lib/dns/godaddy.js +2 -1
  89. package/lib/dns/godaddy.js.map +1 -1
  90. package/lib/fs/file-system-state-store.d.ts.map +1 -1
  91. package/lib/fs/file-system-state-store.js +14 -2
  92. package/lib/fs/file-system-state-store.js.map +1 -1
  93. package/lib/fs/static-json-file.d.ts +1 -0
  94. package/lib/fs/static-json-file.d.ts.map +1 -1
  95. package/lib/fs/static-json-file.js +11 -8
  96. package/lib/fs/static-json-file.js.map +1 -1
  97. package/lib/github/secret.d.ts.map +1 -1
  98. package/lib/github/secret.js +1 -0
  99. package/lib/github/secret.js.map +1 -1
  100. package/lib/neon/api.d.ts.map +1 -1
  101. package/lib/neon/api.js +2 -1
  102. package/lib/neon/api.js.map +1 -1
  103. package/lib/runtime/shims.d.ts +6 -0
  104. package/lib/runtime/shims.d.ts.map +1 -0
  105. package/lib/runtime/shims.js +27 -0
  106. package/lib/runtime/shims.js.map +1 -0
  107. package/lib/sentry/api.d.ts.map +1 -1
  108. package/lib/sentry/api.js +2 -1
  109. package/lib/sentry/api.js.map +1 -1
  110. package/lib/test/bun.d.ts.map +1 -1
  111. package/lib/test/bun.js +1 -7
  112. package/lib/test/bun.js.map +1 -1
  113. package/lib/test/prune.d.ts +2 -1
  114. package/lib/test/prune.d.ts.map +1 -1
  115. package/lib/test/prune.js +29 -5
  116. package/lib/test/prune.js.map +1 -1
  117. package/lib/test/vitest.d.ts +92 -0
  118. package/lib/test/vitest.d.ts.map +1 -0
  119. package/lib/test/vitest.js +94 -0
  120. package/lib/test/vitest.js.map +1 -0
  121. package/lib/upstash/api.d.ts.map +1 -1
  122. package/lib/upstash/api.js +2 -1
  123. package/lib/upstash/api.js.map +1 -1
  124. package/lib/util/retry.js +2 -2
  125. package/lib/util/retry.js.map +1 -1
  126. package/lib/util/safe-fetch.d.ts +2 -0
  127. package/lib/util/safe-fetch.d.ts.map +1 -0
  128. package/lib/util/safe-fetch.js +48 -0
  129. package/lib/util/safe-fetch.js.map +1 -0
  130. package/lib/vercel/api.d.ts.map +1 -1
  131. package/lib/vercel/api.js +2 -1
  132. package/lib/vercel/api.js.map +1 -1
  133. package/lib/web/vitepress/process-front-matter-files.d.ts.map +1 -1
  134. package/lib/web/vitepress/process-front-matter-files.js +11 -7
  135. package/lib/web/vitepress/process-front-matter-files.js.map +1 -1
  136. package/package.json +12 -1
  137. package/src/alchemy.ts +2 -2
  138. package/src/aws/bucket.ts +55 -36
  139. package/src/aws/control/client.ts +13 -13
  140. package/src/aws/control/resource.ts +19 -3
  141. package/src/aws/function.ts +233 -178
  142. package/src/aws/index.ts +1 -0
  143. package/src/aws/policy-attachment.ts +15 -10
  144. package/src/aws/policy.ts +75 -54
  145. package/src/aws/queue.ts +53 -36
  146. package/src/aws/retry.ts +38 -0
  147. package/src/aws/role.ts +121 -90
  148. package/src/aws/ses.ts +101 -73
  149. package/src/aws/ssm-parameter.ts +315 -0
  150. package/src/aws/table.ts +61 -90
  151. package/src/cloudflare/account-id.ts +2 -0
  152. package/src/cloudflare/api.ts +26 -3
  153. package/src/cloudflare/bindings.ts +2 -0
  154. package/src/cloudflare/bound.ts +8 -5
  155. package/src/cloudflare/index.ts +2 -0
  156. package/src/cloudflare/queue.ts +2 -0
  157. package/src/cloudflare/route.ts +86 -2
  158. package/src/cloudflare/version-metadata.ts +6 -0
  159. package/src/cloudflare/worker-metadata.ts +6 -2
  160. package/src/cloudflare/worker.ts +21 -9
  161. package/src/cloudflare/workflow.ts +8 -0
  162. package/src/cloudflare/wrangler.json.ts +29 -3
  163. package/src/destroy.ts +1 -1
  164. package/src/dns/godaddy.ts +3 -1
  165. package/src/fs/file-system-state-store.ts +16 -2
  166. package/src/fs/static-json-file.ts +12 -8
  167. package/src/github/secret.ts +2 -0
  168. package/src/neon/api.ts +2 -1
  169. package/src/sentry/api.ts +2 -1
  170. package/src/test/bun.ts +1 -7
  171. package/src/test/prune.ts +31 -4
  172. package/src/test/vitest.ts +209 -0
  173. package/src/upstash/api.ts +2 -1
  174. package/src/util/retry.ts +3 -3
  175. package/src/util/safe-fetch.ts +61 -0
  176. package/src/vercel/api.ts +2 -1
  177. package/src/web/vitepress/process-front-matter-files.ts +15 -7
  178. package/lib/tsconfig.test.tsbuildinfo +0 -1
@@ -0,0 +1,209 @@
1
+ import path from "node:path";
2
+ import { afterAll, beforeAll, it } from "vitest";
3
+ import { alchemy } from "../alchemy.ts";
4
+ import { R2RestStateStore } from "../cloudflare/r2-rest-state-store.ts";
5
+ import { Scope } from "../scope.ts";
6
+ import type { StateStoreType } from "../state.ts";
7
+
8
+ /**
9
+ * Extend the Alchemy interface to include test functionality
10
+ */
11
+ declare module "../alchemy.ts" {
12
+ interface Alchemy {
13
+ test: typeof test;
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Add test functionality to alchemy instance
19
+ */
20
+ alchemy.test = test;
21
+
22
+ /**
23
+ * Options for configuring test behavior
24
+ */
25
+ export interface TestOptions {
26
+ /**
27
+ * Whether to suppress logging output.
28
+ * @default false.
29
+ */
30
+ quiet?: boolean;
31
+
32
+ /**
33
+ * Password to use for test resources.
34
+ * @default "test-password".
35
+ */
36
+ password?: string;
37
+
38
+ /**
39
+ * Override the default state store for the test.
40
+ */
41
+ stateStore?: StateStoreType;
42
+
43
+ /**
44
+ * Prefix to use for the scope to isolate tests and environments.
45
+ */
46
+ prefix?: string;
47
+ }
48
+
49
+ /**
50
+ * Test function type definition with overloads
51
+ */
52
+ type test = {
53
+ /**
54
+ * Create a test with default options
55
+ * @param name Test name
56
+ * @param fn Test function
57
+ * @param timeout Optional timeout in milliseconds
58
+ */
59
+ (name: string, fn: (scope: Scope) => Promise<any>, timeout?: number): void;
60
+
61
+ /**
62
+ * Create a test with custom options
63
+ * @param name Test name
64
+ * @param options Test configuration options
65
+ * @param fn Test function
66
+ * @param timeout Optional timeout in milliseconds
67
+ */
68
+ (
69
+ name: string,
70
+ options: TestOptions,
71
+ fn: (scope: Scope) => Promise<any>,
72
+ timeout?: number,
73
+ ): void;
74
+
75
+ /**
76
+ * Skip test conditionally
77
+ * @param condition If true, test will be skipped
78
+ */
79
+ skipIf(condition: boolean): test;
80
+
81
+ beforeAll(fn: (scope: Scope) => Promise<void>): void;
82
+
83
+ afterAll(fn: (scope: Scope) => Promise<void>): void;
84
+
85
+ /**
86
+ * Current test scope
87
+ */
88
+ scope: Scope;
89
+ };
90
+
91
+ /**
92
+ * Creates a test helper function that provides scoped resource management
93
+ *
94
+ * @param meta Import meta object from the test file
95
+ * @param defaultOptions Default options to apply to all tests
96
+ * @returns Test function with scope management
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * const test = alchemy.test(import.meta, {
101
+ prefix: BRANCH_PREFIX
102
+ });
103
+ *
104
+ * describe("My Resource", () => {
105
+ * test("create and delete", async (scope) => {
106
+ * try {
107
+ * const resource = await MyResource("test", { ... });
108
+ * expect(resource.id).toBeTruthy();
109
+ * } finally {
110
+ * await alchemy.destroy(scope);
111
+ * }
112
+ * });
113
+ * });
114
+ * ```
115
+ */
116
+ export function test(meta: ImportMeta, defaultOptions?: TestOptions): test {
117
+ defaultOptions = defaultOptions ?? {};
118
+ if (
119
+ defaultOptions.stateStore === undefined &&
120
+ process.env.ALCHEMY_STATE_STORE === "cloudflare"
121
+ ) {
122
+ defaultOptions.stateStore = (scope) =>
123
+ new R2RestStateStore(scope, {
124
+ apiKey: alchemy.secret(process.env.CLOUDFLARE_API_KEY),
125
+ email: process.env.CLOUDFLARE_EMAIL,
126
+ bucketName: process.env.CLOUDFLARE_BUCKET_NAME!,
127
+ });
128
+ }
129
+
130
+ test.skipIf = (condition: boolean) => {
131
+ if (condition) {
132
+ return (..._args: any[]) => {};
133
+ }
134
+ return test;
135
+ };
136
+
137
+ const scope = new Scope({
138
+ scopeName: `${defaultOptions.prefix ? `${defaultOptions.prefix}-` : ""}${path.basename(meta.filename)}`,
139
+ stateStore: defaultOptions?.stateStore,
140
+ phase: "up",
141
+ });
142
+
143
+ test.beforeAll = (fn: (scope: Scope) => Promise<void>) => {
144
+ return beforeAll(() => scope.run(() => fn(scope)));
145
+ };
146
+
147
+ test.afterAll = (fn: (scope: Scope) => Promise<void>) => {
148
+ return afterAll(() => scope.run(() => fn(scope)));
149
+ };
150
+
151
+ return test as test;
152
+
153
+ function test(
154
+ ...args:
155
+ | [
156
+ name: string,
157
+ options: TestOptions,
158
+ fn: (scope: Scope) => Promise<void>,
159
+ ]
160
+ | [name: string, fn: (scope: Scope) => Promise<void>]
161
+ ) {
162
+ const testName = args[0];
163
+ const _options = typeof args[1] === "object" ? args[1] : undefined;
164
+ const timeout =
165
+ typeof args[args.length - 1] === "number"
166
+ ? (args[args.length - 1] as number)
167
+ : 120000;
168
+ const spread = (obj: any) =>
169
+ obj && typeof obj === "object"
170
+ ? Object.fromEntries(
171
+ Object.entries(obj).flatMap(([k, v]) =>
172
+ v !== undefined ? [[k, v]] : [],
173
+ ),
174
+ )
175
+ : {};
176
+
177
+ const options: TestOptions = {
178
+ quiet: false,
179
+ password: "test-password",
180
+ ...spread(defaultOptions),
181
+ ...spread(_options),
182
+ };
183
+
184
+ const fn = typeof args[1] === "function" ? args[1] : args[2]!;
185
+
186
+ return it(
187
+ testName,
188
+ (ctx) => {
189
+ // Get the current describe block name from the test context
190
+ let describeBlockName = "";
191
+ if (ctx.task.suite?.name) {
192
+ describeBlockName = `${ctx.task.suite.name}/`;
193
+ }
194
+
195
+ return alchemy.run(
196
+ `${describeBlockName}${testName}`,
197
+ {
198
+ ...options,
199
+ parent: scope,
200
+ },
201
+ async (scope) => {
202
+ await scope.run(() => fn(scope));
203
+ },
204
+ );
205
+ },
206
+ timeout,
207
+ );
208
+ }
209
+ }
@@ -1,4 +1,5 @@
1
1
  import type { Secret } from "../secret.ts";
2
+ import { safeFetch } from "../util/safe-fetch.ts";
2
3
 
3
4
  /**
4
5
  * Options for Upstash API requests
@@ -68,7 +69,7 @@ export class UpstashApi {
68
69
  });
69
70
  }
70
71
 
71
- return fetch(`${this.baseUrl}${path}`, {
72
+ return safeFetch(`${this.baseUrl}${path}`, {
72
73
  ...init,
73
74
  headers,
74
75
  });
package/src/util/retry.ts CHANGED
@@ -21,11 +21,10 @@ export async function withExponentialBackoff<T>(
21
21
  while (true) {
22
22
  try {
23
23
  return await operation();
24
- } catch (error) {
25
- console.log("error", error);
24
+ } catch (error: any) {
25
+ console.log(error.message);
26
26
  attempt++;
27
27
  if (attempt >= maxAttempts || !isRetryable(error)) {
28
- console.warn("Failed after ", attempt, " attempts");
29
28
  throw error;
30
29
  }
31
30
 
@@ -33,6 +32,7 @@ export async function withExponentialBackoff<T>(
33
32
  const jitter = Math.random() * 0.1 * delay;
34
33
  await new Promise((resolve) => setTimeout(resolve, delay + jitter));
35
34
  delay *= 2; // Double the delay for next attempt
35
+ delay = Math.min(delay, 10000); // Cap at 10 seconds
36
36
  }
37
37
  }
38
38
  }
@@ -0,0 +1,61 @@
1
+ import type { Agent } from "undici";
2
+
3
+ export async function safeFetch(
4
+ url: string | URL | Request,
5
+ options: RequestInit = {},
6
+ retries = 3,
7
+ ) {
8
+ let latestErr: any;
9
+ for (let attempt = 1; attempt <= retries; attempt++) {
10
+ try {
11
+ return await fetch(url, {
12
+ ...options,
13
+ // Type assertion to work around Cloudflare Workers type definitions
14
+ dispatcher: await dispatcher(),
15
+ } as RequestInit);
16
+ } catch (err: any) {
17
+ latestErr = err;
18
+ const shouldRetry =
19
+ err?.code === "UND_ERR_SOCKET" ||
20
+ err?.code === "ECONNRESET" ||
21
+ err?.name === "FetchError";
22
+
23
+ if (!shouldRetry || attempt === retries) {
24
+ throw err;
25
+ }
26
+
27
+ console.warn(`Retry ${attempt}/${retries} for ${url}: ${err.code}`);
28
+ await new Promise((r) => setTimeout(r, 250 * attempt)); // exponential backoff
29
+ }
30
+ }
31
+ throw latestErr;
32
+ }
33
+
34
+ let agent:
35
+ | {
36
+ value: Agent | undefined;
37
+ }
38
+ | undefined;
39
+
40
+ async function dispatcher() {
41
+ if (agent) {
42
+ return agent.value;
43
+ }
44
+ try {
45
+ // undici not available in all runtimes
46
+ const { Agent } = await import("undici");
47
+
48
+ if (!agent) {
49
+ // Configure agent with connection limits to avoid socket errors
50
+ agent = {
51
+ value: new Agent({
52
+ pipelining: 0, // Disable pipelining to avoid connection issues
53
+ // connections: 5, // Limit concurrent connections per host
54
+ }),
55
+ };
56
+ }
57
+ } catch {
58
+ agent = { value: undefined };
59
+ }
60
+ return agent.value;
61
+ }
package/src/vercel/api.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { alchemy } from "../alchemy.ts";
2
2
  import type { Secret } from "../secret.ts";
3
+ import { safeFetch } from "../util/safe-fetch.ts";
3
4
 
4
5
  /**
5
6
  * Options for Vercel API requests
@@ -85,7 +86,7 @@ export class VercelApi {
85
86
  delete headers["Content-Type"];
86
87
  }
87
88
 
88
- const response = await fetch(`${this.baseUrl}${path}`, {
89
+ const response = await safeFetch(`${this.baseUrl}${path}`, {
89
90
  ...init,
90
91
  headers,
91
92
  });
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { parse as parseYaml } from "yaml";
3
4
 
4
5
  /**
5
6
  * Process markdown files with frontmatter to generate navigation items
@@ -56,14 +57,21 @@ export async function processFrontmatterFiles(
56
57
  let title;
57
58
 
58
59
  if (frontmatterMatch) {
59
- const frontmatter = frontmatterMatch[1];
60
- const orderMatch = frontmatter.match(/order:\s*(\d+)/);
61
- if (orderMatch) {
62
- order = Number.parseFloat(orderMatch[1]);
60
+ const frontmatter = parseYaml(frontmatterMatch[1]);
61
+
62
+ // Parse order as a number (supports both integers and floats)
63
+ if (frontmatter.order !== undefined) {
64
+ if (typeof frontmatter.order !== "number") {
65
+ throw new Error(
66
+ `Invalid order field in ${fullPath}: expected number, got ${typeof frontmatter.order}`,
67
+ );
68
+ }
69
+ order = frontmatter.order;
63
70
  }
64
- const titleMatch = frontmatter.match(/title:\s*(.+)/);
65
- if (titleMatch) {
66
- title = titleMatch[1].trim();
71
+
72
+ // Get title from frontmatter
73
+ if (frontmatter.title) {
74
+ title = String(frontmatter.title).trim();
67
75
  }
68
76
  }
69
77