diff --git a/src/commands/query.mjs b/src/commands/query.mjs index 5dd7cbd9..2fda3def 100644 --- a/src/commands/query.mjs +++ b/src/commands/query.mjs @@ -92,6 +92,9 @@ async function queryCommand(argv) { performanceHints, color, include, + maxAttempts, + maxBackoff, + maxContentionRetries, } = argv; // resolve the input @@ -114,6 +117,9 @@ async function queryCommand(argv) { performanceHints, format: outputFormat, color: useColor, + maxAttempts, + maxBackoff, + maxContentionRetries, }); if (include.length > 0) { diff --git a/src/commands/shell.mjs b/src/commands/shell.mjs index 94ef2ca9..f941f9de 100644 --- a/src/commands/shell.mjs +++ b/src/commands/shell.mjs @@ -158,12 +158,23 @@ async function buildCustomEval(argv) { return async (cmd, ctx, _filename, cb) => { try { + if (cmd.trim() === "") return cb(); + const logger = container.resolve("logger"); - if (cmd.trim() === "") return cb(); + const secret = await getSecret(argv); // These are options used for querying and formatting the response - const { apiVersion, color } = argv; + const { + apiVersion, + color, + timeout, + typecheck, + url, + maxAttempts, + maxBackoff, + maxContentionRetries, + } = argv; const include = getArgvOrCtx("include", argv, ctx); const performanceHints = getArgvOrCtx("performanceHints", argv, ctx); @@ -180,9 +191,6 @@ async function buildCustomEval(argv) { let res; try { - const secret = await getSecret(argv); - const { color, timeout, typecheck, url } = argv; - res = await runQueryFromString(cmd, { apiVersion, secret, @@ -191,6 +199,9 @@ async function buildCustomEval(argv) { typecheck, performanceHints, format: outputFormat, + maxAttempts, + maxBackoff, + maxContentionRetries, }); // If any query info should be displayed, print to stderr. diff --git a/src/lib/fauna-client.mjs b/src/lib/fauna-client.mjs index 54d8d255..b8a844b6 100644 --- a/src/lib/fauna-client.mjs +++ b/src/lib/fauna-client.mjs @@ -53,18 +53,35 @@ export const runQueryFromString = (expression, argv) => { const faunaV10 = container.resolve("faunaClientV10"); if (argv.apiVersion === "4") { - const { secret, url, timeout } = argv; + const { secret, url, timeout, maxContentionRetries } = argv; + let headers; + if (maxContentionRetries) { + headers = { + "x-fauna-max-contention-retries": maxContentionRetries, + }; + } return retryInvalidCredsOnce(secret, (secret) => faunaV4.runQueryFromString({ expression, secret, url, client: undefined, - options: { queryTimeout: timeout }, + options: { queryTimeout: timeout, headers }, }), ); } else { - const { secret, url, timeout, format, performanceHints, ...rest } = argv; + const { + secret, + url, + timeout, + format, + performanceHints, + maxAttempts, + maxBackoff, + maxContentionRetries, + ...rest + } = argv; + let apiFormat = "decorated"; if (format === Format.JSON) { apiFormat = "simple"; @@ -80,6 +97,9 @@ export const runQueryFromString = (expression, argv) => { /* eslint-disable camelcase */ query_timeout_ms: timeout, performance_hints: performanceHints, + max_attempts: maxAttempts, + max_backoff: maxBackoff, + max_contention_retries: maxContentionRetries, /* eslint-enable camelcase */ format: apiFormat, ...rest, diff --git a/src/lib/options.mjs b/src/lib/options.mjs index da42b1b8..a96c38b1 100644 --- a/src/lib/options.mjs +++ b/src/lib/options.mjs @@ -133,11 +133,32 @@ export const QUERY_OPTIONS = { default: false, group: "API:", }, + "max-attempts": { + type: "number", + description: + "Maximum number of retry attempts when queries fail with throttling errors. Only applies to v10 queries.", + default: undefined, + group: "API:", + }, + "max-backoff": { + type: "number", + description: + "Maximum backoff time (in milliseconds) between retry attempts. Only applies to v10 queries.", + default: undefined, + group: "API:", + }, + "max-contention-retries": { + type: "number", + description: + "Maximum number of retry attempts when queries fail with contention errors.", + default: undefined, + group: "API:", + }, include: { type: "array", choices: ["all", "none", ...QUERY_INFO_CHOICES], default: ["summary"], - describe: + description: "Query response info to output. Pass values as a space-separated list. Ex: --include summary queryTags.", }, }; diff --git a/test/commands/query/v10.mjs b/test/commands/query/v10.mjs index 851cb3b6..3cdb961c 100644 --- a/test/commands/query/v10.mjs +++ b/test/commands/query/v10.mjs @@ -122,25 +122,21 @@ describe("query v10", function () { ); }); - it("can set the typecheck option to true", async function () { - await run(`query "Database.all()" --typecheck --secret=foo`, container); - expect(runQueryFromString).to.have.been.calledWith( - '"Database.all()"', - sinon.match({ - typecheck: true, - }), - ); - }); - - it("can set the performanceHints option to true", async function () { + it("can set various query options", async function () { await run( - `query "Database.all()" --performance-hints --secret=foo`, + `query "Database.all()" --secret=foo --typecheck --performance-hints --max-attempts 5 --max-backoff 2000 --timeout 10000 --max-contention-retries 3`, container, ); + expect(runQueryFromString).to.have.been.calledWith( - '"Database.all()"', + sinon.match(""), sinon.match({ + timeout: 10000, + typecheck: true, performanceHints: true, + maxAttempts: 5, + maxBackoff: 2000, + maxContentionRetries: 3, }), ); }); diff --git a/test/commands/query/v4.mjs b/test/commands/query/v4.mjs index beb887b0..7ed8aecd 100644 --- a/test/commands/query/v4.mjs +++ b/test/commands/query/v4.mjs @@ -103,6 +103,20 @@ describe("query v4", function () { ); }); + it("can set various query options", async function () { + await run( + `query "Collection('test')" --apiVersion 4 --secret=foo --timeout 10000 --max-contention-retries 3`, + container, + ); + expect(runQueryFromString).to.have.been.calledWith( + sinon.match(""), + sinon.match({ + timeout: 10000, + maxContentionRetries: 3, + }), + ); + }); + describe("query info", function () { it("displays metrics if `--include stats` is used", async function () { const testResponse = createV4QuerySuccess("test response"); diff --git a/test/commands/shell.mjs b/test/commands/shell.mjs index f82185bf..43371988 100644 --- a/test/commands/shell.mjs +++ b/test/commands/shell.mjs @@ -338,25 +338,33 @@ describe("shell", function () { it.skip("does not colorize output if --no-color is used", async function () {}); - it.skip("can eval a query with typechecking enabled", async function () { - container.resolve("performV10Query").resolves(v10Object1); + it("can open a shell and run queries with options", async function () { + runQueryFromString.resolves(v10Object1); let query = "Database.all().take(1)"; - // start the shell - const runPromise = run(`shell --secret "secret" --typecheck`, container); + const runPromise = run( + "shell --secret=foo --typecheck --performance-hints --max-attempts 5 --max-backoff 2000 --timeout 10000 --max-contention-retries 3", + container, + ); // send one command stdin.push(`${query}\n`); stdin.push(null); await stdout.waitForWritten(); - await runPromise; - expect(container.resolve("performV10Query")).to.have.been.calledWith( + expect(runQueryFromString).to.have.been.calledWith( sinon.match.any, - sinon.match(query), - undefined, - sinon.match({ version: "10", typecheck: true }), + sinon.match({ + timeout: 10000, + typecheck: true, + performanceHints: true, + maxAttempts: 5, + maxBackoff: 2000, + maxContentionRetries: 3, + }), ); + + return runPromise; }); it("can display performance hints", async function () {