From 6471600f6616acfea630832fad9f94a34efe3a7f Mon Sep 17 00:00:00 2001 From: Matt Wilde Date: Fri, 4 Oct 2024 12:58:43 -0400 Subject: [PATCH 1/9] merge in stuff --- src/cli.mjs | 20 ++++--- src/config/setup-container.mjs | 2 + src/lib/auth/oauth-client.mjs | 19 ++----- src/lib/file-util.mjs | 96 ++++++++++++++++++++++++++++++++++ src/yargs-commands/login.mjs | 58 +++++++++++--------- 5 files changed, 149 insertions(+), 46 deletions(-) diff --git a/src/cli.mjs b/src/cli.mjs index 80f052d4..8a885fb5 100644 --- a/src/cli.mjs +++ b/src/cli.mjs @@ -1,13 +1,12 @@ import yargs from "yargs"; import chalk from "chalk"; -import evalCommand from "./yargs-commands/eval.mjs"; -import loginCommand from "./yargs-commands/login.mjs"; -import schemaCommand from "./yargs-commands/schema/schema.mjs"; -import { logArgv } from "./lib/middleware.mjs"; - -export let container; -export let builtYargs; +import evalCommand from './yargs-commands/eval.mjs' +import loginCommand from './yargs-commands/login.mjs' +import schemaCommand from './yargs-commands/schema/schema.mjs' +import { logArgv } from './lib/middleware.mjs' +// import { testCreds } from './lib/file-util.mjs' +export let container // import { connect } from 'node:tls' // const socket = connect({ port: 443, host: 'db.fauna.com', checkServerIdentity: () => {} }) @@ -43,6 +42,13 @@ function buildYargs(argvInput) { .middleware([logArgv], true) .command("eval", "evaluate a query", evalCommand) .command("login", "login via website", loginCommand) + // .command("creds", "test creds", testCreds).options({ + // user: { + // type: "string", + // description: "some user", + // default: "default" + // } + // }) .command(schemaCommand) .command("throw", false, { handler: () => { diff --git a/src/config/setup-container.mjs b/src/config/setup-container.mjs index 4350e108..6e15764c 100644 --- a/src/config/setup-container.mjs +++ b/src/config/setup-container.mjs @@ -23,6 +23,7 @@ import open from "open"; import OAuthClient from "../lib/auth/oauth-client.mjs"; import { Lifetime } from "awilix"; import fs from "node:fs"; +import { AccountKey } from "../lib/file-util.mjs"; // import { findUpSync } from 'find-up' // import fs from 'node:fs' @@ -58,6 +59,7 @@ export const injectables = { }), oauthClient: awilix.asClass(OAuthClient, { lifetime: Lifetime.SCOPED }), makeFaunaRequest: awilix.asValue(makeFaunaRequest), + accountCreds: awilix.asClass(AccountKey, { lifetime: Lifetime.SCOPED }), // feature-specific lib (homemade utilities) gatherFSL: awilix.asValue(gatherFSL), diff --git a/src/lib/auth/oauth-client.mjs b/src/lib/auth/oauth-client.mjs index 2d979897..cf62bd2e 100644 --- a/src/lib/auth/oauth-client.mjs +++ b/src/lib/auth/oauth-client.mjs @@ -12,17 +12,6 @@ const clientSecret = const REDIRECT_URI = `http://127.0.0.1`; class OAuthClient { - server; //: http.Server; - - port; //: number; - - code_verifier; //: string; - - code_challenge; //: string; - - auth_code; //: string; - - state; //: string; constructor() { this.server = http.createServer(this._handleRequest.bind(this)); @@ -127,11 +116,11 @@ class OAuthClient { async start() { try { - this.server.on("listening", () => { - this.port = this.server.address().port; - this.server.emit("ready"); - }); if (!this.server.listening) { + this.server.on("listening", () => { + this.port = (this.server.address()).port; + this.server.emit("ready"); + }); this.server.listen(0); } } catch (e) { diff --git a/src/lib/file-util.mjs b/src/lib/file-util.mjs index 6e5f18c1..376f79c6 100644 --- a/src/lib/file-util.mjs +++ b/src/lib/file-util.mjs @@ -1,5 +1,7 @@ import fs from "node:fs"; import { normalize } from "node:path"; +import * as os from "node:os"; +import { container } from "../cli.mjs"; // path: string, returns boolean export function dirExists(path) { @@ -25,3 +27,97 @@ export function dirIsWriteable(path) { return true; } + +export function fileExists(path) { + try { + fs.readFileSync(path); + return true; + } catch (e) { + return false; + } +} + +export function testCreds() { + const a = new SecretKey(); + console.log("before", a.get()); + a.save({ myrefreshtoken: "myaccountkey" }); + console.log("after", a.get()); + console.log("getting creds", a.get("default")); +} + +export class Credentials { + logger = container.resolve("logger"); + exit = container.resolve("exit"); + + constructor(filename = "") { + this.filename = filename; + this.credsDir = `${os.homedir()}/.fauna/credentials`; + if (!dirExists(this.credsDir)) { + fs.mkdirSync(this.credsDir, { recursive: true }); + } + this.filepath = `${this.credsDir}/${this.filename}`; + if (!fileExists(this.filepath)) { + fs.writeFileSync(this.filepath, "{}"); + } + } + + get(key) { + try { + // Open file for reading and writing without truncating + const fileContent = fs + .readFileSync(this.filepath, { flag: "r+" }) + .toString(); + if (!isJSON(fileContent)) { + this.logger.stderr( + "Credentials file contains invalid formatting: ", + this.filepath + ); + this.exit(1); + } + const parsed = JSON.parse(fileContent); + return key ? parsed[key] : parsed; + } catch (err) { + this.logger.stderr( + "Error while parsing credentials file: ", + this.filepath, + err + ); + this.exit(1); + } + } + + save({ creds, overwrite = false, user }) { + try { + const existingContent = overwrite ? {} : this.get(); + const newContent = { + ...existingContent, + [user]: creds, + }; + fs.writeFileSync(this.filepath, JSON.stringify(newContent, null, 2)); + } catch (err) { + this.logger.stderr("Error while saving credentials: ", err); + this.exit(1); + } + } +} + +export class SecretKey extends Credentials { + constructor() { + super("secret_keys"); + } +} + +export class AccountKey extends Credentials { + constructor() { + super("access_keys"); + } +} + +function isJSON(value) { + try { + JSON.parse(value); + return true; + } catch { + return false; + } +} diff --git a/src/yargs-commands/login.mjs b/src/yargs-commands/login.mjs index e9f1cbef..2427c490 100644 --- a/src/yargs-commands/login.mjs +++ b/src/yargs-commands/login.mjs @@ -1,32 +1,42 @@ -import { container } from "../cli.mjs"; +import { builtYargs, container } from "../cli.mjs"; -async function doLogin() { +async function doLogin(argv) { const logger = container.resolve("logger"); const open = container.resolve("open"); const accountClient = container.resolve("accountClient"); const oAuth = container.resolve("oauthClient"); - - oAuth.server.on("ready", async () => { - const authCodeParams = oAuth.getOAuthParams(); - const dashboardOAuthURL = await accountClient.startOAuthRequest( - authCodeParams - ); - open(dashboardOAuthURL); - logger.stdout(`To login, open your browser to:\n ${dashboardOAuthURL}`); - }); - oAuth.server.on("auth_code_received", async () => { - try { - const tokenParams = oAuth.getTokenParams(); - const accessToken = await accountClient.getToken(tokenParams); - const { account_key } = await accountClient.getSession(accessToken); - logger.stdout("Listing Databases..."); - const databases = await accountClient.listDatabases(account_key); - logger.stdout(databases); - } catch (err) { - console.error(err); - } - }); - + const accountCreds = container.resolve("accountCreds"); + if (!oAuth.server.listenerCount("ready")) { + oAuth.server.on("ready", async () => { + const authCodeParams = oAuth.getOAuthParams(); + const dashboardOAuthURL = await accountClient.startOAuthRequest( + authCodeParams + ); + open(dashboardOAuthURL); + logger.stdout(`To login, open your browser to:\n ${dashboardOAuthURL}`); + }); + } + if (!oAuth.server.listenerCount("auth_code_received")) { + oAuth.server.on("auth_code_received", async () => { + try { + const tokenParams = oAuth.getTokenParams(); + const accessToken = await accountClient.getToken(tokenParams); + const { account_key, refresh_token } = await accountClient.getSession( + accessToken + ); + accountCreds.save({ + creds: { account_key, refresh_token }, + user: argv.user, + }); + logger.stdout(`Login Success!\n\n`) + logger.stdout("Listing Databases..."); + const databases = await accountClient.listDatabases(account_key); + logger.stdout(databases); + } catch (err) { + console.error(err); + } + }); + } await oAuth.start(); } From 6669958a56ace73454ab7500266095c0386b641f Mon Sep 17 00:00:00 2001 From: Matt Wilde Date: Fri, 4 Oct 2024 15:28:33 -0400 Subject: [PATCH 2/9] merge conflict fixes --- eslint.config.mjs | 1 + src/cli.mjs | 11 ++++++----- src/config/setup-test-container.mjs | 1 + src/lib/auth/oauth-client.mjs | 3 +-- src/lib/file-util.mjs | 17 +++++++++-------- src/yargs-commands/login.mjs | 4 ++-- 6 files changed, 20 insertions(+), 17 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 74ae36b8..6837dac1 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -23,6 +23,7 @@ export default [ "coverage/**/*", "fsl/**/*", "test/**/*", + ".history", ], }, ...compat.extends("oclif", "plugin:prettier/recommended"), diff --git a/src/cli.mjs b/src/cli.mjs index 8a885fb5..6fa749e3 100644 --- a/src/cli.mjs +++ b/src/cli.mjs @@ -1,12 +1,13 @@ import yargs from "yargs"; import chalk from "chalk"; -import evalCommand from './yargs-commands/eval.mjs' -import loginCommand from './yargs-commands/login.mjs' -import schemaCommand from './yargs-commands/schema/schema.mjs' -import { logArgv } from './lib/middleware.mjs' +import evalCommand from "./yargs-commands/eval.mjs"; +import loginCommand from "./yargs-commands/login.mjs"; +import schemaCommand from "./yargs-commands/schema/schema.mjs"; +import { logArgv } from "./lib/middleware.mjs"; // import { testCreds } from './lib/file-util.mjs' -export let container +export let container; +export let builtYargs; // import { connect } from 'node:tls' // const socket = connect({ port: 443, host: 'db.fauna.com', checkServerIdentity: () => {} }) diff --git a/src/config/setup-test-container.mjs b/src/config/setup-test-container.mjs index 43942fac..a519c0e9 100644 --- a/src/config/setup-test-container.mjs +++ b/src/config/setup-test-container.mjs @@ -61,6 +61,7 @@ export function setupTestContainer() { ), accountClient: awilix.asFunction(stub()), oauthClient: awilix.asFunction(stub()), + accountCreds: awilix.asFunction(stub()), // in tests, let's exit by throwing exit: awilix.asValue(() => { throw new Error(1); diff --git a/src/lib/auth/oauth-client.mjs b/src/lib/auth/oauth-client.mjs index cf62bd2e..65763d1f 100644 --- a/src/lib/auth/oauth-client.mjs +++ b/src/lib/auth/oauth-client.mjs @@ -12,7 +12,6 @@ const clientSecret = const REDIRECT_URI = `http://127.0.0.1`; class OAuthClient { - constructor() { this.server = http.createServer(this._handleRequest.bind(this)); this.code_verifier = Buffer.from(randomBytes(20)).toString("base64url"); @@ -118,7 +117,7 @@ class OAuthClient { try { if (!this.server.listening) { this.server.on("listening", () => { - this.port = (this.server.address()).port; + this.port = this.server.address().port; this.server.emit("ready"); }); this.server.listen(0); diff --git a/src/lib/file-util.mjs b/src/lib/file-util.mjs index 376f79c6..312ce73d 100644 --- a/src/lib/file-util.mjs +++ b/src/lib/file-util.mjs @@ -37,16 +37,9 @@ export function fileExists(path) { } } -export function testCreds() { - const a = new SecretKey(); - console.log("before", a.get()); - a.save({ myrefreshtoken: "myaccountkey" }); - console.log("after", a.get()); - console.log("getting creds", a.get("default")); -} - export class Credentials { logger = container.resolve("logger"); + exit = container.resolve("exit"); constructor(filename = "") { @@ -113,6 +106,14 @@ export class AccountKey extends Credentials { } } +export function testCreds() { + const a = new SecretKey(); + console.log("before", a.get()); + a.save({ myrefreshtoken: "myaccountkey" }); + console.log("after", a.get()); + console.log("getting creds", a.get("default")); +} + function isJSON(value) { try { JSON.parse(value); diff --git a/src/yargs-commands/login.mjs b/src/yargs-commands/login.mjs index 2427c490..2221372d 100644 --- a/src/yargs-commands/login.mjs +++ b/src/yargs-commands/login.mjs @@ -1,4 +1,4 @@ -import { builtYargs, container } from "../cli.mjs"; +import { container } from "../cli.mjs"; async function doLogin(argv) { const logger = container.resolve("logger"); @@ -28,7 +28,7 @@ async function doLogin(argv) { creds: { account_key, refresh_token }, user: argv.user, }); - logger.stdout(`Login Success!\n\n`) + logger.stdout(`Login Success!\n\n`); logger.stdout("Listing Databases..."); const databases = await accountClient.listDatabases(account_key); logger.stdout(databases); From cc3d12f07125ceb0b4ae131d56b4c9a76163d6ed Mon Sep 17 00:00:00 2001 From: Matt Wilde Date: Sat, 5 Oct 2024 01:33:57 -0400 Subject: [PATCH 3/9] basic list databases with reading creds file --- src/cli.mjs | 2 ++ src/lib/logger.mjs | 28 ++++++++--------- src/yargs-commands/database.mjs | 44 ++++++++++++++++++++++++++ src/yargs-commands/login.mjs | 55 +++++++++++++++------------------ 4 files changed, 85 insertions(+), 44 deletions(-) create mode 100644 src/yargs-commands/database.mjs diff --git a/src/cli.mjs b/src/cli.mjs index 6fa749e3..e4bc8010 100644 --- a/src/cli.mjs +++ b/src/cli.mjs @@ -4,6 +4,7 @@ import chalk from "chalk"; import evalCommand from "./yargs-commands/eval.mjs"; import loginCommand from "./yargs-commands/login.mjs"; import schemaCommand from "./yargs-commands/schema/schema.mjs"; +import databaseCommand from "./yargs-commands/database.mjs"; import { logArgv } from "./lib/middleware.mjs"; // import { testCreds } from './lib/file-util.mjs' export let container; @@ -43,6 +44,7 @@ function buildYargs(argvInput) { .middleware([logArgv], true) .command("eval", "evaluate a query", evalCommand) .command("login", "login via website", loginCommand) + .command(databaseCommand) // .command("creds", "test creds", testCreds).options({ // user: { // type: "string", diff --git a/src/lib/logger.mjs b/src/lib/logger.mjs index db3c113f..5d6ce1de 100644 --- a/src/lib/logger.mjs +++ b/src/lib/logger.mjs @@ -9,21 +9,21 @@ export function log( formatter, argv ) { - if (!argv) { - argv = builtYargs.argv; - } + // if (!argv) { + // argv = builtYargs.argv; + // } - if ( - !argv.then && - (argv.verbosity >= verbosity || argv.verboseComponent.includes(component)) - ) { - // fails on intentional multi-line output - // demo with `--verbose-component argv` - // const prefix = /^(\n*)(.*)$/gm.exec(text)[1] - // const strippedText = /^(\n*)(.*)$/gm.exec(text)[2] - // stream(`${prefix}[${formatter(component)}]: ${formatter(strippedText)}`) - stream(`[${formatter(component)}]: ${formatter(text)}`); - } + // if ( + // !argv.then && + // (argv.verbosity >= verbosity || argv.verboseComponent.includes(component)) + // ) { + // // fails on intentional multi-line output + // // demo with `--verbose-component argv` + // // const prefix = /^(\n*)(.*)$/gm.exec(text)[1] + // // const strippedText = /^(\n*)(.*)$/gm.exec(text)[2] + // // stream(`${prefix}[${formatter(component)}]: ${formatter(strippedText)}`) + // stream(`[${formatter(component)}]: ${formatter(text)}`); + // } } const logger = { diff --git a/src/yargs-commands/database.mjs b/src/yargs-commands/database.mjs new file mode 100644 index 00000000..bf955ebd --- /dev/null +++ b/src/yargs-commands/database.mjs @@ -0,0 +1,44 @@ +import { container } from "../cli.mjs"; + +async function listDatabases(user) { + const logger = container.resolve("logger"); + const accountClient = container.resolve("accountClient"); + const accountCreds = container.resolve("accountCreds"); + const account_key = accountCreds.get(user).account_key; + logger.stdout("Listing Databases..."); + const databases = await accountClient.listDatabases(account_key); + logger.stdout(databases); +} + +function buildDatabaseCommand(yargs) { + return yargs + .positional("method", { + type: "string", + choices: ["create", "list", "delete"], + describe: "choose a method to interact with your databases", + }) + .options({ + user: { + type: "string", + description: "a user profile", + default: "default", + }, + }) + .help("help", "show help") + .example([["$0 db list"]]); +} + +function databaseHandler(argv) { + const method = argv.method; + if (method === "list") { + listDatabases(argv.user); + } +} + +export default { + command: "database ", + aliases: ["db"], + description: "Interact with your databases:", + builder: buildDatabaseCommand, + handler: databaseHandler, +}; diff --git a/src/yargs-commands/login.mjs b/src/yargs-commands/login.mjs index 2221372d..e5772be2 100644 --- a/src/yargs-commands/login.mjs +++ b/src/yargs-commands/login.mjs @@ -6,37 +6,32 @@ async function doLogin(argv) { const accountClient = container.resolve("accountClient"); const oAuth = container.resolve("oauthClient"); const accountCreds = container.resolve("accountCreds"); - if (!oAuth.server.listenerCount("ready")) { - oAuth.server.on("ready", async () => { - const authCodeParams = oAuth.getOAuthParams(); - const dashboardOAuthURL = await accountClient.startOAuthRequest( - authCodeParams + console.log('running doLogin') + oAuth.server.on("ready", async () => { + const authCodeParams = oAuth.getOAuthParams(); + const dashboardOAuthURL = await accountClient.startOAuthRequest( + authCodeParams + ); + open(dashboardOAuthURL); + logger.stdout(`To login, open your browser to:\n ${dashboardOAuthURL}`); + }); + oAuth.server.on("auth_code_received", async () => { + try { + const tokenParams = oAuth.getTokenParams(); + const accessToken = await accountClient.getToken(tokenParams); + const { account_key, refresh_token } = await accountClient.getSession( + accessToken ); - open(dashboardOAuthURL); - logger.stdout(`To login, open your browser to:\n ${dashboardOAuthURL}`); - }); - } - if (!oAuth.server.listenerCount("auth_code_received")) { - oAuth.server.on("auth_code_received", async () => { - try { - const tokenParams = oAuth.getTokenParams(); - const accessToken = await accountClient.getToken(tokenParams); - const { account_key, refresh_token } = await accountClient.getSession( - accessToken - ); - accountCreds.save({ - creds: { account_key, refresh_token }, - user: argv.user, - }); - logger.stdout(`Login Success!\n\n`); - logger.stdout("Listing Databases..."); - const databases = await accountClient.listDatabases(account_key); - logger.stdout(databases); - } catch (err) { - console.error(err); - } - }); - } + accountCreds.save({ + creds: { account_key, refresh_token }, + user: argv.user, + }); + logger.stdout(`Login Success!\n\n`); + + } catch (err) { + console.error(err); + } + }); await oAuth.start(); } From fa99e440cc248d4b1970066310ab6c916fdc974f Mon Sep 17 00:00:00 2001 From: Matt Wilde Date: Mon, 7 Oct 2024 16:30:37 -0400 Subject: [PATCH 4/9] merge in v3 --- src/lib/file-util.mjs | 11 +++++------ src/yargs-commands/database.mjs | 20 +++++++++++++++----- src/yargs-commands/login.mjs | 7 +++---- yargs-test/login.mjs | 22 +++++++++++++++++----- 4 files changed, 40 insertions(+), 20 deletions(-) diff --git a/src/lib/file-util.mjs b/src/lib/file-util.mjs index 312ce73d..467eb355 100644 --- a/src/lib/file-util.mjs +++ b/src/lib/file-util.mjs @@ -38,11 +38,10 @@ export function fileExists(path) { } export class Credentials { - logger = container.resolve("logger"); - - exit = container.resolve("exit"); - constructor(filename = "") { + console.log(container, "yo"); + this.logger = container.resolve("logger"); + this.exit = container.resolve("exit"); this.filename = filename; this.credsDir = `${os.homedir()}/.fauna/credentials`; if (!dirExists(this.credsDir)) { @@ -79,12 +78,12 @@ export class Credentials { } } - save({ creds, overwrite = false, user }) { + save({ creds, overwrite = false, profile }) { try { const existingContent = overwrite ? {} : this.get(); const newContent = { ...existingContent, - [user]: creds, + [profile]: creds, }; fs.writeFileSync(this.filepath, JSON.stringify(newContent, null, 2)); } catch (err) { diff --git a/src/yargs-commands/database.mjs b/src/yargs-commands/database.mjs index bf955ebd..c607e953 100644 --- a/src/yargs-commands/database.mjs +++ b/src/yargs-commands/database.mjs @@ -1,10 +1,10 @@ import { container } from "../cli.mjs"; -async function listDatabases(user) { +async function listDatabases(profile) { const logger = container.resolve("logger"); const accountClient = container.resolve("accountClient"); const accountCreds = container.resolve("accountCreds"); - const account_key = accountCreds.get(user).account_key; + const account_key = accountCreds.get(profile).account_key; logger.stdout("Listing Databases..."); const databases = await accountClient.listDatabases(account_key); logger.stdout(databases); @@ -18,7 +18,7 @@ function buildDatabaseCommand(yargs) { describe: "choose a method to interact with your databases", }) .options({ - user: { + profile: { type: "string", description: "a user profile", default: "default", @@ -30,8 +30,18 @@ function buildDatabaseCommand(yargs) { function databaseHandler(argv) { const method = argv.method; - if (method === "list") { - listDatabases(argv.user); + switch (method) { + case "create": + console.log("Creating database..."); + break; + case "delete": + console.log("Deleting database..."); + break; + case "list": + listDatabases(argv.profile); + break; + default: + break; } } diff --git a/src/yargs-commands/login.mjs b/src/yargs-commands/login.mjs index e5772be2..0b423f84 100644 --- a/src/yargs-commands/login.mjs +++ b/src/yargs-commands/login.mjs @@ -6,7 +6,6 @@ async function doLogin(argv) { const accountClient = container.resolve("accountClient"); const oAuth = container.resolve("oauthClient"); const accountCreds = container.resolve("accountCreds"); - console.log('running doLogin') oAuth.server.on("ready", async () => { const authCodeParams = oAuth.getOAuthParams(); const dashboardOAuthURL = await accountClient.startOAuthRequest( @@ -24,9 +23,9 @@ async function doLogin(argv) { ); accountCreds.save({ creds: { account_key, refresh_token }, - user: argv.user, + profile: argv.profile, }); - logger.stdout(`Login Success!\n\n`); + logger.stdout(`Login Success!\n`); } catch (err) { console.error(err); @@ -38,7 +37,7 @@ async function doLogin(argv) { function buildLoginCommand(yargs) { return yargs .options({ - user: { + profile: { type: "string", description: "a user profile", default: "default", diff --git a/yargs-test/login.mjs b/yargs-test/login.mjs index 4911c65a..0f5effd8 100644 --- a/yargs-test/login.mjs +++ b/yargs-test/login.mjs @@ -3,9 +3,11 @@ import { run } from "../src/cli.mjs"; import { setupTestContainer as setupContainer } from "../src/config/setup-test-container.mjs"; import * as awilix from "awilix/lib/awilix.module.mjs"; import { stub, spy } from "sinon"; +import { AccountKey } from "../src/lib/file-util.mjs"; describe("login", function () { let container; + let fs; const mockOAuth = () => { let handlers = {}; @@ -59,12 +61,15 @@ describe("login", function () { container.register({ oauthClient: awilix.asFunction(mockOAuth).scoped(), accountClient: awilix.asFunction(mockAccountClient).scoped(), + accountCreds: awilix.asClass(AccountKey), }); + fs = container.resolve("fs"); }); it("can login", async function () { const oauthClient = container.resolve("oauthClient"); const logger = container.resolve("logger"); + const accountCreds = container.resolve("accountCreds"); await run(`login`, container); // We start the loopback server @@ -76,12 +81,19 @@ describe("login", function () { "To login, open your browser to:\n dashboard-url" ) ); + fs.readFileSync.returns( + JSON.stringify({ + default: { + account_key: "test", + refresh_token: "test", + }, + }) + ); // Trigger server event with mocked auth code await oauthClient._receiveAuthCode(); - // We create a session and list databases - expect(logger.stdout.args.flat()).to.include( - "Listing Databases...", - "test databases" - ); + // We create a session + expect(logger.stdout.args.flat()).to.include("Login Success!\n"); + console.log(accountCreds); + expect(accountCreds).to.have.been.called; }); }); From d99af06826c7659658dd40a1b32e5a8c7fc4ba83 Mon Sep 17 00:00:00 2001 From: Matt Wilde Date: Mon, 7 Oct 2024 16:50:22 -0400 Subject: [PATCH 5/9] fix vs code jsdoc type inference --- src/cli.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli.mjs b/src/cli.mjs index d150bf09..3ac4a4ce 100644 --- a/src/cli.mjs +++ b/src/cli.mjs @@ -13,7 +13,7 @@ import { logArgv } from "./lib/middleware.mjs"; /** @type {cliContainer} */ export let container; -/** @type {yargs.Argv} */ +/** @type {import('yargs').Argv} */ export let builtYargs; /** @@ -42,7 +42,7 @@ export async function run(argvInput, _container) { /** * @function buildYargs * @param {string} argvInput - * @returns {yargs.Argv} + * @returns {import('yargs').Argv} */ function buildYargs(argvInput) { // have to build a yargsInstance _before_ chaining off it From 00d3b94e5225cb9d5eadc2874fc6d25f48279a03 Mon Sep 17 00:00:00 2001 From: Matt Wilde Date: Tue, 8 Oct 2024 09:54:05 -0400 Subject: [PATCH 6/9] basic tests for writing account key creds --- src/cli.mjs | 9 ++++- src/lib/file-util.mjs | 9 ----- src/yargs-commands/login.mjs | 1 - yargs-test/login.mjs | 70 +++++++++++++++++++++++++++--------- 4 files changed, 62 insertions(+), 27 deletions(-) diff --git a/src/cli.mjs b/src/cli.mjs index 3ac4a4ce..a6452985 100644 --- a/src/cli.mjs +++ b/src/cli.mjs @@ -39,6 +39,13 @@ export async function run(argvInput, _container) { } } +// we split this out so it can be injected/mocked +// this lets us record calls against it and, e.g., +// ensure it's only run once per command invocation +export async function parseYargs(builtYargs) { + return builtYargs.parseAsync(); +} + /** * @function buildYargs * @param {string} argvInput @@ -47,7 +54,7 @@ export async function run(argvInput, _container) { function buildYargs(argvInput) { // have to build a yargsInstance _before_ chaining off it // https://github.com/yargs/yargs/blob/main/docs/typescript.md?plain=1#L124 - const yargsInstance = yargs(argvInput) + const yargsInstance = yargs(argvInput); return ( yargsInstance diff --git a/src/lib/file-util.mjs b/src/lib/file-util.mjs index 467eb355..921eb8f5 100644 --- a/src/lib/file-util.mjs +++ b/src/lib/file-util.mjs @@ -39,7 +39,6 @@ export function fileExists(path) { export class Credentials { constructor(filename = "") { - console.log(container, "yo"); this.logger = container.resolve("logger"); this.exit = container.resolve("exit"); this.filename = filename; @@ -105,14 +104,6 @@ export class AccountKey extends Credentials { } } -export function testCreds() { - const a = new SecretKey(); - console.log("before", a.get()); - a.save({ myrefreshtoken: "myaccountkey" }); - console.log("after", a.get()); - console.log("getting creds", a.get("default")); -} - function isJSON(value) { try { JSON.parse(value); diff --git a/src/yargs-commands/login.mjs b/src/yargs-commands/login.mjs index 0b423f84..95b3a0dd 100644 --- a/src/yargs-commands/login.mjs +++ b/src/yargs-commands/login.mjs @@ -26,7 +26,6 @@ async function doLogin(argv) { profile: argv.profile, }); logger.stdout(`Login Success!\n`); - } catch (err) { console.error(err); } diff --git a/yargs-test/login.mjs b/yargs-test/login.mjs index 0f5effd8..1fe56a8d 100644 --- a/yargs-test/login.mjs +++ b/yargs-test/login.mjs @@ -8,6 +8,10 @@ import { AccountKey } from "../src/lib/file-util.mjs"; describe("login", function () { let container; let fs; + const sessionCreds = { + account_key: "account-key", + refresh_token: "refresh-token", + }; const mockOAuth = () => { let handlers = {}; @@ -49,10 +53,7 @@ describe("login", function () { return { startOAuthRequest: stub().resolves("dashboard-url"), listDatabases: stub().resolves("test databases"), - getSession: stub().resolves({ - account_key: "account-key", - refresh_token: "refresh-token", - }), + getSession: stub().resolves(sessionCreds), getToken: stub().resolves({ access_token: "access-token" }), }; }; @@ -61,16 +62,28 @@ describe("login", function () { container.register({ oauthClient: awilix.asFunction(mockOAuth).scoped(), accountClient: awilix.asFunction(mockAccountClient).scoped(), - accountCreds: awilix.asClass(AccountKey), + accountCreds: awilix.asClass(AccountKey).scoped(), }); fs = container.resolve("fs"); }); it("can login", async function () { + // Run the command first so container is set. + await run(`login`, container); + // After container is set, we can get the mocks const oauthClient = container.resolve("oauthClient"); const logger = container.resolve("logger"); const accountCreds = container.resolve("accountCreds"); - await run(`login`, container); + const existingCreds = { + test_profile: { + account_key: "test", + refresh_token: "test", + }, + }; + const expectedCreds = { + ...existingCreds, + default: sessionCreds, + }; // We start the loopback server expect(oauthClient.start.called).to.be.true; @@ -81,19 +94,44 @@ describe("login", function () { "To login, open your browser to:\n dashboard-url" ) ); - fs.readFileSync.returns( - JSON.stringify({ - default: { - account_key: "test", - refresh_token: "test", - }, - }) + accountCreds.get = stub().returns(existingCreds); + // Trigger server event with mocked auth code + await oauthClient._receiveAuthCode(); + // Show login success message + expect(logger.stdout.args.flat()).to.include("Login Success!\n"); + // We save the session credentials alongside existing credential contents + expect(accountCreds.filepath).to.include(".fauna/credentials/access_keys"); + expect(JSON.parse(fs.writeFileSync.args[0][1])).to.deep.equal( + expectedCreds ); + }); + + it("overwrites credentials on login", async function () { + const existingCreds = { + test_profile: { + account_key: "oldkey", + refresh_token: "oldtoken", + }, + }; + const expectedCreds = { + test_profile: { + account_key: "account-key", + refresh_token: "refresh-token", + }, + }; + await run(`login --profile test_profile`, container); + const accountCreds = container.resolve("accountCreds"); + const oauthClient = container.resolve("oauthClient"); + const logger = container.resolve("logger"); + // Local file read returns old creds + accountCreds.get = stub().returns(existingCreds); // Trigger server event with mocked auth code await oauthClient._receiveAuthCode(); - // We create a session + // Show login success message expect(logger.stdout.args.flat()).to.include("Login Success!\n"); - console.log(accountCreds); - expect(accountCreds).to.have.been.called; + // We save the session credentials and overwrite the profile of the same name locally + expect(JSON.parse(fs.writeFileSync.args[0][1])).to.deep.equal( + expectedCreds + ); }); }); From 62006db65792ad16cf7f27ac1594a452e3f97799 Mon Sep 17 00:00:00 2001 From: Matt Wilde Date: Tue, 8 Oct 2024 11:12:50 -0400 Subject: [PATCH 7/9] jsdocs for creds files --- src/lib/file-util.mjs | 50 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/src/lib/file-util.mjs b/src/lib/file-util.mjs index 921eb8f5..3a9dab1d 100644 --- a/src/lib/file-util.mjs +++ b/src/lib/file-util.mjs @@ -28,7 +28,13 @@ export function dirIsWriteable(path) { return true; } -export function fileExists(path) { +/** + * Checks if a file exists at the given path. + * + * @param {string} path - The path to the file. + * @returns {boolean} - Returns true if the file exists, otherwise false. + */ +function fileExists(path) { try { fs.readFileSync(path); return true; @@ -37,7 +43,15 @@ export function fileExists(path) { } } +/** + * Class representing credentials management. + */ export class Credentials { + /** + * Creates an instance of Credentials. + * + * @param {string} [filename=""] - The name of the credentials file. + */ constructor(filename = "") { this.logger = container.resolve("logger"); this.exit = container.resolve("exit"); @@ -52,6 +66,12 @@ export class Credentials { } } + /** + * Retrieves the value associated with the given key from the credentials file. + * + * @param {string} [key] - The key to retrieve the value for. + * @returns {any} - The value associated with the key, or the entire parsed content if no key is provided. + */ get(key) { try { // Open file for reading and writing without truncating @@ -77,6 +97,14 @@ export class Credentials { } } + /** + * Saves the credentials to the file. + * + * @param {Object} params - The parameters for saving credentials. + * @param {Record} params.creds - The credentials to save. + * @param {boolean} [params.overwrite=false] - Whether to overwrite existing credentials. + * @param {string} params.profile - The profile name to save the credentials under. + */ save({ creds, overwrite = false, profile }) { try { const existingContent = overwrite ? {} : this.get(); @@ -92,18 +120,38 @@ export class Credentials { } } +/** + * Class representing secret key management. + * @extends Credentials + */ export class SecretKey extends Credentials { + /** + * Creates an instance of SecretKey. + */ constructor() { super("secret_keys"); } } +/** + * Class representing account key management. + * @extends Credentials + */ export class AccountKey extends Credentials { + /** + * Creates an instance of AccountKey. + */ constructor() { super("access_keys"); } } +/** + * Checks if a value is a valid JSON string. + * + * @param {string} value - The value to check. + * @returns {boolean} - Returns true if the value is a valid JSON string, otherwise false. + */ function isJSON(value) { try { JSON.parse(value); From 4a43f8c8ba6fce0d10f2cc62eaa6c8b4ec3cb1d9 Mon Sep 17 00:00:00 2001 From: Matt Wilde Date: Tue, 8 Oct 2024 11:21:02 -0400 Subject: [PATCH 8/9] frontdoor client jsdocs --- src/lib/fauna-account-client.mjs | 75 +++++++++++++++++++++++++------- 1 file changed, 59 insertions(+), 16 deletions(-) diff --git a/src/lib/fauna-account-client.mjs b/src/lib/fauna-account-client.mjs index ad3ff4c9..cb481039 100644 --- a/src/lib/fauna-account-client.mjs +++ b/src/lib/fauna-account-client.mjs @@ -2,13 +2,35 @@ import { container } from "../cli.mjs"; +/** + * Class representing a client for interacting with the Fauna account API. + */ export class FaunaAccountClient { + /** + * Creates an instance of FaunaAccountClient. + */ constructor() { + /** + * The base URL for the Fauna account API. + * @type {string} + */ this.url = process.env.FAUNA_ACCOUNT_URL ?? "https://account.fauna.com/api/v1"; + + /** + * The fetch function for making HTTP requests. + * @type {Function} + */ this.fetch = container.resolve("fetch"); } + /** + * Starts an OAuth request to the Fauna account API. + * + * @param {Object} authCodeParams - The parameters for the OAuth authorization code request. + * @returns {Promise} - The URL to the Fauna dashboard for OAuth authorization. + * @throws {Error} - Throws an error if there is an issue during login. + */ async startOAuthRequest(authCodeParams) { const OAuthUrl = `${this.url}/api/v1/oauth/authorize?${new URLSearchParams( authCodeParams @@ -21,6 +43,18 @@ export class FaunaAccountClient { return dashboardOAuthURL; } + /** + * Retrieves an access token from the Fauna account API. + * + * @param {Object} opts - The options for the token request. + * @param {string} opts.clientId - The client ID for the OAuth application. + * @param {string} opts.clientSecret - The client secret for the OAuth application. + * @param {string} opts.authCode - The authorization code received from the OAuth authorization. + * @param {string} opts.redirectURI - The redirect URI for the OAuth application. + * @param {string} opts.codeVerifier - The code verifier for the OAuth PKCE flow. + * @returns {Promise} - The access token. + * @throws {Error} - Throws an error if there is an issue during token retrieval. + */ async getToken(opts) { const params = { grant_type: "authorization_code", @@ -43,20 +77,26 @@ export class FaunaAccountClient { `Failure to authorize with Fauna (${response.status}): ${response.statusText}` ); } - const { /*state,*/ access_token } = await response.json(); + const { access_token } = await response.json(); return access_token; } catch (err) { throw new Error("Failure to authorize with Fauna: " + err.message); } } - // TODO: remove access_token param and use credential manager helper + /** + * Retrieves the session information from the Fauna account API. + * + * @param {string} accessToken - The access token for the session. + * @returns {Promise<{account_key: string, refresh_token: string}>} - The session information. + * @throws {Error} - Throws an error if there is an issue during session retrieval. + */ async getSession(accessToken) { const headers = new Headers(); headers.append("Authorization", `Bearer ${accessToken}`); const requestOptions = { - method: "POST", + method: "GET", headers, }; try { @@ -66,22 +106,26 @@ export class FaunaAccountClient { ); if (response.status >= 400) { throw new Error( - `Error creating session (${response.status}): ${response.statusText}` + `Failure to get session with Fauna (${response.status}): ${response.statusText}` ); } - const session = await response.json(); - return session; + return await response.json(); } catch (err) { - throw new Error( - "Failure to create session with Fauna: " + JSON.stringify(err) - ); + throw new Error("Failure to get session with Fauna: " + err.message); } } - // TODO: remove account_key param and use credential manager helper - async listDatabases(account_key) { + /** + * Lists databases associated with the given account key. + * + * @param {string} accountKey - The account key to list databases for. + * @returns {Promise} - The list of databases. + * @throws {Error} - Throws an error if there is an issue during the request. + */ + async listDatabases(accountKey) { const headers = new Headers(); - headers.append("Authorization", `Bearer ${account_key}`); + headers.append("Authorization", `Bearer ${accountKey}`); + const requestOptions = { method: "GET", headers, @@ -93,13 +137,12 @@ export class FaunaAccountClient { ); if (response.status >= 400) { throw new Error( - `Error listing databases (${response.status}): ${response.statusText}` + `Failure to list databases. (${response.status}): ${response.statusText}` ); } - const databases = await response.json(); - return databases; + return await response.json(); } catch (err) { - throw new Error("Failure to list databases: ", err.message); + throw new Error("Failure to list databases with Fauna: " + err.message); } } } From 3e8e58e95ab9dc70b3652577264f25efe6a6f975 Mon Sep 17 00:00:00 2001 From: Matt Wilde Date: Tue, 8 Oct 2024 15:28:57 -0400 Subject: [PATCH 9/9] throw errors and update jsdocs --- src/lib/db.mjs | 20 ++++++++++---------- src/lib/fauna-account-client.mjs | 2 +- src/lib/file-util.mjs | 18 ++++++------------ 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/src/lib/db.mjs b/src/lib/db.mjs index 23ee0682..0c6ad845 100644 --- a/src/lib/db.mjs +++ b/src/lib/db.mjs @@ -3,16 +3,16 @@ import { container } from "../cli.mjs"; /** - * @function makeFaunaRequest - * @param {object} args - * @param {string} args.secret - The secret to include in the AUTHORIZATION header of the request. - * @param {string} args.baseUrl - The base URL from the scheme up through the top level domain and optional port; defaults to "https://db.fauna.com:443". - * @param {string} args.path - The path part of the URL. Added to the baseUrl and params to build the full URL. - * @param {Record} [args.params] - The parameters (and their values) to set in the query string. - * @param {('GET'|'HEAD'|'OPTIONS'|'PATCH'|'PUT'|'POST'|'DELETE'|'PATCH')} args.method - The HTTP method to use when making the request. - * @param {object} [args.body] - The body to include in the request. - * @param {boolean} [args.shouldThrow] - Whether or not to throw if the network request succeeds but is not a 2XX. If this is set to false, makeFaunaRequest will return the error instead of throwing. - */ + * @function makeFaunaRequest + * @param {object} args + * @param {string} args.secret - The secret to include in the AUTHORIZATION header of the request. + * @param {string} args.baseUrl - The base URL from the scheme up through the top level domain and optional port; defaults to "https://db.fauna.com:443". + * @param {string} args.path - The path part of the URL. Added to the baseUrl and params to build the full URL. + * @param {Record} [args.params] - The parameters (and their values) to set in the query string. + * @param {('GET'|'HEAD'|'OPTIONS'|'PATCH'|'PUT'|'POST'|'DELETE'|'PATCH')} args.method - The HTTP method to use when making the request. + * @param {object} [args.body] - The body to include in the request. + * @param {boolean} [args.shouldThrow] - Whether or not to throw if the network request succeeds but is not a 2XX. If this is set to false, makeFaunaRequest will return the error instead of throwing. + */ export async function makeFaunaRequest({ secret, baseUrl, diff --git a/src/lib/fauna-account-client.mjs b/src/lib/fauna-account-client.mjs index cb481039..c7d794b6 100644 --- a/src/lib/fauna-account-client.mjs +++ b/src/lib/fauna-account-client.mjs @@ -96,7 +96,7 @@ export class FaunaAccountClient { headers.append("Authorization", `Bearer ${accessToken}`); const requestOptions = { - method: "GET", + method: "POST", headers, }; try { diff --git a/src/lib/file-util.mjs b/src/lib/file-util.mjs index 92549f04..24024f0c 100644 --- a/src/lib/file-util.mjs +++ b/src/lib/file-util.mjs @@ -72,7 +72,7 @@ export class Credentials { * Retrieves the value associated with the given key from the credentials file. * * @param {string} [key] - The key to retrieve the value for. - * @returns {any} - The value associated with the key, or the entire parsed content if no key is provided. + * @returns {Object.} credentialsObject - The value associated with the key, or the entire parsed content if no key is provided. */ get(key) { try { @@ -81,21 +81,16 @@ export class Credentials { .readFileSync(this.filepath, { flag: "r+" }) .toString(); if (!isJSON(fileContent)) { - this.logger.stderr( - "Credentials file contains invalid formatting: ", - this.filepath + throw new Error( + `Credentials file at ${this.filepath} contains invalid formatting.` ); - this.exit(1); } const parsed = JSON.parse(fileContent); return key ? parsed[key] : parsed; } catch (err) { - this.logger.stderr( - "Error while parsing credentials file: ", - this.filepath, - err + throw new Error( + `Error while parsing credentials file at ${this.filepath}: ${err}` ); - this.exit(1); } } @@ -116,8 +111,7 @@ export class Credentials { }; fs.writeFileSync(this.filepath, JSON.stringify(newContent, null, 2)); } catch (err) { - this.logger.stderr("Error while saving credentials: ", err); - this.exit(1); + throw new Error(`Error while saving credentials: ${err}`); } } }