alchemy 0.87.0 → 0.90.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 (79) hide show
  1. package/bin/alchemy.js +485 -543
  2. package/lib/cloudflare/compatibility-date.d.ts +1 -1
  3. package/lib/cloudflare/compatibility-date.d.ts.map +1 -1
  4. package/lib/cloudflare/dns-records.d.ts +6 -0
  5. package/lib/cloudflare/dns-records.d.ts.map +1 -1
  6. package/lib/cloudflare/dns-records.js +14 -12
  7. package/lib/cloudflare/dns-records.js.map +1 -1
  8. package/lib/cloudflare/index.d.ts +1 -0
  9. package/lib/cloudflare/index.d.ts.map +1 -1
  10. package/lib/cloudflare/index.js +1 -0
  11. package/lib/cloudflare/index.js.map +1 -1
  12. package/lib/cloudflare/r2-bucket-notification.d.ts +312 -0
  13. package/lib/cloudflare/r2-bucket-notification.d.ts.map +1 -0
  14. package/lib/cloudflare/r2-bucket-notification.js +387 -0
  15. package/lib/cloudflare/r2-bucket-notification.js.map +1 -0
  16. package/lib/cloudflare/worker.d.ts.map +1 -1
  17. package/lib/cloudflare/worker.js +8 -1
  18. package/lib/cloudflare/worker.js.map +1 -1
  19. package/lib/cloudflare/workflow.d.ts +17 -0
  20. package/lib/cloudflare/workflow.d.ts.map +1 -1
  21. package/lib/cloudflare/workflow.js +1 -0
  22. package/lib/cloudflare/workflow.js.map +1 -1
  23. package/lib/cloudflare/wrangler.json.js +1 -0
  24. package/lib/cloudflare/wrangler.json.js.map +1 -1
  25. package/lib/docker/api.d.ts +40 -0
  26. package/lib/docker/api.d.ts.map +1 -1
  27. package/lib/docker/api.js +30 -0
  28. package/lib/docker/api.js.map +1 -1
  29. package/lib/docker/network.d.ts.map +1 -1
  30. package/lib/docker/network.js +24 -7
  31. package/lib/docker/network.js.map +1 -1
  32. package/lib/encrypt.d.ts.map +1 -1
  33. package/lib/encrypt.js +2 -2
  34. package/lib/encrypt.js.map +1 -1
  35. package/lib/planetscale/api.d.ts +1 -0
  36. package/lib/planetscale/api.d.ts.map +1 -1
  37. package/lib/planetscale/api.js +1 -1
  38. package/lib/planetscale/api.js.map +1 -1
  39. package/lib/planetscale/branch.d.ts +28 -0
  40. package/lib/planetscale/branch.d.ts.map +1 -1
  41. package/lib/planetscale/branch.js +12 -0
  42. package/lib/planetscale/branch.js.map +1 -1
  43. package/lib/planetscale/database-extensions.d.ts +515 -0
  44. package/lib/planetscale/database-extensions.d.ts.map +1 -0
  45. package/lib/planetscale/database-extensions.js +389 -0
  46. package/lib/planetscale/database-extensions.js.map +1 -0
  47. package/lib/planetscale/database.d.ts +40 -2
  48. package/lib/planetscale/database.d.ts.map +1 -1
  49. package/lib/planetscale/database.js +76 -1
  50. package/lib/planetscale/database.js.map +1 -1
  51. package/lib/scope.d.ts.map +1 -1
  52. package/lib/scope.js +1 -2
  53. package/lib/scope.js.map +1 -1
  54. package/lib/util/telemetry.d.ts +1 -6
  55. package/lib/util/telemetry.d.ts.map +1 -1
  56. package/lib/util/telemetry.js.map +1 -1
  57. package/package.json +6 -6
  58. package/src/cloudflare/dns-records.ts +25 -16
  59. package/src/cloudflare/index.ts +1 -0
  60. package/src/cloudflare/r2-bucket-notification.ts +809 -0
  61. package/src/cloudflare/worker.ts +8 -1
  62. package/src/cloudflare/workflow.ts +18 -0
  63. package/src/cloudflare/wrangler.json.ts +1 -0
  64. package/src/docker/api.ts +61 -0
  65. package/src/docker/network.ts +27 -7
  66. package/src/encrypt.ts +2 -0
  67. package/src/planetscale/api.ts +1 -1
  68. package/src/planetscale/branch.ts +45 -0
  69. package/src/planetscale/database-extensions.ts +947 -0
  70. package/src/planetscale/database.ts +141 -2
  71. package/src/scope.ts +1 -2
  72. package/src/util/telemetry.ts +1 -20
  73. package/workers/cloudflare-state-store.js +57 -57
  74. package/workers/tunnel-proxy.js +1 -1
  75. package/lib/state/instrumented-state-store.d.ts +0 -19
  76. package/lib/state/instrumented-state-store.d.ts.map +0 -1
  77. package/lib/state/instrumented-state-store.js +0 -68
  78. package/lib/state/instrumented-state-store.js.map +0 -1
  79. package/src/state/instrumented-state-store.ts +0 -108
