alchemy 0.86.0 → 0.89.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 (75) hide show
  1. package/bin/alchemy.js +483 -541
  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 +5 -1
  5. package/lib/cloudflare/dns-records.d.ts.map +1 -1
  6. package/lib/cloudflare/dns-records.js +54 -31
  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/workflow.d.ts +17 -0
  17. package/lib/cloudflare/workflow.d.ts.map +1 -1
  18. package/lib/cloudflare/workflow.js +1 -0
  19. package/lib/cloudflare/workflow.js.map +1 -1
  20. package/lib/cloudflare/wrangler.json.js +1 -0
  21. package/lib/cloudflare/wrangler.json.js.map +1 -1
  22. package/lib/docker/api.d.ts +51 -0
  23. package/lib/docker/api.d.ts.map +1 -1
  24. package/lib/docker/api.js +30 -0
  25. package/lib/docker/api.js.map +1 -1
  26. package/lib/docker/container.d.ts +7 -1
  27. package/lib/docker/container.d.ts.map +1 -1
  28. package/lib/docker/container.js +51 -3
  29. package/lib/docker/container.js.map +1 -1
  30. package/lib/docker/network.d.ts.map +1 -1
  31. package/lib/docker/network.js +24 -7
  32. package/lib/docker/network.js.map +1 -1
  33. package/lib/planetscale/api.d.ts +1 -0
  34. package/lib/planetscale/api.d.ts.map +1 -1
  35. package/lib/planetscale/api.js +1 -1
  36. package/lib/planetscale/api.js.map +1 -1
  37. package/lib/planetscale/branch.d.ts +28 -0
  38. package/lib/planetscale/branch.d.ts.map +1 -1
  39. package/lib/planetscale/branch.js +12 -0
  40. package/lib/planetscale/branch.js.map +1 -1
  41. package/lib/planetscale/database-extensions.d.ts +515 -0
  42. package/lib/planetscale/database-extensions.d.ts.map +1 -0
  43. package/lib/planetscale/database-extensions.js +389 -0
  44. package/lib/planetscale/database-extensions.js.map +1 -0
  45. package/lib/planetscale/database.d.ts +40 -2
  46. package/lib/planetscale/database.d.ts.map +1 -1
  47. package/lib/planetscale/database.js +76 -1
  48. package/lib/planetscale/database.js.map +1 -1
  49. package/lib/scope.d.ts.map +1 -1
  50. package/lib/scope.js +1 -2
  51. package/lib/scope.js.map +1 -1
  52. package/lib/util/telemetry.d.ts +1 -6
  53. package/lib/util/telemetry.d.ts.map +1 -1
  54. package/lib/util/telemetry.js.map +1 -1
  55. package/package.json +4 -4
  56. package/src/cloudflare/dns-records.ts +71 -51
  57. package/src/cloudflare/index.ts +1 -0
  58. package/src/cloudflare/r2-bucket-notification.ts +809 -0
  59. package/src/cloudflare/workflow.ts +18 -0
  60. package/src/cloudflare/wrangler.json.ts +1 -0
  61. package/src/docker/api.ts +73 -0
  62. package/src/docker/container.ts +78 -4
  63. package/src/docker/network.ts +27 -7
  64. package/src/planetscale/api.ts +1 -1
  65. package/src/planetscale/branch.ts +45 -0
  66. package/src/planetscale/database-extensions.ts +947 -0
  67. package/src/planetscale/database.ts +141 -2
  68. package/src/scope.ts +1 -2
  69. package/src/util/telemetry.ts +1 -20
  70. package/workers/tunnel-proxy.js +1 -1
  71. package/lib/state/instrumented-state-store.d.ts +0 -19
  72. package/lib/state/instrumented-state-store.d.ts.map +0 -1
  73. package/lib/state/instrumented-state-store.js +0 -68
  74. package/lib/state/instrumented-state-store.js.map +0 -1
  75. package/src/state/instrumented-state-store.ts +0 -108
@@ -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
@@ -106,9 +106,48 @@ export type ContainerInfo = {
106
106
  Aliases: string[] | null;
107
107
  }
108
108
  > | null;
109
+ Ports?: Record<
110
+ string,
111
+ Array<{ HostIp: string; HostPort: string }> | null
112
+ > | null;
109
113
  };
110
114
  };
111
115
 
116
+ export type ContainerRuntimeInfo = {
117
+ /**
118
+ * Map of internal container ports to their bound host ports.
119
+ * Format: "internalPort/protocol" -> hostPort (number)
120
+ */
121
+ ports: Record<string, number>;
122
+ };
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
+
112
151
  /**
113
152
  * Docker API client that wraps Docker CLI commands
114
153
  */
@@ -457,6 +496,40 @@ export class DockerApi {
457
496
  await this.exec(["network", "rm", networkId]);
458
497
  }
