wrangler 2.1.5 → 2.1.7
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.
- package/miniflare-dist/index.mjs +5 -20
- package/package.json +13 -3
- package/src/__tests__/api-dev.test.ts +20 -0
- package/src/__tests__/configuration.test.ts +128 -22
- package/src/__tests__/dev.test.tsx +0 -2
- package/src/__tests__/helpers/mock-oauth-flow.ts +4 -2
- package/src/__tests__/index.test.ts +2 -0
- package/src/__tests__/init.test.ts +72 -0
- package/src/__tests__/paths.test.ts +23 -1
- package/src/__tests__/publish.test.ts +65 -11
- package/src/__tests__/user.test.ts +4 -4
- package/src/__tests__/whoami.test.tsx +0 -1
- package/src/__tests__/worker-namespace.test.ts +102 -112
- package/src/api/dev.ts +12 -11
- package/src/bundle.ts +48 -0
- package/src/cfetch/internal.ts +37 -21
- package/src/config/config.ts +12 -0
- package/src/config/environment.ts +20 -0
- package/src/config/index.ts +32 -0
- package/src/config/validation.ts +68 -0
- package/src/config-cache.ts +1 -1
- package/src/create-worker-upload-form.ts +12 -2
- package/src/d1/backups.tsx +212 -0
- package/src/d1/create.tsx +54 -0
- package/src/d1/delete.tsx +56 -0
- package/src/d1/execute.tsx +294 -0
- package/src/d1/formatTimeAgo.ts +14 -0
- package/src/d1/index.ts +75 -0
- package/src/d1/list.tsx +48 -0
- package/src/d1/options.ts +12 -0
- package/src/d1/types.tsx +14 -0
- package/src/d1/utils.ts +39 -0
- package/src/dev/dev.tsx +26 -3
- package/src/dev/get-local-persistence-path.tsx +31 -0
- package/src/dev/local.tsx +8 -8
- package/src/dev/remote.tsx +1 -1
- package/src/dev/start-server.ts +2 -3
- package/src/dev/use-esbuild.ts +8 -1
- package/src/dev.tsx +28 -25
- package/src/dialogs.tsx +4 -0
- package/src/environment-variables.ts +17 -2
- package/src/index.tsx +26 -17
- package/src/init.ts +3 -1
- package/src/logger.ts +11 -4
- package/src/miniflare-cli/index.ts +11 -16
- package/src/pages/dev.tsx +13 -9
- package/src/paths.ts +30 -4
- package/src/proxy.ts +21 -1
- package/src/publish.ts +9 -1
- package/src/user/user.tsx +1 -0
- package/src/worker.ts +31 -1
- package/templates/d1-beta-facade.js +174 -0
- package/wrangler-dist/cli.d.ts +438 -6
- package/wrangler-dist/cli.js +10947 -3227
- package/src/miniflare-cli/enum-keys.ts +0 -17
package/src/config/index.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { findUpSync } from "find-up";
|
|
2
2
|
import { logger } from "../logger";
|
|
3
3
|
import { parseTOML, readFileSync } from "../parse";
|
|
4
|
+
import { removeD1BetaPrefix } from "../worker";
|
|
4
5
|
import { normalizeAndValidateConfig } from "./validation";
|
|
5
6
|
import type { CfWorkerInit } from "../worker";
|
|
6
7
|
import type { Config, RawConfig } from "./config";
|
|
8
|
+
import type { CamelCaseKey } from "yargs";
|
|
7
9
|
|
|
8
10
|
export type {
|
|
9
11
|
Config,
|
|
@@ -84,6 +86,7 @@ export function printBindings(bindings: CfWorkerInit["bindings"]) {
|
|
|
84
86
|
data_blobs,
|
|
85
87
|
durable_objects,
|
|
86
88
|
kv_namespaces,
|
|
89
|
+
d1_databases,
|
|
87
90
|
r2_buckets,
|
|
88
91
|
logfwdr,
|
|
89
92
|
services,
|
|
@@ -138,6 +141,20 @@ export function printBindings(bindings: CfWorkerInit["bindings"]) {
|
|
|
138
141
|
});
|
|
139
142
|
}
|
|
140
143
|
|
|
144
|
+
if (d1_databases !== undefined && d1_databases.length > 0) {
|
|
145
|
+
output.push({
|
|
146
|
+
type: "D1 Databases",
|
|
147
|
+
entries: d1_databases.map(({ binding, database_name, database_id }) => {
|
|
148
|
+
return {
|
|
149
|
+
key: removeD1BetaPrefix(binding),
|
|
150
|
+
value: database_name
|
|
151
|
+
? `${database_name} (${database_id})`
|
|
152
|
+
: database_id,
|
|
153
|
+
};
|
|
154
|
+
}),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
141
158
|
if (r2_buckets !== undefined && r2_buckets.length > 0) {
|
|
142
159
|
output.push({
|
|
143
160
|
type: "R2 Buckets",
|
|
@@ -249,3 +266,18 @@ export function printBindings(bindings: CfWorkerInit["bindings"]) {
|
|
|
249
266
|
|
|
250
267
|
logger.log(message);
|
|
251
268
|
}
|
|
269
|
+
|
|
270
|
+
type CamelCase<T> = {
|
|
271
|
+
[key in keyof T as key | CamelCaseKey<key>]: T[key];
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
export function withConfig<T extends { config?: string }>(
|
|
275
|
+
handler: (
|
|
276
|
+
t: Omit<CamelCase<T>, "config"> & { config: Config }
|
|
277
|
+
) => Promise<void>
|
|
278
|
+
) {
|
|
279
|
+
return (t: CamelCase<T>) => {
|
|
280
|
+
const { config: configPath, ...rest } = t;
|
|
281
|
+
return handler({ ...rest, config: readConfig(configPath, rest) });
|
|
282
|
+
};
|
|
283
|
+
}
|
package/src/config/validation.ts
CHANGED
|
@@ -102,6 +102,14 @@ export function normalizeAndValidateConfig(
|
|
|
102
102
|
"boolean"
|
|
103
103
|
);
|
|
104
104
|
|
|
105
|
+
validateOptionalProperty(
|
|
106
|
+
diagnostics,
|
|
107
|
+
"",
|
|
108
|
+
"keep_vars",
|
|
109
|
+
rawConfig.keep_vars,
|
|
110
|
+
"boolean"
|
|
111
|
+
);
|
|
112
|
+
|
|
105
113
|
// TODO: set the default to false to turn on service environments as the default
|
|
106
114
|
const isLegacyEnv =
|
|
107
115
|
(args as { "legacy-env": boolean | undefined })["legacy-env"] ??
|
|
@@ -182,6 +190,7 @@ export function normalizeAndValidateConfig(
|
|
|
182
190
|
configPath,
|
|
183
191
|
legacy_env: isLegacyEnv,
|
|
184
192
|
send_metrics: rawConfig.send_metrics,
|
|
193
|
+
keep_vars: rawConfig.keep_vars,
|
|
185
194
|
...activeEnv,
|
|
186
195
|
dev: normalizeAndValidateDev(diagnostics, rawConfig.dev ?? {}),
|
|
187
196
|
migrations: normalizeAndValidateMigrations(
|
|
@@ -889,6 +898,7 @@ function normalizeAndValidateEnvironment(
|
|
|
889
898
|
);
|
|
890
899
|
|
|
891
900
|
// The field "experimental_services" doesn't exist anymore in the config, but we still want to error about any older usage.
|
|
901
|
+
|
|
892
902
|
deprecated(
|
|
893
903
|
diagnostics,
|
|
894
904
|
rawEnv,
|
|
@@ -1075,6 +1085,16 @@ function normalizeAndValidateEnvironment(
|
|
|
1075
1085
|
validateBindingArray(envName, validateR2Binding),
|
|
1076
1086
|
[]
|
|
1077
1087
|
),
|
|
1088
|
+
d1_databases: notInheritable(
|
|
1089
|
+
diagnostics,
|
|
1090
|
+
topLevelEnv,
|
|
1091
|
+
rawConfig,
|
|
1092
|
+
rawEnv,
|
|
1093
|
+
envName,
|
|
1094
|
+
"d1_databases",
|
|
1095
|
+
validateBindingArray(envName, validateD1Binding),
|
|
1096
|
+
[]
|
|
1097
|
+
),
|
|
1078
1098
|
services: notInheritable(
|
|
1079
1099
|
diagnostics,
|
|
1080
1100
|
topLevelEnv,
|
|
@@ -1539,6 +1559,7 @@ const validateUnsafeBinding: ValidatorFn = (diagnostics, field, value) => {
|
|
|
1539
1559
|
"text_blob",
|
|
1540
1560
|
"kv_namespace",
|
|
1541
1561
|
"durable_object_namespace",
|
|
1562
|
+
"d1_database",
|
|
1542
1563
|
"r2_bucket",
|
|
1543
1564
|
"service",
|
|
1544
1565
|
"logfwdr",
|
|
@@ -1693,6 +1714,53 @@ const validateR2Binding: ValidatorFn = (diagnostics, field, value) => {
|
|
|
1693
1714
|
return isValid;
|
|
1694
1715
|
};
|
|
1695
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
|
+
|
|
1696
1764
|
/**
|
|
1697
1765
|
* Check that bindings whose names might conflict, don't.
|
|
1698
1766
|
*
|
package/src/config-cache.ts
CHANGED
|
@@ -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.
|
|
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
|
| {
|
|
@@ -59,7 +60,7 @@ export interface WorkerMetadata {
|
|
|
59
60
|
migrations?: CfDurableObjectMigrations;
|
|
60
61
|
capnp_schema?: string;
|
|
61
62
|
bindings: WorkerMetadataBinding[];
|
|
62
|
-
keep_bindings
|
|
63
|
+
keep_bindings?: WorkerMetadataBinding["type"][];
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
/**
|
|
@@ -74,6 +75,7 @@ export function createWorkerUploadForm(worker: CfWorkerInit): FormData {
|
|
|
74
75
|
usage_model,
|
|
75
76
|
compatibility_date,
|
|
76
77
|
compatibility_flags,
|
|
78
|
+
keepVars,
|
|
77
79
|
} = worker;
|
|
78
80
|
|
|
79
81
|
let { modules } = worker;
|
|
@@ -116,6 +118,14 @@ export function createWorkerUploadForm(worker: CfWorkerInit): FormData {
|
|
|
116
118
|
});
|
|
117
119
|
});
|
|
118
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
|
+
|
|
119
129
|
bindings.services?.forEach(({ binding, service, environment }) => {
|
|
120
130
|
metadataBindings.push({
|
|
121
131
|
name: binding,
|
|
@@ -257,7 +267,7 @@ export function createWorkerUploadForm(worker: CfWorkerInit): FormData {
|
|
|
257
267
|
...(usage_model && { usage_model }),
|
|
258
268
|
...(migrations && { migrations }),
|
|
259
269
|
capnp_schema: bindings.logfwdr?.schema,
|
|
260
|
-
keep_bindings: ["plain_text", "json"],
|
|
270
|
+
...(keepVars && { keep_bindings: ["plain_text", "json"] }),
|
|
261
271
|
};
|
|
262
272
|
|
|
263
273
|
formData.set("metadata", JSON.stringify(metadata));
|
|
@@ -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 '{db.name}'!</Text>
|
|
41
|
+
<Text> </Text>
|
|
42
|
+
<Text>
|
|
43
|
+
Add the following to your wrangler.toml to connect to it from a Worker:
|
|
44
|
+
</Text>
|
|
45
|
+
<Text> </Text>
|
|
46
|
+
<Text>[[ d1_databases ]]</Text>
|
|
47
|
+
<Text>
|
|
48
|
+
binding = "DB" # i.e. available in your Worker on env.DB
|
|
49
|
+
</Text>
|
|
50
|
+
<Text>database_name = "{db.name}"</Text>
|
|
51
|
+
<Text>database_id = "{db.uuid}"</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
|
+
);
|