@@ -1563,7 +1563,14 @@ async function provisionResources<B extends Bindings>(
1563
1563
  input.eventSources
1564
1564
  ? Promise.all(
1565
1565
  input.eventSources.map(async (eventSource) => {
1566
- return await QueueConsumer(`${eventSource.queue.id}-consumer`, {
1566
+ // In local dev mode, queue.id is "" (no Cloudflare API call).
1567
+ // Use queue.dev.id (resource ID, e.g. "email-queue") to avoid
1568
+ // all consumers colliding on the same "-consumer" resource ID.
1569
+ // See: https://github.com/alchemy-run/alchemy/issues/1363
1570
+ const queueConsumerId = options.local
1571
+ ? eventSource.queue.dev?.id || eventSource.queue.id
1572
+ : eventSource.queue.id;
1573
+ return await QueueConsumer(`${queueConsumerId}-consumer`, {
1567
1574
  queue: eventSource.queue,
1568
1575
  scriptName: options.name,
1569
1576
  settings: eventSource.settings,
@@ -25,6 +25,20 @@ export interface WorkflowProps {
25
25
  * @default - bound worker script
26
26
  */
27
27
  scriptName?: string;
28
+ /**
29
+ * Limits for the workflow instance
30
+ */
31
+ limits?: {
32
+ /**
33
+ * Maximum number of steps per workflow instance.
34
+ *
35
+ * Workers Free: 1,024 (not configurable)
36
+ * Workers Paid: 10,000 default, configurable up to 25,000
37
+ *
38
+ * @see https://developers.cloudflare.com/workflows/reference/limits/
39
+ */
40
+ steps?: number;
41
+ };
28
42
  dev?: {
29
43
  /**
30
44
  * Whether to run the workflow remotely instead of locally
@@ -45,6 +59,9 @@ export type Workflow<PARAMS = unknown> = {
45
59
  workflowName: string;
46
60
  className: string;
47
61
  scriptName?: string;
62
+ limits?: {
63
+ steps?: number;
64
+ };
48
65
  };
49
66
 
50
67
  export function isWorkflow(binding: Binding): binding is Workflow {
@@ -77,6 +94,7 @@ export function Workflow<PARAMS = unknown>(
77
94
  workflowName,
78
95
  className,
79
96
  scriptName: props.scriptName,
97
+ limits: props.limits,
80
98
  };
81
99
  }
82
100
 
@@ -403,6 +403,7 @@ async function processBindings(
403
403
  binding: bindingName,
404
404
  class_name: binding.className,
405
405
  script_name: binding.scriptName,
406
+ ...(binding.limits ? { limits: binding.limits } : {}),
406
407
  });
407
408
  } else if (binding.type === "d1") {
408
409
  const id =
package/src/docker/api.ts CHANGED
@@ -121,6 +121,33 @@ export type ContainerRuntimeInfo = {
121
121
  ports: Record<string, number>;
122
122
  };
123
123
 
124
+ export type NetworkInfo = {
125
+ Name: string;
126
+ Id: string;
127
+ Created: string;
128
+ Scope: string;
129
+ Driver: string;
130
+ EnableIPv6: boolean;
131
+ IPAM: {
132
+ Driver: string;
133
+ Options: Record<string, string> | null;
134
+ Config: Array<{
135
+ Subnet: string;
136
+ Gateway: string;
137
+ }> | null;
138
+ };
139
+ Internal: boolean;
140
+ Attachable: boolean;
141
+ Ingress: boolean;
142
+ ConfigFrom: {
143
+ Network: string;
144
+ };
145
+ ConfigOnly: boolean;
146
+ Containers: Record<string, any>;
147
+ Options: Record<string, string>;
148
+ Labels: Record<string, string>;
149
+ };
150
+
124
151
  /**
125
152
  * Docker API client that wraps Docker CLI commands
126
153
  */
@@ -469,6 +496,40 @@ export class DockerApi {
469
496
  await this.exec(["network", "rm", networkId]);
470
497
  }
471
498
 
499
+ /**
500
+ * Get Docker network information
501
+ *
502
+ * @param networkId Network ID or name
503
+ * @returns Network details in JSON format
504
+ */
505
+ async inspectNetwork(networkId: string): Promise<NetworkInfo[]> {
506
+ const { stdout } = await this.exec(
507
+ ["network", "inspect", networkId],
508
+ undefined,
509
+ true,
510
+ );
511
+ try {
512
+ return JSON.parse(stdout.trim()) as NetworkInfo[];
513
+ } catch (_error) {
514
+ return [];
515
+ }
516
+ }
517
+
518
+ /**
519
+ * Check if a network exists
520
+ *
521
+ * @param networkId Network ID or name
522
+ * @returns True if network exists
523
+ */
524
+ async networkExists(networkId: string): Promise<boolean> {
525
+ try {
526
+ const networks = await this.inspectNetwork(networkId);
527
+ return networks.length > 0;
528
+ } catch (_error) {
529
+ return false;
530
+ }
531
+ }
532
+
472
533
  /**
473
534
  * Connect container to network
474
535
  *
@@ -102,14 +102,34 @@ export const Network = Resource(
102
102
  } else {
103
103
  // Create the network
104
104
  props.driver = props.driver || "bridge";
105
- const networkId = await api.createNetwork(networkName, props.driver);
105
+ try {
106
+ const networkId = await api.createNetwork(networkName, props.driver);
106
107
 
107
- return {
108
- ...props,
109
- id: networkId,
110
- name: networkName,
111
- createdAt: Date.now(),
112
- };
108
+ return {
109
+ ...props,
110
+ id: networkId,
111
+ name: networkName,
112
+ createdAt: Date.now(),
113
+ };
114
+ } catch (error: any) {
115
+ const errorMessage = error.message || String(error);
116
+ if (
117
+ errorMessage.includes(
118
+ `network with name ${networkName} already exists`,
119
+ )
120
+ ) {
121
+ const [details] = await api.inspectNetwork(networkName);
122
+ if (details) {
123
+ return {
124
+ ...props,
125
+ id: details.Id,
126
+ name: networkName,
127
+ createdAt: Date.parse(details.Created) || Date.now(),
128
+ };
129
+ }
130
+ }
131
+ throw error;
132
+ }
113
133
  }
114
134
  },
115
135
  );
package/src/encrypt.ts CHANGED
@@ -125,6 +125,7 @@ export async function libsodiumEncrypt(
125
125
  const cryptoKey = sodium.crypto_generichash(
126
126
  sodium.crypto_secretbox_KEYBYTES,
127
127
  sodium.from_string(key),
128
+ null,
128
129
  );
129
130
 
130
131
  // Generate a random nonce
@@ -165,6 +166,7 @@ export async function libsodiumDecrypt(
165
166
  const cryptoKey = sodium.crypto_generichash(
166
167
  sodium.crypto_secretbox_KEYBYTES,
167
168
  sodium.from_string(key),
169
+ null,
168
170
  );
169
171
 
170
172
  // Decode the base64 combined value
@@ -78,7 +78,7 @@ export function createPlanetScaleClient(
78
78
  });
79
79
  }
80
80
 
81
- const extractToken = (props: PlanetScaleProps) => {
81
+ export const extractToken = (props: PlanetScaleProps) => {
82
82
  if (props.apiKey) {
83
83
  return props.apiKey.unencrypted;
84
84
  } else if (props.serviceTokenId && props.serviceToken) {
@@ -83,6 +83,22 @@ export interface BranchProps extends PlanetScaleProps {
83
83
  * Enable or disable safe migrations on this branch
84
84
  */
85
85
  safeMigrations?: boolean;
86
+
87
+ /**
88
+ * The region to create the branch in.
89
+ * If not provided, the branch will be created in the default region for its database.
90
+ * On adopt/update, if specified, the actual branch region is validated against this value.
91
+ *
92
+ * @see https://planetscale.com/docs/concepts/regions
93
+ */
94
+ region?: {
95
+ /**
96
+ * The slug identifier of the region (e.g. "us-east", "eu-west", "gcp-us-central1")
97
+ *
98
+ * @see https://planetscale.com/docs/concepts/regions#available-regions
99
+ */
100
+ slug: string;
101
+ };
86
102
  }
87
103
 
88
104
  /**
@@ -113,6 +129,20 @@ export interface Branch extends BranchProps {
113
129
  * HTML URL to access the branch
114
130
  */
115
131
  htmlUrl: string;
132
+
133
+ /**
134
+ * The region of the branch as reported by PlanetScale.
135
+ *
136
+ * @see https://planetscale.com/docs/concepts/regions
137
+ */
138
+ region: {
139
+ /**
140
+ * The slug identifier of the region (e.g. "us-east", "eu-west", "gcp-us-central1")
141
+ *
142
+ * @see https://planetscale.com/docs/concepts/regions#available-regions
143
+ */
144
+ slug: string;
145
+ };
116
146
  }
117
147
 
118
148
  /**
@@ -259,6 +289,18 @@ export const Branch = Resource(
259
289
  const data = getResponse.data;
260
290
  const currentParentBranch = data.parent_branch || "main";
261
291
 
292
+ // Validate region matches if specified
293
+ if (props.region) {
294
+ const actualSlug = data.region.slug;
295
+ if (actualSlug !== props.region.slug) {
296
+ throw new Error(
297
+ `Branch "${branchName}" is in region "${actualSlug}" but expected "${props.region.slug}". ` +
298
+ `PlanetScale branch regions cannot be changed after creation. ` +
299
+ `Either update the region in your configuration to match, or create a new branch in the correct region.`,
300
+ );
301
+ }
302
+ }
303
+
262
304
  // Check immutable properties
263
305
  if (props.parentBranch && parentBranchName !== currentParentBranch) {
264
306
  throw new Error(
@@ -323,6 +365,7 @@ export const Branch = Resource(
323
365
  createdAt: data.created_at,
324
366
  updatedAt: data.updated_at,
325
367
  htmlUrl: data.html_url,
368
+ region: { slug: data.region.slug },
326
369
  };
327
370
  }
328
371
  let clusterSize: string | undefined;
@@ -352,6 +395,7 @@ export const Branch = Resource(
352
395
  parent_branch: parentBranchName,
353
396
  backup_id: props.backupId,
354
397
  seed_data: props.seedData,
398
+ region: props.region?.slug,
355
399
  // This is ignored unless props.backupId is provided
356
400
  cluster_size: clusterSize,
357
401
  },
@@ -391,6 +435,7 @@ export const Branch = Resource(
391
435
  createdAt: data.created_at,
392
436
  updatedAt: data.updated_at,
393
437
  htmlUrl: data.html_url,
438
+ region: { slug: data.region.slug },
394
439
  };
395
440
  },
396
441
  );