alchemy 2.0.0-beta.20 → 2.0.0-beta.22

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 (136) hide show
  1. package/bin/alchemy.js +1 -1
  2. package/bin/alchemy.js.map +1 -1
  3. package/bin/commands/bootstrap.ts +59 -57
  4. package/bin/commands/deploy.ts +8 -2
  5. package/bin/commands/login.ts +56 -49
  6. package/bin/commands/logs.ts +92 -90
  7. package/bin/commands/profile.ts +74 -70
  8. package/bin/commands/state.ts +160 -135
  9. package/bin/commands/tail.ts +94 -92
  10. package/lib/AWS/CloudFront/CachePolicy.d.ts.map +1 -1
  11. package/lib/AWS/CloudFront/CachePolicy.js.map +1 -1
  12. package/lib/AWS/CloudFront/KeyGroup.d.ts.map +1 -1
  13. package/lib/AWS/CloudFront/KeyGroup.js.map +1 -1
  14. package/lib/AWS/CloudFront/OriginRequestPolicy.d.ts.map +1 -1
  15. package/lib/AWS/CloudFront/OriginRequestPolicy.js +3 -1
  16. package/lib/AWS/CloudFront/OriginRequestPolicy.js.map +1 -1
  17. package/lib/AWS/CloudFront/PublicKey.d.ts.map +1 -1
  18. package/lib/AWS/CloudFront/PublicKey.js.map +1 -1
  19. package/lib/AWS/ECS/CapacityProvider.d.ts.map +1 -1
  20. package/lib/AWS/ECS/CapacityProvider.js +5 -4
  21. package/lib/AWS/ECS/CapacityProvider.js.map +1 -1
  22. package/lib/Axiom/Annotation.d.ts +3 -3
  23. package/lib/Axiom/Annotation.d.ts.map +1 -1
  24. package/lib/Axiom/Annotation.js +5 -5
  25. package/lib/Axiom/Annotation.js.map +1 -1
  26. package/lib/Axiom/ApiToken.d.ts +3 -3
  27. package/lib/Axiom/ApiToken.d.ts.map +1 -1
  28. package/lib/Axiom/ApiToken.js +4 -4
  29. package/lib/Axiom/ApiToken.js.map +1 -1
  30. package/lib/Axiom/AuthProvider.js.map +1 -1
  31. package/lib/Axiom/Credentials.d.ts +1 -1
  32. package/lib/Axiom/Credentials.d.ts.map +1 -1
  33. package/lib/Axiom/Credentials.js +2 -3
  34. package/lib/Axiom/Credentials.js.map +1 -1
  35. package/lib/Axiom/Dashboard.d.ts +4 -4
  36. package/lib/Axiom/Dashboard.d.ts.map +1 -1
  37. package/lib/Axiom/Dashboard.js +5 -5
  38. package/lib/Axiom/Dashboard.js.map +1 -1
  39. package/lib/Axiom/Dataset.d.ts.map +1 -1
  40. package/lib/Axiom/Dataset.js +5 -5
  41. package/lib/Axiom/Dataset.js.map +1 -1
  42. package/lib/Axiom/Monitor.d.ts +3 -3
  43. package/lib/Axiom/Monitor.d.ts.map +1 -1
  44. package/lib/Axiom/Monitor.js +6 -6
  45. package/lib/Axiom/Monitor.js.map +1 -1
  46. package/lib/Axiom/Notifier.d.ts +3 -3
  47. package/lib/Axiom/Notifier.d.ts.map +1 -1
  48. package/lib/Axiom/Notifier.js +9 -6
  49. package/lib/Axiom/Notifier.js.map +1 -1
  50. package/lib/Axiom/View.d.ts +3 -3
  51. package/lib/Axiom/View.d.ts.map +1 -1
  52. package/lib/Axiom/View.js +5 -5
  53. package/lib/Axiom/View.js.map +1 -1
  54. package/lib/Axiom/VirtualField.d.ts +3 -3
  55. package/lib/Axiom/VirtualField.d.ts.map +1 -1
  56. package/lib/Axiom/VirtualField.js +5 -5
  57. package/lib/Axiom/VirtualField.js.map +1 -1
  58. package/lib/Bundle/PurePlugin.d.ts.map +1 -1
  59. package/lib/Bundle/PurePlugin.js +4 -4
  60. package/lib/Bundle/PurePlugin.js.map +1 -1
  61. package/lib/Cloudflare/ApiToken/AccountApiToken.d.ts.map +1 -1
  62. package/lib/Cloudflare/ApiToken/AccountApiToken.js.map +1 -1
  63. package/lib/Cloudflare/ApiToken/Common.d.ts.map +1 -1
  64. package/lib/Cloudflare/ApiToken/Common.js.map +1 -1
  65. package/lib/Cloudflare/ApiToken/UserApiToken.d.ts.map +1 -1
  66. package/lib/Cloudflare/ApiToken/UserApiToken.js.map +1 -1
  67. package/lib/Cloudflare/D1/D1Clone.d.ts +13 -0
  68. package/lib/Cloudflare/D1/D1Clone.d.ts.map +1 -0
  69. package/lib/Cloudflare/D1/D1Clone.js +31 -0
  70. package/lib/Cloudflare/D1/D1Clone.js.map +1 -0
  71. package/lib/Cloudflare/D1/D1Database.d.ts +150 -5
  72. package/lib/Cloudflare/D1/D1Database.d.ts.map +1 -1
  73. package/lib/Cloudflare/D1/D1Database.js +253 -0
  74. package/lib/Cloudflare/D1/D1Database.js.map +1 -1
  75. package/lib/Cloudflare/D1/D1Export.d.ts +24 -0
  76. package/lib/Cloudflare/D1/D1Export.d.ts.map +1 -0
  77. package/lib/Cloudflare/D1/D1Export.js +34 -0
  78. package/lib/Cloudflare/D1/D1Export.js.map +1 -0
  79. package/lib/Cloudflare/D1/D1Import.d.ts +21 -0
  80. package/lib/Cloudflare/D1/D1Import.d.ts.map +1 -0
  81. package/lib/Cloudflare/D1/D1Import.js +87 -0
  82. package/lib/Cloudflare/D1/D1Import.js.map +1 -0
  83. package/lib/Cloudflare/D1/D1Migrations.d.ts +16 -0
  84. package/lib/Cloudflare/D1/D1Migrations.d.ts.map +1 -0
  85. package/lib/Cloudflare/D1/D1Migrations.js +110 -0
  86. package/lib/Cloudflare/D1/D1Migrations.js.map +1 -0
  87. package/lib/Cloudflare/D1/D1SqlFile.d.ts +20 -0
  88. package/lib/Cloudflare/D1/D1SqlFile.d.ts.map +1 -0
  89. package/lib/Cloudflare/D1/D1SqlFile.js +46 -0
  90. package/lib/Cloudflare/D1/D1SqlFile.js.map +1 -0
  91. package/lib/Cloudflare/Fetcher.d.ts.map +1 -1
  92. package/lib/Cloudflare/Fetcher.js +3 -1
  93. package/lib/Cloudflare/Fetcher.js.map +1 -1
  94. package/lib/Cloudflare/Providers.d.ts +1 -1
  95. package/lib/Cloudflare/Providers.d.ts.map +1 -1
  96. package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
  97. package/lib/Cloudflare/Workers/Worker.js +12 -3
  98. package/lib/Cloudflare/Workers/Worker.js.map +1 -1
  99. package/lib/Output.d.ts.map +1 -1
  100. package/lib/Output.js +1 -1
  101. package/lib/Output.js.map +1 -1
  102. package/lib/Telemetry/Layer.d.ts.map +1 -1
  103. package/lib/Telemetry/Layer.js.map +1 -1
  104. package/lib/Telemetry/Metrics.d.ts.map +1 -1
  105. package/lib/Telemetry/Metrics.js.map +1 -1
  106. package/package.json +5 -5
  107. package/src/AWS/CloudFront/CachePolicy.ts +5 -2
  108. package/src/AWS/CloudFront/KeyGroup.ts +2 -1
  109. package/src/AWS/CloudFront/OriginRequestPolicy.ts +8 -2
  110. package/src/AWS/CloudFront/PublicKey.ts +2 -1
  111. package/src/AWS/ECS/CapacityProvider.ts +12 -9
  112. package/src/Axiom/Annotation.ts +7 -7
  113. package/src/Axiom/ApiToken.ts +6 -6
  114. package/src/Axiom/AuthProvider.ts +2 -7
  115. package/src/Axiom/Credentials.ts +2 -6
  116. package/src/Axiom/Dashboard.ts +10 -13
  117. package/src/Axiom/Dataset.ts +8 -12
  118. package/src/Axiom/Monitor.ts +8 -8
  119. package/src/Axiom/Notifier.ts +11 -8
  120. package/src/Axiom/View.ts +7 -7
  121. package/src/Axiom/VirtualField.ts +7 -7
  122. package/src/Bundle/PurePlugin.ts +6 -5
  123. package/src/Cloudflare/ApiToken/AccountApiToken.ts +1 -3
  124. package/src/Cloudflare/ApiToken/Common.ts +1 -3
  125. package/src/Cloudflare/ApiToken/UserApiToken.ts +1 -3
  126. package/src/Cloudflare/D1/D1Clone.ts +42 -0
  127. package/src/Cloudflare/D1/D1Database.ts +386 -3
  128. package/src/Cloudflare/D1/D1Export.ts +70 -0
  129. package/src/Cloudflare/D1/D1Import.ts +156 -0
  130. package/src/Cloudflare/D1/D1Migrations.ts +229 -0
  131. package/src/Cloudflare/D1/D1SqlFile.ts +57 -0
  132. package/src/Cloudflare/Fetcher.ts +3 -1
  133. package/src/Cloudflare/Workers/Worker.ts +40 -27
  134. package/src/Output.ts +1 -2
  135. package/src/Telemetry/Layer.ts +9 -10
  136. package/src/Telemetry/Metrics.ts +1 -6
