diff --git a/src/cli.mjs b/src/cli.mjs index 1a160b72..4b9d9e44 100644 --- a/src/cli.mjs +++ b/src/cli.mjs @@ -197,9 +197,9 @@ function buildYargs(argvInput) { default: false, group: "Output:", }, - verboseComponent: { + "verbose-component": { description: - "Components to emit diagnostic logs for. Takes precedence over the `--verbosity` flag. Pass components as a space-separated list, such as `--verboseComponent fetch error`, or as separate flags, such as `--verboseComponent fetch --verboseComponent error`.", + "Components to emit diagnostic logs for. Takes precedence over the `--verbosity` flag. Pass components as a space-separated list, such as `--verbose-component fetch error`, or as separate flags, such as `--verbose-component fetch --verbose-component error`.", type: "array", default: [], choices: ["fetch", "error", "config", "argv", "creds", "completion"], diff --git a/src/commands/database/create.mjs b/src/commands/database/create.mjs index db1691d7..fe19d606 100644 --- a/src/commands/database/create.mjs +++ b/src/commands/database/create.mjs @@ -79,7 +79,7 @@ function buildCreateCommand(yargs) { description: "Name of the child database to create.", }, typechecked: { - type: "string", + type: "boolean", description: "Enable typechecking for the database. Defaults to the typechecking setting of the parent database.", }, diff --git a/src/commands/database/list.mjs b/src/commands/database/list.mjs index 8fb27e7c..7dbfac3a 100644 --- a/src/commands/database/list.mjs +++ b/src/commands/database/list.mjs @@ -1,30 +1,11 @@ //@ts-check +import chalk from "chalk"; import { container } from "../../cli.mjs"; import { faunaToCommandError } from "../../lib/fauna.mjs"; import { FaunaAccountClient } from "../../lib/fauna-account-client.mjs"; import { colorize, Format } from "../../lib/formatting/colorize.mjs"; -// Narrow the output fields based on the provided flags. -const getOutputFields = (argv) => { - if (!argv.secret && !argv.database) { - // If we are listing top level databases the region group - // needs to be included as database names can be re-used across - // regions. - return ["name", "region_group"]; - } - return ["name"]; -}; - -function pickOutputFields(databases, argv) { - return databases.map((d) => - getOutputFields(argv).reduce((acc, field) => { - acc[field] = d[field]; - return acc; - }, {}), - ); -} - async function listDatabasesWithAccountAPI(argv) { const { pageSize, database } = argv; const accountClient = new FaunaAccountClient(); @@ -32,7 +13,8 @@ async function listDatabasesWithAccountAPI(argv) { pageSize, path: database, }); - return pickOutputFields(response.results, argv); + + return response.results.map(({ path, name }) => ({ path, name })); } async function listDatabasesWithSecret(argv) { @@ -40,43 +22,52 @@ async function listDatabasesWithSecret(argv) { const { runQueryFromString } = container.resolve("faunaClientV10"); try { - return await runQueryFromString({ + const res = await runQueryFromString({ url, secret, // This gives us back an array of database names. If we want to // provide the after token at some point this query will need to be updated. - expression: `Database.all().paginate(${pageSize}).data { ${getOutputFields(argv)} }`, + expression: `Database.all().paginate(${pageSize}).data { name }`, }); + return res.data; } catch (e) { return faunaToCommandError(e); } } export async function listDatabases(argv) { - let databases; if (argv.secret) { - databases = await listDatabasesWithSecret(argv); + return await listDatabasesWithSecret(argv); } else { - databases = await listDatabasesWithAccountAPI(argv); + return await listDatabasesWithAccountAPI(argv); } - return databases; } async function doListDatabases(argv) { const logger = container.resolve("logger"); - const { formatQueryResponse } = container.resolve("faunaClientV10"); const res = await listDatabases(argv); + if (argv.secret) { - logger.stdout(formatQueryResponse(res, argv)); - } else { + logger.stderr( + chalk.yellow( + "Warning: Full database paths are not available when using --secret. Use --database if a full path, including the Region Group identified and hierarchy, is needed.", + ), + ); + } + + if (argv.json) { logger.stdout(colorize(res, { format: Format.JSON, color: argv.color })); + } else { + res.forEach(({ path, name }) => { + logger.stdout(path ?? name); + }); } } function buildListCommand(yargs) { return yargs .options({ - pageSize: { + "page-size": { type: "number", description: "Maximum number of databases to return.", default: 1000, @@ -97,7 +88,7 @@ function buildListCommand(yargs) { "List all top-level databases and output as JSON.", ], [ - "$0 database list --pageSize 10", + "$0 database list --page-size 10", "List the first 10 top-level databases.", ], ]); diff --git a/src/commands/local.mjs b/src/commands/local.mjs index c78d0555..426b2d9e 100644 --- a/src/commands/local.mjs +++ b/src/commands/local.mjs @@ -123,18 +123,18 @@ ${chalk.red("Please use choose a different name using --name or align the --type function buildLocalCommand(yargs) { return yargs .options({ - containerPort: { + "container-port": { describe: "The port inside the container Fauna listens on.", type: "number", default: 8443, }, - hostPort: { + "host-port": { describe: "The port on the host machine mapped to the container's port. This is the port you'll connect to Fauna on.", type: "number", default: 8443, }, - hostIp: { + "host-ip": { describe: `The IP address to bind the container's exposed port on the host.`, type: "string", default: "0.0.0.0", @@ -145,7 +145,7 @@ function buildLocalCommand(yargs) { type: "number", default: 10000, }, - maxAttempts: { + "max-attempts": { describe: "The maximum number of health check attempts before declaring the start Fauna continer process as failed.", type: "number", @@ -202,7 +202,7 @@ function buildLocalCommand(yargs) { }) .check((argv) => { if (argv.maxAttempts < 1) { - throw new ValidationError("--maxAttempts must be greater than 0."); + throw new ValidationError("--max-attempts must be greater than 0."); } if (argv.interval < 0) { throw new ValidationError( diff --git a/src/lib/auth/DEV-README.md b/src/lib/auth/README.md similarity index 99% rename from src/lib/auth/DEV-README.md rename to src/lib/auth/README.md index d164b270..a35c8cbc 100644 --- a/src/lib/auth/DEV-README.md +++ b/src/lib/auth/README.md @@ -26,7 +26,7 @@ If no account key is provided, the CLI will prompt a login via the dashboard whe ### The CLI will look for account keys in this order: -- `--accountKey` flag +- `--account-key` flag - `FAUNA_ACCOUNT_KEY` environment variable - `--config` file `accountKey` value - `~/.fauna/credentials/access_keys` diff --git a/src/lib/command-helpers.mjs b/src/lib/command-helpers.mjs index fda16e61..8d522a08 100644 --- a/src/lib/command-helpers.mjs +++ b/src/lib/command-helpers.mjs @@ -6,19 +6,19 @@ import { Format } from "./formatting/colorize.mjs"; const COMMON_OPTIONS = { // hidden - accountUrl: { + "account-url": { type: "string", description: "the Fauna account URL to query", default: "https://account.fauna.com", hidden: true, }, - clientId: { + "client-id": { type: "string", description: "the client id to use when calling Fauna", required: false, hidden: true, }, - clientSecret: { + "client-secret": { type: "string", description: "the client secret to use when calling Fauna", required: false, @@ -56,7 +56,7 @@ const COMMON_QUERY_OPTIONS = { required: false, group: "API:", }, - accountKey: { + "account-key": { type: "string", description: "Fauna account key used for authentication. Negates the need for a user login. The key is used to generate short-lived database secrets for the CLI. Mutually exclusive with `--user` and `--secret`.", @@ -90,7 +90,7 @@ export const QUERY_INFO_CHOICES = [ // used for queries customers can configure const COMMON_CONFIGURABLE_QUERY_OPTIONS = { ...COMMON_QUERY_OPTIONS, - apiVersion: { + "api-version": { description: "FQL version to use.", type: "string", alias: "v", diff --git a/src/lib/docker-containers.mjs b/src/lib/docker-containers.mjs index 22f301e2..23fd99e1 100644 --- a/src/lib/docker-containers.mjs +++ b/src/lib/docker-containers.mjs @@ -210,7 +210,7 @@ async function createContainer({ if (occupied) { throw new CommandError( `[StartContainer] The hostPort '${hostPort}' on IP '${hostIp}' is already occupied. \ -Please pass a --hostPort other than '${hostPort}'.`, +Please pass a --host-port other than '${hostPort}'.`, ); } const dockerContainer = await docker.createContainer({ @@ -390,7 +390,7 @@ async function waitForHealthCheck({ `[HealthCheck] Max attempts reached. Service at ${url} did not respond.`, ); throw new CommandError( - `[HealthCheck] Fauna at ${url} is not ready after ${maxAttempts} attempts. Consider increasing --interval or --maxAttempts.`, + `[HealthCheck] Fauna at ${url} is not ready after ${maxAttempts} attempts. Consider increasing --interval or --max-attempts.`, ); } diff --git a/test/database/create.mjs b/test/database/create.mjs index c997c57c..4033ddee 100644 --- a/test/database/create.mjs +++ b/test/database/create.mjs @@ -45,6 +45,29 @@ describe("database create", () => { }); }); + [ + { + flag: "--typechecked", + value: "imastring", + }, + { + flag: "--protected", + value: "imastring", + }, + ].forEach(({ flag, value }) => { + it(`handles invalid option types: ${flag} ${value}`, async () => { + try { + await run( + `database create --name 'testdb' --secret 'secret' ${flag} ${value}`, + container, + ); + } catch (e) {} + // Make sure we bail before calling fauna + expect(runQuery).to.not.have.been.called; + expect(logger.stderr).to.have.been.called; + }); + }); + [ { error: new ServiceError({ diff --git a/test/database/list.mjs b/test/database/list.mjs index 7dc92409..bd951254 100644 --- a/test/database/list.mjs +++ b/test/database/list.mjs @@ -11,12 +11,7 @@ import { colorize } from "../../src/lib/formatting/colorize.mjs"; import { mockAccessKeysFile } from "../helpers.mjs"; describe("database list", () => { - let container, - fs, - logger, - runQueryFromString, - makeAccountRequest, - formatQueryResponse; + let container, fs, logger, stdout, runQueryFromString, makeAccountRequest; beforeEach(() => { // reset the container before each test @@ -24,9 +19,8 @@ describe("database list", () => { fs = container.resolve("fs"); logger = container.resolve("logger"); runQueryFromString = container.resolve("faunaClientV10").runQueryFromString; - formatQueryResponse = - container.resolve("faunaClientV10").formatQueryResponse; makeAccountRequest = container.resolve("makeAccountRequest"); + stdout = container.resolve("stdoutStream"); }); describe("when --local is provided", () => { @@ -51,17 +45,11 @@ describe("database list", () => { url: "http://0.0.0.0:8443", }, }, - { - args: "--local --json", - expected: { - secret: "secret", - json: true, - url: "http://0.0.0.0:8443", - }, - }, ].forEach(({ args, expected }) => { it(`calls fauna with the correct args: ${args}`, async () => { - const stubbedResponse = { data: [{ name: "testdb" }] }; + const stubbedResponse = { + data: [{ name: "testdb" }, { name: "testdb2" }], + }; runQueryFromString.resolves(stubbedResponse); await run(`database list ${args}`, container); @@ -72,13 +60,9 @@ describe("database list", () => { expression: `Database.all().paginate(${expected.pageSize ?? 1000}).data { name }`, }); - expect(logger.stdout).to.have.been.calledOnceWith( - formatQueryResponse(stubbedResponse, { - format: "json", - color: true, - }), - ); + await stdout.waitForWritten(); + expect(stdout.getWritten()).to.equal("testdb\ntestdb2\n"); expect(makeAccountRequest).to.not.have.been.called; }); }); @@ -94,10 +78,6 @@ describe("database list", () => { args: "--secret 'secret' --pageSize 10", expected: { secret: "secret", pageSize: 10 }, }, - { - args: "--secret 'secret' --json", - expected: { secret: "secret", json: true }, - }, ].forEach(({ args, expected }) => { it(`calls fauna with the correct args: ${args}`, async () => { const stubbedResponse = { data: [{ name: "testdb" }] }; @@ -111,13 +91,7 @@ describe("database list", () => { expression: `Database.all().paginate(${expected.pageSize ?? 1000}).data { name }`, }); - expect(logger.stdout).to.have.been.calledOnceWith( - formatQueryResponse(stubbedResponse, { - format: "json", - color: true, - }), - ); - + expect(stdout.getWritten()).to.equal("testdb\n"); expect(makeAccountRequest).to.not.have.been.called; }); }); @@ -158,10 +132,6 @@ describe("database list", () => { args: "--database 'us/example'", expected: { database: "us-std/example" }, }, - { - args: "--database 'us/example' --json", - expected: { database: "us-std/example", json: true }, - }, ].forEach(({ args, expected }) => { it(`calls the account api with the correct args: ${args}`, async () => { mockAccessKeysFile({ fs }); @@ -172,6 +142,9 @@ describe("database list", () => { ...(expected.regionGroup ? { region_group: expected.regionGroup } : {}), + path: expected.regionGroup + ? `${expected.regionGroup}/test` + : `${expected.database}/test`, }, ], }; @@ -189,19 +162,44 @@ describe("database list", () => { }, }); - const expectedOutput = stubbedResponse.results.map((d) => ({ - name: d.name, - // region group is only returned when listing top level databases - ...(expected.regionGroup - ? { region_group: expected.regionGroup } - : {}), - })); - - expect(logger.stdout).to.have.been.calledOnceWith( - colorize(expectedOutput, { - format: "json", - color: true, - }), + expect(stdout.getWritten()).to.equal( + `${stubbedResponse.results.map((d) => d.path).join("\n")}\n`, + ); + }); + }); + }); + + describe("when --json is provided", () => { + [ + "--local", + "--secret=test-secret", + "--database=us/example", + "--pageSize 10", + ].forEach((args) => { + it(`outputs json when using ${args}`, async () => { + mockAccessKeysFile({ fs }); + + let data; + if (args.includes("--local") || args.includes("--secret")) { + data = [{ name: "testdb" }]; + runQueryFromString.resolves({ data }); + } else { + data = [ + { + path: "us-std/test", + name: "test", + }, + ]; + makeAccountRequest.resolves({ + results: data, + }); + } + + await run(`database list ${args} --json`, container); + await stdout.waitForWritten(); + + expect(stdout.getWritten().trim()).to.equal( + `${colorize(data, { format: "json" })}`, ); }); }); diff --git a/test/lib/formatting/redact.mjs b/test/lib/formatting/redact.mjs index f2464281..451408bc 100644 --- a/test/lib/formatting/redact.mjs +++ b/test/lib/formatting/redact.mjs @@ -34,6 +34,7 @@ describe("redactedStringify", () => { secret: "hide-me", mySecret: "hide-this-too", secret_key: "also-hidden", + "account-key": "also-hidden", bigSecret: "this-is-a-long-secret", }; const result = JSON.parse(redactedStringify(obj)); @@ -41,7 +42,7 @@ describe("redactedStringify", () => { expect(result.normal).to.equal("visible"); expect(result.secret).to.equal("*******"); expect(result.mySecret).to.equal("*********-too"); - expect(result.secret_key).to.equal("***********"); + expect(result["account-key"]).to.equal("***********"); expect(result.bigSecret).to.equal("this*************cret"); }); diff --git a/test/local.mjs b/test/local.mjs index 17e68781..8f3b42bc 100644 --- a/test/local.mjs +++ b/test/local.mjs @@ -136,7 +136,7 @@ describe("local command", () => { // Assertions expect(written).to.contain( "[StartContainer] The hostPort '8443' on IP '0.0.0.0' is already occupied. \ -Please pass a --hostPort other than '8443'.", +Please pass a --host-port other than '8443'.", ); expect(written).not.to.contain("fauna local"); expect(written).not.to.contain("An unexpected"); @@ -394,13 +394,13 @@ https://support.fauna.com/hc/en-us/requests/new`, fetch.onCall(0).rejects(); fetch.resolves(f({}, 503)); // fail from http try { - await run("local --no-color --interval 0 --maxAttempts 3", container); + await run("local --no-color --interval 0 --max-attempts 3", container); } catch (_) {} const written = stderrStream.getWritten(); expect(written).to.contain("with HTTP status: '503'"); expect(written).to.contain("with error:"); expect(written).to.contain( - "[HealthCheck] Fauna at http://0.0.0.0:8443 is not ready after 3 attempts. Consider increasing --interval or --maxAttempts.", + "[HealthCheck] Fauna at http://0.0.0.0:8443 is not ready after 3 attempts. Consider increasing --interval or --max-attempts.", ); expect(written).not.to.contain("An unexpected"); expect(written).not.to.contain("fauna local"); // help text @@ -452,10 +452,10 @@ https://support.fauna.com/hc/en-us/requests/new`, it("throws an error if maxAttempts is less than 1", async () => { try { - await run("local --no-color --maxAttempts 0", container); + await run("local --no-color --max-attempts 0", container); } catch (_) {} const written = stderrStream.getWritten(); - expect(written).to.contain("--maxAttempts must be greater than 0."); + expect(written).to.contain("--max-attempts must be greater than 0."); expect(written).to.contain("fauna local"); // help text expect(written).not.to.contain("An unexpected"); });