Skip to content

Commit e21e429

Browse files
E. Cooperjrodewig
E. Cooper
andauthored
Add beta support for export commands (#568)
* Add TSV colorization * Add create, list, and get export commands --------- Co-authored-by: James Rodewig <[email protected]>
1 parent 36461e2 commit e21e429

22 files changed

+1959
-414
lines changed

src/cli.mjs

+4-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import chalk from "chalk";
88
import yargs from "yargs";
99

1010
import databaseCommand from "./commands/database/database.mjs";
11+
import exportCommand from "./commands/export/export.mjs";
1112
import localCommand from "./commands/local.mjs";
1213
import loginCommand from "./commands/login.mjs";
1314
import queryCommand from "./commands/query.mjs";
@@ -115,11 +116,12 @@ function buildYargs(argvInput) {
115116
[applyLocalArg, fixPaths, applyAccountUrl, buildCredentials, scopeSecret],
116117
false,
117118
)
119+
.command(loginCommand)
120+
.command(databaseCommand)
118121
.command(queryCommand)
119122
.command(shellCommand)
120-
.command(loginCommand)
121123
.command(schemaCommand)
122-
.command(databaseCommand)
124+
.command(exportCommand)
123125
.command(localCommand)
124126
.demandCommand()
125127
.strictCommands(true)

src/commands/database/list.mjs

+3-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@ async function doListDatabases(argv) {
6262
logger.stdout(colorize(res, { format: Format.JSON, color: argv.color }));
6363
} else {
6464
res.forEach(({ path, name }) => {
65-
logger.stdout(path ?? name);
65+
logger.stdout(
66+
colorize(path ?? name, { format: Format.CSV, color: argv.color }),
67+
);
6668
});
6769
}
6870
}

src/commands/export/create.mjs

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// @ts-check
2+
3+
import { container } from "../../config/container.mjs";
4+
import { EXPORT_TERMINAL_STATES } from "../../lib/account-api.mjs";
5+
import { ValidationError } from "../../lib/errors.mjs";
6+
import { colorize, Format } from "../../lib/formatting/colorize.mjs";
7+
import { DATABASE_PATH_OPTIONS } from "../../lib/options.mjs";
8+
import { WAIT_OPTIONS, waitUntilExportIsReady } from "./wait.mjs";
9+
10+
async function createS3Export(argv) {
11+
const {
12+
database,
13+
path,
14+
bucket,
15+
format,
16+
json,
17+
color,
18+
collection: collections,
19+
wait,
20+
maxWait,
21+
quiet,
22+
} = argv;
23+
const logger = container.resolve("logger");
24+
const { createExport } = container.resolve("accountAPI");
25+
26+
let createdExport = await createExport({
27+
database,
28+
collections,
29+
destination: {
30+
s3: {
31+
bucket,
32+
path,
33+
},
34+
},
35+
format,
36+
});
37+
38+
if (wait && !EXPORT_TERMINAL_STATES.includes(createdExport.state)) {
39+
createdExport = await waitUntilExportIsReady({
40+
id: createdExport.id,
41+
opts: {
42+
maxWait,
43+
quiet,
44+
},
45+
});
46+
}
47+
48+
if (json) {
49+
logger.stdout(colorize(createdExport, { color, format: Format.JSON }));
50+
} else {
51+
logger.stdout(colorize(createdExport, { color, format: Format.YAML }));
52+
}
53+
}
54+
55+
const sharedExamples = [
56+
[
57+
"$0 export create s3 --database us/my_db --bucket my-bucket --path exports/my_db",
58+
"Export the 'us-std/my_db' database to the 'exports/my_db' path of the 'my-bucket' S3 bucket. Outputs the export ID.",
59+
],
60+
[
61+
"$0 export create s3 --database us/my_db --bucket my-bucket --path my-prefix --json",
62+
"Output the full JSON of the export request.",
63+
],
64+
[
65+
"$0 export create s3 --database us/my_db --bucket my-bucket --path my-prefix --collection my-collection",
66+
"Export the 'my-collection' collection only.",
67+
],
68+
[
69+
"$0 export create s3 --database us/my_db --bucket my-bucket --path my-prefix --format tagged",
70+
"Encode the export's document data using the 'tagged' format.",
71+
],
72+
[
73+
"$0 export create s3 --database us/my_db --bucket my-bucket --path my-prefix --wait --max-wait 180",
74+
"Wait for the export to complete or fail before exiting. Waits up to 180 minutes.",
75+
],
76+
];
77+
78+
function buildCreateS3ExportCommand(yargs) {
79+
return yargs
80+
.options({
81+
bucket: {
82+
type: "string",
83+
required: true,
84+
description: "Name of the S3 bucket where the export will be stored.",
85+
group: "API:",
86+
},
87+
path: {
88+
type: "string",
89+
required: true,
90+
description:
91+
"Path prefix for the S3 bucket. Separate subfolders using a slash (`/`).",
92+
group: "API:",
93+
},
94+
format: {
95+
type: "string",
96+
required: true,
97+
description:
98+
"Data format used to encode the exported FQL document data as JSON.",
99+
choices: ["simple", "tagged"],
100+
default: "simple",
101+
group: "API:",
102+
},
103+
})
104+
.options(WAIT_OPTIONS)
105+
.check((argv) => {
106+
if (!argv.database) {
107+
throw new ValidationError(
108+
"--database is required to create an export.",
109+
);
110+
}
111+
112+
return true;
113+
})
114+
.example(sharedExamples);
115+
}
116+
117+
function buildCreateCommand(yargs) {
118+
return yargs
119+
.options(DATABASE_PATH_OPTIONS)
120+
.options({
121+
collection: {
122+
type: "array",
123+
required: false,
124+
description:
125+
"Used-defined collections to export. Pass values as a space-separated list. If omitted, all user-defined collections are exported.",
126+
default: [],
127+
group: "API:",
128+
},
129+
})
130+
.command({
131+
command: "s3",
132+
description: "Export to an S3 bucket.",
133+
builder: buildCreateS3ExportCommand,
134+
handler: createS3Export,
135+
})
136+
.example(sharedExamples)
137+
.demandCommand();
138+
}
139+
140+
export default {
141+
command: "create <destination-type>",
142+
description:
143+
"Start the export of a database or collections. Outputs the export ID.",
144+
builder: buildCreateCommand,
145+
};