@@ -0,0 +1,70 @@
1
+ import * as d1 from "@distilled.cloud/cloudflare/d1";
2
+ import * as Effect from "effect/Effect";
3
+ import type * as HttpClient from "effect/unstable/http/HttpClient";
4
+ import type { Credentials } from "@distilled.cloud/cloudflare/Credentials";
5
+
6
+ export interface ExportD1DatabaseOptions {
7
+ accountId: string;
8
+ databaseId: string;
9
+ dumpOptions?: {
10
+ tables?: string[];
11
+ noSchema?: boolean;
12
+ noData?: boolean;
13
+ };
14
+ }
15
+
16
+ export interface ExportD1DatabaseResult {
17
+ filename: string;
18
+ signedUrl: string;
19
+ }
20
+
21
+ /**
22
+ * Initiates an export of a Cloudflare D1 database and returns a signed
23
+ * download URL. Recursively polls with the bookmark until the export
24
+ * completes or fails.
25
+ */
26
+ export const exportD1Database = (
27
+ options: ExportD1DatabaseOptions,
28
+ ): Effect.Effect<
29
+ ExportD1DatabaseResult,
30
+ d1.ExportDatabaseError,
31
+ Credentials | HttpClient.HttpClient
32
+ > =>
33
+ Effect.gen(function* () {
34
+ const exportDb = yield* d1.exportDatabase;
35
+
36
+ const poll = (
37
+ currentBookmark?: string,
38
+ ): Effect.Effect<
39
+ ExportD1DatabaseResult,
40
+ d1.ExportDatabaseError,
41
+ never
42
+ > =>
43
+ Effect.gen(function* () {
44
+ const data = yield* exportDb({
45
+ accountId: options.accountId,
46
+ databaseId: options.databaseId,
47
+ outputFormat: "polling",
48
+ currentBookmark,
49
+ dumpOptions: options.dumpOptions,
50
+ });
51
+
52
+ if (data.status === "complete" && data.result) {
53
+ if (!data.result.filename || !data.result.signedUrl) {
54
+ return yield* Effect.die(
55
+ "D1 export completed but missing filename/signedUrl",
56
+ );
57
+ }
58
+ return {
59
+ filename: data.result.filename,
60
+ signedUrl: data.result.signedUrl,
61
+ };
62
+ }
63
+ if (data.status === "error") {
64
+ return yield* Effect.die(data.error ?? "Error during D1 export");
65
+ }
66
+ return yield* poll(data.atBookmark ?? undefined);
67
+ });
68
+
69
+ return yield* poll();
70
+ });
@@ -0,0 +1,156 @@
1
+ import * as d1 from "@distilled.cloud/cloudflare/d1";
2
+ import {
3
+ Credentials,
4
+ formatHeaders,
5
+ } from "@distilled.cloud/cloudflare/Credentials";
6
+ import crypto from "node:crypto";
7
+ import * as Effect from "effect/Effect";
8
+ import * as HttpClient from "effect/unstable/http/HttpClient";
9
+ import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
10
+
11
+ export interface ImportD1DatabaseOptions {
12
+ accountId: string;
13
+ databaseId: string;
14
+ sqlData: string | Uint8Array;
15
+ filename?: string;
16
+ }
17
+
18
+ export interface ImportD1DatabaseResult {
19
+ filename: string;
20
+ numQueries: number;
21
+ }
22
+
23
+ interface ImportPollingResponse {
24
+ at_bookmark?: string | null;
25
+ error?: string | null;
26
+ filename?: string | null;
27
+ messages?: string[] | null;
28
+ result?: {
29
+ final_bookmark?: string | null;
30
+ num_queries?: number | null;
31
+ } | null;
32
+ status?: "complete" | "error" | null;
33
+ success?: boolean | null;
34
+ upload_url?: string | null;
35
+ }
36
+
37
+ const md5 = (data: string | Uint8Array): string =>
38
+ crypto.createHash("md5").update(data).digest("hex");
39
+
40
+ const importEndpoint = (
41
+ apiBaseUrl: string,
42
+ accountId: string,
43
+ databaseId: string,
44
+ ) => `${apiBaseUrl}/accounts/${accountId}/d1/database/${databaseId}/import`;
45
+
46
+ /**
47
+ * Import SQL into a D1 database via the multi-step Cloudflare flow:
48
+ * 1. POST `action: "init"` -> returns presigned `upload_url`
49
+ * 2. PUT raw SQL to `upload_url`
50
+ * 3. POST `action: "ingest"` -> returns `at_bookmark`
51
+ * 4. POST `action: "poll"` until `status === "complete"`
52
+ */
53
+ export const importD1Database = (options: ImportD1DatabaseOptions) =>
54
+ Effect.gen(function* () {
55
+ const credentialsEff = yield* Credentials;
56
+ const credentials = yield* credentialsEff;
57
+ const authHeaders = formatHeaders(credentials);
58
+ const url = importEndpoint(
59
+ credentials.apiBaseUrl,
60
+ options.accountId,
61
+ options.databaseId,
62
+ );
63
+ const client = yield* HttpClient.HttpClient;
64
+
65
+ const postJson = (
66
+ body: unknown,
67
+ ): Effect.Effect<ImportPollingResponse, never, never> =>
68
+ Effect.gen(function* () {
69
+ const req = HttpClientRequest.post(url).pipe(
70
+ HttpClientRequest.setHeaders(authHeaders),
71
+ HttpClientRequest.bodyJsonUnsafe(body),
72
+ );
73
+ const res = yield* client.execute(req).pipe(Effect.orDie);
74
+ if (res.status < 200 || res.status >= 300) {
75
+ const text = yield* res.text.pipe(Effect.orElseSucceed(() => ""));
76
+ return yield* Effect.die(
77
+ `D1 import request failed (${res.status}): ${text}`,
78
+ );
79
+ }
80
+ const text = yield* res.text.pipe(Effect.orDie);
81
+ const json = JSON.parse(text) as { result: ImportPollingResponse };
82
+ return json.result;
83
+ });
84
+
85
+ const etag = md5(options.sqlData);
86
+
87
+ // Step 1: init via the typed d1 client
88
+ const initDb = yield* d1.importDatabase;
89
+ const init = yield* initDb({
90
+ accountId: options.accountId,
91
+ databaseId: options.databaseId,
92
+ action: "init",
93
+ etag,
94
+ });
95
+
96
+ if (!init.uploadUrl) {
97
+ return yield* Effect.die(
98
+ init.error ?? "Failed to get upload URL for D1 import",
99
+ );
100
+ }
101
+ const uploadFilename = options.filename ?? init.filename ?? "import.sql";
102
+
103
+ // Step 2: PUT to the presigned upload URL
104
+ const bytes =
105
+ typeof options.sqlData === "string"
106
+ ? new TextEncoder().encode(options.sqlData)
107
+ : options.sqlData;
108
+ const putReq = HttpClientRequest.put(init.uploadUrl).pipe(
109
+ HttpClientRequest.bodyUint8Array(bytes, "application/sql"),
110
+ );
111
+ const putRes = yield* client.execute(putReq).pipe(Effect.orDie);
112
+ if (putRes.status < 200 || putRes.status >= 300) {
113
+ const text = yield* putRes.text.pipe(Effect.orElseSucceed(() => ""));
114
+ return yield* Effect.die(
115
+ `Failed to upload SQL file to D1 (${putRes.status}): ${text}`,
116
+ );
117
+ }
118
+
119
+ // Step 3: ingest
120
+ const ingest = yield* postJson({
121
+ action: "ingest",
122
+ etag,
123
+ filename: init.filename,
124
+ });
125
+ if (!ingest.at_bookmark) {
126
+ return yield* Effect.die(
127
+ ingest.error ?? "Ingest response missing bookmark",
128
+ );
129
+ }
130
+
131
+ // Step 4: poll until complete
132
+ const poll = (
133
+ bookmark: string,
134
+ ): Effect.Effect<ImportD1DatabaseResult, never, never> =>
135
+ Effect.gen(function* () {
136
+ const data = yield* postJson({
137
+ action: "poll",
138
+ current_bookmark: bookmark,
139
+ });
140
+ if (data.status === "complete" && data.result) {
141
+ return {
142
+ filename: data.filename ?? uploadFilename,
143
+ numQueries: data.result.num_queries ?? 0,
144
+ };
145
+ }
146
+ if (data.status === "error") {
147
+ return yield* Effect.die(data.error ?? "Error during D1 import");
148
+ }
149
+ if (!data.at_bookmark) {
150
+ return yield* Effect.die("D1 import poll missing bookmark");
151
+ }
152
+ return yield* poll(data.at_bookmark);
153
+ });
154
+
155
+ return yield* poll(ingest.at_bookmark);
156
+ });
@@ -0,0 +1,229 @@
1
+ import * as d1 from "@distilled.cloud/cloudflare/d1";
2
+ import * as Effect from "effect/Effect";
3
+ import type * as HttpClient from "effect/unstable/http/HttpClient";
4
+ import type { Credentials } from "@distilled.cloud/cloudflare/Credentials";
5
+ import type { D1SqlFile } from "./D1SqlFile.ts";
6
+
7
+ export interface ApplyMigrationsOptions {
8
+ accountId: string;
9
+ databaseId: string;
10
+ migrationsTable: string;
11
+ migrationsFiles: ReadonlyArray<D1SqlFile>;
12
+ }
13
+
14
+ interface TableColumn {
15
+ name: string;
16
+ type: string;
17
+ pk: number;
18
+ }
19
+
20
+ interface SchemaInfo {
21
+ exists: boolean;
22
+ hasIdColumn: boolean;
23
+ hasNameColumn: boolean;
24
+ isLegacySchema: boolean;
25
+ columns: TableColumn[];
26
+ }
27
+
28
+ const executeSQL = (
29
+ accountId: string,
30
+ databaseId: string,
31
+ sql: string,
32
+ ): Effect.Effect<
33
+ d1.QueryDatabaseResponse,
34
+ d1.QueryDatabaseError,
35
+ Credentials | HttpClient.HttpClient
36
+ > =>
37
+ Effect.gen(function* () {
38
+ const queryDb = yield* d1.queryDatabase;
39
+ return yield* queryDb({ accountId, databaseId, sql });
40
+ });
41
+
42
+ const detectSchema = (
43
+ accountId: string,
44
+ databaseId: string,
45
+ migrationsTable: string,
46
+ ) =>
47
+ Effect.gen(function* () {
48
+ const result = yield* executeSQL(
49
+ accountId,
50
+ databaseId,
51
+ `PRAGMA table_info(${migrationsTable});`,
52
+ ).pipe(Effect.option);
53
+
54
+ const columns: TableColumn[] = [];
55
+ if (result._tag === "Some") {
56
+ const rows = (result.value.result[0]?.results ?? []) as Array<{
57
+ name: string;
58
+ type: string;
59
+ pk: number;
60
+ }>;
61
+ for (const row of rows) {
62
+ columns.push({ name: row.name, type: row.type, pk: row.pk });
63
+ }
64
+ }
65
+
66
+ if (columns.length === 0) {
67
+ return {
68
+ exists: false,
69
+ hasIdColumn: false,
70
+ hasNameColumn: false,
71
+ isLegacySchema: false,
72
+ columns,
73
+ } satisfies SchemaInfo;
74
+ }
75
+
76
+ const names = columns.map((c) => c.name);
77
+ const hasIdColumn = names.includes("id");
78
+ const hasNameColumn = names.includes("name");
79
+ const isLegacySchema =
80
+ columns.length === 2 && !(hasIdColumn && hasNameColumn);
81
+
82
+ return {
83
+ exists: true,
84
+ hasIdColumn,
85
+ hasNameColumn,
86
+ isLegacySchema,
87
+ columns,
88
+ } satisfies SchemaInfo;
89
+ });
90
+
91
+ const migrateLegacySchema = (
92
+ accountId: string,
93
+ databaseId: string,
94
+ migrationsTable: string,
95
+ schema: SchemaInfo,
96
+ ) =>
97
+ Effect.gen(function* () {
98
+ const primaryColumn =
99
+ schema.columns.find((c) => c.pk === 1)?.name ?? schema.columns[0]?.name;
100
+ if (!primaryColumn) {
101
+ return yield* Effect.die(
102
+ "Cannot migrate legacy migration table: no columns found",
103
+ );
104
+ }
105
+ const tempTable = `${migrationsTable}_temp_migration`;
106
+ yield* executeSQL(
107
+ accountId,
108
+ databaseId,
109
+ `CREATE TABLE ${tempTable} (
110
+ id TEXT PRIMARY KEY,
111
+ name TEXT NOT NULL,
112
+ applied_at TEXT NOT NULL
113
+ );`,
114
+ );
115
+ yield* executeSQL(
116
+ accountId,
117
+ databaseId,
118
+ `INSERT INTO ${tempTable} (id, name, applied_at)
119
+ SELECT
120
+ printf('%05d', row_number() OVER (ORDER BY applied_at)) as id,
121
+ ${primaryColumn} as name,
122
+ applied_at
123
+ FROM ${migrationsTable}
124
+ ORDER BY applied_at;`,
125
+ );
126
+ yield* executeSQL(accountId, databaseId, `DROP TABLE ${migrationsTable};`);
127
+ yield* executeSQL(
128
+ accountId,
129
+ databaseId,
130
+ `ALTER TABLE ${tempTable} RENAME TO ${migrationsTable};`,
131
+ );
132
+ });
133
+
134
+ const ensureMigrationsTable = (
135
+ accountId: string,
136
+ databaseId: string,
137
+ migrationsTable: string,
138
+ ) =>
139
+ Effect.gen(function* () {
140
+ const schema = yield* detectSchema(accountId, databaseId, migrationsTable);
141
+ if (!schema.exists) {
142
+ yield* executeSQL(
143
+ accountId,
144
+ databaseId,
145
+ `CREATE TABLE ${migrationsTable} (
146
+ id TEXT PRIMARY KEY,
147
+ name TEXT NOT NULL,
148
+ applied_at TEXT NOT NULL
149
+ );`,
150
+ );
151
+ return;
152
+ }
153
+ if (schema.isLegacySchema || !schema.hasIdColumn || !schema.hasNameColumn) {
154
+ yield* migrateLegacySchema(
155
+ accountId,
156
+ databaseId,
157
+ migrationsTable,
158
+ schema,
159
+ );
160
+ }
161
+ });
162
+
163
+ const getAppliedMigrations = (
164
+ accountId: string,
165
+ databaseId: string,
166
+ migrationsTable: string,
167
+ ) =>
168
+ Effect.gen(function* () {
169
+ const result = yield* executeSQL(
170
+ accountId,
171
+ databaseId,
172
+ `SELECT name FROM ${migrationsTable};`,
173
+ );
174
+ const rows = (result.result[0]?.results ?? []) as Array<{ name: string }>;
175
+ return new Set(rows.map((r) => r.name));
176
+ });
177
+
178
+ const getNextSeq = (
179
+ accountId: string,
180
+ databaseId: string,
181
+ migrationsTable: string,
182
+ ) =>
183
+ Effect.gen(function* () {
184
+ const result = yield* executeSQL(
185
+ accountId,
186
+ databaseId,
187
+ `SELECT id FROM ${migrationsTable} ORDER BY id;`,
188
+ );
189
+ const rows = (result.result[0]?.results ?? []) as Array<{ id: string }>;
190
+ let max = 0;
191
+ for (const { id } of rows) {
192
+ if (/^\d+$/.test(id)) {
193
+ max = Math.max(max, Number.parseInt(id, 10));
194
+ }
195
+ }
196
+ return max + 1;
197
+ });
198
+
199
+ /**
200
+ * Apply pending D1 migrations in order. Uses the wrangler-compatible
201
+ * 3-column schema `(id TEXT PK, name TEXT, applied_at TEXT)`.
202
+ */
203
+ export const applyMigrations = (options: ApplyMigrationsOptions) =>
204
+ Effect.gen(function* () {
205
+ const { accountId, databaseId, migrationsTable, migrationsFiles } = options;
206
+ yield* ensureMigrationsTable(accountId, databaseId, migrationsTable);
207
+ const applied = yield* getAppliedMigrations(
208
+ accountId,
209
+ databaseId,
210
+ migrationsTable,
211
+ );
212
+ let nextSeq = yield* getNextSeq(accountId, databaseId, migrationsTable);
213
+
214
+ for (const migration of migrationsFiles) {
215
+ if (applied.has(migration.id)) continue;
216
+ const migrationId = nextSeq.toString().padStart(5, "0");
217
+ nextSeq += 1;
218
+ // D1 over HTTP doesn't support transactions; run migration + record
219
+ // in a single batched query.
220
+ yield* executeSQL(
221
+ accountId,
222
+ databaseId,
223
+ [
224
+ migration.sql,
225
+ `INSERT INTO ${migrationsTable} (id, name, applied_at) VALUES ('${migrationId}', '${migration.id}', datetime('now'));`,
226
+ ].join("\n"),
227
+ );
228
+ }
229
+ });
@@ -0,0 +1,57 @@
1
+ import crypto from "node:crypto";
2
+ import * as Effect from "effect/Effect";
3
+ import * as FileSystem from "effect/FileSystem";
4
+ import * as Path from "effect/Path";
5
+
6
+ export interface D1SqlFile {
7
+ id: string;
8
+ sql: string;
9
+ hash: string;
10
+ }
11
+
12
+ /**
13
+ * Recursively list `.sql` files under `directory`, sorted by their numeric
14
+ * prefix (e.g. `0001_init.sql`) and then by name.
15
+ */
16
+ export const listSqlFiles = (directory: string) =>
17
+ Effect.gen(function* () {
18
+ const fs = yield* FileSystem.FileSystem;
19
+ const entries = yield* fs.readDirectory(directory, { recursive: true });
20
+
21
+ const sqlFiles = entries
22
+ .filter((name) => name.endsWith(".sql"))
23
+ .sort((a, b) => {
24
+ const aNum = getPrefix(a);
25
+ const bNum = getPrefix(b);
26
+ if (aNum !== null && bNum !== null) return aNum - bNum;
27
+ if (aNum !== null) return -1;
28
+ if (bNum !== null) return 1;
29
+ return a.localeCompare(b);
30
+ });
31
+
32
+ return yield* Effect.all(
33
+ sqlFiles.map((id) => readSqlFile(directory, id)),
34
+ );
35
+ });
36
+
37
+ /**
38
+ * Read a single `.sql` file relative to `directory` and compute its content
39
+ * hash. The `sql` field is marked non-enumerable so it isn't serialized into
40
+ * resource state.
41
+ */
42
+ export const readSqlFile = (directory: string, name: string) =>
43
+ Effect.gen(function* () {
44
+ const fs = yield* FileSystem.FileSystem;
45
+ const path = yield* Path.Path;
46
+ const sql = yield* fs.readFileString(path.resolve(directory, name));
47
+ const hash = crypto.createHash("sha256").update(sql).digest("hex");
48
+ const file: D1SqlFile = { id: name, sql, hash };
49
+ Object.defineProperty(file, "sql", { enumerable: false });
50
+ return file;
51
+ });
52
+
53
+ const getPrefix = (name: string): number | null => {
54
+ const prefix = name.split("_")[0];
55
+ const num = Number.parseInt(prefix, 10);
56
+ return Number.isNaN(num) ? null : num;
57
+ };
@@ -240,7 +240,9 @@ export const fromCloudflareSocket = (cfSocket: cf.Socket): Socket.Socket => {
240
240
  ): Effect.Effect<void, Socket.SocketError | E, R> =>
