alchemy 0.81.0 → 0.81.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 (49) hide show
  1. package/bin/alchemy.js +1652 -1378
  2. package/lib/apply.js +5 -0
  3. package/lib/apply.js.map +1 -1
  4. package/lib/cloudflare/bundle/plugin-wasm.d.ts.map +1 -1
  5. package/lib/cloudflare/bundle/plugin-wasm.js +8 -7
  6. package/lib/cloudflare/bundle/plugin-wasm.js.map +1 -1
  7. package/lib/cloudflare/compatibility-date.d.ts +5 -0
  8. package/lib/cloudflare/compatibility-date.d.ts.map +1 -0
  9. package/lib/cloudflare/compatibility-date.js +6 -0
  10. package/lib/cloudflare/compatibility-date.js.map +1 -0
  11. package/lib/cloudflare/index.d.ts +1 -1
  12. package/lib/cloudflare/index.d.ts.map +1 -1
  13. package/lib/cloudflare/index.js +1 -1
  14. package/lib/cloudflare/index.js.map +1 -1
  15. package/lib/cloudflare/next.d.ts.map +1 -1
  16. package/lib/cloudflare/next.js +49 -6
  17. package/lib/cloudflare/next.js.map +1 -1
  18. package/lib/cloudflare/redirect-rule.d.ts +12 -2
  19. package/lib/cloudflare/redirect-rule.d.ts.map +1 -1
  20. package/lib/cloudflare/redirect-rule.js +73 -98
  21. package/lib/cloudflare/redirect-rule.js.map +1 -1
  22. package/lib/cloudflare/website.js +1 -1
  23. package/lib/cloudflare/website.js.map +1 -1
  24. package/lib/cloudflare/worker.js +1 -1
  25. package/lib/cloudflare/worker.js.map +1 -1
  26. package/lib/fs/folder.js +1 -1
  27. package/lib/fs/folder.js.map +1 -1
  28. package/lib/prisma-postgres/connection.js +1 -1
  29. package/lib/prisma-postgres/connection.js.map +1 -1
  30. package/lib/state/cloudflare-state-store.js +1 -1
  31. package/lib/state/cloudflare-state-store.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/apply.ts +4 -0
  34. package/src/cloudflare/bundle/plugin-wasm.ts +12 -8
  35. package/src/cloudflare/compatibility-date.ts +6 -0
  36. package/src/cloudflare/index.ts +1 -1
  37. package/src/cloudflare/next.ts +40 -6
  38. package/src/cloudflare/redirect-rule.ts +129 -150
  39. package/src/cloudflare/website.ts +1 -1
  40. package/src/cloudflare/worker.ts +1 -1
  41. package/src/fs/folder.ts +1 -1
  42. package/src/prisma-postgres/connection.ts +1 -1
  43. package/src/state/cloudflare-state-store.ts +1 -1
  44. package/workers/tunnel-proxy.js +1 -1
  45. package/lib/cloudflare/compatibility-date.gen.d.ts +0 -5
  46. package/lib/cloudflare/compatibility-date.gen.d.ts.map +0 -1
  47. package/lib/cloudflare/compatibility-date.gen.js +0 -7
  48. package/lib/cloudflare/compatibility-date.gen.js.map +0 -1
  49. package/src/cloudflare/compatibility-date.gen.ts +0 -7
@@ -6,12 +6,19 @@ import {
6
6
  type CloudflareApiOptions,
7
7
  } from "./api.ts";
8
8
  import type { CloudflareResponse } from "./response.ts";
9
- import type { Zone } from "./zone.ts";
9
+ import { getZoneByDomain, type Zone } from "./zone.ts";
10
10
 
11
11
  /**
12
12
  * Properties for creating or updating a RedirectRule
13
13
  */
