Skip to content

Commit

Permalink
add bash/zsh completions
Browse files Browse the repository at this point in the history
this change adds:
- the default yargs completions for commands, options, etc.
- custom completions for profiles (given that you have a config
  specified)
- custom completions for database path (given that you have specified
  enough information to make a request to fauna)
  • Loading branch information
echo-bravo-yahoo committed Dec 12, 2024
1 parent 2071e2e commit dbf877f
Show file tree
Hide file tree
Showing 9 changed files with 364 additions and 29 deletions.
53 changes: 47 additions & 6 deletions src/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import queryCommand from "./commands/query.mjs";
import schemaCommand from "./commands/schema/schema.mjs";
import shellCommand from "./commands/shell.mjs";
import { buildCredentials } from "./lib/auth/credentials.mjs";
import { getDbCompletions, getProfileCompletions } from "./lib/completions.mjs";
import { configParser } from "./lib/config/config.mjs";
import { handleParseYargsError } from "./lib/errors.mjs";
import {
Expand Down Expand Up @@ -112,6 +113,50 @@ function buildYargs(argvInput) {
.command(localCommand)
.demandCommand()
.strictCommands(true)
.completion(
"completion",
"Output bash/zsh script to enable shell completions. See command output for installation instructions.",
)
.completion(
"completion",
async function (currentWord, argv, defaultCompletions, done) {
// this is pretty hard to debug - if you need to, run
// `fauna --get-yargs-completions <command> <flag> <string to match>`
// for example: `fauna --get-yargs-completions --profile he`
// note that you need to have empty quotes to get all matches:
// `fauna --get-yargs-completions --profile ""`

// then, call the done callback with an array of strings for debugging, like:
// done(
// [
// `currentWord: ${currentWord}, currentWordFlag: ${currentWordFlag}, argv: ${JSON.stringify(argv)}`,
// ],
// );
const previousWord = process.argv.slice(-2, -1)[0].replace(/-/g, "");
const currentWordFlag = Object.keys(argv)
.filter((key) => previousWord === key)
.pop();

// TODO: this doesn't handle aliasing, and it needs to

Check warning on line 140 in src/cli.mjs

View workflow job for this annotation

GitHub Actions / lint

Unexpected 'todo' comment: 'TODO: this doesn't handle aliasing, and...'
if (
currentWord === "--profile" ||
currentWordFlag === "profile" ||
currentWord === "-p" ||
currentWordFlag === "p"
) {
done(getProfileCompletions(currentWord, argv));
} else if (
currentWord === "--database" ||
currentWordFlag === "database" ||
currentWord === "-d" ||
currentWordFlag === "d"
) {
done(await getDbCompletions(currentWord, argv));
} else {
defaultCompletions();
}
},
)
.options({
color: {
description:
Expand Down Expand Up @@ -153,7 +198,7 @@ function buildYargs(argvInput) {
"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`.",
type: "array",
default: [],
choices: ["fetch", "error", "config", "argv", "creds"],
choices: ["fetch", "error", "config", "argv", "creds", "completion"],
group: "Debug:",
},
verbosity: {
Expand All @@ -169,9 +214,5 @@ function buildYargs(argvInput) {
.alias("help", "h")
.fail(false)
.exitProcess(false)
.version()
.completion(
"completion",
"Output bash/zsh script to enable shell completions. See command output for installation instructions.",
);
.version();
}
42 changes: 22 additions & 20 deletions src/commands/database/list.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,38 +29,27 @@ function pickOutputFields(databases, argv) {
}

async function listDatabasesWithAccountAPI(argv) {
const { pageSize, database, color } = argv;
const { pageSize, database } = argv;
const accountClient = new FaunaAccountClient();
const response = await accountClient.listDatabases({
pageSize,
path: database,
});
const output = pickOutputFields(response.results, argv);

container.resolve("logger").stdout(
colorize(output, {
format: Format.JSON,
color: color,
}),
);
return pickOutputFields(response.results, argv);
}

async function listDatabasesWithSecret(argv) {
const { url, secret, pageSize, color } = argv;
const { runQueryFromString, formatQueryResponse } =
container.resolve("faunaClientV10");
const { url, secret, pageSize } = argv;
const { runQueryFromString } = container.resolve("faunaClientV10");

try {
const result = await runQueryFromString({
return 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)} }`,
});
container
.resolve("logger")
.stdout(formatQueryResponse(result, { format: Format.JSON, color }));
} catch (e) {
if (e instanceof FaunaError) {
throwForError(e);
Expand All @@ -69,11 +58,24 @@ async function listDatabasesWithSecret(argv) {
}
}

async function listDatabases(argv) {
export async function listDatabases(argv) {
let databases;
if (argv.secret) {
databases = await listDatabasesWithSecret(argv);
} else {
databases = 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) {
return listDatabasesWithSecret(argv);
logger.stdout(formatQueryResponse(res, argv));
} else {
return listDatabasesWithAccountAPI(argv);
logger.stdout(colorize(res, { format: Format.JSON, color: argv.color }));
}
}

Expand Down Expand Up @@ -111,5 +113,5 @@ export default {
command: "list",
description: "List databases.",
builder: buildListCommand,
handler: listDatabases,
handler: doListDatabases,
};
3 changes: 3 additions & 0 deletions src/commands/query.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ function validate(argv) {
const { existsSync, accessSync, constants } = container.resolve("fs");
const dirname = container.resolve("dirname");

// don't validate completion invocations
if (argv.getYargsCompletions) return;

if (argv.input && argv.fql) {
throw new ValidationError("Cannot specify both --input and [fql]");
}
Expand Down
4 changes: 4 additions & 0 deletions src/lib/command-helpers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,12 @@ export const resolveFormat = (argv) => {
* @param {string} argv.database - The database to use
* @param {string} argv.secret - The secret to use
* @param {boolean} argv.local - Whether to use a local Fauna container
* @param {boolean|undefined} argv.getYargsCompletions - Whether this CLI run is to generate completions
*/
export const validateDatabaseOrSecret = (argv) => {
// don't validate completion invocations
if (argv.getYargsCompletions) return true;

if (!argv.database && !argv.secret && !argv.local) {
throw new ValidationError(
"No database or secret specified. Please use either --database, --secret, or --local to connect to your desired Fauna database.",
Expand Down
46 changes: 46 additions & 0 deletions src/lib/completions.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// @ts-check

import * as path from "node:path";

import { FaunaAccountClient } from "../lib/fauna-account-client.mjs";
import { buildCredentials } from "./auth/credentials.mjs";
import { getConfig, locateConfig } from "./config/config.mjs";

export function getProfileCompletions(currentWord, argv) {
const configPath = locateConfig(path.resolve(argv.config));
if (!configPath) return undefined;
return Object.keys(getConfig(configPath).toJSON());
}

export async function getDbCompletions(currentWord, argv) {
const regionGroups = ["us-std", "eu-std", "global"];

function getRegionGroup(currentWord) {
const rg = regionGroups.filter((rg) => currentWord.startsWith(rg));
return rg.length ? rg[0] : undefined;
}

if (!getRegionGroup(currentWord)) {
return regionGroups;
} else {
const { pageSize } = argv;
buildCredentials({ ...argv, user: "default" });
const accountClient = new FaunaAccountClient();
try {
const response = await accountClient.listDatabases({
pageSize,
path: currentWord,
});
return response.results.map(({ name }) => path.join(currentWord, name));
} catch (e) {
const response = await accountClient.listDatabases({
pageSize,
// TO-DO: needs to handle aliases, does not
path: path.dirname(currentWord),
});
return response.results.map(({ name }) =>
path.join(path.dirname(currentWord), name),
);
}
}
}
6 changes: 5 additions & 1 deletion src/lib/config/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ export function getConfig(path) {
return yaml.parseDocument(fileBody);
}

export function locateConfig(path) {
return path === process.cwd() ? checkForDefaultConfig(process.cwd()) : path;
}

/**
* Checks the specified directory for default configuration files.
*
Expand Down Expand Up @@ -96,7 +100,7 @@ function validateConfig(profileName, profileBody, configPath) {
*
* @param {string|string[]} argvInput - The raw command line arguments.
* @param {string} path
* @returns {object} - The yargs parser
* @returns {object} - The parsed argv
*/
export function configParser(argvInput, path) {
const userProvidedConfigPath =
Expand Down
4 changes: 2 additions & 2 deletions src/lib/middleware.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,11 @@ export function checkForUpdates(argv) {
* If --local is provided and --secret is not, argv.secret is
* set to 'secret'.
* @param {import('yargs').Arguments} argv
* @returns {import('yargs').Arguments}
* @returns {void}
*/
export function applyLocalArg(argv) {
applyLocalToUrl(argv);
return applyLocalToSecret(argv);
applyLocalToSecret(argv);
}

/**
Expand Down
Loading

0 comments on commit dbf877f

Please sign in to comment.