459
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
+
460
533
  /**
461
534
  * Connect container to network
462
535
  *
@@ -1,6 +1,12 @@
1
1
  import type { Context } from "../context.ts";
2
2
  import { Resource } from "../resource.ts";
3
- import { DockerApi, normalizeDuration, type ContainerInfo } from "./api.ts";
3
+ import { Secret } from "../secret.ts";
4
+ import {
5
+ DockerApi,
6
+ normalizeDuration,
7
+ type ContainerInfo,
8
+ type ContainerRuntimeInfo,
9
+ } from "./api.ts";
4
10
  import type { Image } from "./image.ts";
5
11
  import type { RemoteImage } from "./remote-image.ts";
6
12
 
@@ -140,7 +146,7 @@ export interface ContainerProps {
140
146
  /**
141
147
  * Environment variables
142
148
  */
143
- environment?: Record<string, string>;
149
+ environment?: Record<string, string | Secret>;
144
150
 
145
151
  /**
146
152
  * Port mappings
@@ -207,6 +213,11 @@ export interface Container extends ContainerProps {
207
213
  * Time when the container was created
208
214
  */
209
215
  createdAt: number;
216
+
217
+ /**
218
+ * Inspect the container to get detailed information
219
+ */
220
+ inspect(): Promise<ContainerRuntimeInfo>;
210
221
  }
211
222
 
212
223
  /**
@@ -370,6 +381,13 @@ export const Container = Resource(
370
381
  name: containerName,
371
382
  state: containerState,
372
383
  createdAt: new Date(containerInfo.Created).getTime(),
384
+ inspect: async () => {
385
+ const [info] = await api.inspectContainer(containerName);
386
+ if (!info) {
387
+ throw new Error(`Container ${containerName} not found`);
388
+ }
389
+ return toRuntimeInfo(info);
390
+ },
373
391
  };
374
392
  }
375
393
  }
@@ -396,7 +414,7 @@ export const Container = Resource(
396
414
  // Create new container
397
415
  const containerId = await api.createContainer(imageRef, containerName, {
398
416
  ports: portMappings,
399
- env: props.environment,
417
+ env: normalizeEnvironment(props.environment),
400
418
  volumes: volumeMappings,
401
419
  cmd: props.command,
402
420
  healthcheck: props.healthcheck,
@@ -424,10 +442,46 @@ export const Container = Resource(
424
442
  name: containerName,
425
443
  state: containerState,
426
444
  createdAt: Date.now(),
445
+ inspect: async () => {
446
+ const [info] = await api.inspectContainer(containerName);
447
+ if (!info) {
448
+ throw new Error(`Container ${containerName} not found`);
449
+ }
450
+ return toRuntimeInfo(info);
451
+ },
427
452
  };
428
453
  },
429
454
  );
430
455
 
456
+ function toRuntimeInfo(info: ContainerInfo): ContainerRuntimeInfo {
457
+ const ports: Record<string, number> = {};
458
+ const networkSettings = info.NetworkSettings;
459
+
460
+ if (networkSettings?.Ports) {
461
+ for (const [internal, bindings] of Object.entries(networkSettings.Ports)) {
462
+ if (bindings && bindings.length > 0) {
463
+ ports[internal] = parseInt(bindings[0].HostPort, 10);
464
+ }
465
+ }
466
+ }
467
+
468
+ // Also check HostConfig.PortBindings as a fallback or additional source
469
+ // though NetworkSettings.Ports is usually the source of truth for running containers
470
+ if (info.HostConfig.PortBindings) {
471
+ for (const [internal, bindings] of Object.entries(
472
+ info.HostConfig.PortBindings,
473
+ )) {
474
+ if (bindings && bindings.length > 0 && !(internal in ports)) {
475
+ ports[internal] = parseInt(bindings[0].HostPort, 10);
476
+ }
477
+ }
478
+ }
479
+
480
+ return {
481
+ ports,
482
+ };
483
+ }
484
+
431
485
  function getNetworkChanges(
432
486
  props: ContainerProps,
433
487
  containerInfo: ContainerInfo,
@@ -477,7 +531,12 @@ function shouldReplace(
477
531
  }
478
532
 
479
533
  // Environment variables
480
- if (!compareEnv(props.environment, containerInfo.Config.Env)) {
534
+ if (
535
+ !compareEnv(
536
+ normalizeEnvironment(props.environment),
537
+ containerInfo.Config.Env,
538
+ )
539
+ ) {
481
540
  return true;
482
541
  }
483
542
 
@@ -513,6 +572,21 @@ function shouldReplace(
513
572
  return false;
514
573
  }
515
574
 
575
+ /**
576
+ * Unwrap secrets in given environment variables
577
+ * @internal
578
+ */
579
+ function normalizeEnvironment(
580
+ environment: Record<string, string | Secret> | undefined,
581
+ ): Record<string, string> {
582
+ return Object.fromEntries(
583
+ Object.entries(environment ?? {}).map(([key, value]) => [
584
+ key,
585
+ Secret.unwrap(value),
586
+ ]),
587
+ );
588
+ }
589
+
516
590
  /**
517
591
  * Normalize port mappings to a comparable format
518
592
  * @internal
@@ -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
  );
@@ -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
  );