src/commands/export/export.mjs

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import chalk from "chalk";
2+
3+
import { container } from "../../config/container.mjs";
4+
import { ValidationError } from "../../lib/errors.mjs";
5+
import { ACCOUNT_OPTIONS } from "../../lib/options.mjs";
6+
import createCommand from "./create.mjs";
7+
import getCommand from "./get.mjs";
8+
import listCommand from "./list.mjs";
9+
10+
/**
11+
* Validates the arguments do not include Core API authentication options.
12+
* In the CLI, we don't validate unknown options, but because these commands are unique and
13+
* only used the Account API, we aggressively validate the options here to avoid confusion.
14+
* @param {import("yargs").Arguments} argv
15+
* @returns {boolean}
16+
*/
17+
function validateAccountOnlyOptions(argv) {
18+
const { secret, local } = argv;
19+
20+
if (local) {
21+
throw new ValidationError(
22+
"Exports do not support --local or Fauna containers.",
23+
);
24+
}
25+
26+
if (secret) {
27+
throw new ValidationError("Exports do not support --secret.");
28+
}
29+
30+
return true;
31+
}
32+
33+
function buildExportCommand(yargs) {
34+
return yargs
35+
.options(ACCOUNT_OPTIONS)
36+
.middleware(() => {
37+
const logger = container.resolve("logger");
38+
logger.stderr(
39+
chalk.yellow(
40+
`Warning: fauna export is currently in beta. To learn more, visit https://docs.fauna.com/fauna/current/build/cli/v4/commands/export/\n`,
41+
),
42+
);
43+
})
44+
.check(validateAccountOnlyOptions)
45+
.command(createCommand)
46+
.command(listCommand)
47+
.command(getCommand)
48+
.example([
49+
[
50+
"$0 export create s3 --database us/my_db --bucket my-bucket --path exports/my_db",
51+
"Export the 'us-std/my_db' database to the 'exports/my_db' path of the 'my-bucket' S3 bucket. Outputs the export ID.",
52+
],
53+
[
54+
"$0 export get 123456789",
55+
"Output the YAML for the export with an ID of '123456789'.",
56+
],
57+
["$0 export list", "List exports in TSV format."],
58+
])
59+
.demandCommand();
60+
}
61+
62+
export default {
63+
command: "export <method>",
64+
description: "Create and manage exports. Currently in beta.",
65+
builder: buildExportCommand,
66+
};

