wrangler 2.1.6 → 2.1.8

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 (52) hide show
  1. package/miniflare-dist/index.mjs +5 -20
  2. package/package.json +14 -3
  3. package/src/__tests__/api-dev.test.ts +20 -0
  4. package/src/__tests__/configuration.test.ts +125 -22
  5. package/src/__tests__/dev.test.tsx +0 -2
  6. package/src/__tests__/helpers/mock-oauth-flow.ts +4 -2
  7. package/src/__tests__/index.test.ts +2 -0
  8. package/src/__tests__/paths.test.ts +23 -1
  9. package/src/__tests__/publish.test.ts +8 -10
  10. package/src/__tests__/user.test.ts +4 -4
  11. package/src/__tests__/whoami.test.tsx +0 -1
  12. package/src/__tests__/worker-namespace.test.ts +102 -112
  13. package/src/api/dev.ts +12 -12
  14. package/src/bundle.ts +59 -1
  15. package/src/cfetch/internal.ts +37 -21
  16. package/src/config/environment.ts +20 -0
  17. package/src/config/index.ts +32 -0
  18. package/src/config/validation.ts +59 -0
  19. package/src/config-cache.ts +1 -1
  20. package/src/create-worker-upload-form.ts +9 -0
  21. package/src/d1/backups.tsx +212 -0
  22. package/src/d1/create.tsx +54 -0
  23. package/src/d1/delete.tsx +56 -0
  24. package/src/d1/execute.tsx +294 -0
  25. package/src/d1/formatTimeAgo.ts +14 -0
  26. package/src/d1/index.ts +75 -0
  27. package/src/d1/list.tsx +48 -0
  28. package/src/d1/options.ts +12 -0
  29. package/src/d1/types.tsx +14 -0
  30. package/src/d1/utils.ts +39 -0
  31. package/src/dev/dev.tsx +30 -3
  32. package/src/dev/get-local-persistence-path.tsx +31 -0
  33. package/src/dev/local.tsx +73 -11
  34. package/src/dev/start-server.ts +6 -3
  35. package/src/dev/use-esbuild.ts +12 -1
  36. package/src/dev.tsx +48 -29
  37. package/src/dialogs.tsx +4 -0
  38. package/src/environment-variables.ts +17 -2
  39. package/src/index.tsx +18 -16
  40. package/src/logger.ts +11 -4
  41. package/src/miniflare-cli/index.ts +11 -16
  42. package/src/pages/dev.tsx +13 -9
  43. package/src/paths.ts +30 -4
  44. package/src/proxy.ts +21 -1
  45. package/src/publish.ts +7 -0
  46. package/src/user/user.tsx +1 -0
  47. package/src/worker.ts +30 -0
  48. package/templates/d1-beta-facade.js +174 -0
  49. package/templates/experimental-local-cache-stubs.js +27 -0
  50. package/wrangler-dist/cli.d.ts +438 -7
  51. package/wrangler-dist/cli.js +11679 -3911
  52. package/src/miniflare-cli/enum-keys.ts +0 -17
@@ -898,6 +898,7 @@ function normalizeAndValidateEnvironment(
898
898
  );
899
899
 
900
900
  // The field "experimental_services" doesn't exist anymore in the config, but we still want to error about any older usage.
