From f8f69163b644dd8d6391172ae8ba84bf971977d0 Mon Sep 17 00:00:00 2001 From: Ashton Eby Date: Mon, 18 Nov 2024 16:53:31 -0800 Subject: [PATCH 01/11] update nvmrc version --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index 3c032078..2bd5a0a9 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18 +22 From 54d93f1a5012b8007d1727ef212e4492d22579e7 Mon Sep 17 00:00:00 2001 From: Ashton Eby Date: Mon, 18 Nov 2024 17:08:28 -0800 Subject: [PATCH 02/11] add ts-check annotations to more files, clean up tests --- src/commands/database.mjs | 2 ++ src/commands/key.mjs | 2 ++ test/eval.mjs | 2 ++ test/general-cli.mjs | 2 ++ test/login.mjs | 12 ++++++------ test/mocha-root-hooks.mjs | 2 ++ test/schema/abandon.mjs | 2 ++ test/schema/commit.mjs | 2 ++ test/schema/diff.mjs | 2 ++ test/schema/pull.mjs | 2 ++ test/schema/push.mjs | 2 ++ 11 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/commands/database.mjs b/src/commands/database.mjs index e770ef6f..cba5837f 100644 --- a/src/commands/database.mjs +++ b/src/commands/database.mjs @@ -1,3 +1,5 @@ +//@ts-check + import { container } from "../cli.mjs"; async function listDatabases(profile) { diff --git a/src/commands/key.mjs b/src/commands/key.mjs index 426fb82e..cbb160d4 100644 --- a/src/commands/key.mjs +++ b/src/commands/key.mjs @@ -1,3 +1,5 @@ +//@ts-check + import { container } from "../cli.mjs"; import { getAccountKey, getDBKey } from "../lib/auth/authNZ.mjs"; diff --git a/test/eval.mjs b/test/eval.mjs index 3f2f320b..6f4f1585 100644 --- a/test/eval.mjs +++ b/test/eval.mjs @@ -1,3 +1,5 @@ +//@ts-check + import { expect } from "chai"; import { run } from "../src/cli.mjs"; diff --git a/test/general-cli.mjs b/test/general-cli.mjs index 363f3b86..8d7c3013 100644 --- a/test/general-cli.mjs +++ b/test/general-cli.mjs @@ -1,3 +1,5 @@ +//@ts-check + import * as fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; diff --git a/test/login.mjs b/test/login.mjs index 055e1a04..d6ecc35e 100644 --- a/test/login.mjs +++ b/test/login.mjs @@ -1,3 +1,5 @@ +//@ts-check + import * as awilix from "awilix"; import { expect } from "chai"; import { spy, stub } from "sinon"; @@ -90,16 +92,14 @@ describe("login", function () { expect(oauthClient.start.called).to.be.true; // We open auth url in the browser and prompt user expect(container.resolve("open").calledWith("dashboard-url")); - expect( - logger.stdout.calledWith( - "To login, open your browser to:\n dashboard-url", - ), + expect(logger.stdout).to.have.been.calledWith( + "To login, open your browser to:\n dashboard-url", ); 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"); + expect(logger.stdout).to.have.been.calledWith("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( @@ -129,7 +129,7 @@ describe("login", function () { // Trigger server event with mocked auth code await oauthClient._receiveAuthCode(); // Show login success message - expect(logger.stdout.args.flat()).to.include("Login Success!\n"); + expect(logger.stdout).to.have.been.calledWith("Login Success!\n"); // 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, diff --git a/test/mocha-root-hooks.mjs b/test/mocha-root-hooks.mjs index ad48ecdd..ae6b09d4 100644 --- a/test/mocha-root-hooks.mjs +++ b/test/mocha-root-hooks.mjs @@ -1,3 +1,5 @@ +//@ts-check + import * as chai from "chai"; import sinon from "sinon"; import sinonChai from "sinon-chai"; diff --git a/test/schema/abandon.mjs b/test/schema/abandon.mjs index dd2ce7d8..8db1e675 100644 --- a/test/schema/abandon.mjs +++ b/test/schema/abandon.mjs @@ -1,3 +1,5 @@ +//@ts-check + import { expect } from "chai"; import chalk from "chalk"; import sinon from "sinon"; diff --git a/test/schema/commit.mjs b/test/schema/commit.mjs index ea0dc2d1..a090b6d9 100644 --- a/test/schema/commit.mjs +++ b/test/schema/commit.mjs @@ -1,3 +1,5 @@ +//@ts-check + import { expect } from "chai"; import chalk from "chalk"; import sinon from "sinon"; diff --git a/test/schema/diff.mjs b/test/schema/diff.mjs index 3474311f..d2643baa 100644 --- a/test/schema/diff.mjs +++ b/test/schema/diff.mjs @@ -1,3 +1,5 @@ +//@ts-check + import { expect } from "chai"; import { run } from "../../src/cli.mjs"; diff --git a/test/schema/pull.mjs b/test/schema/pull.mjs index 526d54ca..d356d309 100644 --- a/test/schema/pull.mjs +++ b/test/schema/pull.mjs @@ -1,3 +1,5 @@ +//@ts-check + import * as awilix from "awilix"; import { expect } from "chai"; import sinon from "sinon"; diff --git a/test/schema/push.mjs b/test/schema/push.mjs index 3dc63d9b..29721824 100644 --- a/test/schema/push.mjs +++ b/test/schema/push.mjs @@ -1,3 +1,5 @@ +//@ts-check + import { expect } from "chai"; import sinon from "sinon"; From de230740cf44fc1bc9f0dd0bc7411ce3301f86d7 Mon Sep 17 00:00:00 2001 From: Ashton Eby Date: Wed, 13 Nov 2024 15:13:21 -0800 Subject: [PATCH 03/11] add shell command --- .github/workflows/build-binaries.yml | 3 + src/cli.mjs | 2 + src/commands/eval.mjs | 39 +++------- src/commands/shell.mjs | 108 +++++++++++++++++++++++++++ src/config/setup-container.mjs | 9 ++- src/config/setup-test-container.mjs | 29 +++---- src/lib/command-helpers.mjs | 35 +++++++++ src/lib/logger.mjs | 58 ++++++++------ test/helpers.mjs | 65 +++++++++++++++- test/shell.mjs | 79 ++++++++++++++++++++ 10 files changed, 352 insertions(+), 75 deletions(-) create mode 100644 src/commands/shell.mjs create mode 100644 test/shell.mjs diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index c531269a..c6b956d2 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -24,6 +24,9 @@ jobs: - runner: windows-latest os: windows arch: x64 + # github-hosted (free) linux arm64 runner is planned by end of 2024 + # it's currently available on team/enterprise github plans: + # https://github.blog/news-insights/product-news/arm64-on-github-actions-powering-faster-more-efficient-build-systems/ runs-on: ${{ matrix.runner }} steps: diff --git a/src/cli.mjs b/src/cli.mjs index 965bb9a2..6e337172 100644 --- a/src/cli.mjs +++ b/src/cli.mjs @@ -8,6 +8,7 @@ import evalCommand from "./commands/eval.mjs"; import keyCommand from "./commands/key.mjs"; import loginCommand from "./commands/login.mjs"; import schemaCommand from "./commands/schema/schema.mjs"; +import shellCommand from "./commands/shell.mjs"; import { authNZMiddleware } from "./lib/auth/authNZ.mjs"; import { checkForUpdates, fixPaths, logArgv } from "./lib/middleware.mjs"; @@ -65,6 +66,7 @@ function buildYargs(argvInput) { .middleware([checkForUpdates, logArgv], true) .middleware([fixPaths, authNZMiddleware], false) .command("eval", "evaluate a query", evalCommand) + .command("shell", "start an interactive shell", shellCommand) .command("login", "login via website", loginCommand) .command(keyCommand) .command(schemaCommand) diff --git a/src/commands/eval.mjs b/src/commands/eval.mjs index b9f06133..a55799d7 100644 --- a/src/commands/eval.mjs +++ b/src/commands/eval.mjs @@ -8,7 +8,7 @@ import util from "util"; import { container } from "../cli.mjs"; import { // ensureDbScopeClient, - commonQueryOptions, + commonConfigurableQueryOptions, } from "../lib/command-helpers.mjs"; import * as misc from "../lib/misc.mjs"; @@ -33,11 +33,13 @@ async function writeFormattedJson(file, data) { /** * Write fauna shell encoded output * - * @param {String} file Target filename + * @param {String | undefined} file Target filename * @param {any} str Data to encode */ async function writeFormattedShell(file, str) { - if (file === null) { + // TODO: this should really normalize the line endings + // using os.EOL. + if (file === undefined) { return str; } else { // await writeFile(file, str); @@ -95,7 +97,7 @@ async function writeFormattedOutputV10(file, res, format) { } } -async function performV10Query(client, fqlQuery, outputFile, flags) { +export async function performV10Query(client, fqlQuery, outputFile, flags) { let format; if (flags.format === "shell") { format = "decorated"; @@ -113,7 +115,7 @@ async function performV10Query(client, fqlQuery, outputFile, flags) { return writeFormattedOutputV10(outputFile, res, flags.format); } -async function performV4Query(client, fqlQuery, outputFile, flags) { +export async function performV4Query(client, fqlQuery, outputFile, flags) { const faunadb = (await import("faunadb")).default; // why...? @@ -151,7 +153,7 @@ async function performV4Query(client, fqlQuery, outputFile, flags) { * * @param {Object} client - An instance of the client used to execute the query. * @param {string} fqlQuery - The FQL v4 query to be executed. - * @param {string} outputFile - Target filename + * @param {string | undefined} outputFile - Target filename * @param {Object} flags - Options for the query execution. * @param {("4" | "10")} flags.version - FQL version number * @param {("json" | "json-tagged" | "shell")} flags.format - Result format @@ -159,8 +161,7 @@ async function performV4Query(client, fqlQuery, outputFile, flags) { */ export async function performQuery(client, fqlQuery, outputFile, flags) { if (flags.version === "4") { - const res = performV4Query(client, fqlQuery, outputFile, flags); - return res; + return performV4Query(client, fqlQuery, outputFile, flags); } else { return performV10Query(client, fqlQuery, outputFile, flags); } @@ -267,27 +268,7 @@ function buildEvalCommand(yargs) { default: "shell", options: EVAL_OUTPUT_FORMATS, }, - version: { - description: "which FQL version to use", - type: "string", - alias: "v", - default: "10", - choices: ["4", "10"], - }, - // TODO: is this unused? i think it might be - timeout: { - type: "number", - description: "connection timeout in milliseconds", - default: 5000, - }, - - // v10 specific options - typecheck: { - type: "boolean", - description: "enable typechecking", - default: undefined, - }, - ...commonQueryOptions, + ...commonConfigurableQueryOptions, }) .example([ ['$0 eval "Collection.all()"'], diff --git a/src/commands/shell.mjs b/src/commands/shell.mjs new file mode 100644 index 00000000..e4c621c0 --- /dev/null +++ b/src/commands/shell.mjs @@ -0,0 +1,108 @@ +//@ts-check + +import repl from "node:repl"; + +import { container } from "../cli.mjs"; +import { + // ensureDbScopeClient, + commonConfigurableQueryOptions, + getSimpleClient, +} from "../lib/command-helpers.mjs"; + +async function doShell(argv) { + const logger = container.resolve("logger"); + let completionPromise; + + if (argv.dbPath) logger.stdout(`Starting shell for database ${argv.dbPath}`); + logger.stdout("Type Ctrl+D or .exit to exit the shell"); + + /** @type {import('node:repl').ReplOptions} */ + const replArgs = { + prompt: `${argv.db_path || ""}> `, + ignoreUndefined: true, + preview: argv.version !== "10", + // TODO: integrate with fql-analyzer for completions + completer: argv.version === "10" ? () => [] : undefined, + output: container.resolve("stdoutStream"), + input: container.resolve("stdinStream"), + eval: await customEval(argv), + terminal: true, + }; + + const shell = repl.start(replArgs); + + completionPromise = new Promise((resolve) => { + shell.on("exit", resolve); + }); + + [ + { + cmd: "clear", + help: "Clear the repl", + action: () => { + // eslint-disable-next-line no-console + console.clear(); + shell.prompt(); + }, + }, + { + cmd: "last_error", + help: "Display the last error", + action: () => { + logger.stdout(shell.context.lastError); + shell.prompt(); + }, + }, + ].forEach(({ cmd, ...cmdOptions }) => shell.defineCommand(cmd, cmdOptions)); + + return completionPromise; +} + +async function customEval(argv) { + const logger = container.resolve("logger"); + const client = await getSimpleClient(argv); + const performQuery = container.resolve("performQuery"); + + return async (cmd, ctx, filename, cb) => { + try { + if (cmd.trim() === "") return cb(); + + let res; + try { + res = await performQuery(client, cmd, undefined, { + ...argv, + format: "shell", + }); + } catch (err) { + let errString = ""; + if (err.code) { + errString = errString.concat(`${err.code}\n`); + } + errString = errString.concat(err.message); + logger.stderr(errString); + return cb(null); + } + + logger.stdout(res); + + return cb(null); + } catch (e) { + return cb(e); + } + }; +} + +function buildShellCommand(yargs) { + return yargs + .options({ + ...commonConfigurableQueryOptions, + }) + .example([["$0 shell"], ["$0 shell root_db/child_db"]]) + .version(false) + .help("help", "show help"); +} + +export default { + builder: buildShellCommand, + handler: doShell, +}; diff --git a/src/config/setup-container.mjs b/src/config/setup-container.mjs index 3ca56be5..45eab57e 100644 --- a/src/config/setup-container.mjs +++ b/src/config/setup-container.mjs @@ -19,7 +19,7 @@ import { makeFaunaRequest } from "../lib/db.mjs"; import { FaunaAccountClient } from "../lib/fauna-account-client.mjs"; import fetchWrapper from "../lib/fetch-wrapper.mjs"; import { AccountKey, SecretKey } from "../lib/file-util.mjs"; -import logger from "../lib/logger.mjs"; +import buildLogger from "../lib/logger.mjs"; import { deleteUnusedSchemaFiles, gatherFSL, @@ -51,6 +51,11 @@ export function setupCommonContainer() { /** @typedef {Resolvers} modifiedInjectables */ export const injectables = { + // process specifics + stdinStream: awilix.asValue(process.stdin), + stdoutStream: awilix.asValue(process.stdout), + stderrStream: awilix.asValue(process.stderr), + // node libraries exit: awilix.asValue(exit), fetch: awilix.asValue(fetchWrapper), @@ -66,7 +71,7 @@ export const injectables = { // generic lib (homemade utilities) parseYargs: awilix.asValue(parseYargs), - logger: awilix.asValue(logger), + logger: awilix.asFunction(buildLogger), performQuery: awilix.asValue(performQuery), getSimpleClient: awilix.asValue(getSimpleClient), accountClient: awilix.asClass(FaunaAccountClient, { diff --git a/src/config/setup-test-container.mjs b/src/config/setup-test-container.mjs index 96005d39..203df87e 100644 --- a/src/config/setup-test-container.mjs +++ b/src/config/setup-test-container.mjs @@ -1,14 +1,15 @@ import fs from "node:fs"; import { normalize } from "node:path"; +import { Readable } from "node:stream"; import * as awilix from "awilix"; import { spy, stub } from "sinon"; -import { f } from "../../test/helpers.mjs"; +import { f, InMemoryWritableStream } from "../../test/helpers.mjs"; import { parseYargs } from "../cli.mjs"; import { makeAccountRequest } from "../lib/account.mjs"; import { makeFaunaRequest } from "../lib/db.mjs"; -import logger from "../lib/logger.mjs"; +import buildLogger from "../lib/logger.mjs"; import { injectables, setupCommonContainer } from "./setup-container.mjs"; // Mocks all _functions_ declared on the injectables export from setup-container.mjs @@ -41,6 +42,11 @@ export function setupTestContainer() { const thingsToManuallyMock = automock(container); const manualMocks = { + // process specifics + stdinStream: awilix.asValue(Readable.from("")), + stdoutStream: awilix.asValue(new InMemoryWritableStream()), + stderrStream: awilix.asValue(new InMemoryWritableStream()), + // wrap it in a spy so we can record calls, but use the // real implementation parseYargs: awilix.asValue(spy(parseYargs)), @@ -50,24 +56,7 @@ export function setupTestContainer() { writeFile: stub(), }), updateNotifier: awilix.asValue(stub().returns({ notify: stub() })), - logger: awilix.asValue({ - // use these for making dev, support tickets easier. - // they're not mocked because we shouldn't test them - // as part of our public interface. this way, we can - // add `--verbosity 5` to a command in a test to get - // more output. - debug: logger.debug, - info: logger.info, - warn: logger.warn, - error: logger.error, - fatal: logger.fatal, - - // use these for communicating with customers. - // mocked because they _are_ part of our public - // interface and should be tested. - stdout: stub(), - stderr: stub(), - }), + logger: awilix.asFunction((cradle) => spy(buildLogger(cradle))).singleton(), getSimpleClient: awilix.asValue( stub().returns({ close: () => Promise.resolve() }), ), diff --git a/src/lib/command-helpers.mjs b/src/lib/command-helpers.mjs index 3eb2545c..ffa85e1e 100644 --- a/src/lib/command-helpers.mjs +++ b/src/lib/command-helpers.mjs @@ -90,6 +90,7 @@ export async function getSimpleClient(argv) { // }); // } +// used for queries customers can't configure that are made on their behalf export const commonQueryOptions = { url: { type: "string", @@ -102,3 +103,37 @@ export const commonQueryOptions = { required: true, }, }; + +// used for queries customers can configure +export const commonConfigurableQueryOptions = { + ...commonQueryOptions, + // TODO: is this unused? i think it might be + version: { + description: "which FQL version to use", + type: "string", + alias: "v", + default: "10", + choices: ["4", "10"], + }, + // v10 specific options + typecheck: { + type: "boolean", + description: "enable typechecking", + default: undefined, + }, + timeout: { + type: "number", + description: "connection timeout in milliseconds", + default: 5000, + }, + // format: { + // type: "string", + // description: "output format", + // default: "shell", + // options: EVAL_OUTPUT_FORMATS, + // }, + // dbname: { + // type: "string", + // description: "the database to run the query against", + // }, +}; diff --git a/src/lib/logger.mjs b/src/lib/logger.mjs index 05362fd3..dfa20e95 100644 --- a/src/lib/logger.mjs +++ b/src/lib/logger.mjs @@ -1,6 +1,7 @@ /* eslint-disable no-console */ import chalk from "chalk"; +import { Console } from "console"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; @@ -68,16 +69,16 @@ export function log({ * Log text at a debug log level (verbosity 5). * * @function + * @param {any} customConsole * @param {string} text - The text to log. * @param {string} [component] - A string that identifies what "component" of the application this is. Included in the log line as a prefix and also used for formatting with verboseComponents * @param {argv} [argv] - The parsed yargs argv. Used to determine the current verbosity and if any components are included in verboseComponents. */ -function debug(text, component, argv) { +function debug(customConsole, text, component, argv) { log({ text, verbosity: 5, - - stream: console.log, + stream: customConsole.log, component, formatter: chalk.blue, argv, @@ -88,15 +89,16 @@ function debug(text, component, argv) { * Log text at a info log level (verbosity 4). * * @function + * @param {any} customConsole * @param {string} text - The text to log. * @param {string} [component] - A string that identifies what "component" of the application this is. Included in the log line as a prefix and also used for formatting with verboseComponents * @param {argv} [argv] - The parsed yargs argv. Used to determine the current verbosity and if any components are included in verboseComponents. */ -function info(text, component, argv) { +function info(customConsole, text, component, argv) { log({ text, verbosity: 4, - stream: console.log, + stream: customConsole.log, component, formatter: chalk.green, argv, @@ -107,15 +109,16 @@ function info(text, component, argv) { * Log text at a warn log level (verbosity 3). * * @function + * @param {any} customConsole * @param {string} text - The text to log. * @param {string} [component] - A string that identifies what "component" of the application this is. Included in the log line as a prefix and also used for formatting with verboseComponents * @param {argv} [argv] - The parsed yargs argv. Used to determine the current verbosity and if any components are included in verboseComponents. */ -function warn(text, component, argv) { +function warn(customConsole, text, component, argv) { log({ text, verbosity: 3, - stream: console.warn, + stream: customConsole.warn, component, formatter: chalk.yellow, argv, @@ -126,15 +129,16 @@ function warn(text, component, argv) { * Log text at a error log level (verbosity 2). * * @function + * @param {any} customConsole * @param {string} text - The text to log. * @param {string} [component] - A string that identifies what "component" of the application this is. Included in the log line as a prefix and also used for formatting with verboseComponents * @param {argv} [argv] - The parsed yargs argv. Used to determine the current verbosity and if any components are included in verboseComponents. */ -function error(text, component, argv) { +function error(customConsole, text, component, argv) { log({ text, verbosity: 2, - stream: console.error, + stream: customConsole.error, component, formatter: chalk.red, argv, @@ -145,32 +149,40 @@ function error(text, component, argv) { * Log text at a fatal log level (verbosity 1). * * @function + * @param {any} customConsole * @param {string} text - The text to log. * @param {string} [component] - A string that identifies what "component" of the application this is. Included in the log line as a prefix and also used for formatting with verboseComponents * @param {argv} [argv] - The parsed yargs argv. Used to determine the current verbosity and if any components are included in verboseComponents. */ -function fatal(text, component, argv) { +function fatal(customConsole, text, component, argv) { log({ text, verbosity: 1, - stream: console.error, + stream: customConsole.error, component, formatter: chalk.redBright, argv, }); } -const logger = { - // use these for making dev, support tickets easier - debug, - info, - warn, - error, - fatal, +function buildLogger({ stderrStream, stdoutStream }) { + const customConsole = new Console({ + stderr: stderrStream, + stdout: stdoutStream, + }); + + return { + // use these for making dev, support tickets easier + debug: debug.bind(null, customConsole), + info: info.bind(null, customConsole), + warn: warn.bind(null, customConsole), + error: error.bind(null, customConsole), + fatal: fatal.bind(null, customConsole), - // use these for communicating with customers - stdout: console.log, - stderr: console.error, -}; + // use these for communicating with customers + stdout: customConsole.log, + stderr: customConsole.error, + }; +} -export default logger; +export default buildLogger; diff --git a/test/helpers.mjs b/test/helpers.mjs index e098bda2..b448746a 100644 --- a/test/helpers.mjs +++ b/test/helpers.mjs @@ -1,4 +1,7 @@ +//@ts-check + import { join } from "node:path"; +import { Writable } from "node:stream"; // small helper for sinon to wrap your return value // in the shape fetch would return it from the network @@ -19,7 +22,7 @@ export const commonFetchParams = { * from different sorting strategies. * * @param {string} path - the path to build a URL for - * @param {Record} paramObject - the params to include in the querystring + * @param {Record} [paramObject] - the params to include in the querystring */ export function buildUrl(path, paramObject) { const params = new URLSearchParams(paramObject); @@ -29,3 +32,63 @@ export function buildUrl(path, paramObject) { if (params.size) result = `${result}?${params}`; return `https://${result}`; } + +export function logStringBytes(str1, str2) { + let max = Math.max(str1.length, str2.length); + for (let i = 0; i < max; i++) { + const charCode1 = str1.charCodeAt(i); + const charCode2 = str2.charCodeAt(i); + // eslint-disable-next-line no-console + console.log(`Byte ${i}: ${charCode1}, ${charCode2}`); + } +} + +/** + * for use when non-printing characters cause test comparisons to fail. use like: + * + * logDifferentStringBytes( + * container.resolve("stdoutStream").getWritten(), + * `Type Ctrl+D or .exit to exit the shell${prompt}Database.all()\r${EOL}${stringifiedObj}${prompt}`, + * ); + */ +export function logDifferentStringBytes(str1, str2) { + let max = Math.max(str1.length, str2.length); + for (let i = 0; i < max; i++) { + const charCode1 = str1.charCodeAt(i); + const charCode2 = str2.charCodeAt(i); + if (charCode1 !== charCode2) + // eslint-disable-next-line no-console + console.log(`Byte ${i}: ${charCode1}, ${charCode2}`); + } +} + +/** + * Class representing a no-frills writable stream that, instead of writing data + * to a destination, holds it in memory. Can be queried for this data later; + * use it for testing interfaces that use streams. + */ +export class InMemoryWritableStream extends Writable { + /** + * Create an in-memory writable stream. + * @param {import('node:stream').WritableOptions} options + */ + constructor(options) { + super(options); + this.written = ""; + } + + _write(chunk, encoding, callback) { + this.written += chunk.toString("utf8"); + callback(); + } + + getWritten(clear = false) { + let written = this.written; + if (clear) this.clear(); + return written; + } + + clear() { + this.written = ""; + } +} diff --git a/test/shell.mjs b/test/shell.mjs new file mode 100644 index 00000000..25771388 --- /dev/null +++ b/test/shell.mjs @@ -0,0 +1,79 @@ +//@ts-check + +import { EOL } from "node:os"; +import util from "node:util"; + +import { expect } from "chai"; + +import { run } from "../src/cli.mjs"; +import { setupTestContainer as setupContainer } from "../src/config/setup-test-container.mjs"; + +describe("shell", function () { + let container; + let prompt = `${EOL}\x1B[1G\x1B[0J> \x1B[3G`; + + beforeEach(() => { + container = setupContainer(); + }); + + describe("common", function () { + it.skip('can output results in "json" format', async function () {}); + + it.skip('can output results in "json-tagged" format', async function () {}); + + it.skip('can output results in "shell" format', async function () {}); + + it.skip("can output results to a file", async function () {}); + + it.skip("can read input from stdin", async function () {}); + + it.skip("can read input from a file", async function () {}); + + it.skip("can set a connection timeout", async function () {}); + }); + + describe("v10", function () { + it("can open a shell and run a query", async function () { + container.resolve("performQuery").resolves({ + data: [ + { + name: "v4-test", + coll: "Database", + ts: 'Time("2024-07-16T19:16:15.980Z")', + global_id: "asd7zi8pharfn", + }, + ], + }); + + const stdin = container.resolve("stdinStream"); + const logger = container.resolve("logger"); + const runPromise = run(`shell --secret "secret" --no-color`, container); + + stdin.push(`Database.all()${EOL}`); + stdin.push(null); + + await runPromise; + const stringifiedObj = util.inspect({ + data: [ + { + name: "v4-test", + coll: "Database", + ts: 'Time("2024-07-16T19:16:15.980Z")', + global_id: "asd7zi8pharfn", + }, + ], + }); + + expect(container.resolve("stdoutStream").getWritten()).to.equal( + `Type Ctrl+D or .exit to exit the shell${prompt}Database.all()\r${EOL}${stringifiedObj}${prompt}`, + ); + expect(logger.stderr).to.not.be.called; + }); + + it.skip("can eval a query with typechecking enabled", async function () {}); + }); + + describe("v4", function () { + it.skip("can eval a query", async function () {}); + }); +}); From ac50ebb7ec98147e0b861b531a5095c04b553e98 Mon Sep 17 00:00:00 2001 From: Ashton Eby Date: Tue, 19 Nov 2024 09:59:26 -0800 Subject: [PATCH 04/11] fix tests --- src/commands/eval.mjs | 6 +++--- test/shell.mjs | 37 ++++++++++++++----------------------- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/src/commands/eval.mjs b/src/commands/eval.mjs index a55799d7..db7ff1e1 100644 --- a/src/commands/eval.mjs +++ b/src/commands/eval.mjs @@ -17,12 +17,12 @@ const { runQuery } = misc; /** * Write json encoded output * - * @param {String} file Target filename + * @param {String | undefined} file Target filename * @param {any} data Data to encode */ async function writeFormattedJson(file, data) { let str = JSON.stringify(data); - if (file === null) { + if (file === undefined) { return str; } else { // await writeFile(file, str); @@ -260,7 +260,7 @@ function buildEvalCommand(yargs) { output: { type: "string", description: "file to write output to", - default: null, + default: undefined, }, format: { type: "string", diff --git a/test/shell.mjs b/test/shell.mjs index 25771388..4c2488e2 100644 --- a/test/shell.mjs +++ b/test/shell.mjs @@ -1,13 +1,24 @@ //@ts-check import { EOL } from "node:os"; -import util from "node:util"; import { expect } from "chai"; import { run } from "../src/cli.mjs"; import { setupTestContainer as setupContainer } from "../src/config/setup-test-container.mjs"; +// this is defined up here so the indentation doesn't make it harder to use :( +const objectOne = `{ + data: [ + { + name: "v4-test", + coll: Database, + ts: Time("2024-07-16T19:16:15.980Z"), + global_id: "asd7zi8pharfn", + }, + ], +}`; + describe("shell", function () { let container; let prompt = `${EOL}\x1B[1G\x1B[0J> \x1B[3G`; @@ -34,16 +45,7 @@ describe("shell", function () { describe("v10", function () { it("can open a shell and run a query", async function () { - container.resolve("performQuery").resolves({ - data: [ - { - name: "v4-test", - coll: "Database", - ts: 'Time("2024-07-16T19:16:15.980Z")', - global_id: "asd7zi8pharfn", - }, - ], - }); + container.resolve("performQuery").resolves(objectOne); const stdin = container.resolve("stdinStream"); const logger = container.resolve("logger"); @@ -53,19 +55,8 @@ describe("shell", function () { stdin.push(null); await runPromise; - const stringifiedObj = util.inspect({ - data: [ - { - name: "v4-test", - coll: "Database", - ts: 'Time("2024-07-16T19:16:15.980Z")', - global_id: "asd7zi8pharfn", - }, - ], - }); - expect(container.resolve("stdoutStream").getWritten()).to.equal( - `Type Ctrl+D or .exit to exit the shell${prompt}Database.all()\r${EOL}${stringifiedObj}${prompt}`, + `Type Ctrl+D or .exit to exit the shell${prompt}Database.all()\r${EOL}${objectOne}${prompt}`, ); expect(logger.stderr).to.not.be.called; }); From cbd25695b99ba1a33662afb1c04ea28c442fface Mon Sep 17 00:00:00 2001 From: Ashton Eby Date: Tue, 19 Nov 2024 10:17:09 -0800 Subject: [PATCH 05/11] improve fn name --- src/commands/shell.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/commands/shell.mjs b/src/commands/shell.mjs index e4c621c0..adf2cfeb 100644 --- a/src/commands/shell.mjs +++ b/src/commands/shell.mjs @@ -25,7 +25,7 @@ async function doShell(argv) { completer: argv.version === "10" ? () => [] : undefined, output: container.resolve("stdoutStream"), input: container.resolve("stdinStream"), - eval: await customEval(argv), + eval: await buildCustomEval(argv), terminal: true, }; @@ -58,7 +58,8 @@ async function doShell(argv) { return completionPromise; } -async function customEval(argv) { +// caches the logger, client, and performQuery for subsequent shell calls +async function buildCustomEval(argv) { const logger = container.resolve("logger"); const client = await getSimpleClient(argv); const performQuery = container.resolve("performQuery"); From 7954da1a6508caa99160b9a9ba4513530637fb0b Mon Sep 17 00:00:00 2001 From: Ashton Eby Date: Tue, 19 Nov 2024 17:03:30 -0800 Subject: [PATCH 06/11] add v4 test and extend tests for 2+ shell commands --- src/commands/eval.mjs | 15 +++-- src/commands/shell.mjs | 9 +-- src/config/setup-container.mjs | 5 +- src/config/setup-test-container.mjs | 8 +-- test/eval.mjs | 2 +- test/helpers.mjs | 41 +++++++++--- test/shell.mjs | 99 +++++++++++++++++++++++++---- 7 files changed, 141 insertions(+), 38 deletions(-) diff --git a/src/commands/eval.mjs b/src/commands/eval.mjs index db7ff1e1..be45cdbb 100644 --- a/src/commands/eval.mjs +++ b/src/commands/eval.mjs @@ -58,7 +58,8 @@ async function writeFormattedOutput(file, data, format) { if (format === "json") { return writeFormattedJson(file, data); } else if (format === "shell") { - return writeFormattedShell(file, util.inspect(data, { depth: null })); + const fmtd = util.inspect(data, { depth: null, compact: false }); + return writeFormattedShell(file, fmtd); } else { throw new Error(`Unrecognized format ${format}.`); } @@ -125,7 +126,12 @@ export async function performV4Query(client, fqlQuery, outputFile, flags) { try { const response = await runQuery(fqlQuery, client); - return await writeFormattedOutput(outputFile, response, flags.format); + const formatted = await writeFormattedOutput( + outputFile, + response, + flags.format, + ); + return formatted; } catch (error) { if (error.faunaError === undefined) { // this happens when wrapQueries fails during the runInContext step @@ -160,6 +166,9 @@ export async function performV4Query(client, fqlQuery, outputFile, flags) { * @param {boolean} [flags.typecheck] - (Optional) Flag to enable typechecking */ export async function performQuery(client, fqlQuery, outputFile, flags) { + const performV4Query = container.resolve("performV4Query"); + const performV10Query = container.resolve("performV10Query"); + if (flags.version === "4") { return performV4Query(client, fqlQuery, outputFile, flags); } else { @@ -214,8 +223,6 @@ async function doEval(argv) { const format = argv.format ?? (process.stdout.isTTY ? "shell" : "json"); - const performQuery = container.resolve("performQuery"); - const result = await performQuery( client, queryFromFile || argv.query, diff --git a/src/commands/shell.mjs b/src/commands/shell.mjs index adf2cfeb..4b843778 100644 --- a/src/commands/shell.mjs +++ b/src/commands/shell.mjs @@ -6,8 +6,8 @@ import { container } from "../cli.mjs"; import { // ensureDbScopeClient, commonConfigurableQueryOptions, - getSimpleClient, } from "../lib/command-helpers.mjs"; +import { performQuery } from "./eval.mjs"; async function doShell(argv) { const logger = container.resolve("logger"); @@ -30,6 +30,7 @@ async function doShell(argv) { }; const shell = repl.start(replArgs); + shell.on("error", console.error); completionPromise = new Promise((resolve) => { shell.on("exit", resolve); @@ -60,12 +61,12 @@ async function doShell(argv) { // caches the logger, client, and performQuery for subsequent shell calls async function buildCustomEval(argv) { - const logger = container.resolve("logger"); - const client = await getSimpleClient(argv); - const performQuery = container.resolve("performQuery"); + const client = await container.resolve("getSimpleClient")(argv); return async (cmd, ctx, filename, cb) => { try { + const logger = container.resolve("logger"); + if (cmd.trim() === "") return cb(); let res; diff --git a/src/config/setup-container.mjs b/src/config/setup-container.mjs index 45eab57e..b8d8f511 100644 --- a/src/config/setup-container.mjs +++ b/src/config/setup-container.mjs @@ -11,7 +11,7 @@ import open from "open"; import updateNotifier from "update-notifier"; import { parseYargs } from "../cli.mjs"; -import { performQuery } from "../commands/eval.mjs"; +import { performV4Query, performV10Query } from "../commands/eval.mjs"; import { makeAccountRequest } from "../lib/account.mjs"; import OAuthClient from "../lib/auth/oauth-client.mjs"; import { getSimpleClient } from "../lib/command-helpers.mjs"; @@ -72,7 +72,8 @@ export const injectables = { // generic lib (homemade utilities) parseYargs: awilix.asValue(parseYargs), logger: awilix.asFunction(buildLogger), - performQuery: awilix.asValue(performQuery), + performV4Query: awilix.asValue(performV4Query), + performV10Query: awilix.asValue(performV10Query), getSimpleClient: awilix.asValue(getSimpleClient), accountClient: awilix.asClass(FaunaAccountClient, { lifetime: Lifetime.SCOPED, diff --git a/src/config/setup-test-container.mjs b/src/config/setup-test-container.mjs index 203df87e..289d3053 100644 --- a/src/config/setup-test-container.mjs +++ b/src/config/setup-test-container.mjs @@ -1,6 +1,6 @@ import fs from "node:fs"; import { normalize } from "node:path"; -import { Readable } from "node:stream"; +import { PassThrough } from "node:stream"; import * as awilix from "awilix"; import { spy, stub } from "sinon"; @@ -43,9 +43,9 @@ export function setupTestContainer() { const manualMocks = { // process specifics - stdinStream: awilix.asValue(Readable.from("")), - stdoutStream: awilix.asValue(new InMemoryWritableStream()), - stderrStream: awilix.asValue(new InMemoryWritableStream()), + stdinStream: awilix.asValue(new PassThrough()), + stdoutStream: awilix.asClass(InMemoryWritableStream).singleton(), + stderrStream: awilix.asClass(InMemoryWritableStream).singleton(), // wrap it in a spy so we can record calls, but use the // real implementation diff --git a/test/eval.mjs b/test/eval.mjs index 6f4f1585..d0a5c53b 100644 --- a/test/eval.mjs +++ b/test/eval.mjs @@ -31,7 +31,7 @@ describe("eval", function () { describe("v10", function () { it("can eval a query", async function () { const logger = container.resolve("logger"); - container.resolve("performQuery").resolves({ + container.resolve("performV10Query").resolves({ data: [ { name: "v4-test", diff --git a/test/helpers.mjs b/test/helpers.mjs index b448746a..4043b781 100644 --- a/test/helpers.mjs +++ b/test/helpers.mjs @@ -51,14 +51,20 @@ export function logStringBytes(str1, str2) { * `Type Ctrl+D or .exit to exit the shell${prompt}Database.all()\r${EOL}${stringifiedObj}${prompt}`, * ); */ -export function logDifferentStringBytes(str1, str2) { +export function logDifferentStringBytes(str1, str2, stringRepr = false) { let max = Math.max(str1.length, str2.length); for (let i = 0; i < max; i++) { const charCode1 = str1.charCodeAt(i); const charCode2 = str2.charCodeAt(i); - if (charCode1 !== charCode2) - // eslint-disable-next-line no-console - console.log(`Byte ${i}: ${charCode1}, ${charCode2}`); + if (charCode1 !== charCode2) { + if (stringRepr) { + // eslint-disable-next-line no-console + console.log(`Byte ${i}: ${str1[i]}, ${str2[i]}`); + } else { + // eslint-disable-next-line no-console + console.log(`Byte ${i}: ${charCode1}, ${charCode2}`); + } + } } } @@ -70,16 +76,33 @@ export function logDifferentStringBytes(str1, str2) { export class InMemoryWritableStream extends Writable { /** * Create an in-memory writable stream. - * @param {import('node:stream').WritableOptions} options */ - constructor(options) { - super(options); + constructor() { + super(); this.written = ""; } _write(chunk, encoding, callback) { - this.written += chunk.toString("utf8"); - callback(); + try { + this.written += chunk.toString("utf8"); + callback(); + } catch (e) { + callback(e); + } + } + + async waitForWritten() { + function recurse(cb) { + if (this.written.length > 0 && this.writableLength === 0) { + cb(); + } else { + setTimeout(recurse.bind(this, cb), 100); + } + } + + return new Promise((resolve) => { + recurse.bind(this, resolve)(); + }); } getWritten(clear = false) { diff --git a/test/shell.mjs b/test/shell.mjs index 4c2488e2..12690e6f 100644 --- a/test/shell.mjs +++ b/test/shell.mjs @@ -8,7 +8,7 @@ import { run } from "../src/cli.mjs"; import { setupTestContainer as setupContainer } from "../src/config/setup-test-container.mjs"; // this is defined up here so the indentation doesn't make it harder to use :( -const objectOne = `{ +const v10Object1 = `{ data: [ { name: "v4-test", @@ -19,12 +19,38 @@ const objectOne = `{ ], }`; +const v10Object2 = `{ + data: [ + { + name: "alpacas", + coll: Database, + ts: Time("2024-07-16T19:16:15.980Z"), + global_id: "msdmkl82h8rwo", + }, + ], +}`; + +const v4Object1 = `{ + data: [ + Database("v4-test") + ] +}`; + +const v4Object2 = `{ + data: [ + Database("alpacas") + ] +}`; + describe("shell", function () { - let container; - let prompt = `${EOL}\x1B[1G\x1B[0J> \x1B[3G`; + let container, stdin, stdout, logger; + let prompt = `${EOL}> `; beforeEach(() => { container = setupContainer(); + stdin = container.resolve("stdinStream"); + stdout = container.resolve("stdoutStream"); + logger = container.resolve("logger"); }); describe("common", function () { @@ -44,27 +70,72 @@ describe("shell", function () { }); describe("v10", function () { - it("can open a shell and run a query", async function () { - container.resolve("performQuery").resolves(objectOne); + it("can open a shell and run several queries", async function () { + container.resolve("performV10Query").resolves(v10Object1); - const stdin = container.resolve("stdinStream"); - const logger = container.resolve("logger"); - const runPromise = run(`shell --secret "secret" --no-color`, container); + // start the shell + const runPromise = run(`shell --secret "secret"`, container); - stdin.push(`Database.all()${EOL}`); - stdin.push(null); + // send our first command + stdin.push("Database.all().take(1)\n"); + await stdout.waitForWritten(); - await runPromise; - expect(container.resolve("stdoutStream").getWritten()).to.equal( - `Type Ctrl+D or .exit to exit the shell${prompt}Database.all()\r${EOL}${objectOne}${prompt}`, + // validate + expect(stdout.getWritten()).to.equal( + `Type Ctrl+D or .exit to exit the shell${prompt}${v10Object1}\n> `, ); expect(logger.stderr).to.not.be.called; + + // reset + stdout.clear(); + container.resolve("performV10Query").resolves(v10Object2); + + // send our second command + stdin.push(`Database.all().drop(1).take(1)`); + stdin.push(null); // terminate the shell + await stdout.waitForWritten(); + + // validate second object + expect(stdout.getWritten()).to.equal(`${v10Object2}${prompt}`); + expect(logger.stderr).to.not.be.called; + + return runPromise; }); it.skip("can eval a query with typechecking enabled", async function () {}); }); describe("v4", function () { - it.skip("can eval a query", async function () {}); + it("can open a shell and run a query", async function () { + container.resolve("performV4Query").resolves(v4Object1); + + // start the shell + const runPromise = run(`shell --secret "secret" --version 4`, container); + + // send our first command + stdin.push("Database.all().take(1)\n"); + await stdout.waitForWritten(); + + // validate + expect(stdout.getWritten()).to.equal( + `Type Ctrl+D or .exit to exit the shell${prompt}${v4Object1}\n> `, + ); + expect(logger.stderr).to.not.be.called; + + // reset + stdout.clear(); + container.resolve("performV4Query").resolves(v4Object2); + + // send our second command + stdin.push(`Database.all().drop(1).take(1)`); + stdin.push(null); // terminate the shell + await stdout.waitForWritten(); + + // validate second object + expect(stdout.getWritten()).to.equal(`${v4Object2}${prompt}`); + expect(logger.stderr).to.not.be.called; + + return runPromise; + }); }); }); From 7c87a025a86dfe3d2712a486d27c65e17db4b481 Mon Sep 17 00:00:00 2001 From: Ashton Eby Date: Wed, 20 Nov 2024 09:13:35 -0800 Subject: [PATCH 07/11] tweak tests --- test/shell.mjs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/test/shell.mjs b/test/shell.mjs index 12690e6f..38b0f244 100644 --- a/test/shell.mjs +++ b/test/shell.mjs @@ -44,7 +44,7 @@ const v4Object2 = `{ describe("shell", function () { let container, stdin, stdout, logger; - let prompt = `${EOL}> `; + let prompt = `${EOL}\x1B[1G\x1B[0J> \x1B[3G`; beforeEach(() => { container = setupContainer(); @@ -72,17 +72,18 @@ describe("shell", function () { describe("v10", function () { it("can open a shell and run several queries", async function () { container.resolve("performV10Query").resolves(v10Object1); + let query = "Database.all().take(1)"; // start the shell const runPromise = run(`shell --secret "secret"`, container); // send our first command - stdin.push("Database.all().take(1)\n"); + stdin.push(`${query}\n`); await stdout.waitForWritten(); // validate expect(stdout.getWritten()).to.equal( - `Type Ctrl+D or .exit to exit the shell${prompt}${v10Object1}\n> `, + `Type Ctrl+D or .exit to exit the shell${prompt}${query}\r\n${v10Object1}${prompt}`, ); expect(logger.stderr).to.not.be.called; @@ -91,12 +92,15 @@ describe("shell", function () { container.resolve("performV10Query").resolves(v10Object2); // send our second command - stdin.push(`Database.all().drop(1).take(1)`); + query = "Database.all().take(1)"; + stdin.push(`${query}\n`); stdin.push(null); // terminate the shell await stdout.waitForWritten(); // validate second object - expect(stdout.getWritten()).to.equal(`${v10Object2}${prompt}`); + expect(stdout.getWritten()).to.equal( + `${query}\r\n${v10Object2}${prompt}`, + ); expect(logger.stderr).to.not.be.called; return runPromise; @@ -108,17 +112,18 @@ describe("shell", function () { describe("v4", function () { it("can open a shell and run a query", async function () { container.resolve("performV4Query").resolves(v4Object1); + let query = "Select(0, Paginate(Databases()))"; // start the shell const runPromise = run(`shell --secret "secret" --version 4`, container); // send our first command - stdin.push("Database.all().take(1)\n"); + stdin.push(`${query}\n`); await stdout.waitForWritten(); // validate expect(stdout.getWritten()).to.equal( - `Type Ctrl+D or .exit to exit the shell${prompt}${v4Object1}\n> `, + `Type Ctrl+D or .exit to exit the shell${prompt}${query}\r\n${v4Object1}${prompt}`, ); expect(logger.stderr).to.not.be.called; @@ -127,12 +132,13 @@ describe("shell", function () { container.resolve("performV4Query").resolves(v4Object2); // send our second command - stdin.push(`Database.all().drop(1).take(1)`); + query = "Select(1, Paginate(Databases()))"; + stdin.push(`${query}\n`); stdin.push(null); // terminate the shell await stdout.waitForWritten(); // validate second object - expect(stdout.getWritten()).to.equal(`${v4Object2}${prompt}`); + expect(stdout.getWritten()).to.equal(`${query}\r\n${v4Object2}${prompt}`); expect(logger.stderr).to.not.be.called; return runPromise; From c0b0138e574ade6e07ebf685e7e3762ad9f54c29 Mon Sep 17 00:00:00 2001 From: Ashton Eby Date: Wed, 20 Nov 2024 09:45:50 -0800 Subject: [PATCH 08/11] add typecheck test --- src/commands/eval.mjs | 17 +++++++++-------- test/shell.mjs | 22 +++++++++++++++++++++- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/commands/eval.mjs b/src/commands/eval.mjs index be45cdbb..03f92ad0 100644 --- a/src/commands/eval.mjs +++ b/src/commands/eval.mjs @@ -160,19 +160,20 @@ export async function performV4Query(client, fqlQuery, outputFile, flags) { * @param {Object} client - An instance of the client used to execute the query. * @param {string} fqlQuery - The FQL v4 query to be executed. * @param {string | undefined} outputFile - Target filename - * @param {Object} flags - Options for the query execution. - * @param {("4" | "10")} flags.version - FQL version number - * @param {("json" | "json-tagged" | "shell")} flags.format - Result format - * @param {boolean} [flags.typecheck] - (Optional) Flag to enable typechecking + * + * @param {Object} argv - Options for the query execution. + * @param {("4" | "10")} argv.version - FQL version number + * @param {("json" | "json-tagged" | "shell")} argv.format - Result format + * @param {boolean} [argv.typecheck] - (Optional) Flag to enable typechecking */ -export async function performQuery(client, fqlQuery, outputFile, flags) { +export async function performQuery(client, fqlQuery, outputFile, argv) { const performV4Query = container.resolve("performV4Query"); const performV10Query = container.resolve("performV10Query"); - if (flags.version === "4") { - return performV4Query(client, fqlQuery, outputFile, flags); + if (argv.version === "4") { + return performV4Query(client, fqlQuery, outputFile, argv); } else { - return performV10Query(client, fqlQuery, outputFile, flags); + return performV10Query(client, fqlQuery, outputFile, argv); } } diff --git a/test/shell.mjs b/test/shell.mjs index 38b0f244..01d72234 100644 --- a/test/shell.mjs +++ b/test/shell.mjs @@ -3,6 +3,7 @@ import { EOL } from "node:os"; import { expect } from "chai"; +import sinon from "sinon"; import { run } from "../src/cli.mjs"; import { setupTestContainer as setupContainer } from "../src/config/setup-test-container.mjs"; @@ -106,7 +107,26 @@ describe("shell", function () { return runPromise; }); - it.skip("can eval a query with typechecking enabled", async function () {}); + it("can eval a query with typechecking enabled", async function () { + container.resolve("performV10Query").resolves(v10Object1); + let query = "Database.all().take(1)"; + + // start the shell + const runPromise = run(`shell --secret "secret" --typecheck`, container); + + // send one command + stdin.push(`${query}\n`); + stdin.push(null); + await stdout.waitForWritten(); + await runPromise; + + expect(container.resolve("performV10Query")).to.have.been.calledWith( + sinon.match.any, + sinon.match(query), + undefined, + sinon.match({ version: "10", typecheck: true }), + ); + }); }); describe("v4", function () { From ff73fec8428d49d873203aafab4381f80c6a5e12 Mon Sep 17 00:00:00 2001 From: Ashton Eby Date: Wed, 20 Nov 2024 09:56:19 -0800 Subject: [PATCH 09/11] add another test --- test/shell.mjs | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/test/shell.mjs b/test/shell.mjs index 01d72234..10519da0 100644 --- a/test/shell.mjs +++ b/test/shell.mjs @@ -55,12 +55,33 @@ describe("shell", function () { }); describe("common", function () { + it('outputs results in "shell" format by default', async function () { + container.resolve("performV10Query").resolves(v10Object1); + let query = "Database.all().take(1)"; + + // start the shell + const runPromise = run(`shell --secret "secret" --typecheck`, container); + + // send one command + stdin.push(`${query}\n`); + stdin.push(null); + await stdout.waitForWritten(); + await runPromise; + + expect(container.resolve("performV10Query")).to.have.been.calledWith( + sinon.match.any, + sinon.match(query), + undefined, + // the "shell" CLI format gets renamed by performV10Query to "decorated" + // before being sent to the API + sinon.match({ version: "10", format: "shell" }), + ); + }); + it.skip('can output results in "json" format', async function () {}); it.skip('can output results in "json-tagged" format', async function () {}); - it.skip('can output results in "shell" format', async function () {}); - it.skip("can output results to a file", async function () {}); it.skip("can read input from stdin", async function () {}); @@ -130,7 +151,7 @@ describe("shell", function () { }); describe("v4", function () { - it("can open a shell and run a query", async function () { + it("can open a shell and run several queries", async function () { container.resolve("performV4Query").resolves(v4Object1); let query = "Select(0, Paginate(Databases()))"; From 53a92e7f57820277b8a0b35a9e14ae13b00f7275 Mon Sep 17 00:00:00 2001 From: Ashton Eby Date: Wed, 20 Nov 2024 10:00:04 -0800 Subject: [PATCH 10/11] fix linting --- eslint.config.mjs | 1 + src/commands/shell.mjs | 1 + src/lib/logger.mjs | 2 -- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 58d67142..8f12798f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -8,6 +8,7 @@ export default [ languageOptions: { globals: { ...globals.mocha, + ...globals.nodeBuiltin, // cjs globals require: "readonly", diff --git a/src/commands/shell.mjs b/src/commands/shell.mjs index 4b843778..a08f7333 100644 --- a/src/commands/shell.mjs +++ b/src/commands/shell.mjs @@ -30,6 +30,7 @@ async function doShell(argv) { }; const shell = repl.start(replArgs); + // eslint-disable-next-line no-console shell.on("error", console.error); completionPromise = new Promise((resolve) => { diff --git a/src/lib/logger.mjs b/src/lib/logger.mjs index dfa20e95..67b0e7d3 100644 --- a/src/lib/logger.mjs +++ b/src/lib/logger.mjs @@ -1,5 +1,3 @@ -/* eslint-disable no-console */ - import chalk from "chalk"; import { Console } from "console"; import yargs from "yargs"; From 24dab069ae34c8c1b8119d5b0e004760e6ff0fc0 Mon Sep 17 00:00:00 2001 From: echo-bravo-yahoo Date: Wed, 20 Nov 2024 10:50:31 -0800 Subject: [PATCH 11/11] add github linting workflow step (#418) --- .github/workflows/lint.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..8be99273 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,30 @@ +# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs + +name: Lint + +on: + # temporarily "v3"; change to "main" after merge + push: + branches: ["v3"] + pull_request: + branches: ["v3"] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Lint (nodeJS 22.x) + uses: actions/setup-node@v4 + with: + node-version: 22.x + cache: "npm" + - run: npm ci + - run: npm run lint + env: + TERM: xterm-256color + # Set to the correct color level; 2 is 256 colors + # https://github.com/chalk/chalk?tab=readme-ov-file#supportscolor + FORCE_COLOR: 2