alchemy 0.22.4 → 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.
- package/lib/aws/bucket.d.ts.map +1 -1
- package/lib/aws/bucket.js +13 -12
- package/lib/aws/bucket.js.map +1 -1
- package/lib/aws/control/client.d.ts.map +1 -1
- package/lib/aws/control/client.js +5 -3
- package/lib/aws/control/client.js.map +1 -1
- package/lib/aws/control/resource.d.ts.map +1 -1
- package/lib/aws/control/resource.js +13 -4
- package/lib/aws/control/resource.js.map +1 -1
- package/lib/aws/control/types.d.ts +85129 -0
- package/lib/aws/function.d.ts.map +1 -1
- package/lib/aws/function.js +51 -42
- package/lib/aws/function.js.map +1 -1
- package/lib/aws/index.d.ts +1 -0
- package/lib/aws/index.d.ts.map +1 -1
- package/lib/aws/index.js +1 -0
- package/lib/aws/index.js.map +1 -1
- package/lib/aws/policy-attachment.d.ts.map +1 -1
- package/lib/aws/policy-attachment.js +5 -4
- package/lib/aws/policy-attachment.js.map +1 -1
- package/lib/aws/policy.d.ts.map +1 -1
- package/lib/aws/policy.js +21 -20
- package/lib/aws/policy.js.map +1 -1
- package/lib/aws/queue.d.ts.map +1 -1
- package/lib/aws/queue.js +19 -15
- package/lib/aws/queue.js.map +1 -1
- package/lib/aws/retry.d.ts +5 -0
- package/lib/aws/retry.d.ts.map +1 -0
- package/lib/aws/retry.js +27 -0
- package/lib/aws/retry.js.map +1 -0
- package/lib/aws/role.d.ts.map +1 -1
- package/lib/aws/role.js +31 -30
- package/lib/aws/role.js.map +1 -1
- package/lib/aws/ses.d.ts.map +1 -1
- package/lib/aws/ses.js +27 -26
- package/lib/aws/ses.js.map +1 -1
- package/lib/aws/ssm-parameter.d.ts +150 -0
- package/lib/aws/ssm-parameter.d.ts.map +1 -0
- package/lib/aws/ssm-parameter.js +184 -0
- package/lib/aws/ssm-parameter.js.map +1 -0
- package/lib/aws/table.d.ts.map +1 -1
- package/lib/aws/table.js +9 -27
- package/lib/aws/table.js.map +1 -1
- package/lib/cloudflare/account-id.d.ts +1 -0
- package/lib/cloudflare/account-id.d.ts.map +1 -1
- package/lib/cloudflare/account-id.js.map +1 -1
- package/lib/cloudflare/api.d.ts.map +1 -1
- package/lib/cloudflare/api.js +22 -2
- package/lib/cloudflare/api.js.map +1 -1
- package/lib/cloudflare/index.d.ts +2 -1
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +2 -1
- package/lib/cloudflare/index.js.map +1 -1
- package/lib/cloudflare/queue.d.ts.map +1 -1
- package/lib/cloudflare/queue.js +1 -0
- package/lib/cloudflare/queue.js.map +1 -1
- package/lib/cloudflare/route.d.ts +7 -0
- package/lib/cloudflare/route.d.ts.map +1 -1
- package/lib/cloudflare/route.js +44 -2
- package/lib/cloudflare/route.js.map +1 -1
- package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
- package/lib/cloudflare/worker-metadata.js +1 -2
- package/lib/cloudflare/worker-metadata.js.map +1 -1
- package/lib/cloudflare/worker.d.ts +2 -1
- package/lib/cloudflare/worker.d.ts.map +1 -1
- package/lib/cloudflare/worker.js +17 -8
- package/lib/cloudflare/worker.js.map +1 -1
- package/lib/cloudflare/workflow.d.ts +7 -0
- package/lib/cloudflare/workflow.d.ts.map +1 -1
- package/lib/cloudflare/workflow.js +2 -0
- package/lib/cloudflare/workflow.js.map +1 -1
- package/lib/cloudflare/wrangler.json.d.ts +1 -0
- package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
- package/lib/cloudflare/wrangler.json.js +1 -0
- package/lib/cloudflare/wrangler.json.js.map +1 -1
- package/lib/destroy.js +1 -1
- package/lib/destroy.js.map +1 -1
- package/lib/dns/godaddy.d.ts.map +1 -1
- package/lib/dns/godaddy.js +2 -1
- package/lib/dns/godaddy.js.map +1 -1
- package/lib/fs/file-system-state-store.d.ts.map +1 -1
- package/lib/fs/file-system-state-store.js +14 -2
- package/lib/fs/file-system-state-store.js.map +1 -1
- package/lib/github/secret.d.ts.map +1 -1
- package/lib/github/secret.js +1 -0
- package/lib/github/secret.js.map +1 -1
- package/lib/neon/api.d.ts.map +1 -1
- package/lib/neon/api.js +2 -1
- package/lib/neon/api.js.map +1 -1
- package/lib/runtime/shims.d.ts +6 -0
- package/lib/runtime/shims.d.ts.map +1 -0
- package/lib/runtime/shims.js +27 -0
- package/lib/runtime/shims.js.map +1 -0
- package/lib/sentry/api.d.ts.map +1 -1
- package/lib/sentry/api.js +2 -1
- package/lib/sentry/api.js.map +1 -1
- package/lib/test/bun.d.ts.map +1 -1
- package/lib/test/bun.js +1 -7
- package/lib/test/bun.js.map +1 -1
- package/lib/test/prune.d.ts +2 -1
- package/lib/test/prune.d.ts.map +1 -1
- package/lib/test/prune.js +29 -5
- package/lib/test/prune.js.map +1 -1
- package/lib/test/vitest.d.ts +92 -0
- package/lib/test/vitest.d.ts.map +1 -0
- package/lib/test/vitest.js +94 -0
- package/lib/test/vitest.js.map +1 -0
- package/lib/upstash/api.d.ts.map +1 -1
- package/lib/upstash/api.js +2 -1
- package/lib/upstash/api.js.map +1 -1
- package/lib/util/retry.js +2 -2
- package/lib/util/retry.js.map +1 -1
- package/lib/util/safe-fetch.d.ts +2 -0
- package/lib/util/safe-fetch.d.ts.map +1 -0
- package/lib/util/safe-fetch.js +48 -0
- package/lib/util/safe-fetch.js.map +1 -0
- package/lib/vercel/api.d.ts.map +1 -1
- package/lib/vercel/api.js +2 -1
- package/lib/vercel/api.js.map +1 -1
- package/lib/web/vitepress/process-front-matter-files.d.ts.map +1 -1
- package/lib/web/vitepress/process-front-matter-files.js +11 -7
- package/lib/web/vitepress/process-front-matter-files.js.map +1 -1
- package/package.json +10 -6
- package/src/alchemy.ts +2 -2
- package/src/aws/bucket.ts +55 -36
- package/src/aws/control/client.ts +13 -13
- package/src/aws/control/resource.ts +19 -3
- package/src/aws/function.ts +233 -178
- package/src/aws/index.ts +1 -0
- package/src/aws/policy-attachment.ts +15 -10
- package/src/aws/policy.ts +75 -54
- package/src/aws/queue.ts +53 -36
- package/src/aws/retry.ts +38 -0
- package/src/aws/role.ts +121 -90
- package/src/aws/ses.ts +101 -73
- package/src/aws/ssm-parameter.ts +315 -0
- package/src/aws/table.ts +61 -90
- package/src/cloudflare/account-id.ts +2 -0
- package/src/cloudflare/api.ts +26 -3
- package/src/cloudflare/index.ts +2 -1
- package/src/cloudflare/queue.ts +2 -0
- package/src/cloudflare/route.ts +86 -2
- package/src/cloudflare/worker-metadata.ts +1 -2
- package/src/cloudflare/worker.ts +21 -9
- package/src/cloudflare/workflow.ts +8 -0
- package/src/cloudflare/wrangler.json.ts +8 -1
- package/src/destroy.ts +1 -1
- package/src/dns/godaddy.ts +3 -1
- package/src/fs/file-system-state-store.ts +16 -2
- package/src/github/secret.ts +2 -0
- package/src/neon/api.ts +2 -1
- package/src/sentry/api.ts +2 -1
- package/src/test/bun.ts +1 -7
- package/src/test/prune.ts +31 -4
- package/src/test/vitest.ts +209 -0
- package/src/upstash/api.ts +2 -1
- package/src/util/retry.ts +3 -3
- package/src/util/safe-fetch.ts +61 -0
- package/src/vercel/api.ts +2 -1
- package/src/web/vitepress/process-front-matter-files.ts +15 -7
package/src/cloudflare/route.ts
CHANGED
|
@@ -28,6 +28,14 @@ export interface RouteProps extends CloudflareApiOptions {
|
|
|
28
28
|
* Zone ID for the route
|
|
29
29
|
*/
|
|
30
30
|
zoneId: string;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Whether to adopt an existing route with the same pattern if it exists
|
|
34
|
+
* If true and a route with the same pattern exists, it will be adopted rather than creating a new one
|
|
35
|
+
*
|
|
36
|
+
* @default false
|
|
37
|
+
*/
|
|
38
|
+
adopt?: boolean;
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
/**
|
|
@@ -133,8 +141,44 @@ export const Route = Resource(
|
|
|
133
141
|
} else {
|
|
134
142
|
console.log("Creating Route:", props.pattern);
|
|
135
143
|
|
|
136
|
-
|
|
137
|
-
|
|
144
|
+
try {
|
|
145
|
+
// Create new route
|
|
146
|
+
routeData = await createRoute(api, zoneId, props.pattern, scriptName);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
// Check if this is a "route already exists" error and adopt is enabled
|
|
149
|
+
if (
|
|
150
|
+
props.adopt &&
|
|
151
|
+
error instanceof CloudflareApiError &&
|
|
152
|
+
error.status === 409
|
|
153
|
+
) {
|
|
154
|
+
console.log(
|
|
155
|
+
`Route with pattern '${props.pattern}' already exists, adopting it`,
|
|
156
|
+
);
|
|
157
|
+
// Find the existing route by pattern
|
|
158
|
+
const existingRoute = await findRouteByPattern(
|
|
159
|
+
api,
|
|
160
|
+
zoneId,
|
|
161
|
+
props.pattern,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
if (!existingRoute) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Failed to find existing route with pattern '${props.pattern}' for adoption`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Use the existing route data
|
|
171
|
+
routeData = {
|
|
172
|
+
result: existingRoute,
|
|
173
|
+
success: true,
|
|
174
|
+
errors: [],
|
|
175
|
+
messages: [],
|
|
176
|
+
};
|
|
177
|
+
} else {
|
|
178
|
+
// Re-throw the error if adopt is false or it's not a 409 error
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
138
182
|
}
|
|
139
183
|
|
|
140
184
|
// Return the route resource
|
|
@@ -266,3 +310,43 @@ export async function listRoutes(
|
|
|
266
310
|
|
|
267
311
|
return (await response.json()) as CloudflareRouteResponse;
|
|
268
312
|
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Find a route by pattern
|
|
316
|
+
*/
|
|
317
|
+
async function findRouteByPattern(
|
|
318
|
+
api: CloudflareApi,
|
|
319
|
+
zoneId: string,
|
|
320
|
+
pattern: string,
|
|
321
|
+
): Promise<{ id: string; pattern: string; script: string } | null> {
|
|
322
|
+
const response = await api.get(`/zones/${zoneId}/workers/routes`);
|
|
323
|
+
|
|
324
|
+
if (!response.ok) {
|
|
325
|
+
throw new CloudflareApiError(
|
|
326
|
+
`Failed to list routes: ${response.statusText}`,
|
|
327
|
+
response,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const data = (await response.json()) as {
|
|
332
|
+
result: Array<{
|
|
333
|
+
id: string;
|
|
334
|
+
pattern: string;
|
|
335
|
+
script: string;
|
|
336
|
+
}>;
|
|
337
|
+
success: boolean;
|
|
338
|
+
errors: Array<{ code: number; message: string }>;
|
|
339
|
+
messages: string[];
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
if (!data.success) {
|
|
343
|
+
throw new CloudflareApiError(
|
|
344
|
+
`Failed to list routes: ${data.errors?.[0]?.message || "Unknown error"}`,
|
|
345
|
+
response,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Look for a route with matching pattern
|
|
350
|
+
const match = data.result.find((route) => route.pattern === pattern);
|
|
351
|
+
return match || null;
|
|
352
|
+
}
|
|
@@ -403,8 +403,7 @@ export async function prepareWorkerMetadata<B extends Bindings>(
|
|
|
403
403
|
name: bindingName,
|
|
404
404
|
workflow_name: binding.workflowName,
|
|
405
405
|
class_name: binding.className,
|
|
406
|
-
|
|
407
|
-
// script_name: ??,
|
|
406
|
+
script_name: binding.scriptName,
|
|
408
407
|
});
|
|
409
408
|
// it's unclear whether this is needed, but it works both ways
|
|
410
409
|
// configureClassMigration(binding, binding.id, binding.className);
|
package/src/cloudflare/worker.ts
CHANGED
|
@@ -58,7 +58,7 @@ import {
|
|
|
58
58
|
} from "./worker-metadata.ts";
|
|
59
59
|
import type { SingleStepMigration } from "./worker-migration.ts";
|
|
60
60
|
import { WorkerStub, isWorkerStub } from "./worker-stub.ts";
|
|
61
|
-
import {
|
|
61
|
+
import { Workflow, isWorkflow, upsertWorkflow } from "./workflow.ts";
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
64
|
* Configuration options for static assets
|
|
@@ -351,6 +351,8 @@ export type Worker<
|
|
|
351
351
|
/**
|
|
352
352
|
* The worker's URL if enabled
|
|
353
353
|
* Format: {name}.{subdomain}.workers.dev
|
|
354
|
+
*
|
|
355
|
+
* @default true
|
|
354
356
|
*/
|
|
355
357
|
url?: string;
|
|
356
358
|
|
|
@@ -391,7 +393,6 @@ export type Worker<
|
|
|
391
393
|
* const api = await Worker("api", {
|
|
392
394
|
* name: "api-worker",
|
|
393
395
|
* entrypoint: "./src/api.ts",
|
|
394
|
-
* routes: ["api.example.com/*"],
|
|
395
396
|
* url: true
|
|
396
397
|
* });
|
|
397
398
|
*
|
|
@@ -802,11 +803,16 @@ export const _Worker = Resource(
|
|
|
802
803
|
await putWorker(api, workerName, scriptBundle, scriptMetadata);
|
|
803
804
|
|
|
804
805
|
for (const workflow of workflowsBindings) {
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
806
|
+
if (
|
|
807
|
+
workflow.scriptName === undefined ||
|
|
808
|
+
workflow.scriptName === workerName
|
|
809
|
+
) {
|
|
810
|
+
await upsertWorkflow(api, {
|
|
811
|
+
workflowName: workflow.workflowName,
|
|
812
|
+
className: workflow.className,
|
|
813
|
+
scriptName: workflow.scriptName ?? workerName,
|
|
814
|
+
});
|
|
815
|
+
}
|
|
810
816
|
}
|
|
811
817
|
|
|
812
818
|
await Promise.all(
|
|
@@ -833,7 +839,7 @@ export const _Worker = Resource(
|
|
|
833
839
|
this,
|
|
834
840
|
api,
|
|
835
841
|
workerName,
|
|
836
|
-
props.url ??
|
|
842
|
+
props.url ?? true,
|
|
837
843
|
);
|
|
838
844
|
|
|
839
845
|
// Get current timestamp
|
|
@@ -922,7 +928,13 @@ export const _Worker = Resource(
|
|
|
922
928
|
// re-export this binding mapping to the host worker (this worker)
|
|
923
929
|
scriptName: workerName,
|
|
924
930
|
})
|
|
925
|
-
: binding
|
|
931
|
+
: isWorkflow(binding) && binding.scriptName === undefined
|
|
932
|
+
? new Workflow(binding.id, {
|
|
933
|
+
...binding,
|
|
934
|
+
// re-export this binding mapping to the host worker (this worker)
|
|
935
|
+
scriptName: workerName,
|
|
936
|
+
})
|
|
937
|
+
: binding,
|
|
926
938
|
],
|
|
927
939
|
),
|
|
928
940
|
);
|
|
@@ -19,6 +19,12 @@ export interface WorkflowProps {
|
|
|
19
19
|
* @default - workflowName if provided, otherwise id
|
|
20
20
|
*/
|
|
21
21
|
className?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Name of the script containing the workflow implementation
|
|
24
|
+
*
|
|
25
|
+
* @default - bound worker script
|
|
26
|
+
*/
|
|
27
|
+
scriptName?: string;
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
export function isWorkflow(binding: Binding): binding is Workflow {
|
|
@@ -36,6 +42,7 @@ export class Workflow<PARAMS = unknown> {
|
|
|
36
42
|
|
|
37
43
|
public readonly workflowName: string;
|
|
38
44
|
public readonly className: string;
|
|
45
|
+
public readonly scriptName?: string;
|
|
39
46
|
|
|
40
47
|
constructor(
|
|
41
48
|
public readonly id: string,
|
|
@@ -43,6 +50,7 @@ export class Workflow<PARAMS = unknown> {
|
|
|
43
50
|
) {
|
|
44
51
|
this.workflowName = props.workflowName ?? props.className ?? id;
|
|
45
52
|
this.className = props.className ?? this.workflowName;
|
|
53
|
+
this.scriptName = props.scriptName;
|
|
46
54
|
}
|
|
47
55
|
}
|
|
48
56
|
|
|
@@ -264,6 +264,7 @@ export interface WranglerJsonSpec {
|
|
|
264
264
|
name: string;
|
|
265
265
|
binding: string;
|
|
266
266
|
class_name: string;
|
|
267
|
+
script_name?: string;
|
|
267
268
|
}[];
|
|
268
269
|
|
|
269
270
|
/**
|
|
@@ -369,7 +370,12 @@ function processBindings(
|
|
|
369
370
|
const services: { binding: string; service: string; environment?: string }[] =
|
|
370
371
|
[];
|
|
371
372
|
const secrets: string[] = [];
|
|
372
|
-
const workflows: {
|
|
373
|
+
const workflows: {
|
|
374
|
+
name: string;
|
|
375
|
+
binding: string;
|
|
376
|
+
class_name: string;
|
|
377
|
+
script_name?: string;
|
|
378
|
+
}[] = [];
|
|
373
379
|
const d1Databases: {
|
|
374
380
|
binding: string;
|
|
375
381
|
database_id: string;
|
|
@@ -484,6 +490,7 @@ function processBindings(
|
|
|
484
490
|
name: binding.workflowName,
|
|
485
491
|
binding: bindingName,
|
|
486
492
|
class_name: binding.className,
|
|
493
|
+
script_name: binding.scriptName,
|
|
487
494
|
});
|
|
488
495
|
} else if (binding.type === "d1") {
|
|
489
496
|
d1Databases.push({
|
package/src/destroy.ts
CHANGED
|
@@ -72,7 +72,7 @@ export async function destroy<Type extends string>(
|
|
|
72
72
|
const Provider = resolveDeletionHandler(instance[ResourceKind]);
|
|
73
73
|
if (!Provider) {
|
|
74
74
|
throw new Error(
|
|
75
|
-
`Cannot destroy resource "${instance[ResourceFQN]}" type ${instance[ResourceKind]} - no provider found. You may need to import the provider in your alchemy.
|
|
75
|
+
`Cannot destroy resource "${instance[ResourceFQN]}" type ${instance[ResourceKind]} - no provider found. You may need to import the provider in your alchemy.run.ts.`,
|
|
76
76
|
);
|
|
77
77
|
}
|
|
78
78
|
|
package/src/dns/godaddy.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { safeFetch } from "../util/safe-fetch.ts";
|
|
2
|
+
|
|
1
3
|
export type UpdateNameserversOptions = {
|
|
2
4
|
domain: string;
|
|
3
5
|
apiKey: string;
|
|
@@ -8,7 +10,7 @@ export type UpdateNameserversOptions = {
|
|
|
8
10
|
export async function updateNameservers(options: UpdateNameserversOptions) {
|
|
9
11
|
const url = `https://api.godaddy.com/v1/domains/${options.domain}/nameservers`;
|
|
10
12
|
|
|
11
|
-
const response = await
|
|
13
|
+
const response = await safeFetch(url, {
|
|
12
14
|
method: "PUT",
|
|
13
15
|
headers: {
|
|
14
16
|
Authorization: `sso-key ${options.apiKey}:${options.apiSecret}`,
|
|
@@ -24,7 +24,19 @@ export class FileSystemStateStore implements StateStore {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
async deinit(): Promise<void> {
|
|
27
|
-
await ignore("ENOENT", () =>
|
|
27
|
+
await ignore("ENOENT", async () => {
|
|
28
|
+
const entries = await fs.promises.readdir(this.dir, {
|
|
29
|
+
withFileTypes: true,
|
|
30
|
+
});
|
|
31
|
+
const files = entries.filter((entry) => entry.isFile());
|
|
32
|
+
if (files.length > 0) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`Cannot deinit: directory "${this.dir}" contains files and cannot be deleted.`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
// Only folders (or empty), delete recursively
|
|
38
|
+
await fs.promises.rm(this.dir, { recursive: true, force: true });
|
|
39
|
+
});
|
|
28
40
|
}
|
|
29
41
|
|
|
30
42
|
async count(): Promise<number> {
|
|
@@ -67,8 +79,10 @@ export class FileSystemStateStore implements StateStore {
|
|
|
67
79
|
|
|
68
80
|
async set(key: string, value: State): Promise<void> {
|
|
69
81
|
await this.init();
|
|
82
|
+
const file = this.getPath(key);
|
|
83
|
+
await fs.promises.mkdir(path.dirname(file), { recursive: true });
|
|
70
84
|
await fs.promises.writeFile(
|
|
71
|
-
|
|
85
|
+
file,
|
|
72
86
|
JSON.stringify(await serialize(this.scope, value), null, 2),
|
|
73
87
|
);
|
|
74
88
|
}
|
package/src/github/secret.ts
CHANGED
package/src/neon/api.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Secret } from "../secret.ts";
|
|
2
2
|
import { withExponentialBackoff } from "../util/retry.ts";
|
|
3
|
+
import { safeFetch } from "../util/safe-fetch.ts";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Options for Neon API requests
|
|
@@ -98,7 +99,7 @@ export class NeonApi {
|
|
|
98
99
|
// Use withExponentialBackoff for automatic retry on network errors
|
|
99
100
|
return withExponentialBackoff(
|
|
100
101
|
() =>
|
|
101
|
-
|
|
102
|
+
safeFetch(`${this.baseUrl}${path}`, {
|
|
102
103
|
...init,
|
|
103
104
|
headers,
|
|
104
105
|
}),
|
package/src/sentry/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 Sentry API requests
|
|
@@ -60,7 +61,7 @@ export class SentryApi {
|
|
|
60
61
|
delete headers["Content-Type"];
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
return
|
|
64
|
+
return safeFetch(`${this.baseUrl}${path}`, {
|
|
64
65
|
...init,
|
|
65
66
|
headers,
|
|
66
67
|
});
|
package/src/test/bun.ts
CHANGED
|
@@ -131,13 +131,7 @@ export function test(meta: ImportMeta, defaultOptions?: TestOptions): test {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
// Add skipIf functionality
|
|
134
|
-
test.skipIf = (
|
|
135
|
-
if (condition) {
|
|
136
|
-
// TODO: proxy through to bun:test.skipIf
|
|
137
|
-
return (..._args: any[]) => {};
|
|
138
|
-
}
|
|
139
|
-
return test;
|
|
140
|
-
};
|
|
134
|
+
test.skipIf = it.skipIf.bind(it);
|
|
141
135
|
|
|
142
136
|
// Create local test scope based on filename
|
|
143
137
|
const scope = new Scope({
|
package/src/test/prune.ts
CHANGED
|
@@ -10,23 +10,50 @@ const execAsync = promisify(exec);
|
|
|
10
10
|
* Runs only tests that have changed dependencies
|
|
11
11
|
* @param directory Directory to scan for test files
|
|
12
12
|
* @param baseCommit Optional base commit to compare against
|
|
13
|
+
* @param useVitest Whether to use vitest instead of bun test
|
|
13
14
|
*/
|
|
14
15
|
export async function runChangedTests(
|
|
15
16
|
directory: string,
|
|
16
17
|
baseCommit?: string,
|
|
18
|
+
useVitest?: boolean,
|
|
17
19
|
): Promise<void> {
|
|
18
20
|
const changedTests = await findChangedTestFiles(directory, baseCommit);
|
|
19
21
|
|
|
22
|
+
// Always include smoke test
|
|
23
|
+
const smokeTestPath = "alchemy/test/smoke.test.ts";
|
|
24
|
+
if (!changedTests.includes(smokeTestPath)) {
|
|
25
|
+
changedTests.unshift(smokeTestPath); // Add at the beginning
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (process.env.CI) {
|
|
29
|
+
// these tests are being really weird in GitHub CI.
|
|
30
|
+
// not the priority to test in CI
|
|
31
|
+
const testsToRemove = [
|
|
32
|
+
"alchemy/test/cloudflare/dns-records.test.ts",
|
|
33
|
+
"alchemy/test/cloudflare/route.test.ts",
|
|
34
|
+
];
|
|
35
|
+
for (const test of testsToRemove) {
|
|
36
|
+
const idx = changedTests.indexOf(test);
|
|
37
|
+
if (idx !== -1) {
|
|
38
|
+
changedTests.splice(idx, 1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
20
43
|
if (changedTests.length === 0) {
|
|
21
44
|
console.log("No tests affected by recent changes.");
|
|
22
45
|
return;
|
|
23
46
|
}
|
|
24
47
|
|
|
25
|
-
// Run the tests
|
|
48
|
+
// Run the tests using spawn for stdio inheritance
|
|
26
49
|
return new Promise<void>((resolve, reject) => {
|
|
27
|
-
|
|
50
|
+
const command = useVitest ? "vitest" : "bun";
|
|
51
|
+
const args = useVitest
|
|
52
|
+
? ["run", ...changedTests]
|
|
53
|
+
: ["test", ...changedTests];
|
|
54
|
+
console.log(`${command} ${args.join(" ")}`);
|
|
28
55
|
// resolve();
|
|
29
|
-
const child = spawn(
|
|
56
|
+
const child = spawn(command, args, { stdio: "inherit" });
|
|
30
57
|
|
|
31
58
|
child.on("close", (code) => {
|
|
32
59
|
if (code === 0) resolve();
|
|
@@ -129,7 +156,7 @@ async function getDependencies(testFile: string): Promise<Set<string>> {
|
|
|
129
156
|
format: "esm",
|
|
130
157
|
bundle: true,
|
|
131
158
|
metafile: true,
|
|
132
|
-
external: ["@cloudflare/workers-types", "bun:test", "@swc/*"],
|
|
159
|
+
external: ["@cloudflare/workers-types", "bun:test", "vitest", "@swc/*"],
|
|
133
160
|
logLevel: "error",
|
|
134
161
|
});
|
|
135
162
|
|
|
@@ -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
|
+
}
|
package/src/upstash/api.ts
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
}
|