src/commands/export/get.mjs

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { container } from "../../config/container.mjs";
2+
import { EXPORT_TERMINAL_STATES } from "../../lib/account-api.mjs";
3+
import { colorize, Format } from "../../lib/formatting/colorize.mjs";
4+
import { WAIT_OPTIONS, waitUntilExportIsReady } from "./wait.mjs";
5+
6+
async function getExport(argv) {
7+
const logger = container.resolve("logger");
8+
const { getExport } = container.resolve("accountAPI");
9+
const { exportId, json, color, wait, maxWait, quiet } = argv;
10+
11+
let response = await getExport({ exportId });
12+
if (wait && !EXPORT_TERMINAL_STATES.includes(response.state)) {
13+
response = await waitUntilExportIsReady({
14+
id: exportId,
15+
opts: {
16+
maxWait,
17+
quiet,
18+
},
19+
});
20+
}
21+
22+
if (json) {
23+
logger.stdout(colorize(response, { color, format: Format.JSON }));
24+
} else {
25+
logger.stdout(colorize(response, { color, format: Format.YAML }));
26+
}
27+
}
28+
29+
function buildGetExportCommand(yargs) {
30+
return yargs
31+
.positional("exportId", {
32+
type: "string",
33+
description: "ID of the export to retrieve.",
34+
nargs: 1,
35+
required: true,
36+
})
37+
.options(WAIT_OPTIONS)
38+
.example([
39+
[
40+
"$0 export get 123456789",
41+
"Output the YAML for the export with an ID of '123456789'.",
42+
],
43+
["$0 export get 123456789 --json", "Output the export as JSON."],
44+
[
45+
"$0 export get 123456789 --wait",
46+
"Wait for the export to complete or fail before exiting.",
47+
],
48+
]);
49+
}
50+
51+
export default {
52+
command: "get <exportId>",
53+
description: "Get an export by ID.",
54+
builder: buildGetExportCommand,
55+
handler: getExport,
56+
};

src/commands/export/list.mjs

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { container } from "../../config/container.mjs";
2+
import { EXPORT_STATES } from "../../lib/account-api.mjs";
3+
import { colorize, Format } from "../../lib/formatting/colorize.mjs";
4+
5+
const COLUMN_SEPARATOR = "\t";
6+
const COLLECTION_SEPARATOR = ",";
7+
8+
async function listExports(argv) {
9+
const logger = container.resolve("logger");
10+
const { json, color, maxResults, state } = argv;
11+
const { listExports } = container.resolve("accountAPI");
12+
13+
const { results } = await listExports({
14+
maxResults,
15+
state: state,
16+
});
17+
18+
if (json) {
19+
logger.stdout(colorize(results, { color, format: Format.JSON }));
20+
} else {
21+
if (!results.length) {
22+
return;
23+
}
24+
25+
results.forEach((r) => {
26+
const row = [
27+
r.id,
28+
r.database,
29+
(r.collections ?? []).join(COLLECTION_SEPARATOR),
30+
r.destination_uri,
31+
r.state,
32+
];
33+
logger.stdout(
34+
colorize(row.join(COLUMN_SEPARATOR), {
35+
color,
36+
format: Format.TSV,
37+
}),
38+
);
39+
});
40+
}
41+
}
42+
43+
function buildListExportsCommand(yargs) {
44+
return yargs
45+
.options({
46+
"max-results": {
47+
alias: "max",
48+
type: "number",
49+
description: "Maximum number of exports to return. Defaults to 10.",
50+
default: 10,
51+
group: "API:",
52+
},
53+
state: {
54+
type: "array",
55+
description: "Filter exports by state.",
56+
default: [],
57+
group: "API:",
58+
choices: EXPORT_STATES,
59+
},
60+
})
61+
.example([
62+
[
63+
"$0 export list",
64+
"List exports in TSV format with export ID, database, collections, destination, and state as the columns.",
65+
],
66+
["$0 export list --json", "List exports in JSON format."],
67+
["$0 export list --max-results 50", "List up to 50 exports."],
68+
[
69+
"$0 export list --states Pending Complete",
70+
"List exports in the 'Pending' or 'Complete' state.",
71+
],
72+
]);
73+
}
74+
75+
export default {
76+
command: "list",
77+
describe: "List exports.",
78+
builder: buildListExportsCommand,
79+
handler: listExports,
80+
};

0 commit comments

Comments
 (0)