14
14
  export interface RedirectRuleProps extends CloudflareApiOptions {
15
+ /**
16
+ * Description of the redirect rule
17
+ *
18
+ * @default ${app.name}-${app.stage}-${id}
19
+ */
20
+ description?: string;
21
+
15
22
  /**
16
23
  * The zone where the redirect rule will be applied
17
24
  * Can be a zone ID string or a Zone resource
@@ -110,6 +117,11 @@ export interface RedirectRule {
110
117
  */
111
118
  zoneId: string;
112
119
 
120
+ /**
121
+ * Description of the redirect rule
122
+ */
123
+ description: string;
124
+
113
125
  /**
114
126
  * The request URL pattern (for wildcard redirects)
115
127
  */
@@ -146,6 +158,23 @@ export interface RedirectRule {
146
158
  lastUpdated: string;
147
159
  }
148
160
 
161
+ /**
162
+ * Internal rule data structure for API operations
163
+ */
164
+ interface RuleData {
165
+ api: CloudflareApi;
166
+ zoneId: string;
167
+ rulesetId: string;
168
+ /** The rule ID (for updates/deletes) */
169
+ ruleId?: string;
170
+ /** Description of the rule */
171
+ description: string;
172
+ expression: string;
173
+ targetUrl: string;
174
+ statusCode: number;
175
+ preserveQueryString: boolean;
176
+ }
177
+
149
178
  /**
150
179
  * A Cloudflare Redirect Rule enables URL redirects and rewrites using Cloudflare's Rules engine.
151
180
  * Supports wildcard redirects, static redirects, and dynamic redirects with expressions.
@@ -200,24 +229,33 @@ export const RedirectRule = Resource(
200
229
  "cloudflare::RedirectRule",
201
230
  async function (
202
231
  this: Context<RedirectRule>,
203
- _id: string,
232
+ id: string,
204
233
  props: RedirectRuleProps,
205
234
  ): Promise<RedirectRule> {
206
235
  // Create Cloudflare API client
207
236
  const api = await createCloudflareApi(props);
208
237
 
238
+ const description = props.description ?? this.scope.createPhysicalName(id);
239
+
209
240
  // Get zone ID
210
- const zoneId = typeof props.zone === "string" ? props.zone : props.zone.id;
241
+ const zoneId =
242
+ typeof props.zone === "string"
243
+ ? props.zone.includes(".")
244
+ ? (await getZoneByDomain(api, props.zone))?.id
245
+ : props.zone
246
+ : props.zone.id;
247
+ if (!zoneId) {
248
+ throw new Error(`Zone ${props.zone} not found`);
249
+ }
211
250
 
212
251
  if (this.phase === "delete") {
213
252
  if (this.output?.ruleId && this.output?.rulesetId) {
214
- // Let delete errors propagate instead of swallowing them
215
- await deleteRedirectRule(
253
+ await deleteRule({
216
254
  api,
217
255
  zoneId,
218
- this.output.rulesetId,
219
- this.output.ruleId,
220
- );
256
+ rulesetId: this.output.rulesetId,
257
+ ruleId: this.output.ruleId,
258
+ });
221
259
  }
222
260
  return this.destroy();
223
261
  }
@@ -249,24 +287,24 @@ export const RedirectRule = Resource(
249
287
  this.output?.ruleId &&
250
288
  this.output?.rulesetId
251
289
  ) {
252
- // Update existing rule
253
- const updatedRule = await updateRedirectRule(
290
+ // Update existing rule using PATCH API
291
+ const updatedRule = await updateRule({
254
292
  api,
255
293
  zoneId,
256
- this.output.rulesetId,
257
- this.output.ruleId,
258
- {
259
- expression: ruleExpression,
260
- targetUrl: props.targetUrl,
261
- statusCode,
262
- preserveQueryString,
263
- },
264
- );
294
+ rulesetId: this.output.rulesetId,
295
+ ruleId: this.output.ruleId,
296
+ description,
297
+ expression: ruleExpression,
298
+ targetUrl: props.targetUrl,
299
+ statusCode,
300
+ preserveQueryString,
301
+ });
265
302
 
266
303
  return {
267
304
  ruleId: updatedRule.id,
268
305
  rulesetId: this.output.rulesetId,
269
306
  zoneId,
307
+ description,
270
308
  requestUrl: props.requestUrl,
271
309
  expression: props.expression,
272
310
  targetUrl: props.targetUrl,
@@ -280,8 +318,12 @@ export const RedirectRule = Resource(
280
318
  // Get or create the redirect ruleset for this zone
281
319
  const rulesetId = await getOrCreateRedirectRuleset(api, zoneId);
282
320
 
283
- // Create the rule
284
- const createdRule = await createRedirectRule(api, zoneId, rulesetId, {
321
+ // Create the rule using POST API
322
+ const createdRule = await createRule({
323
+ api,
324
+ zoneId,
325
+ rulesetId,
326
+ description,
285
327
  expression: ruleExpression,
286
328
  targetUrl: props.targetUrl,
287
329
  statusCode,
@@ -292,6 +334,7 @@ export const RedirectRule = Resource(
292
334
  ruleId: createdRule.id,
293
335
  rulesetId,
294
336
  zoneId,
337
+ description,
295
338
  requestUrl: props.requestUrl,
296
339
  expression: props.expression,
297
340
  targetUrl: props.targetUrl,
@@ -367,118 +410,80 @@ async function getOrCreateRedirectRuleset(
367
410
  }
368
411
 
369
412
  /**
370
- * Create a new redirect rule by updating the ruleset
413
+ * Build the redirect rule body for API requests
371
414
  */
372
- async function createRedirectRule(
373
- api: CloudflareApi,
374
- zoneId: string,
375
- rulesetId: string,
376
- ruleData: {
377
- expression: string;
378
- targetUrl: string;
379
- statusCode: number;
380
- preserveQueryString: boolean;
381
- },
382
- ): Promise<CloudflareRule> {
383
- // Get current ruleset
384
- const ruleset = await getRuleset(api, zoneId, rulesetId);
385
- if (!ruleset) {
386
- throw new Error(`Ruleset ${rulesetId} not found`);
387
- }
388
-
389
- // Create new rule object
390
- const newRule = {
415
+ function buildRuleBody(
416
+ data: Omit<RuleData, "api" | "zoneId" | "rulesetId" | "ruleId">,
417
+ ) {
418
+ return {
391
419
  action: "redirect" as const,
392
- expression: ruleData.expression,
420
+ description: data.description,
421
+ expression: data.expression,
422
+ enabled: true,
393
423
  action_parameters: {
394
424
  from_value: {
395
- status_code: ruleData.statusCode,
425
+ status_code: data.statusCode,
396
426
  target_url: {
397
- value: ruleData.targetUrl,
427
+ value: data.targetUrl,
398
428
  },
399
- preserve_query_string: ruleData.preserveQueryString,
429
+ preserve_query_string: data.preserveQueryString,
400
430
  },
401
431
  },
402
- enabled: true,
403
432
  };
433
+ }
404
434
 
405
- // Update ruleset with new rule
406
- const response = await api.put(`/zones/${zoneId}/rulesets/${rulesetId}`, {
407
- name: ruleset.name,
408
- description: ruleset.description,
409
- kind: ruleset.kind,
410
- phase: ruleset.phase,
411
- rules: [...(ruleset.rules ?? []), newRule],
412
- });
435
+ /**
436
+ * Create a new redirect rule using the individual rule API
437
+ * @see https://developers.cloudflare.com/api/resources/rulesets/subresources/rules/methods/create/
438
+ */
439
+ async function createRule(
440
+ data: Omit<RuleData, "ruleId">,
441
+ ): Promise<CloudflareRule> {
442
+ const { api, zoneId, rulesetId, ...ruleFields } = data;
443
+
444
+ const response = await api.post(
445
+ `/zones/${zoneId}/rulesets/${rulesetId}/rules`,
446
+ buildRuleBody(ruleFields),
447
+ );
413
448
 
414
449
  if (!response.ok) {
415
450
  const errorBody = await response.text();
416
451
  throw new Error(
417
- `Failed to create redirect rule: ${response.statusText}\nResponse: ${errorBody}`,
452
+ `Failed to create redirect rule: ${response.status} ${response.statusText}\nResponse: ${errorBody}`,
418
453
  );
419
454
  }
420
455
 
421
456
  const result =
422
457
  (await response.json()) as CloudflareResponse<CloudflareRuleset>;
423
- // Return the last rule (the one we just added)
458
+ // The API returns the updated ruleset, find the newly created rule (last one)
424
459
  const createdRule = result.result.rules[result.result.rules.length - 1];
460
+ if (!createdRule) {
461
+ throw new Error("Created rule not found in response");
462
+ }
425
463
  return createdRule;
426
464
  }
427
465
 
428
466
  /**
429
- * Update an existing redirect rule by updating the ruleset
467
+ * Update an existing redirect rule using the individual rule API
468
+ * @see https://developers.cloudflare.com/api/resources/rulesets/subresources/rules/methods/edit/
430
469
  */
431
- async function updateRedirectRule(
432
- api: CloudflareApi,
433
- zoneId: string,
434
- rulesetId: string,
435
- ruleId: string,
436
- ruleData: {
437
- expression: string;
438
- targetUrl: string;
439
- statusCode: number;
440
- preserveQueryString: boolean;
441
- },
442
- ): Promise<CloudflareRule> {
443
- // Get current ruleset
444
- const ruleset = await getRuleset(api, zoneId, rulesetId);
445
- if (!ruleset) {
446
- throw new Error(`Ruleset ${rulesetId} not found`);
447
- }
470
+ async function updateRule(data: RuleData): Promise<CloudflareRule> {
471
+ const { api, zoneId, rulesetId, ruleId, ...ruleFields } = data;
448
472
 
449
- // Find and update the rule
450
- const updatedRules = ruleset.rules.map((rule) => {
451
- if (rule.id === ruleId) {
452
- return {
453
- ...rule,
454
- action: "redirect" as const,
455
- expression: ruleData.expression,
456
- action_parameters: {
457
- from_value: {
458
- status_code: ruleData.statusCode,
459
- target_url: {
460
- value: ruleData.targetUrl,
461
- },
462
- preserve_query_string: ruleData.preserveQueryString,
463
- },
464
- },
465
- enabled: true,
466
- };
467
- }
468
- return rule;
469
- });
473
+ if (!ruleId) {
474
+ throw new Error("ruleId is required for update");
475
+ }
470
476
 
471
- // Update ruleset with modified rules
472
- const response = await api.put(`/zones/${zoneId}/rulesets/${rulesetId}`, {
473
- name: ruleset.name,
474
- description: ruleset.description,
475
- kind: ruleset.kind,
476
- phase: ruleset.phase,
477
- rules: updatedRules,
478
- });
477
+ const response = await api.patch(
478
+ `/zones/${zoneId}/rulesets/${rulesetId}/rules/${ruleId}`,
479
+ buildRuleBody(ruleFields),
480
+ );
479
481
 
480
482
  if (!response.ok) {
481
- throw new Error(`Failed to update redirect rule: ${response.statusText}`);
483
+ const errorBody = await response.text();
484
+ throw new Error(
485
+ `Failed to update redirect rule: ${response.status} ${response.statusText}\nResponse: ${errorBody}`,
486
+ );
482
487
  }
483
488
 
484
489
  const result =
@@ -492,22 +497,28 @@ async function updateRedirectRule(
492
497
  }
493
498
 
494
499
  /**
495
- * Get a ruleset with its rules
500
+ * Delete a redirect rule using the individual rule API
501
+ * @see https://developers.cloudflare.com/api/resources/rulesets/subresources/rules/methods/delete/
496
502
  */
497
- async function getRuleset(
498
- api: CloudflareApi,
499
- zoneId: string,
500
- rulesetId: string,
501
- ): Promise<CloudflareRuleset | null> {
502
- const response = await api.get(`/zones/${zoneId}/rulesets/${rulesetId}`);
503
+ async function deleteRule(data: {
504
+ api: CloudflareApi;
505
+ zoneId: string;
506
+ rulesetId: string;
507
+ ruleId: string;
508
+ }): Promise<void> {
509
+ const { api, zoneId, rulesetId, ruleId } = data;
503
510
 
504
- if (!response.ok) {
505
- return null;
506
- }
511
+ const response = await api.delete(
512
+ `/zones/${zoneId}/rulesets/${rulesetId}/rules/${ruleId}`,
513
+ );
507
514
 
508
- const result =
509
- (await response.json()) as CloudflareResponse<CloudflareRuleset>;
510
- return result.result;
515
+ // 404 is acceptable - rule may already be deleted
516
+ if (!response.ok && response.status !== 404) {
517
+ const errorBody = await response.text();
518
+ throw new Error(
519
+ `Failed to delete redirect rule: ${response.status} ${response.statusText}\nResponse: ${errorBody}`,
520
+ );
521
+ }
511
522
  }
512
523
 
513
524
  /**
@@ -529,43 +540,11 @@ export async function findRuleInRuleset(
529
540
 
530
541
  const rulesetData =
531
542
  (await response.json()) as CloudflareResponse<CloudflareRuleset>;
532
- const rule = rulesetData.result.rules.find((r) => r.id === ruleId);
543
+ const rule = rulesetData.result.rules?.find((r) => r.id === ruleId);
533
544
 
534
545
  return rule || null;
535
546
  }
536
547
 
537
- /**
538
- * Delete a redirect rule by updating the ruleset to exclude it
539
- */
540
- async function deleteRedirectRule(
541
- api: CloudflareApi,
542
- zoneId: string,
543
- rulesetId: string,
544
- ruleId: string,
545
- ): Promise<void> {
546
- const ruleset = await getRuleset(api, zoneId, rulesetId);
547
-
548
- if (!ruleset) {
549
- throw new Error(`Ruleset ${rulesetId} not found for deletion`);
550
- }
551
-
552
- // Filter out the rule to delete
553
- const updatedRules = ruleset.rules.filter((rule) => rule.id !== ruleId);
554
-
555
- // Update the ruleset with the filtered rules
556
- const response = await api.put(`/zones/${zoneId}/rulesets/${rulesetId}`, {
557
- name: ruleset.name,
558
- description: ruleset.description,
559
- kind: ruleset.kind,
560
- phase: ruleset.phase,
561
- rules: updatedRules,
562
- });
563
-
564
- if (!response.ok) {
565
- throw new Error(`Failed to delete redirect rule: ${response.statusText}`);
566
- }
567
- }
568
-
569
548
  /**
570
549
  * Convert a wildcard URL pattern to a Cloudflare Rules expression
571
550
  * Uses operators available on Free plans (no regex matching)
@@ -8,7 +8,7 @@ import { logger } from "../util/logger.ts";
8
8
  import { createCloudflareApi } from "./api.ts";
9
9
  import { Assets } from "./assets.ts";
10
10
  import type { Bindings } from "./bindings.ts";
11
- import { DEFAULT_COMPATIBILITY_DATE } from "./compatibility-date.gen.ts";
11
+ import { DEFAULT_COMPATIBILITY_DATE } from "./compatibility-date.ts";
12
12
  import { unionCompatibilityFlags } from "./compatibility-presets.ts";
13
13
  import {
14
14
  extractStringAndSecretBindings,
@@ -20,7 +20,7 @@ import type {
20
20
  WorkerBindingSpec,
21
21
  } from "./bindings.ts";
22
22
  import type { Bound } from "./bound.ts";
23
- import { DEFAULT_COMPATIBILITY_DATE } from "./compatibility-date.gen.ts";
23
+ import { DEFAULT_COMPATIBILITY_DATE } from "./compatibility-date.ts";
24
24
  import {
25
25
  type CompatibilityPreset,
26
26
  unionCompatibilityFlags,
package/src/fs/folder.ts CHANGED
@@ -68,7 +68,7 @@ export const Folder = Resource(
68
68
  if (props?.delete !== false) {
69
69
  // we just do a best effort attempt
70
70
  await ignore(["ENOENT", "ENOTEMPTY"], async () =>
71
- fs.promises.rmdir(dirPath, { recursive: props?.clean ?? false }),
71
+ fs.promises.rm(dirPath, { recursive: props?.clean ?? false }),
72
72
  );
73
73
  }
74
74
  return this.destroy();
@@ -143,7 +143,7 @@ export const Connection = Resource(
143
143
  createdAt: connection.createdAt,
144
144
  prismaConnectionString: secret(connection.connectionString),
145
145
  connectionString: secret(
146
- `postgres://${connection.user}:${connection.pass}@${connection.host}/${connection.database}`,
146
+ `postgres://${connection.user}:${connection.pass}@${connection.host}/${connection.database.name}`,
147
147
  ),
148
148
  database: database,
149
149
  host: connection.host,
@@ -2,7 +2,7 @@ import { alchemy } from "../alchemy.ts";
2
2
  import type { CloudflareApiOptions } from "../cloudflare/api.ts";
3
3
  import { createCloudflareApi } from "../cloudflare/api.ts";
4
4
  import { getInternalWorkerBundle } from "../cloudflare/bundle/internal-worker-bundle.ts";
5
- import { DEFAULT_COMPATIBILITY_DATE } from "../cloudflare/compatibility-date.gen.ts";
5
+ import { DEFAULT_COMPATIBILITY_DATE } from "../cloudflare/compatibility-date.ts";
6
6
  import { DurableObjectNamespace } from "../cloudflare/durable-object-namespace.ts";
7
7
  import { getWorkerSettings } from "../cloudflare/worker-metadata.ts";
8
8
  import {
@@ -83,7 +83,7 @@ const renderErrorHtml = (props) => `
83
83
  Alchemy</a>.</p>
84
84
  </div>
85
85
  <div class="bg-slate-200 px-5 py-3 flex flex-col w-full max-w-lg">
86
- <p class="text-sm text-slate-500">Alchemy 0.81.0</p>
86
+ <p class="text-sm text-slate-500">Alchemy 0.81.2</p>
87
87
  </div>
88
88
  </div>
89
89
  </body>
@@ -1,5 +0,0 @@
1
- /**
2
- * The default Cloudflare Workers compatibility date, set based on the latest workerd release at the time of build.
3
- */
4
- export declare const DEFAULT_COMPATIBILITY_DATE = "2025-12-09";
5
- //# sourceMappingURL=compatibility-date.gen.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"compatibility-date.gen.d.ts","sourceRoot":"","sources":["../../src/cloudflare/compatibility-date.gen.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,eAAO,MAAM,0BAA0B,eAAe,CAAC"}
@@ -1,7 +0,0 @@
1
- // This file is auto-generated during build
2
- // Do not edit manually
3
- /**
4
- * The default Cloudflare Workers compatibility date, set based on the latest workerd release at the time of build.
5
- */
6
- export const DEFAULT_COMPATIBILITY_DATE = "2025-12-09";
7
- //# sourceMappingURL=compatibility-date.gen.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"compatibility-date.gen.js","sourceRoot":"","sources":["../../src/cloudflare/compatibility-date.gen.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAC3C,uBAAuB;AAEvB;;GAEG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,YAAY,CAAC"}
@@ -1,7 +0,0 @@
1
- // This file is auto-generated during build
2
- // Do not edit manually
3
-
4
- /**
5
- * The default Cloudflare Workers compatibility date, set based on the latest workerd release at the time of build.
6
- */
7
- export const DEFAULT_COMPATIBILITY_DATE = "2025-12-09";