Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add beta support for export commands #568

Merged
merged 20 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading