alchemy 0.2.5 → 0.3.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 (151) hide show
  1. package/README.md +3 -3
  2. package/lib/ai/astro-file.d.ts +137 -0
  3. package/lib/ai/astro-file.js +173 -0
  4. package/lib/ai/client.d.ts +2 -10
  5. package/lib/ai/client.js +8 -24
  6. package/lib/ai/css-file.d.ts +131 -0
  7. package/lib/ai/css-file.js +158 -0
  8. package/lib/ai/data.d.ts +45 -5
  9. package/lib/ai/data.js +45 -10
  10. package/lib/ai/document.d.ts +44 -20
  11. package/lib/ai/document.js +95 -28
  12. package/lib/ai/html-file.d.ts +128 -0
  13. package/lib/ai/html-file.js +155 -0
  14. package/lib/ai/index.d.ts +7 -0
  15. package/lib/ai/index.js +7 -0
  16. package/lib/ai/json-file.d.ts +160 -0
  17. package/lib/ai/json-file.js +208 -0
  18. package/lib/ai/typescript-file.d.ts +140 -0
  19. package/lib/ai/typescript-file.js +176 -0
  20. package/lib/ai/vue-file.d.ts +126 -0
  21. package/lib/ai/vue-file.js +153 -0
  22. package/lib/ai/yaml-file.d.ts +169 -0
  23. package/lib/ai/yaml-file.js +236 -0
  24. package/lib/cloudflare/dns.d.ts +88 -0
  25. package/lib/cloudflare/dns.js +192 -0
  26. package/lib/cloudflare/index.d.ts +2 -1
  27. package/lib/cloudflare/index.js +2 -1
  28. package/lib/cloudflare/r2-rest-state-store.d.ts +125 -0
  29. package/lib/cloudflare/r2-rest-state-store.js +254 -0
  30. package/lib/cloudflare/response.d.ts +12 -0
  31. package/lib/cloudflare/response.js +0 -0
  32. package/lib/cloudflare/zone.d.ts +18 -0
  33. package/lib/cloudflare/zone.js +17 -4
  34. package/lib/dns/godaddy.d.ts +7 -0
  35. package/lib/dns/godaddy.js +21 -0
  36. package/lib/dns/import-dns.d.ts +82 -0
  37. package/lib/dns/import-dns.js +114 -0
  38. package/lib/dns/index.d.ts +2 -0
  39. package/lib/dns/index.js +2 -0
  40. package/lib/dns/record.d.ts +88 -0
  41. package/lib/dns/record.js +13 -0
  42. package/lib/fs/file-system-state-store.d.ts +17 -0
  43. package/lib/fs/file-system-state-store.js +87 -0
  44. package/lib/fs/folder.d.ts +10 -0
  45. package/lib/fs/folder.js +2 -2
  46. package/lib/fs/index.d.ts +4 -4
  47. package/lib/fs/index.js +4 -4
  48. package/lib/fs/{json-file.d.ts → static-json-file.d.ts} +2 -2
  49. package/lib/fs/static-json-file.js +14 -0
  50. package/lib/fs/{text-file.d.ts → static-text-file.d.ts} +2 -2
  51. package/lib/fs/{text-file.js → static-text-file.js} +1 -1
  52. package/lib/fs/{typescript-file.d.ts → static-typescript-file.d.ts} +2 -2
  53. package/lib/fs/{typescript-file.js → static-typescript-file.js} +1 -1
  54. package/lib/fs/{yaml-file.d.ts → static-yaml-file.d.ts} +2 -2
  55. package/lib/fs/{yaml-file.js → static-yaml-file.js} +1 -1
  56. package/lib/internal/getting-started.d.ts +10 -0
  57. package/lib/internal/getting-started.js +94 -0
  58. package/lib/internal/project.d.ts +13 -0
  59. package/lib/internal/project.js +101 -0
  60. package/lib/internal/providers.d.ts +23 -0
  61. package/lib/internal/providers.js +148 -0
  62. package/lib/resource.js +8 -1
  63. package/lib/scope.d.ts +1 -1
  64. package/lib/scope.js +4 -2
  65. package/lib/state.d.ts +1 -16
  66. package/lib/state.js +0 -87
  67. package/lib/test/bun.d.ts +11 -0
  68. package/lib/test/bun.js +36 -8
  69. package/lib/web/astro.d.ts +147 -0
  70. package/lib/web/astro.js +414 -0
  71. package/lib/{shadcn/component.js → web/shadcn-component.js} +2 -9
  72. package/lib/web/shadcn.d.ts +97 -0
  73. package/lib/web/shadcn.js +99 -0
  74. package/lib/web/tailwind.d.ts +64 -0
  75. package/lib/web/tailwind.js +97 -0
  76. package/lib/{vite → web}/vite.d.ts +5 -0
  77. package/lib/{vite → web}/vite.js +44 -44
  78. package/lib/web/vitepress/custom-theme.d.ts +173 -0
  79. package/lib/web/vitepress/custom-theme.js +336 -0
  80. package/lib/{vitepress → web/vitepress}/dependencies.d.ts +2 -2
  81. package/lib/{vitepress → web/vitepress}/dependencies.js +1 -1
  82. package/lib/web/vitepress/home-page.d.ts +244 -0
  83. package/lib/web/vitepress/home-page.js +134 -0
  84. package/lib/{vitepress → web/vitepress}/index.d.ts +1 -0
  85. package/lib/{vitepress → web/vitepress}/index.js +1 -0
  86. package/lib/{vitepress → web/vitepress}/vitepress.d.ts +10 -8
  87. package/lib/{vitepress → web/vitepress}/vitepress.js +11 -12
  88. package/package.json +6 -2
  89. package/src/ai/astro-file.ts +288 -0
  90. package/src/ai/client.ts +8 -28
  91. package/src/ai/css-file.ts +266 -0
  92. package/src/ai/data.ts +53 -16
  93. package/src/ai/document.ts +130 -33
  94. package/src/ai/html-file.ts +263 -0
  95. package/src/ai/index.ts +7 -0
  96. package/src/ai/json-file.ts +349 -0
  97. package/src/ai/typescript-file.ts +293 -0
  98. package/src/ai/vue-file.ts +261 -0
  99. package/src/ai/yaml-file.ts +368 -0
  100. package/src/apply.ts +0 -1
  101. package/src/cloudflare/dns.ts +344 -0
  102. package/src/cloudflare/index.ts +2 -1
  103. package/src/cloudflare/r2-rest-state-store.ts +353 -0
  104. package/src/cloudflare/response.ts +9 -0
  105. package/src/cloudflare/zone.ts +39 -23
  106. package/src/dns/godaddy.ts +29 -0
  107. package/src/dns/import-dns.ts +213 -0
  108. package/src/dns/index.ts +2 -0
  109. package/src/dns/record.ts +121 -0
  110. package/src/fs/file-system-state-store.ts +109 -0
  111. package/src/fs/folder.ts +14 -2
  112. package/src/fs/index.ts +4 -4
  113. package/src/fs/{json-file.ts → static-json-file.ts} +13 -3
  114. package/src/fs/{text-file.ts → static-text-file.ts} +5 -2
  115. package/src/fs/{typescript-file.ts → static-typescript-file.ts} +3 -3
  116. package/src/fs/{yaml-file.ts → static-yaml-file.ts} +5 -2
  117. package/src/internal/getting-started.ts +107 -0
  118. package/src/internal/project.ts +119 -0
  119. package/src/internal/providers.ts +209 -0
  120. package/src/resource.ts +10 -1
  121. package/src/scope.ts +5 -6
  122. package/src/state.ts +1 -111
  123. package/src/test/bun.ts +60 -10
  124. package/src/web/astro.ts +644 -0
  125. package/src/{shadcn/component.ts → web/shadcn-component.ts} +5 -13
  126. package/src/web/shadcn.ts +219 -0
  127. package/src/web/tailwind.ts +167 -0
  128. package/src/{vite → web}/vite.ts +54 -56
  129. package/src/web/vitepress/custom-theme.ts +514 -0
  130. package/src/{vitepress → web/vitepress}/dependencies.ts +2 -2
  131. package/src/web/vitepress/home-page.ts +363 -0
  132. package/src/{vitepress → web/vitepress}/index.ts +1 -0
  133. package/src/{vitepress → web/vitepress}/vitepress.ts +34 -27
  134. package/lib/cloudflare/state.d.ts +0 -82
  135. package/lib/cloudflare/state.js +0 -108
  136. package/lib/fs/json-file.js +0 -7
  137. package/lib/internal/docs.d.ts +0 -5
  138. package/lib/internal/docs.js +0 -287
  139. package/lib/shadcn/index.d.ts +0 -1
  140. package/lib/shadcn/index.js +0 -1
  141. package/lib/vite/index.d.ts +0 -1
  142. package/lib/vite/index.js +0 -1
  143. package/lib/vitepress/home-page.d.ts +0 -133
  144. package/lib/vitepress/home-page.js +0 -11
  145. package/src/cloudflare/state.ts +0 -138
  146. package/src/internal/docs.ts +0 -338
  147. package/src/shadcn/index.ts +0 -1
  148. package/src/vite/index.ts +0 -1
  149. package/src/vitepress/home-page.ts +0 -166
  150. /package/lib/{shadcn/component.d.ts → web/shadcn-component.d.ts} +0 -0
  151. /package/src/{vitepress → web/vitepress}/index.md +0 -0
