Skip to content

Commit

Permalink
Move container to a singleton in config rather than cli
Browse files Browse the repository at this point in the history
  • Loading branch information
ecooper committed Jan 9, 2025
1 parent 26acf84 commit 1e7963c
Show file tree
Hide file tree
Showing 36 changed files with 386 additions and 127 deletions.
14 changes: 6 additions & 8 deletions src/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import loginCommand from "./commands/login.mjs";
import queryCommand from "./commands/query.mjs";
import schemaCommand from "./commands/schema/schema.mjs";
import shellCommand from "./commands/shell.mjs";
import { container, setContainer } from "./config/container.mjs";
import { buildCredentials } from "./lib/auth/credentials.mjs";
import { getDbCompletions, getProfileCompletions } from "./lib/completions.mjs";
import { configParser } from "./lib/config/config.mjs";
Expand All @@ -25,10 +26,6 @@ import {
logArgv,
} from "./lib/middleware.mjs";

/** @typedef {import('awilix').AwilixContainer<import('./config/setup-container.mjs').modifiedInjectables> } cliContainer */

/** @type {cliContainer} */
export let container;
/** @type {import('yargs').Argv} */
export let builtYargs;

Expand All @@ -37,13 +34,14 @@ const __dirname = path.dirname(__filename);

/**
* @param {string|string[]} _argvInput - The command string provided by the user or test. Parsed by yargs into an argv object.
* @param {cliContainer} _container - A built and ready for use awilix container with registered injectables.
* @param {import('./config/container.mjs').container} _container - A built and ready for use awilix container with registered injectables.
*/
export async function run(_argvInput, _container) {
container = _container;
setContainer(_container);

const argvInput = _argvInput;
const logger = container.resolve("logger");
const parseYargs = container.resolve("parseYargs");
const logger = _container.resolve("logger");
const parseYargs = _container.resolve("parseYargs");
if (process.env.NODE_ENV === "production") {
process.removeAllListeners("warning");
}
Expand Down
2 changes: 1 addition & 1 deletion src/commands/database/create.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//@ts-check
import { ServiceError } from "fauna";

import { container } from "../../cli.mjs";
import { container } from "../../config/container.mjs";
import { CommandError } from "../../lib/errors.mjs";
import { faunaToCommandError } from "../../lib/fauna.mjs";
import { getSecret, retryInvalidCredsOnce } from "../../lib/fauna-client.mjs";
Expand Down
2 changes: 1 addition & 1 deletion src/commands/database/delete.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { ServiceError } from "fauna";

import { container } from "../../cli.mjs";
import { container } from "../../config/container.mjs";
import { CommandError } from "../../lib/errors.mjs";
import { faunaToCommandError } from "../../lib/fauna.mjs";
import { getSecret, retryInvalidCredsOnce } from "../../lib/fauna-client.mjs";
Expand Down
2 changes: 1 addition & 1 deletion src/commands/database/list.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//@ts-check
import chalk from "chalk";

import { container } from "../../cli.mjs";
import { container } from "../../config/container.mjs";
import { faunaToCommandError } from "../../lib/fauna.mjs";
import { colorize, Format } from "../../lib/formatting/colorize.mjs";

Expand Down
2 changes: 1 addition & 1 deletion src/commands/local.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import chalk from "chalk";
import { AbortError } from "fauna";

import { container } from "../cli.mjs";
import { pushSchema } from "../commands/schema/push.mjs";
import { container } from "../config/container.mjs";
import { ensureContainerRunning } from "../lib/docker-containers.mjs";
import { CommandError, ValidationError } from "../lib/errors.mjs";
import { colorize, Format } from "../lib/formatting/colorize.mjs";
Expand Down
2 changes: 1 addition & 1 deletion src/commands/login.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//@ts-check

import { container } from "../cli.mjs";
import { container } from "../config/container.mjs";
import { getToken, startOAuthRequest } from "../lib/account-api.mjs";

async function doLogin(argv) {
Expand Down
2 changes: 1 addition & 1 deletion src/commands/query.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//@ts-check

import { container } from "../cli.mjs";
import { container } from "../config/container.mjs";
import {
CommandError,
isUnknownError,
Expand Down
2 changes: 1 addition & 1 deletion src/commands/schema/abandon.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//@ts-check

import { container } from "../../cli.mjs";
import { container } from "../../config/container.mjs";
import { CommandError } from "../../lib/errors.mjs";
import { getSecret } from "../../lib/fauna-client.mjs";

Expand Down
2 changes: 1 addition & 1 deletion src/commands/schema/commit.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//@ts-check

import { container } from "../../cli.mjs";
import { container } from "../../config/container.mjs";
import { CommandError } from "../../lib/errors.mjs";
import { getSecret } from "../../lib/fauna-client.mjs";

Expand Down
2 changes: 1 addition & 1 deletion src/commands/schema/diff.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import chalk from "chalk";

import { container } from "../../cli.mjs";
import { container } from "../../config/container.mjs";
import { ValidationError } from "../../lib/errors.mjs";
import { getSecret } from "../../lib/fauna-client.mjs";
import { reformatFSL } from "../../lib/schema.mjs";
Expand Down
2 changes: 1 addition & 1 deletion src/commands/schema/pull.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//@ts-check

import { container } from "../../cli.mjs";
import { container } from "../../config/container.mjs";
import { getSecret } from "../../lib/fauna-client.mjs";
import { LOCAL_SCHEMA_OPTIONS } from "./schema.mjs";

Expand Down
2 changes: 1 addition & 1 deletion src/commands/schema/push.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import path from "path";

import { container } from "../../cli.mjs";
import { container } from "../../config/container.mjs";
import { ValidationError } from "../../lib/errors.mjs";
import { getSecret } from "../../lib/fauna-client.mjs";
import { reformatFSL } from "../../lib/schema.mjs";
Expand Down
2 changes: 1 addition & 1 deletion src/commands/schema/status.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import chalk from "chalk";
import path from "path";

import { container } from "../../cli.mjs";
import { container } from "../../config/container.mjs";
import { CommandError } from "../../lib/errors.mjs";
import { getSecret } from "../../lib/fauna-client.mjs";
import { reformatFSL } from "../../lib/schema.mjs";
Expand Down
2 changes: 1 addition & 1 deletion src/commands/shell.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import repl from "node:repl";

import * as esprima from "esprima";

import { container } from "../cli.mjs";
import { container } from "../config/container.mjs";
import { formatQueryResponse, getSecret } from "../lib/fauna-client.mjs";
import { clearHistoryStorage, initHistoryStorage } from "../lib/file-util.mjs";
import { validateDatabaseOrSecret } from "../lib/middleware.mjs";
Expand Down
10 changes: 10 additions & 0 deletions src/config/container.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/** @typedef {import('awilix').AwilixContainer<import('./config/setup-container.mjs').modifiedInjectables> } container */
/** @type {container} */
export let container;

/**
* @param {container} newContainer - The new container to set.
*/
export const setContainer = (newContainer) => {
container = newContainer;
};
114 changes: 88 additions & 26 deletions src/lib/account-api.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
//@ts-check

import console from "node:console";

import { container } from "../cli.mjs";
import { retryAuthorizationErrorOnce } from "./auth/credentials.mjs";
import { container } from "../config/container.mjs";
import {
AuthenticationError,
AuthorizationError,
Expand All @@ -18,10 +15,18 @@ const API_VERSIONS = {

let accountUrl = process.env.FAUNA_ACCOUNT_URL ?? "https://account.fauna.com";

/**
* References the account URL set in the account-api module.
* @returns {string} The account URL
*/
export function getAccountUrl() {
return accountUrl;
}

/**
* Sets the account URL for the account-api module.
* @param {string} url - The account URL to set
*/
export function setAccountUrl(url) {
accountUrl = url;
}
Expand All @@ -44,6 +49,7 @@ export function toResource({
for (const [key, value] of Object.entries(params)) {
url.searchParams.set(key, value);
}

return url;
}

Expand All @@ -56,27 +62,37 @@ export function toResource({
* @returns {Promise<Response>} The response from the account API
*/
export async function fetchWithAccountKey(url, options) {
const logger = container.resolve("logger");
const fetch = container.resolve("fetch");
const accountKeys = container.resolve("credentials").accountKeys;

return retryAuthorizationErrorOnce({
keyProvider: accountKeys,
fn: async (secret) => {
const resolvedOptions = {
...options,
headers: { ...options.headers, Authorization: `Bearer ${secret}` },
};

// If the response is a 401, we need to refresh the secret. We throw the response
// so that the retry mechanism can handle it.
const response = await fetch(url, resolvedOptions);
if (response.status === 401) {
throw response;
}

return response;
},
let response = await fetch(url, {
...options,
headers: { ...options.headers, Authorization: `Bearer ${accountKeys.key}` },
});

if (response.status !== 401) {
return response;
}

logger.debug("Retryable 401 error, attempting to refresh session", "creds");

await accountKeys.onInvalidCreds();

response = await fetch(url, {
...options,
headers: { ...options.headers, Authorization: `Bearer ${accountKeys.key}` },
});

if (response.status === 401) {
logger.debug(
"Failed to refresh session, expired or missing refresh token",
"creds",
);
accountKeys.promptLogin();
}

return response;
}

/**
Expand Down Expand Up @@ -154,6 +170,12 @@ export async function accountToCommandError(response) {
}
}

/**
* Handles the response from the account API.
* @param {Response} response - The response from the account API
* @returns {Promise<Object>} The JSON body of the response
* @throws {AuthorizationError | AuthenticationError | CommandError | Error} If the response is not OK
*/
export async function responseHandler(response) {
if (!response.ok) {
await accountToCommandError(response);
Expand All @@ -162,6 +184,19 @@ export async function responseHandler(response) {
return await response.json();
}

/**
* Starts an OAuth request.
* @param {Object} params - The parameters for the OAuth request
* @param {string} params.client_id - The client ID
* @param {string} params.redirect_uri - The redirect URI
* @param {string} params.code_challenge - The code challenge
* @param {string} params.code_challenge_method - The code challenge method
* @param {string} params.response_type - The response type
* @param {string} params.scope - The scope
* @param {string} params.state - The state
* @returns {Promise<string>} The URL to redirect the user to
* @throws {Error} If the response is not OK
*/
export async function startOAuthRequest(params) {
const fetch = container.resolve("fetch");
const url = toResource({ endpoint: "/oauth/authorize", params });
Expand Down Expand Up @@ -193,6 +228,16 @@ export async function startOAuthRequest(params) {
return dashboardOAuthURL;
}

/**
* Gets an access token from the account API.
* @param {Object} params - The parameters for the access token request
* @param {string} params.client_id - The client ID
* @param {string} params.client_secret - The client secret
* @param {string} params.code - The authorization code
* @param {string} params.redirect_uri - The redirect URI
* @param {string} params.code_verifier - The code verifier
* @returns {Promise<string>} The access token
*/
export async function getToken(params) {
const fetch = container.resolve("fetch");
const url = toResource({ endpoint: "/oauth/token" });
Expand Down Expand Up @@ -221,6 +266,12 @@ export async function getToken(params) {
return accessToken;
}

/**
* Gets a session from the account API.
* @param {string} accessToken - The access token
* @returns {Promise<Object>} The session
* @throws {AuthorizationError | AuthenticationError | CommandError | Error} If the response is not OK
*/
async function getSession(accessToken) {
const fetch = container.resolve("fetch");
const url = toResource({ endpoint: "/session" });
Expand All @@ -238,6 +289,12 @@ async function getSession(accessToken) {
return { accountKey, refreshToken };
}

/**
* Refreshes a session from the account API.
* @param {string} refreshToken - The refresh token
* @returns {Promise<Object>} The session
* @throws {AuthorizationError | AuthenticationError | CommandError | Error} If the response is not OK
*/
async function refreshSession(refreshToken) {
const fetch = container.resolve("fetch");
const url = toResource({ endpoint: "/session/refresh" });
Expand All @@ -259,12 +316,14 @@ async function refreshSession(refreshToken) {
/**
* List all databases for the current account.
*
* @param {Object} params - The parameters for listing databases.
* @param {string | undefined} params.path - The path of the database, including region group
* @param {number} params.pageSize - The number of databases to return per page
* @param {Object} [params] - The parameters for listing databases.
* @param {string} [params.path] - The path of the database, including region group
* @param {number} [params.pageSize] - The number of databases to return per page
* @returns {Promise<Object>} - A promise that resolves to the list of databases
* @throws {AuthorizationError | AuthenticationError | CommandError | Error} If the response is not OK
*/
async function listDatabases({ path = undefined, pageSize = 1000 }) {
async function listDatabases(params = {}) {
const { path, pageSize = 1000 } = params;
const url = toResource({
endpoint: "/databases",
params: {
Expand All @@ -288,7 +347,7 @@ async function listDatabases({ path = undefined, pageSize = 1000 }) {
* @param {string | undefined} params.ttl - ISO String for the key's expiration time, optional
* @param {string | undefined} params.name - The name for the key, optional
* @returns {Promise<Object>} - A promise that resolves when the key is created.
* @throws {Error} - Throws an error if there is an issue during key creation.
* @throws {AuthorizationError | AuthenticationError | CommandError | Error} If the response is not OK
*/
async function createKey({ path, role, ttl, name }) {
const url = toResource({ endpoint: "/databases/keys" });
Expand All @@ -309,6 +368,9 @@ async function createKey({ path, role, ttl, name }) {
return await responseHandler(response);
}

/**
* The account API module with the currently supported endpoints.
*/
const accountAPI = {
listDatabases,
createKey,
Expand Down
2 changes: 1 addition & 1 deletion src/lib/auth/accountKeys.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { container } from "../../cli.mjs";
import { container } from "../../config/container.mjs";
import { AuthenticationError, CommandError } from "../errors.mjs";
import { AccountKeyStorage } from "../file-util.mjs";

Expand Down
Loading

0 comments on commit 1e7963c

Please sign in to comment.