241
241
  runRaw(
242
242
  (data) =>
243
- typeof data === "string" ? handler(data) : handler(decoder.decode(data)),
243
+ typeof data === "string"
244
+ ? handler(data)
245
+ : handler(decoder.decode(data)),
244
246
  opts,
245
247
  );
246
248
 
@@ -690,42 +690,42 @@ export const Worker: Platform<
690
690
  namespace: binding.namespace,
691
691
  } as any)
692
692
  : isDurableObjectNamespaceLike(binding)
693
- ? {
694
- type: "durable_object_namespace",
695
- name: bindingName,
696
- className: binding.className ?? binding.name,
697
- }
698
- : binding.Type === "Cloudflare.D1Database"
699
693
  ? {
700
- type: "d1",
701
- id: binding.databaseId,
694
+ type: "durable_object_namespace",
702
695
  name: bindingName,
696
+ className: binding.className ?? binding.name,
703
697
  }
704
- : binding.Type === "Cloudflare.R2Bucket"
698
+ : binding.Type === "Cloudflare.D1Database"
705
699
  ? {
706
- type: "r2_bucket",
700
+ type: "d1",
701
+ id: binding.databaseId,
707
702
  name: bindingName,
708
- bucketName: binding.bucketName,
709
- jurisdiction: binding.jurisdiction.pipe(
710
- Output.map((jurisdiction) =>
711
- jurisdiction === "default" ? undefined : jurisdiction,
712
- ),
713
- ),
714
703
  }