@@ -0,0 +1,236 @@
1
+ import { generateObject, generateText } from "ai";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { Resource } from "../resource";
5
+ import { ignore } from "../util/ignore";
6
+ import { ark } from "./ark";
7
+ import { createModel } from "./client";
8
+ /**
9
+ * Default system prompt for YAML file generation without schema
10
+ */
11
+ const DEFAULT_YAML_SYSTEM_PROMPT = "You are a YAML generator. Create valid YAML based on the user's requirements. Your response MUST include only YAML inside ```yaml fences. Do not include any other text, explanations, or multiple code blocks. Use standard YAML syntax with proper indentation. Use quotes around strings that contain special characters when necessary.";
12
+ /**
13
+ * Resource for generating YAML files using AI models.
14
+ * Can operate in two modes:
15
+ * 1. With schema: Uses generateObject with type validation, then converts to YAML
16
+ * 2. Without schema: Extracts YAML from between ```yaml fences
17
+ *
18
+ * @example
19
+ * // Generate a serverless configuration file
20
+ * const serverlessConfig = await YAMLFile("serverless-config", {
21
+ * path: "./serverless.yml",
22
+ * prompt: await alchemy`
23
+ * Generate a serverless.yml configuration for an AWS Lambda API with:
24
+ * - A service name "user-api"
25
+ * - Node.js 16.x runtime
26
+ * - Three functions: createUser, getUser, and listUsers
27
+ * - API Gateway endpoints for each function
28
+ * - DynamoDB table for users
29
+ * - IAM permissions for DynamoDB access
30
+ * `,
31
+ * model: {
32
+ * id: "gpt-4o",
33
+ * provider: "openai"
34
+ * }
35
+ * });
36
+ *
37
+ * @example
38
+ * // Generate YAML with schema validation
39
+ * import { type } from "arktype";
40
+ *
41
+ * const k8sConfigSchema = type({
42
+ * apiVersion: "string",
43
+ * kind: "string",
44
+ * metadata: {
45
+ * name: "string",
46
+ * namespace: "string?",
47
+ * labels: "Record<string, string>?"
48
+ * },
49
+ * spec: {
50
+ * replicas: "number",
51
+ * selector: {
52
+ * matchLabels: "Record<string, string>"
53
+ * },
54
+ * template: {
55
+ * metadata: {
56
+ * labels: "Record<string, string>"
57
+ * },
58
+ * spec: {
59
+ * containers: [{
60
+ * name: "string",
61
+ * image: "string",
62
+ * ports: [{
63
+ * containerPort: "number"
64
+ * }]
65
+ * }]
66
+ * }
67
+ * }
68
+ * }
69
+ * });
70
+ *
71
+ * const deployment = await YAMLFile("k8s-deployment", {
72
+ * path: "./kubernetes/deployment.yaml",
73
+ * schema: k8sConfigSchema,
74
+ * prompt: "Generate a Kubernetes deployment for a web application named 'frontend' with 3 replicas using the nginx:latest image and exposing port 80",
75
+ * temperature: 0.2
76
+ * });
77
+ *
78
+ * @example
79
+ * // Generate GitHub Actions workflow with custom system prompt
80
+ * const workflow = await YAMLFile("github-workflow", {
81
+ * path: "./.github/workflows/ci.yml",
82
+ * prompt: await alchemy`
83
+ * Create a GitHub Actions workflow for a Node.js project that:
84
+ * - Runs on push to main and pull requests
85
+ * - Sets up Node.js 18
86
+ * - Installs dependencies with npm
87
+ * - Runs linting and tests
88
+ * - Builds the project
89
+ * - Deploys to GitHub Pages on success (main branch only)
90
+ * `,
91
+ * system: "You are a DevOps expert specializing in GitHub Actions workflows. Create a single YAML file inside ```yaml fences with no additional text. Follow GitHub Actions best practices and use proper YAML syntax.",
92
+ * model: {
93
+ * id: "claude-3-opus-20240229",
94
+ * provider: "anthropic"
95
+ * }
96
+ * });
97
+ */
98
+ export const YAMLFile = Resource("ai::YAMLFile", async function (id, props) {
99
+ // Ensure directory exists
100
+ await fs.mkdir(path.dirname(props.path), { recursive: true });
101
+ if (this.phase === "delete") {
102
+ try {
103
+ await fs.unlink(props.path);
104
+ }
105
+ catch (error) {
106
+ // Ignore if file doesn't exist
107
+ if (error.code !== "ENOENT") {
108
+ throw error;
109
+ }
110
+ }
111
+ return this.destroy();
112
+ }
113
+ // Dynamic import js-yaml to avoid dependency issues
114
+ // This allows the code to work even if js-yaml is not installed
115
+ let yaml;
116
+ try {
117
+ yaml = await import("yaml");
118
+ }
119
+ catch (err) {
120
+ throw new Error("The 'yaml' package is required for the YAMLFile resource. Please install it with: bun add yaml");
121
+ }
122
+ let yamlContent;
123
+ let yamlObject;
124
+ // Check if schema is provided
125
+ if (props.schema) {
126
+ // Use schema-based generation
127
+ const { object } = await generateObject({
128
+ model: createModel(props),
129
+ schema: ark.schema(props.schema),
130
+ providerOptions: props.model?.options,
131
+ system: props.system ||
132
+ "Generate a valid object based on the provided requirements.",
133
+ prompt: props.prompt,
134
+ ...(props.temperature === undefined
135
+ ? {}
136
+ : { temperature: props.temperature }),
137
+ });
138
+ yamlObject = object;
139
+ // Convert object to YAML
140
+ try {
141
+ yamlContent = yaml.stringify(yamlObject, {
142
+ indent: 2,
143
+ });
144
+ }
145
+ catch (error) {
146
+ throw new Error(`Failed to convert object to YAML: ${error.message}`);
147
+ }
148
+ }
149
+ else {
150
+ // Use fence-based extraction
151
+ // Use provided system prompt or default
152
+ const system = props.system || DEFAULT_YAML_SYSTEM_PROMPT;
153
+ // Generate initial content
154
+ const { text } = await generateText({
155
+ model: createModel(props),
156
+ prompt: props.prompt,
157
+ system,
158
+ providerOptions: props.model?.options,
159
+ ...(props.temperature === undefined
160
+ ? {}
161
+ : { temperature: props.temperature }),
162
+ });
163
+ // Extract and validate YAML content
164
+ let { content, error } = await extractYAMLContent(text);
165
+ // Re-prompt if there are validation errors
166
+ if (error) {
167
+ const errorSystem = `${system}\n\nERROR: ${error}\n\nPlease try again and ensure your response contains exactly one YAML block inside \`\`\`yaml fences.`;
168
+ const { text: retryText } = await generateText({
169
+ model: createModel(props),
170
+ prompt: props.prompt,
171
+ system: errorSystem,
172
+ providerOptions: props.model?.options,
173
+ ...(props.temperature === undefined
174
+ ? {}
175
+ : { temperature: props.temperature }),
176
+ });
177
+ const retryResult = await extractYAMLContent(retryText);
178
+ if (retryResult.error) {
179
+ throw new Error(`Failed to generate valid YAML: ${retryResult.error}`);
180
+ }
181
+ content = retryResult.content;
182
+ }
183
+ yamlContent = content;
184
+ // Parse YAML to validate and get object representation
185
+ try {
186
+ yamlObject = yaml.parse(yamlContent);
187
+ }
188
+ catch (error) {
189
+ throw new Error(`Failed to parse YAML: ${error.message}`);
190
+ }
191
+ }
192
+ if (this.phase === "update" && props.path !== this.props.path) {
193
+ await ignore("ENOENT", () => fs.unlink(this.props.path));
194
+ }
195
+ // Write content to file
196
+ await fs.writeFile(props.path, yamlContent);
197
+ // Get file stats for timestamps
198
+ const stats = await fs.stat(props.path);
199
+ // Return the resource
200
+ return this({
201
+ ...props,
202
+ schema: props.schema,
203
+ content: yamlContent,
204
+ yaml: yamlObject,
205
+ createdAt: stats.birthtimeMs,
206
+ updatedAt: stats.mtimeMs,
207
+ });
208
+ });
209
+ /**
210
+ * Extracts YAML content from between ```yaml fences
211
+ * Validates that exactly one YAML code block exists
212
+ *
213
+ * @param text The text to extract YAML from
214
+ * @returns The extracted YAML or error message
215
+ */
216
+ async function extractYAMLContent(text) {
217
+ // Check for yaml or yml fence blocks
218
+ const yamlCodeRegex = /```(yaml|yml)\s*([\s\S]*?)```/g;
219
+ const matches = Array.from(text.matchAll(yamlCodeRegex));
220
+ if (matches.length === 0) {
221
+ return {
222
+ content: "",
223
+ error: "No YAML code block found in the response. Please include your YAML within ```yaml fences.",
224
+ };
225
+ }
226
+ if (matches.length > 1) {
227
+ return {
228
+ content: "",
229
+ error: "Multiple YAML code blocks found in the response. Please provide exactly one YAML block within ```yaml fences.",
230
+ };
231
+ }
232
+ const content = matches[0][2].trim();
233
+ // We don't validate YAML parsing here because js-yaml might not be available
234
+ // Validation will happen at usage time if needed
235
+ return { content };
236
+ }
@@ -0,0 +1,88 @@
1
+ import type { Context } from "../context";
2
+ import type { DnsRecord as BaseDnsRecord, DnsRecordType, DnsRecordWithMetadata } from "../dns/record";
3
+ import { Resource } from "../resource";
4
+ /**
5
+ * Properties for a DNS record
6
+ */
7
+ export interface DnsRecordProps extends Omit<BaseDnsRecord, "type"> {
8
+ /**
9
+ * Record type (A, AAAA, CNAME, etc.)
10
+ */
11
+ type: DnsRecordType;
12
+ }
13
+ /**
14
+ * Output returned after DNS record creation/update
15
+ */
16
+ export interface DnsRecord extends DnsRecordWithMetadata {
17
+ }
18
+ /**
19
+ * Properties for managing multiple DNS records
20
+ */
21
+ export interface DnsRecordsProps {
22
+ /**
23
+ * Zone ID or domain name where records will be created
24
+ */
25
+ zoneId: string;
26
+ /**
27
+ * Array of DNS records to manage
28
+ */
29
+ records: DnsRecordProps[];
30
+ }
31
+ /**
32
+ * Output returned after DNS records creation/update
33
+ */
34
+ export interface DnsRecords extends Resource<"cloudflare::DnsRecords"> {
35
+ /**
36
+ * Zone ID where records are created
37
+ */
38
+ zoneId: string;
39
+ /**
40
+ * Array of created/updated DNS records
41
+ */
42
+ records: DnsRecord[];
43
+ }
44
+ /**
45
+ * Manages a batch of DNS records in a Cloudflare zone.
46
+ * Supports creating, updating, and deleting multiple records at once.
47
+ *
48
+ * @example
49
+ * // Create multiple A and CNAME records
50
+ * const dnsRecords = await DnsRecords("example.com-dns", {
51
+ * zone: "example.com",
52
+ * records: [
53
+ * {
54
+ * name: "www.example.com",
55
+ * type: "A",
56
+ * content: "192.0.2.1",
57
+ * proxied: true
58
+ * },
59
+ * {
60
+ * name: "blog.example.com",
61
+ * type: "CNAME",
62
+ * content: "www.example.com",
63
+ * proxied: true
64
+ * }
65
+ * ]
66
+ * });
67
+ *
68
+ * @example
69
+ * // Create MX records for email routing
70
+ * const emailRecords = await DnsRecords("example.com-email", {
71
+ * zone: "example.com",
72
+ * records: [
73
+ * {
74
+ * name: "example.com",
75
+ * type: "MX",
76
+ * content: "aspmx.l.google.com",
77
+ * priority: 1
78
+ * },
79
+ * {
80
+ * name: "example.com",
81
+ * type: "MX",
82
+ * content: "alt1.aspmx.l.google.com",
83
+ * priority: 5
84
+ * }
85
+ * ]
86
+ * });
87
+ */
88
+ export declare const DnsRecords: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<DnsRecords>, id: string, props: DnsRecordsProps) => Promise<DnsRecords>);
@@ -0,0 +1,192 @@
1
+ import { Resource } from "../resource";
2
+ import { createCloudflareApi } from "./api";
3
+ /**
4
+ * Manages a batch of DNS records in a Cloudflare zone.
5
+ * Supports creating, updating, and deleting multiple records at once.
6
+ *
7
+ * @example
8
+ * // Create multiple A and CNAME records
9
+ * const dnsRecords = await DnsRecords("example.com-dns", {
10
+ * zone: "example.com",
11
+ * records: [
12
+ * {
13
+ * name: "www.example.com",
14
+ * type: "A",
15
+ * content: "192.0.2.1",
16
+ * proxied: true
17
+ * },
18
+ * {
19
+ * name: "blog.example.com",
20
+ * type: "CNAME",
21
+ * content: "www.example.com",
22
+ * proxied: true
23
+ * }
24
+ * ]
25
+ * });
26
+ *
27
+ * @example
28
+ * // Create MX records for email routing
29
+ * const emailRecords = await DnsRecords("example.com-email", {
30
+ * zone: "example.com",
31
+ * records: [
32
+ * {
33
+ * name: "example.com",
34
+ * type: "MX",
35
+ * content: "aspmx.l.google.com",
36
+ * priority: 1
37
+ * },
38
+ * {
39
+ * name: "example.com",
40
+ * type: "MX",
41
+ * content: "alt1.aspmx.l.google.com",
42
+ * priority: 5
43
+ * }
44
+ * ]
45
+ * });
46
+ */
47
+ export const DnsRecords = Resource("cloudflare::DnsRecords", async function (id, props) {
48
+ // Create Cloudflare API client
49
+ const api = await createCloudflareApi();
50
+ // Get zone ID if domain name was provided
51
+ const zoneId = props.zoneId;
52
+ if (this.phase === "delete") {
53
+ if (this.output?.records) {
54
+ // Delete all existing records
55
+ await Promise.all(this.output.records.map(async (record) => {
56
+ try {
57
+ const response = await api.delete(`/zones/${zoneId}/dns_records/${record.id}`);
58
+ if (!response.ok && response.status !== 404) {
59
+ console.error(`Failed to delete DNS record ${record.name}: ${response.statusText}`);
60
+ }
61
+ }
62
+ catch (error) {
63
+ console.error(`Error deleting DNS record ${record.name}:`, error);
64
+ }
65
+ }));
66
+ }
67
+ return this.destroy();
68
+ }
69
+ if (this.phase === "update" && this.output?.records) {
70
+ // Get current records to compare with desired state
71
+ const currentRecords = this.output.records;
72
+ const desiredRecords = props.records;
73
+ // Find records to delete (exist in current but not in desired)
74
+ const recordsToDelete = currentRecords.filter((current) => !desiredRecords.some((desired) => desired.name === current.name && desired.type === current.type));
75
+ // Delete orphaned records
76
+ await Promise.all(recordsToDelete.map(async (record) => {
77
+ try {
78
+ const response = await api.delete(`/zones/${zoneId}/dns_records/${record.id}`);
79
+ if (!response.ok && response.status !== 404) {
80
+ console.error(`Failed to delete DNS record ${record.name}: ${response.statusText}`);
81
+ }
82
+ }
83
+ catch (error) {
84
+ console.error(`Error deleting DNS record ${record.name}:`, error);
85
+ }
86
+ }));
87
+ // Update or create records
88
+ const updatedRecords = await Promise.all(desiredRecords.map(async (desired) => {
89
+ // Find matching existing record
90
+ const existing = currentRecords.find((current) => current.name === desired.name && current.type === desired.type);
91
+ if (existing) {
92
+ // Update if content or other properties changed
93
+ if (existing.content !== desired.content ||
94
+ existing.ttl !== (desired.ttl || 1) ||
95
+ existing.proxied !== (desired.proxied || false) ||
96
+ existing.priority !== desired.priority ||
97
+ existing.comment !== desired.comment) {
98
+ return createOrUpdateRecord(api, zoneId, desired, existing.id);
99
+ }
100
+ return existing;
101
+ }
102
+ else {
103
+ // Create new record
104
+ return createOrUpdateRecord(api, zoneId, desired);
105
+ }
106
+ }));
107
+ return this({
108
+ zoneId,
109
+ records: updatedRecords,
110
+ });
111
+ }
112
+ // Create new records
113
+ const uniqueRecords = props.records.reduce((acc, record) => {
114
+ const key = `${record.name}-${record.type}`;
115
+ acc[key] = record;
116
+ return acc;
117
+ }, {});
118
+ const createdRecords = await Promise.all(Object.values(uniqueRecords).map(async (record) => {
119
+ // First check if record exists
120
+ const listResponse = await api.get(`/zones/${zoneId}/dns_records?type=${record.type}&name=${record.name}`);
121
+ if (!listResponse.ok) {
122
+ throw new Error(`Failed to check existing DNS records: ${listResponse.statusText}`);
123
+ }
124
+ const listResult = (await listResponse.json());
125
+ const existingRecord = listResult.result[0];
126
+ return createOrUpdateRecord(api, zoneId, record, existingRecord?.id);
127
+ }));
128
+ return this({
129
+ zoneId,
130
+ records: createdRecords,
131
+ });
132
+ });
133
+ /**
134
+ * Create or update a DNS record
135
+ */
136
+ async function createOrUpdateRecord(api, zoneId, record, existingId) {
137
+ const payload = getRecordPayload(record);
138
+ const response = await (existingId
139
+ ? api.put(`/zones/${zoneId}/dns_records/${existingId}`, payload)
140
+ : api.post(`/zones/${zoneId}/dns_records`, payload));
141
+ if (!response.ok) {
142
+ const errorBody = await response.text();
143
+ // If it's an update operation and the record doesn't exist, fall back to creation
144
+ if (existingId && response.status === 404) {
145
+ try {
146
+ const createResponse = await api.post(`/zones/${zoneId}/dns_records`, payload);
147
+ if (createResponse.ok) {
148
+ return convertCloudflareRecord((await createResponse.json()).result, zoneId);
149
+ }
150
+ }
151
+ catch (err) {
152
+ // Fall through to the original error
153
+ }
154
+ }
155
+ throw new Error(`Failed to ${existingId ? "update" : "create"} DNS record ${record.name}: ${response.statusText}\nResponse: ${errorBody}`);
156
+ }
157
+ const result = (await response.json());
158
+ return convertCloudflareRecord(result.result, zoneId);
159
+ }
160
+ /**
161
+ * Get the record payload for create/update operations
162
+ */
163
+ function getRecordPayload(record) {
164
+ return {
165
+ type: record.type,
166
+ name: record.name,
167
+ content: record.content,
168
+ ttl: record.ttl || 1,
169
+ proxied: record.proxied || false,
170
+ priority: record.priority,
171
+ comment: record.comment,
172
+ };
173
+ }
174
+ /**
175
+ * Convert a Cloudflare DNS record response to our DnsRecord type
176
+ */
177
+ function convertCloudflareRecord(record, zoneId) {
178
+ return {
179
+ id: record.id,
180
+ name: record.name,
181
+ type: record.type,
182
+ content: record.content,
183
+ ttl: record.ttl,
184
+ proxied: record.proxied,
185
+ priority: record.priority,
186
+ comment: record.comment,
187
+ tags: record.tags,
188
+ createdAt: new Date(record.created_on).getTime(),
189
+ modifiedAt: new Date(record.modified_on).getTime(),
190
+ zoneId,
191
+ };
192
+ }
@@ -1,8 +1,9 @@
1
1
  export * from "./bindings";
