Skip to content

Commit

Permalink
Add beta support for export commands (#568)
Browse files Browse the repository at this point in the history
* Add TSV colorization
* Add create, list, and get export commands

---------

Co-authored-by: James Rodewig <james.rodewig@fauna.com>
  • Loading branch information
ecooper and jrodewig authored Jan 17, 2025
1 parent 36461e2 commit e21e429
Show file tree
Hide file tree
Showing 22 changed files with 1,959 additions and 414 deletions.
6 changes: 4 additions & 2 deletions src/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import chalk from "chalk";
import yargs from "yargs";

import databaseCommand from "./commands/database/database.mjs";
import exportCommand from "./commands/export/export.mjs";
import localCommand from "./commands/local.mjs";
import loginCommand from "./commands/login.mjs";
import queryCommand from "./commands/query.mjs";
Expand Down Expand Up @@ -115,11 +116,12 @@ function buildYargs(argvInput) {
[applyLocalArg, fixPaths, applyAccountUrl, buildCredentials, scopeSecret],
false,
)
.command(loginCommand)
.command(databaseCommand)
.command(queryCommand)
.command(shellCommand)
.command(loginCommand)
.command(schemaCommand)
.command(databaseCommand)
.command(exportCommand)
.command(localCommand)
.demandCommand()
.strictCommands(true)
Expand Down
4 changes: 3 additions & 1 deletion src/commands/database/list.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ async function doListDatabases(argv) {
logger.stdout(colorize(res, { format: Format.JSON, color: argv.color }));
} else {
res.forEach(({ path, name }) => {
logger.stdout(path ?? name);
logger.stdout(
colorize(path ?? name, { format: Format.CSV, color: argv.color }),
);
});
}
}
Expand Down
145 changes: 145 additions & 0 deletions src/commands/export/create.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// @ts-check

import { container } from "../../config/container.mjs";
import { EXPORT_TERMINAL_STATES } from "../../lib/account-api.mjs";
import { ValidationError } from "../../lib/errors.mjs";
import { colorize, Format } from "../../lib/formatting/colorize.mjs";
import { DATABASE_PATH_OPTIONS } from "../../lib/options.mjs";
import { WAIT_OPTIONS, waitUntilExportIsReady } from "./wait.mjs";

async function createS3Export(argv) {
const {
database,
path,
bucket,
format,
json,
color,
collection: collections,
wait,
maxWait,
quiet,
} = argv;
const logger = container.resolve("logger");
const { createExport } = container.resolve("accountAPI");

let createdExport = await createExport({
database,
collections,
destination: {
s3: {
bucket,
path,
},
},
format,
});

if (wait && !EXPORT_TERMINAL_STATES.includes(createdExport.state)) {
createdExport = await waitUntilExportIsReady({
id: createdExport.id,
opts: {
maxWait,
quiet,
},
});
}

if (json) {
logger.stdout(colorize(createdExport, { color, format: Format.JSON }));
} else {
logger.stdout(colorize(createdExport, { color, format: Format.YAML }));
}
}

const sharedExamples = [
[
"$0 export create s3 --database us/my_db --bucket my-bucket --path exports/my_db",
"Export the 'us-std/my_db' database to the 'exports/my_db' path of the 'my-bucket' S3 bucket. Outputs the export ID.",
],
[
"$0 export create s3 --database us/my_db --bucket my-bucket --path my-prefix --json",
"Output the full JSON of the export request.",
],
[
"$0 export create s3 --database us/my_db --bucket my-bucket --path my-prefix --collection my-collection",
"Export the 'my-collection' collection only.",
],
[
"$0 export create s3 --database us/my_db --bucket my-bucket --path my-prefix --format tagged",
"Encode the export's document data using the 'tagged' format.",
],
[
"$0 export create s3 --database us/my_db --bucket my-bucket --path my-prefix --wait --max-wait 180",
"Wait for the export to complete or fail before exiting. Waits up to 180 minutes.",
],
];

function buildCreateS3ExportCommand(yargs) {
return yargs
.options({
bucket: {
type: "string",
required: true,
description: "Name of the S3 bucket where the export will be stored.",
group: "API:",
},
path: {
type: "string",
required: true,
description:
"Path prefix for the S3 bucket. Separate subfolders using a slash (`/`).",
group: "API:",
},
format: {
type: "string",
required: true,
description:
"Data format used to encode the exported FQL document data as JSON.",
choices: ["simple", "tagged"],
default: "simple",
group: "API:",
},
})
.options(WAIT_OPTIONS)
.check((argv) => {
if (!argv.database) {
throw new ValidationError(
"--database is required to create an export.",
);
}

return true;
})
.example(sharedExamples);
}

function buildCreateCommand(yargs) {
return yargs
.options(DATABASE_PATH_OPTIONS)
.options({
collection: {
type: "array",
required: false,
description:
"Used-defined collections to export. Pass values as a space-separated list. If omitted, all user-defined collections are exported.",
default: [],
group: "API:",
},
})
.command({
command: "s3",
description: "Export to an S3 bucket.",
builder: buildCreateS3ExportCommand,
handler: createS3Export,
})
.example(sharedExamples)
.demandCommand();
}

export default {
command: "create <destination-type>",
description:
"Start the export of a database or collections. Outputs the export ID.",
builder: buildCreateCommand,
};
66 changes: 66 additions & 0 deletions src/commands/export/export.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import chalk from "chalk";