715
- : binding.Type === "Cloudflare.KVNamespace"
704
+ : binding.Type === "Cloudflare.R2Bucket"
716
705
  ? {
717
- type: "kv_namespace",
706
+ type: "r2_bucket",
718
707
  name: bindingName,
719
- namespaceId: binding.namespaceId,
708
+ bucketName: binding.bucketName,
709
+ jurisdiction: binding.jurisdiction.pipe(
710
+ Output.map((jurisdiction) =>
711
+ jurisdiction === "default" ? undefined : jurisdiction,
712
+ ),
713
+ ),
720
714
  }
721
- : binding.Type === "Cloudflare.Queue"
715
+ : binding.Type === "Cloudflare.KVNamespace"
722
716
  ? {
723
- type: "queue",
717
+ type: "kv_namespace",
724
718
  name: bindingName,
725
- queueName: binding.queueName,
719
+ namespaceId: binding.namespaceId,
726
720
  }
727
- : // TODO(sam): handle others
728
- undefined;
721
+ : binding.Type === "Cloudflare.Queue"
722
+ ? {
723
+ type: "queue",
724
+ name: bindingName,
725
+ queueName: binding.queueName,
726
+ }
727
+ : // TODO(sam): handle others
728
+ undefined;
729
729
 
730
730
  if (bindingMeta) {
731
731
  yield* resource.bind`${bindingName}`({
@@ -1018,14 +1018,27 @@ export const WorkerProvider = () =>
1018
1018
  maxLength: 54,
1019
1019
  }).pipe(Effect.map((name) => name.toLowerCase()));
1020
1020
 
1021
+ // Convert non-ASCII hostnames (emoji, IDN, etc.) to punycode so the
1022
+ // Cloudflare API receives the form it stores domains in. `new URL(...)`
1023
+ // does IDNA via WHATWG URL parsing — `📦.alchemy.run` → `xn--5z8h.alchemy.run`.
1024
+ const toPunycode = (hostname: string): string => {
1025
+ try {
1026
+ return new URL(`https://${hostname}`).hostname;
1027
+ } catch {
1028
+ return hostname;
1029
+ }
1030
+ };
1031
+
1021
1032
  const normalizeDomains = (
1022
1033
  domain: string | string[] | undefined,
1023
1034
  ): string[] =>
1024
1035
  domain === undefined
1025
1036
  ? []
1026
- : Array.isArray(domain)
1027
- ? Array.from(new Set(domain))
1028
- : [domain];
1037
+ : Array.from(
1038
+ new Set(
1039
+ (Array.isArray(domain) ? domain : [domain]).map(toPunycode),
1040
+ ),
1041
+ );
1029
1042
 
1030
1043
  /**
1031
1044
  * Infer the Cloudflare Zone ID for a given hostname by listing the
package/src/Output.ts CHANGED
@@ -353,8 +353,7 @@ function proxy(self: any): any {
353
353
  prop === ExprSymbol || prop === inspect ? true : prop in self,
354
354
  get: (target, prop) =>
355
355
  prop === Symbol.toPrimitive
356
- ? (hint: string) =>
357
- hint === "number" ? Number.NaN : self.toString()
356
+ ? (hint: string) => (hint === "number" ? Number.NaN : self.toString())
358
357
  : prop === ExprSymbol
359
358
  ? self
360
359
  : prop === inspect
@@ -53,13 +53,12 @@ const buildOtlpLayer = (
53
53
  * resolves to {@link Layer.empty}. Effect's default `Tracer` is a no-op,
54
54
  * so all `withSpan`/`Effect.fn` instrumentation in core stays free.
55
55
  */
56
- export const TelemetryLive: Layer.Layer<never, never, never> =
57
- Layer.unwrap(
58
- Effect.gen(function* () {
59
- if (yield* isTelemetryDisabled) {
60
- return Layer.empty;
61
- }
62
- const attrs = yield* collectAttributes;
63
- return buildOtlpLayer(attrs as unknown as Record<string, unknown>);
64
- }),
65
- );
56
+ export const TelemetryLive: Layer.Layer<never, never, never> = Layer.unwrap(
57
+ Effect.gen(function* () {
58
+ if (yield* isTelemetryDisabled) {
59
+ return Layer.empty;
60
+ }
61
+ const attrs = yield* collectAttributes;
62
+ return buildOtlpLayer(attrs as unknown as Record<string, unknown>);
63
+ }),
64
+ );
@@ -75,12 +75,7 @@ export const recordCli =
75
75
  ),
76
76
  );
77
77
 
78
- export type ResourceOp =
79
- | "precreate"
80
- | "create"
81
- | "update"
82
- | "delete"
83
- | "read";
78
+ export type ResourceOp = "precreate" | "create" | "update" | "delete" | "read";
84
79
 
85
80
  const elapsed = (startNs: bigint): Duration.Duration =>
86
81
  Duration.nanos(process.hrtime.bigint() - startNs);