2
2
  export * from "./bucket";
3
+ export * from "./dns";
3
4
  export * from "./durable-object-namespace";
4
5
  export * from "./kv-namespace";
5
- export * from "./state";
6
+ export * from "./r2-rest-state-store";
6
7
  export * from "./static-site";
7
8
  export * from "./worker";
8
9
  export * from "./wrangler.json";
@@ -1,8 +1,9 @@
1
1
  export * from "./bindings";
2
2
  export * from "./bucket";
3
+ export * from "./dns";
3
4
  export * from "./durable-object-namespace";
4
5
  export * from "./kv-namespace";
5
- export * from "./state";
6
+ export * from "./r2-rest-state-store";
6
7
  export * from "./static-site";
7
8
  export * from "./worker";
8
9
  export * from "./wrangler.json";
@@ -0,0 +1,125 @@
1
+ import type { Scope } from "../scope";
2
+ import type { State, StateStore } from "../state";
3
+ /**
4
+ * Options for CloudflareR2StateStore
5
+ */
6
+ export interface CloudflareR2StateStoreOptions {
7
+ /**
8
+ * The prefix to use for object keys in the R2 bucket
9
+ * This allows multiple state stores to use the same R2 bucket
10
+ */
11
+ prefix?: string;
12
+ /**
13
+ * The R2 bucket name to use
14
+ * Required - the bucket must already exist
15
+ */
16
+ bucketName: string;
17
+ /**
18
+ * API key to use (overrides CLOUDFLARE_API_KEY env var)
19
+ */
20
+ apiKey?: string;
21
+ /**
22
+ * Account ID to use (overrides CLOUDFLARE_ACCOUNT_ID env var)
23
+ */
24
+ accountId?: string;
25
+ /**
26
+ * Email to use with API Key authentication (overrides CLOUDFLARE_EMAIL env var)
27
+ */
28
+ email?: string;
29
+ }
30
+ /**
31
+ * State store implementation using Cloudflare R2 API
32
+ * Uses R2 for immediate consistency compared to KV's eventual consistency
33
+ */
34
+ export declare class R2RestStateStore implements StateStore {
35
+ readonly scope: Scope;
36
+ private api;
37
+ private prefix;
38
+ private bucketName;
39
+ private apiKey;
40
+ private accountId;
41
+ private email;
42
+ private initialized;
43
+ /**
44
+ * Create a new CloudflareR2StateStore
45
+ *
46
+ * @param scope The scope this store belongs to
47
+ * @param options Options for the state store
48
+ */
49
+ constructor(scope: Scope, options: CloudflareR2StateStoreOptions);
50
+ /**
51
+ * Initialize the R2 client
52
+ */
53
+ init(): Promise<void>;
54
+ /**
55
+ * R2 buckets cannot be deleted programmatically via this method
56
+ */
57
+ deinit(): Promise<void>;
58
+ /**
59
+ * List all resources in the state store
60
+ */
61
+ list(): Promise<string[]>;
62
+ /**
63
+ * Count the number of items in the state store
64
+ */
65
+ count(): Promise<number>;
66
+ /**
67
+ * Get a state by key
68
+ *
69
+ * @param key The key to look up
70
+ * @returns The state or undefined if not found
71
+ */
72
+ get(key: string): Promise<State | undefined>;
73
+ /**
74
+ * Get multiple states by their keys
75
+ *
76
+ * @param ids Array of keys to fetch
77
+ * @returns Record mapping keys to their states
78
+ */
79
+ getBatch(ids: string[]): Promise<Record<string, State>>;
80
+ /**
81
+ * Get all states in the store
82
+ *
83
+ * @returns Record mapping all keys to their states
84
+ */
85
+ all(): Promise<Record<string, State>>;
86
+ /**
87
+ * Set a state for a key
88
+ *
89
+ * @param key The key to set
90
+ * @param value The state to store
91
+ */
92
+ set(key: string, value: State): Promise<void>;
93
+ /**
94
+ * Delete a state by key
95
+ *
96
+ * @param key The key to delete
97
+ */
98
+ delete(key: string): Promise<void>;
99
+ /**
100
+ * Convert key for storage by replacing slashes with colons
101
+ * since R2 treats slashes as directory separators
102
+ *
103
+ * @param key The original key
104
+ * @returns Key with slashes replaced by colons
105
+ */
106
+ private convertKeyForStorage;
107
+ /**
108
+ * Convert key from storage by replacing colons with slashes
109
+ *
110
+ * @param key The storage key
111
+ * @returns Key with colons replaced by slashes
112
+ */
113
+ private convertKeyFromStorage;
114
+ /**
115
+ * Get the full object key for storage
116
+ *
117
+ * @param key The original key
118
+ * @returns The key with prefix for use in the R2 bucket
119
+ */
120
+ private getObjectKey;
121
+ /**
122
+ * Ensure the store is initialized before operations
123
+ */
124
+ private ensureInitialized;
125
+ }