import { container } from "../../config/container.mjs";
import { ValidationError } from "../../lib/errors.mjs";
import { ACCOUNT_OPTIONS } from "../../lib/options.mjs";
import createCommand from "./create.mjs";
import getCommand from "./get.mjs";
import listCommand from "./list.mjs";

/**
* Validates the arguments do not include Core API authentication options.
* In the CLI, we don't validate unknown options, but because these commands are unique and
* only used the Account API, we aggressively validate the options here to avoid confusion.
* @param {import("yargs").Arguments} argv
* @returns {boolean}
*/
function validateAccountOnlyOptions(argv) {
const { secret, local } = argv;

if (local) {
throw new ValidationError(
"Exports do not support --local or Fauna containers.",
);
}

if (secret) {
throw new ValidationError("Exports do not support --secret.");
}

return true;
}

function buildExportCommand(yargs) {
return yargs
.options(ACCOUNT_OPTIONS)
.middleware(() => {
const logger = container.resolve("logger");
logger.stderr(
chalk.yellow(
`Warning: fauna export is currently in beta. To learn more, visit https://docs.fauna.com/fauna/current/build/cli/v4/commands/export/\n`,
),
);
})
.check(validateAccountOnlyOptions)
.command(createCommand)
.command(listCommand)
.command(getCommand)
.example([
[
"$0 export create s3 --database us/my_db --bucket my-bucket --path exports/my_db",
"Export the 'us-std/my_db' database to the 'exports/my_db' path of the 'my-bucket' S3 bucket. Outputs the export ID.",
],
[
"$0 export get 123456789",
"Output the YAML for the export with an ID of '123456789'.",
],
["$0 export list", "List exports in TSV format."],
])
.demandCommand();
}

export default {
command: "export <method>",
description: "Create and manage exports. Currently in beta.",
builder: buildExportCommand,
};
56 changes: 56 additions & 0 deletions src/commands/export/get.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { container } from "../../config/container.mjs";
import { EXPORT_TERMINAL_STATES } from "../../lib/account-api.mjs";
import { colorize, Format } from "../../lib/formatting/colorize.mjs";
import { WAIT_OPTIONS, waitUntilExportIsReady } from "./wait.mjs";

async function getExport(argv) {
const logger = container.resolve("logger");
const { getExport } = container.resolve("accountAPI");
const { exportId, json, color, wait, maxWait, quiet } = argv;

let response = await getExport({ exportId });
if (wait && !EXPORT_TERMINAL_STATES.includes(response.state)) {
response = await waitUntilExportIsReady({
id: exportId,
opts: {
maxWait,
quiet,
},
});
}

if (json) {
logger.stdout(colorize(response, { color, format: Format.JSON }));
} else {
logger.stdout(colorize(response, { color, format: Format.YAML }));
}
}

function buildGetExportCommand(yargs) {
return yargs
.positional("exportId", {
type: "string",
description: "ID of the export to retrieve.",
nargs: 1,
required: true,
})
.options(WAIT_OPTIONS)
.example([
[
"$0 export get 123456789",
"Output the YAML for the export with an ID of '123456789'.",
],
["$0 export get 123456789 --json", "Output the export as JSON."],
[
"$0 export get 123456789 --wait",
"Wait for the export to complete or fail before exiting.",
],
]);
}

export default {
command: "get <exportId>",
description: "Get an export by ID.",
builder: buildGetExportCommand,
handler: getExport,
};
80 changes: 80 additions & 0 deletions src/commands/export/list.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { container } from "../../config/container.mjs";
import { EXPORT_STATES } from "../../lib/account-api.mjs";
import { colorize, Format } from "../../lib/formatting/colorize.mjs";

const COLUMN_SEPARATOR = "\t";
const COLLECTION_SEPARATOR = ",";

async function listExports(argv) {
const logger = container.resolve("logger");
const { json, color, maxResults, state } = argv;
const { listExports } = container.resolve("accountAPI");

const { results } = await listExports({
maxResults,
state: state,
});

if (json) {
logger.stdout(colorize(results, { color, format: Format.JSON }));
} else {
if (!results.length) {
return;
}

results.forEach((r) => {
const row = [
r.id,
r.database,
(r.collections ?? []).join(COLLECTION_SEPARATOR),
r.destination_uri,
r.state,
];
logger.stdout(
colorize(row.join(COLUMN_SEPARATOR), {
color,
format: Format.TSV,
}),
);
});
}
}

function buildListExportsCommand(yargs) {
return yargs
.options({
"max-results": {
alias: "max",
type: "number",
description: "Maximum number of exports to return. Defaults to 10.",
default: 10,
group: "API:",
},
state: {
type: "array",
description: "Filter exports by state.",
default: [],
group: "API:",
choices: EXPORT_STATES,
},
})
.example([
[
"$0 export list",
"List exports in TSV format with export ID, database, collections, destination, and state as the columns.",
],
["$0 export list --json", "List exports in JSON format."],
["$0 export list --max-results 50", "List up to 50 exports."],
[
"$0 export list --states Pending Complete",
"List exports in the 'Pending' or 'Complete' state.",
],
]);
}

export default {
command: "list",
describe: "List exports.",
builder: buildListExportsCommand,
handler: listExports,
};
Loading

0 comments on commit e21e429

Please sign in to comment.