901
+
901
902
  deprecated(
902
903
  diagnostics,
903
904
  rawEnv,
@@ -1084,6 +1085,16 @@ function normalizeAndValidateEnvironment(
1084
1085
  validateBindingArray(envName, validateR2Binding),
1085
1086
  []
1086
1087
  ),
1088
+ d1_databases: notInheritable(
1089
+ diagnostics,
1090
+ topLevelEnv,
1091
+ rawConfig,
1092
+ rawEnv,
1093
+ envName,
1094
+ "d1_databases",
1095
+ validateBindingArray(envName, validateD1Binding),
1096
+ []
1097
+ ),
1087
1098
  services: notInheritable(
1088
1099
  diagnostics,
1089
1100
  topLevelEnv,
@@ -1548,6 +1559,7 @@ const validateUnsafeBinding: ValidatorFn = (diagnostics, field, value) => {
1548
1559
  "text_blob",
1549
1560
  "kv_namespace",
1550
1561
  "durable_object_namespace",
1562
+ "d1_database",
1551
1563
  "r2_bucket",
1552
1564
  "service",
1553
1565
  "logfwdr",
@@ -1702,6 +1714,53 @@ const validateR2Binding: ValidatorFn = (diagnostics, field, value) => {
1702
1714
  return isValid;
1703
1715
  };
1704
1716
 
1717
+ const validateD1Binding: ValidatorFn = (diagnostics, field, value) => {
1718
+ if (typeof value !== "object" || value === null) {
1719
+ diagnostics.errors.push(
1720
+ `"d1_databases" bindings should be objects, but got ${JSON.stringify(
1721
+ value
1722
+ )}`
1723
+ );
1724
+ return false;
1725
+ }
1726
+ let isValid = true;
1727
+ // D1 databases must have a binding and either a database_name or database_id.
1728
+ if (!isRequiredProperty(value, "binding", "string")) {
1729
+ diagnostics.errors.push(
1730
+ `"${field}" bindings should have a string "binding" field but got ${JSON.stringify(
1731
+ value
1732
+ )}.`
1733
+ );
1734
+ isValid = false;
1735
+ }
1736
+ if (
1737
+ // TODO: allow name only, where we look up the ID dynamically
1738
+ // !isOptionalProperty(value, "database_name", "string") &&
1739
+ !isRequiredProperty(value, "database_id", "string")
1740
+ ) {
1741
+ diagnostics.errors.push(
1742
+ `"${field}" bindings must have a "database_id" field but got ${JSON.stringify(
1743
+ value
1744
+ )}.`
1745
+ );
1746
+ isValid = false;
1747
+ }
1748
+ if (!isOptionalProperty(value, "preview_database_id", "string")) {
1749
+ diagnostics.errors.push(
1750
+ `"${field}" bindings should, optionally, have a string "preview_database_id" field but got ${JSON.stringify(
1751
+ value
1752
+ )}.`
1753
+ );
1754
+ isValid = false;
1755
+ }
1756
+ if (isValid && !process.env.NO_D1_WARNING) {
1757
+ diagnostics.warnings.push(
1758
+ `D1 Bindings are currently in beta to allow the API to evolve before general availability.\nPlease report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose\nNote: set NO_D1_WARNING=true to hide this message`
1759
+ );
1760
+ }
1761
+ return isValid;
1762
+ };
1763
+
1705
1764
  /**
1706
1765
  * Check that bindings whose names might conflict, don't.
1707
1766
  *
@@ -32,7 +32,7 @@ const arrayFormatter = new Intl.ListFormat("en", {
32
32
  function showCacheMessage(fields: string[], folder: string) {
33
33
  if (!cacheMessageShown && isInteractive() && !CI.isCI()) {
34
34
  if (fields.length > 0) {
35
- logger.log(
35
+ logger.debug(
36
36
  `Retrieving cached values for ${arrayFormatter.format(
37
37
  fields
38
38
  )} from ${path.relative(process.cwd(), folder)}`
@@ -40,6 +40,7 @@ type WorkerMetadataBinding =
40
40
  environment?: string;
41
41
  }
42
42
  | { type: "r2_bucket"; name: string; bucket_name: string }
43
+ | { type: "d1"; name: string; id: string }
43
44
  | { type: "service"; name: string; service: string; environment?: string }
44
45
  | { type: "namespace"; name: string; namespace: string }
45
46
  | {
@@ -117,6 +118,14 @@ export function createWorkerUploadForm(worker: CfWorkerInit): FormData {
117
118
  });
118
119
  });
119
120
 
121
+ bindings.d1_databases?.forEach(({ binding, database_id }) => {
122
+ metadataBindings.push({
123
+ name: binding,
124
+ type: "d1",
125
+ id: database_id,
126
+ });
127
+ });
128
+
120
129
  bindings.services?.forEach(({ binding, service, environment }) => {
121
130
  metadataBindings.push({
122
131
  name: binding,
@@ -0,0 +1,212 @@
1
+ import fs from "node:fs/promises";
2
+ import { render } from "ink";
3
+ import Table from "ink-table";
4
+ import React from "react";
5
+ import { fetchResult } from "../cfetch";
6
+ import { performApiFetch } from "../cfetch/internal";
7
+ import { withConfig } from "../config";
8
+ import { logger } from "../logger";
9
+ import { requireAuth } from "../user";
10
+ import { formatBytes, formatTimeAgo } from "./formatTimeAgo";
11
+ import { Name } from "./options";
12
+ import { d1BetaWarning, getDatabaseByNameOrBinding } from "./utils";
13
+ import type { Backup, Database } from "./types";
14
+ import type { Response } from "undici";
15
+ import type { Argv } from "yargs";
16
+
17
+ type BackupListArgs = { config?: string; name: string };
18
+
19
+ export function ListOptions(yargs: Argv): Argv<BackupListArgs> {
20
+ return Name(yargs);
21
+ }
22
+
23
+ export const ListHandler = withConfig<BackupListArgs>(
24
+ async ({ config, name }): Promise<void> => {
25
+ const accountId = await requireAuth({});
26
+ logger.log(d1BetaWarning);
27
+ const db: Database = await getDatabaseByNameOrBinding(
28
+ config,
29
+ accountId,
30
+ name
31
+ );
32
+
33
+ const backups: Backup[] = await listBackups(accountId, db.uuid);
34
+ render(
35
+ <Table
36
+ data={backups}
37
+ columns={["created_at", "id", "num_tables", "size"]}
38
+ ></Table>
39
+ );
40
+ }
41
+ );
42
+
43
+ export const listBackups = async (
44
+ accountId: string,
45
+ uuid: string
46
+ ): Promise<Array<Backup>> => {
47
+ const json: Backup[] = await fetchResult(
48
+ `/accounts/${accountId}/d1/database/${uuid}/backup`,
49
+ {}
50
+ );
51
+ const results: Record<string, Backup> = {};
52
+
53
+ json
54
+ // First, convert created_at to a Date
55
+ .map((backup) => ({
56
+ ...backup,
57
+ created_at: new Date(backup.created_at),
58
+ }))
59
+ // Then, sort descending based on created_at
60
+ .sort((a, b) => +b.created_at - +a.created_at)
61
+ // then group_by their human-readable timestamp i.e. "2 days ago"
62
+ // (storing only the first of each group)
63
+ // and replace the Date version with this new human-readable one
64
+ .forEach((backup) => {
65
+ const timeAgo = formatTimeAgo(backup.created_at);
66
+ if (!results[timeAgo]) {
67
+ results[timeAgo] = {
68
+ ...backup,
69
+ created_at: timeAgo,
70
+ size: formatBytes(backup.file_size),
71
+ };
72
+ }
73
+ });
74
+
75
+ // Take advantage of JS objects' sorting to return the newest backup of a certain age
76
+ return Object.values(results);
77
+ };
78
+
79
+ type BackupCreateArgs = BackupListArgs;
80
+
81
+ export function CreateOptions(yargs: Argv): Argv<BackupCreateArgs> {
82
+ return ListOptions(yargs);
83
+ }
84
+
85
+ export const CreateHandler = withConfig<BackupCreateArgs>(
86
+ async ({ config, name }): Promise<void> => {
87
+ const accountId = await requireAuth({});
88
+ logger.log(d1BetaWarning);
89
+ const db: Database = await getDatabaseByNameOrBinding(
90
+ config,
91
+ accountId,
92
+ name
93
+ );
94
+
95
+ const backup: Backup = await createBackup(accountId, db.uuid);
96
+ render(
97
+ <Table
98
+ data={[backup]}
99
+ columns={["created_at", "id", "num_tables", "size", "state"]}
100
+ ></Table>
101
+ );
102
+ }
103
+ );
104
+
105
+ export const createBackup = async (
106
+ accountId: string,
107
+ uuid: string
108
+ ): Promise<Backup> => {
109
+ const backup: Backup = await fetchResult(
110
+ `/accounts/${accountId}/d1/database/${uuid}/backup`,
111
+ {
112
+ method: "POST",
113
+ }
114
+ );
115
+ return {
116
+ ...backup,
117
+ size: formatBytes(backup.file_size),
118
+ };
119
+ };
120
+
121
+ type BackupRestoreArgs = BackupListArgs & {
122
+ "backup-id": string;
123
+ };
124
+
125
+ export function RestoreOptions(yargs: Argv): Argv<BackupRestoreArgs> {
126
+ return ListOptions(yargs).positional("backup-id", {
127
+ describe: "The Backup ID to restore",
128
+ type: "string",
129
+ demandOption: true,
130
+ });
131
+ }
132
+
133
+ export const RestoreHandler = withConfig<BackupRestoreArgs>(
134
+ async ({ config, name, backupId }): Promise<void> => {
135
+ const accountId = await requireAuth({});
136
+ logger.log(d1BetaWarning);
137
+ const db: Database = await getDatabaseByNameOrBinding(
138
+ config,
139
+ accountId,
140
+ name
141
+ );
142
+
143
+ console.log(`Restoring ${name} from backup ${backupId}....`);
144
+ await restoreBackup(accountId, db.uuid, backupId);
145
+ console.log(`Done!`);
146
+ }
147
+ );
148
+
149
+ export const restoreBackup = async (
150
+ accountId: string,
151
+ uuid: string,
152
+ backupId: string
153
+ ): Promise<void> => {
154
+ await fetchResult(
155
+ `/accounts/${accountId}/d1/database/${uuid}/backup/${backupId}/restore`,
156
+ {
157
+ method: "POST",
158
+ headers: {
159
+ "Content-Type": "application/json",
160
+ },
161
+ }
162
+ );
163
+ };
164
+
165
+ type BackupDownloadArgs = BackupRestoreArgs & {
166
+ output?: string;
167
+ };
168
+
169
+ export function DownloadOptions(yargs: Argv): Argv<BackupDownloadArgs> {
170
+ return ListOptions(yargs)
171
+ .positional("backup-id", {
172
+ describe: "The Backup ID to download",
173
+ type: "string",
174
+ demandOption: true,
175
+ })
176
+ .option("output", {
177
+ describe:
178
+ "The .sqlite3 file to write to (defaults to '<db-name>.<short-backup-id>.sqlite3'",
179
+ type: "string",
180
+ });
181
+ }
182
+
183
+ export const DownloadHandler = withConfig<BackupDownloadArgs>(
184
+ async ({ name, backupId, output, config }): Promise<void> => {
185
+ const accountId = await requireAuth({});
186
+ logger.log(d1BetaWarning);
187
+ const db: Database = await getDatabaseByNameOrBinding(
188
+ config,
189
+ accountId,
190
+ name
191
+ );
192
+ const filename = output || `./${name}.${backupId.slice(0, 8)}.sqlite3`;
193
+
194
+ console.log(`Downloading backup ${backupId} of ${name} to: ${filename}`);
195
+ const response = await getBackupResponse(accountId, db.uuid, backupId);
196
+ console.log(`Got file. Saving...`);
197
+ // TODO: stream this once we upgrade to Node18 and can use Writable.fromWeb
198
+ const buffer = await response.arrayBuffer();
199
+ await fs.writeFile(filename, new Buffer(buffer));
200
+ console.log(`Done! Wrote ${filename} (${formatBytes(buffer.byteLength)})`);
201
+ }
202
+ );
203
+
204
+ export const getBackupResponse = async (
205
+ accountId: string,
206
+ uuid: string,
207
+ backupId: string
208
+ ): Promise<Response> => {
209
+ return await performApiFetch(
210
+ `/accounts/${accountId}/d1/database/${uuid}/backup/${backupId}/download`
211
+ );
212
+ };
@@ -0,0 +1,54 @@
1
+ import { render, Text, Box } from "ink";
2
+ import React from "react";
3
+ import { fetchResult } from "../cfetch";
4
+ import { logger } from "../logger";
5
+ import { requireAuth } from "../user";
6
+ import { d1BetaWarning } from "./utils";
7
+ import type { Database } from "./types";
8
+ import type { ArgumentsCamelCase, Argv } from "yargs";
9
+
10
+ type CreateArgs = { name: string };
11
+
12
+ export function Options(yargs: Argv): Argv<CreateArgs> {
13
+ return yargs
14
+ .positional("name", {
15
+ describe: "The name of the new DB",
16
+ type: "string",
17
+ demandOption: true,
18
+ })
19
+ .epilogue(d1BetaWarning);
20
+ }
21
+
22
+ export async function Handler({
23
+ name,
24
+ }: ArgumentsCamelCase<CreateArgs>): Promise<void> {
25
+ const accountId = await requireAuth({});
26
+ logger.log(d1BetaWarning);
27
+
28
+ const db: Database = await fetchResult(`/accounts/${accountId}/d1/database`, {
29
+ method: "POST",
30
+ headers: {
31
+ "Content-Type": "application/json",
32
+ },
33
+ body: JSON.stringify({
34
+ name,
35
+ }),
36
+ });
37
+
38
+ render(
39
+ <Box flexDirection="column">
40
+ <Text>✅ Successfully created DB &apos;{db.name}&apos;!</Text>
41
+ <Text>&nbsp;</Text>
42
+ <Text>
43
+ Add the following to your wrangler.toml to connect to it from a Worker:
44
+ </Text>
45
+ <Text>&nbsp;</Text>
46
+ <Text>[[ d1_databases ]]</Text>
47
+ <Text>
48
+ binding = &quot;DB&quot; # i.e. available in your Worker on env.DB
49
+ </Text>
50
+ <Text>database_name = &quot;{db.name}&quot;</Text>
51
+ <Text>database_id = &quot;{db.uuid}&quot;</Text>
52
+ </Box>
53
+ );
54
+ }
@@ -0,0 +1,56 @@
1
+ import { fetchResult } from "../cfetch";
2
+ import { withConfig } from "../config";
3
+ import { confirm } from "../dialogs";
4
+ import { logger } from "../logger";
5
+ import { requireAuth } from "../user";
6
+ import { Name } from "./options";
7
+ import { d1BetaWarning, getDatabaseByNameOrBinding } from "./utils";
8
+ import type { Database } from "./types";
9
+ import type { Argv } from "yargs";
10
+
11
+ type CreateArgs = {
12
+ config?: string;
13
+ name: string;
14
+ "skip-confirmation": boolean;
15
+ };
16
+
17
+ export function Options(d1ListYargs: Argv): Argv<CreateArgs> {
18
+ return Name(d1ListYargs)
19
+ .option("skip-confirmation", {
20
+ describe: "Skip confirmation",
21
+ type: "boolean",
22
+ alias: "y",
23
+ default: false,
24
+ })
25
+ .epilogue(d1BetaWarning);
26
+ }
27
+
28
+ export const Handler = withConfig<CreateArgs>(
29
+ async ({ name, skipConfirmation, config }): Promise<void> => {
30
+ const accountId = await requireAuth({});
31
+ logger.log(d1BetaWarning);
32
+
33
+ const db: Database = await getDatabaseByNameOrBinding(
34
+ config,
35
+ accountId,
36
+ name
37
+ );
38
+
39
+ console.log(`About to delete DB '${name}' (${db.uuid}).`);
40
+ if (!skipConfirmation) {
41
+ const response = await confirm(`Ok to proceed?`);
42
+ if (!response) {
43
+ console.log(`Not deleting.`);
44
+ return;
45
+ }
46
+ }
47
+
48
+ console.log("Deleting...");
49
+
50
+ await fetchResult(`/accounts/${accountId}/d1/database/${db.uuid}`, {
51
+ method: "DELETE",
52
+ });
53
+
54
+ console.log(`Deleted '${name}' successfully.`);
55
+ }
56
+ );