From 550e070210b6b3476e9f1eaff2ec5b25c011b2b3 Mon Sep 17 00:00:00 2001 From: Ashton Eby Date: Mon, 21 Oct 2024 12:34:36 -0700 Subject: [PATCH 1/3] move to --no-input instead of --force --- src/commands/schema/abandon.mjs | 10 +++++----- src/commands/schema/commit.mjs | 10 +++++----- src/commands/schema/push.mjs | 12 ++++++------ test/schema/abandon.mjs | 4 ++-- test/schema/commit.mjs | 4 ++-- test/schema/push.mjs | 6 +++--- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/commands/schema/abandon.mjs b/src/commands/schema/abandon.mjs index 7926fc4c..ea254207 100644 --- a/src/commands/schema/abandon.mjs +++ b/src/commands/schema/abandon.mjs @@ -8,7 +8,7 @@ async function doAbandon(argv) { const logger = container.resolve("logger"); const confirm = container.resolve("confirm"); - if (argv.force) { + if (!argv.input) { const params = new URLSearchParams({ force: "true", // Just abandon, don't pass a schema version through. }); @@ -62,10 +62,10 @@ async function doAbandon(argv) { function buildAbandonCommand(yargs) { return yargs .options({ - force: { - description: "Push the change without a diff or schema version check", - type: "boolean", - default: false, + input: { + description: "Prompt for user input (e.g., confirmations)", + default: true, + type: "boolean" }, ...commonQueryOptions, }) diff --git a/src/commands/schema/commit.mjs b/src/commands/schema/commit.mjs index 603e965d..6e282c48 100644 --- a/src/commands/schema/commit.mjs +++ b/src/commands/schema/commit.mjs @@ -8,7 +8,7 @@ async function doCommit(argv) { const logger = container.resolve("logger"); const confirm = container.resolve("confirm"); - if (argv.force) { + if (!argv.input) { const params = new URLSearchParams({ force: "true", // Just commit, don't pass a schema version through. }); @@ -66,10 +66,10 @@ async function doCommit(argv) { function buildCommitCommand(yargs) { return yargs .options({ - force: { - description: "Push the change without a diff or schema version check", - type: "boolean", - default: false, + input: { + description: "Prompt for user input (e.g., confirmations)", + default: true, + type: "boolean" }, ...commonQueryOptions, }) diff --git a/src/commands/schema/push.mjs b/src/commands/schema/push.mjs index 4db34d94..b10b4487 100644 --- a/src/commands/schema/push.mjs +++ b/src/commands/schema/push.mjs @@ -11,9 +11,9 @@ async function doPush(argv) { const gatherFSL = container.resolve("gatherFSL"); const fsl = reformatFSL(await gatherFSL(argv.dir)); - if (argv.force) { + if (!argv.input) { const params = new URLSearchParams({ - force: argv.force, + force: "true", staged: argv.staged ? "true" : "false", }); @@ -77,10 +77,10 @@ async function doPush(argv) { function buildPushCommand(yargs) { return yargs .options({ - force: { - description: "Push the change without a diff or schema version check", - type: "boolean", - default: false, + input: { + description: "Prompt for user input (e.g., confirmations)", + default: true, + type: "boolean" }, staged: { description: diff --git a/test/schema/abandon.mjs b/test/schema/abandon.mjs index 3106d4b1..7734182f 100644 --- a/test/schema/abandon.mjs +++ b/test/schema/abandon.mjs @@ -21,7 +21,7 @@ describe("schema abandon", function () { confirm = container.resolve("confirm"); }); - it("can force abandon a staged schema change", async function () { + it("can abandon a staged schema change without user input", async function () { fetch.onCall(0).resolves( f({ version: 1728677726190000, @@ -32,7 +32,7 @@ describe("schema abandon", function () { }), ); - await run(`schema abandon --secret "secret" --force`, container); + await run(`schema abandon --secret "secret" --no-input`, container); expect(fetch).to.have.been.calledOnce; expect(fetch).to.have.been.calledWith( diff --git a/test/schema/commit.mjs b/test/schema/commit.mjs index 7189259b..8016f36a 100644 --- a/test/schema/commit.mjs +++ b/test/schema/commit.mjs @@ -54,10 +54,10 @@ describe("schema commit", function () { ); }); - it("can force commit a schema change", async function () { + it("can commit a schema change without user input", async function () { fetch.onCall(0).resolves(f({ version: 1728684456180000 })); - await run(`schema commit --secret "secret" --force`, container); + await run(`schema commit --secret "secret" --no-input`, container); expect(fetch).to.have.been.calledOnce; expect(fetch).to.have.been.calledWith( diff --git a/test/schema/push.mjs b/test/schema/push.mjs index 4a5c90f4..82558166 100644 --- a/test/schema/push.mjs +++ b/test/schema/push.mjs @@ -29,8 +29,8 @@ describe("schema push", function () { gatherFSL.resolves(fsl); }); - it("can force push schema", async function () { - await run(`schema push --secret "secret" --force`, container); + it("can push a schema without user input", async function () { + await run(`schema push --secret "secret" --no-input`, container); expect(gatherFSL).to.have.been.calledWith("."); @@ -167,7 +167,7 @@ describe("schema push", function () { it("can push schema from another directory", async function () { await run( - `schema push --secret "secret" --force --dir "/absolute/path/elsewhere"`, + `schema push --secret "secret" --no-input --dir "/absolute/path/elsewhere"`, container, ); From 61d335512804cdb188eec0a45825de76af028274 Mon Sep 17 00:00:00 2001 From: Ashton Eby Date: Mon, 21 Oct 2024 15:26:10 -0700 Subject: [PATCH 2/3] catch schema commands up to main branch this implements changes in the flags for all schema commands as well large changes in functionality for `schema diff` and `schema status`. --- src/commands/schema/abandon.mjs | 2 +- src/commands/schema/commit.mjs | 2 +- src/commands/schema/diff.mjs | 109 ++++++++++++-- src/commands/schema/pull.mjs | 31 ++-- src/commands/schema/push.mjs | 21 +-- src/commands/schema/status.mjs | 47 +++++- src/config/setup-container.mjs | 8 - src/lib/db.mjs | 5 +- src/lib/schema.mjs | 47 +----- test/general-cli.mjs | 7 + test/helpers.mjs | 19 +++ test/schema/diff.mjs | 57 ++++++-- test/schema/pull.mjs | 32 ++-- test/schema/push.mjs | 51 +++++-- test/schema/status.mjs | 251 +++++++++++++++++++++++++++++++- 15 files changed, 546 insertions(+), 143 deletions(-) diff --git a/src/commands/schema/abandon.mjs b/src/commands/schema/abandon.mjs index ea254207..f6e3ddac 100644 --- a/src/commands/schema/abandon.mjs +++ b/src/commands/schema/abandon.mjs @@ -65,7 +65,7 @@ function buildAbandonCommand(yargs) { input: { description: "Prompt for user input (e.g., confirmations)", default: true, - type: "boolean" + type: "boolean", }, ...commonQueryOptions, }) diff --git a/src/commands/schema/commit.mjs b/src/commands/schema/commit.mjs index 6e282c48..66d60e42 100644 --- a/src/commands/schema/commit.mjs +++ b/src/commands/schema/commit.mjs @@ -69,7 +69,7 @@ function buildCommitCommand(yargs) { input: { description: "Prompt for user input (e.g., confirmations)", default: true, - type: "boolean" + type: "boolean", }, ...commonQueryOptions, }) diff --git a/src/commands/schema/diff.mjs b/src/commands/schema/diff.mjs index 821e17d8..805cf264 100644 --- a/src/commands/schema/diff.mjs +++ b/src/commands/schema/diff.mjs @@ -6,33 +6,94 @@ import { container } from "../../cli.mjs"; import { commonQueryOptions } from "../../lib/command-helpers.mjs"; import { reformatFSL } from "../../lib/schema.mjs"; +/** + * @returns string[] + */ +function parseTarget(argv) { + if (!argv.active && !argv.staged) { + return ["staged", "local"]; + } + + if (argv.active && argv.staged) { + throw new Error("Cannot specify both --active and --staged"); + } + + if (argv.active) { + return ["active", "local"]; + } else if (argv.staged) { + return ["active", "staged"]; + } else { + throw new Error("Invalid target. Expected: active or staged"); + } +} + async function doDiff(argv) { + const [source, target] = parseTarget(argv); + const diffKind = argv.text ? "textual" : "semantic"; + const gatherFSL = container.resolve("gatherFSL"); const logger = container.resolve("logger"); const makeFaunaRequest = container.resolve("makeFaunaRequest"); const files = reformatFSL(await gatherFSL(argv.dir)); - const params = new URLSearchParams({ force: "true" }); + const params = new URLSearchParams({}); if (argv.color) params.set("color", "ansi"); - params.set("staged", argv.staged); + if (target === "staged") params.set("diff", diffKind); - const response = await makeFaunaRequest({ + const { version, status, diff } = await makeFaunaRequest({ baseUrl: argv.url, - path: new URL(`/schema/1/validate?${params}`, argv.url).href, + path: "/schema/1/staged/status", + params, secret: argv.secret, - body: files, - method: "POST", + method: "GET", }); - const bold = argv.color ? chalk.bold : (str) => str; - const description = argv.staged ? "remote, staged" : "remote, active"; - logger.stdout( - `Differences between the ${bold("local")} schema and the ${bold( - description, - )} schema:`, - ); - logger.stdout(response.diff ? response.diff : "No schema differences"); + if (target === "staged") { + logger.stdout( + `Differences from the ${chalk.bold("remote, active")} schema to the ${chalk.bold("remote, staged")} schema:`, + ); + if (status === "none") { + logger.stdout("There is no staged schema present."); + } else { + logger.stdout(diff ? diff : "No schema differences."); + } + } else { + const params = new URLSearchParams({ + diff: diffKind, + staged: String(source === "staged"), + }); + if (argv.color) params.set("color", "ansi"); + if (version) { + params.set("version", version); + } else { + params.set("force", "true"); + } + + const { diff } = await makeFaunaRequest({ + baseUrl: argv.url, + path: "/schema/1/validate", + params, + secret: argv.secret, + body: files, + method: "POST", + }); + + if (status === "none") { + logger.stdout( + `Differences from the ${chalk.bold("remote")} schema to the ${chalk.bold("local")} schema:`, + ); + } else if (source === "active") { + logger.stdout( + `Differences from the ${chalk.bold("remote, active")} schema to the ${chalk.bold("local")} schema:`, + ); + } else { + logger.stdout( + `Differences from the ${chalk.bold("remote, staged")} schema to the ${chalk.bold("local")} schema:`, + ); + } + logger.stdout(diff ? diff : "No schema differences."); + } } function buildDiffCommand(yargs) { @@ -40,13 +101,29 @@ function buildDiffCommand(yargs) { .options({ staged: { description: - "Compare the local schema to the staged schema instead of the active schema.", + "Show the diff between the active and staged schema, instead of the local schema.", + default: false, + type: "boolean", + }, + text: { + description: "Display the text diff instead of the semantic diff.", + default: false, + type: "boolean", + }, + active: { + description: + "Show the diff against the active schema instead of the staged schema.", default: false, type: "boolean", }, ...commonQueryOptions, }) - .example([["$0 schema diff"], ["$0 schema diff --dir schemas/myschema"]]) + .example([ + ["$0 schema diff"], + ["$0 schema diff --dir schemas/myschema"], + ["$0 schema diff --staged"], + ["$0 schema diff --active --text"], + ]) .version(false) .help("help", "show help"); } diff --git a/src/commands/schema/pull.mjs b/src/commands/schema/pull.mjs index db00987f..48886867 100644 --- a/src/commands/schema/pull.mjs +++ b/src/commands/schema/pull.mjs @@ -2,35 +2,38 @@ import { container } from "../../cli.mjs"; import { commonQueryOptions } from "../../lib/command-helpers.mjs"; +import { makeFaunaRequest } from "../../lib/db.mjs"; async function doPull(argv) { const logger = container.resolve("logger"); const gatherFSL = container.resolve("gatherFSL"); const confirm = container.resolve("confirm"); - const getSchemaFiles = container.resolve("getSchemaFiles"); - const getStagedSchemaStatus = container.resolve("getStagedSchemaStatus"); // fetch the list of remote FSL files - const filesResponse = await getSchemaFiles({ - secret: argv.secret, + const filesResponse = await makeFaunaRequest({ baseUrl: argv.url, + path: "/schema/1/files", + method: "GET", + secret: argv.secret, }); // check if there's a staged schema - const statusResponse = await getStagedSchemaStatus({ - params: { version: filesResponse.version }, + const statusResponse = await makeFaunaRequest({ baseUrl: argv.url, + path: "/schema/1/staged/status", + params: new URLSearchParams({ version: filesResponse.version }), + method: "GET", secret: argv.secret, }); - // if there's a staged schema, require the --staged flag. - // getting unstaged FSL while staged FSL exists is not yet + // if there's a staged schema, cannot use the --active flag. + // getting active FSL while staged FSL exists is not yet // implemented at the service level. - if (statusResponse.status !== "none" && !argv.staged) { + if (statusResponse.status !== "none" && argv.active) { throw new Error( - "There is a staged schema change. Use --staged to pull it.", + "There is a staged schema change. Remove the --active flag to pull it.", ); - } else if (statusResponse.status === "none" && argv.staged) { + } else if (statusResponse.status === "none" && !argv.active) { throw new Error("There are no staged schema changes to pull."); } @@ -117,8 +120,8 @@ function buildPullCommand(yargs) { type: "boolean", default: false, }, - staged: { - description: "Pulls staged schema instead of the active schema", + active: { + description: "Pulls the active schema instead of the staged schema", type: "boolean", default: false, }, @@ -126,7 +129,7 @@ function buildPullCommand(yargs) { }) .example([ ["$0 schema pull"], - ["$0 schema pull --staged"], + ["$0 schema pull --active"], ["$0 schema pull --delete"], ]) .version(false) diff --git a/src/commands/schema/push.mjs b/src/commands/schema/push.mjs index b10b4487..d76f4b87 100644 --- a/src/commands/schema/push.mjs +++ b/src/commands/schema/push.mjs @@ -14,12 +14,13 @@ async function doPush(argv) { if (!argv.input) { const params = new URLSearchParams({ force: "true", - staged: argv.staged ? "true" : "false", + staged: argv.active ? "false" : "true", }); await makeFaunaRequest({ baseUrl: argv.url, - path: `/schema/1/update?${params}`, + path: "/schema/1/update", + params, body: fsl, secret: argv.secret, method: "POST", @@ -29,13 +30,14 @@ async function doPush(argv) { // need to pass the last known schema version through. const params = new URLSearchParams({ force: "true", - staged: argv.staged ? "true" : "false", + staged: argv.active ? "false" : "true", }); if (argv.color) params.set("color", "ansi"); const response = await makeFaunaRequest({ baseUrl: argv.url, - path: `/schema/1/validate?${params}`, + path: "/schema/1/validate", + params, body: fsl, secret: argv.secret, method: "POST", @@ -58,12 +60,13 @@ async function doPush(argv) { if (confirmed) { const params = new URLSearchParams({ version: response.version, - staged: argv.staged ? "true" : "false", + staged: argv.active ? "false" : "true", }); await makeFaunaRequest({ baseUrl: argv.url, - path: `/schema/1/update?${params}`, + path: "/schema/1/update", + params, body: fsl, secret: argv.secret, method: "POST", @@ -80,9 +83,9 @@ function buildPushCommand(yargs) { input: { description: "Prompt for user input (e.g., confirmations)", default: true, - type: "boolean" + type: "boolean", }, - staged: { + active: { description: "Stages the schema change instead of applying it immediately", type: "boolean", @@ -93,7 +96,7 @@ function buildPushCommand(yargs) { .example([ ["$0 schema push"], ["$0 schema push --dir schemas/myschema"], - ["$0 schema push --staged"], + ["$0 schema push --active"], ]) .version(false) .help("help", "show help"); diff --git a/src/commands/schema/status.mjs b/src/commands/schema/status.mjs index b07a17ae..ad4c6f78 100644 --- a/src/commands/schema/status.mjs +++ b/src/commands/schema/status.mjs @@ -1,23 +1,62 @@ //@ts-check +import chalk from "chalk"; + import { container } from "../../cli.mjs"; import { commonQueryOptions } from "../../lib/command-helpers.mjs"; +import { reformatFSL } from "../../lib/schema.mjs"; async function doStatus(argv) { const logger = container.resolve("logger"); const makeFaunaRequest = container.resolve("makeFaunaRequest"); - const params = new URLSearchParams({ diff: "true" }); + let params = new URLSearchParams({ diff: "summary" }); if (argv.color) params.set("color", "ansi"); + const gatherFSL = container.resolve("gatherFSL"); + const fsl = reformatFSL(await gatherFSL(argv.dir)); - const response = await makeFaunaRequest({ + const statusResponse = await makeFaunaRequest({ baseUrl: argv.url, - path: `/schema/1/staged/status?${params}`, + path: "/schema/1/staged/status", + params, secret: argv.secret, method: "GET", }); - logger.stdout(response.diff); + params = new URLSearchParams({ + diff: "summary", + staged: "true", + version: statusResponse.version, + }); + const validationResponse = await makeFaunaRequest({ + baseUrl: argv.url, + path: "/schema/1/validate", + params, + secret: argv.secret, + method: "POST", + body: fsl, + }); + + logger.stdout(`Staged changes: ${chalk.bold(statusResponse.status)}`); + if (statusResponse.pending_summary !== "") { + logger.stdout(statusResponse.pending_summary); + } + if (statusResponse.diff) { + logger.stdout("Staged changes:\n"); + logger.stdout(statusResponse.diff.split("\n").join("\n ")); + } + + if (validationResponse.error) { + logger.stdout(`Local changes:`); + throw new Error(validationResponse.error.message); + } else if (validationResponse.diff === "") { + logger.stdout(`Local changes: ${chalk.bold("none")}\n`); + } else { + logger.stdout(`Local changes:\n`); + logger.stdout(` ${validationResponse.diff.split("\n").join("\n ")}`); + logger.stdout("(use `fauna schema diff` to display local changes)"); + logger.stdout("(use `fauna schema push` to stage local changes)"); + } } function buildStatusCommand(yargs) { diff --git a/src/config/setup-container.mjs b/src/config/setup-container.mjs index 2ddbe216..df64ec38 100644 --- a/src/config/setup-container.mjs +++ b/src/config/setup-container.mjs @@ -8,7 +8,6 @@ import { confirm } from "@inquirer/prompts"; import * as awilix from "awilix"; import { Lifetime } from "awilix"; import open from "open"; -import updateNotifier from "update-notifier"; import { parseYargs } from "../cli.mjs"; import { performQuery } from "../commands/eval.mjs"; @@ -24,9 +23,6 @@ import { gatherFSL, gatherRelativeFSLFilePaths, getAllSchemaFileContents, - getSchemaFile, - getSchemaFiles, - getStagedSchemaStatus, writeSchemaFiles, } from "../lib/schema.mjs"; @@ -64,7 +60,6 @@ export const injectables = { // third-party libraries confirm: awilix.asValue(confirm), open: awilix.asValue(open), - updateNotifier: awilix.asValue(updateNotifier), // generic lib (homemade utilities) parseYargs: awilix.asValue(parseYargs), @@ -83,11 +78,8 @@ export const injectables = { // feature-specific lib (homemade utilities) gatherFSL: awilix.asValue(gatherFSL), gatherRelativeFSLFilePaths: awilix.asValue(gatherRelativeFSLFilePaths), - getSchemaFile: awilix.asValue(getSchemaFile), - getSchemaFiles: awilix.asValue(getSchemaFiles), writeSchemaFiles: awilix.asValue(writeSchemaFiles), getAllSchemaFileContents: awilix.asValue(getAllSchemaFileContents), - getStagedSchemaStatus: awilix.asValue(getStagedSchemaStatus), deleteUnusedSchemaFiles: awilix.asValue(deleteUnusedSchemaFiles), }; diff --git a/src/lib/db.mjs b/src/lib/db.mjs index b2e11c25..62b8cf60 100644 --- a/src/lib/db.mjs +++ b/src/lib/db.mjs @@ -11,7 +11,7 @@ import { container } from "../cli.mjs"; * @property {string} secret - The secret to include in the AUTHORIZATION header of the request. * @property {string} baseUrl - The base URL from the scheme up through the top level domain and optional port; defaults to "https://db.fauna.com:443". * @property {string} path - The path part of the URL. Added to the baseUrl and params to build the full URL. - * @property {Record} [params] - The parameters (and their values) to set in the query string. + * @property {URLSearchParams} [params] - The parameters (and their values) to set in the query string. * @property {method} method - The HTTP method to use when making the request. * @property {object} [body] - The body to include in the request. * @property {boolean} [shouldThrow=true] - 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. @@ -30,7 +30,8 @@ export async function makeFaunaRequest({ shouldThrow = true, }) { const fetch = container.resolve("fetch"); - const paramsString = params ? `?${new URLSearchParams(params)}` : ""; + if (params && params.sort) params.sort(); + const paramsString = params && params.size ? `?${params.toString()}` : ""; let fullUrl; try { diff --git a/src/lib/schema.mjs b/src/lib/schema.mjs index f74afb41..37619820 100644 --- a/src/lib/schema.mjs +++ b/src/lib/schema.mjs @@ -175,7 +175,12 @@ export async function getAllSchemaFileContents(filenames, { ...overrides }) { const fileContentCollection = {}; for (const filename of filenames) { promises.push( - getSchemaFile(filename, overrides).then(({ content }) => { + makeFaunaRequest({ + baseUrl: overrides.baseUrl, + secret: overrides.secret, + path: `/schema/1/files/${encodeURIComponent(filename)}`, + method: "GET", + }).then(({ content }) => { fileContentCollection[filename] = content; }), ); @@ -185,43 +190,3 @@ export async function getAllSchemaFileContents(filenames, { ...overrides }) { return fileContentCollection; } - -/** - * @param {Omit} overrides - */ -export async function getSchemaFiles({ ...overrides }) { - /** @type {fetchParameters} */ - const args = { - ...overrides, - path: "/schema/1/files", - method: "GET", - }; - return makeFaunaRequest({ ...args }); -} - -/** - * @param {string} filename - * @param {Omit} overrides - */ -export async function getSchemaFile(filename, { ...overrides }) { - /** @type {fetchParameters} */ - const args = { - ...overrides, - path: `/schema/1/files/${encodeURIComponent(filename)}`, - method: "GET", - }; - return makeFaunaRequest({ ...args }); -} - -/** - * @param {Omit} overrides - */ -export async function getStagedSchemaStatus({ ...overrides }) { - /** @type {fetchParameters} */ - const args = { - ...overrides, - path: "/schema/1/staged/status", - method: "GET", - }; - return makeFaunaRequest({ ...args }); -} diff --git a/test/general-cli.mjs b/test/general-cli.mjs index 064aaea3..82b2b041 100644 --- a/test/general-cli.mjs +++ b/test/general-cli.mjs @@ -7,6 +7,7 @@ import { expect } from "chai"; import chalk from "chalk"; import { stub } from "sinon"; +import { f } from "../helpers.mjs"; import { builtYargs, run } from "../src/cli.mjs"; import { setupTestContainer as setupContainer } from "../src/config/setup-test-container.mjs"; @@ -83,6 +84,12 @@ describe("cli operations", function () { }); it("should check for updates when run", async function () { + fetch.onCall(0).resolves( + f({ + version: 0, + diff: "", + }), + ); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const packagePath = path.join(__dirname, "../package.json"); diff --git a/test/helpers.mjs b/test/helpers.mjs index eddb5472..e098bda2 100644 --- a/test/helpers.mjs +++ b/test/helpers.mjs @@ -1,3 +1,5 @@ +import { join } from "node:path"; + // small helper for sinon to wrap your return value // in the shape fetch would return it from the network export function f(returnValue) { @@ -10,3 +12,20 @@ export const commonFetchParams = { AUTHORIZATION: "Bearer secret", }, }; + +/** + * this method sorts the query parameters - since makeFaunaRequest does as well, + * this allows comparing the resulting string URLs without false negatives + * from different sorting strategies. + * + * @param {string} path - the path to build a URL for + * @param {Record} paramObject - the params to include in the querystring + */ +export function buildUrl(path, paramObject) { + const params = new URLSearchParams(paramObject); + params.sort(); + let result = "db.fauna.com"; + if (path) result = join(result, path); + if (params.size) result = `${result}?${params}`; + return `https://${result}`; +} diff --git a/test/schema/diff.mjs b/test/schema/diff.mjs index 7ad0f5fe..3474311f 100644 --- a/test/schema/diff.mjs +++ b/test/schema/diff.mjs @@ -3,7 +3,7 @@ import { expect } from "chai"; import { run } from "../../src/cli.mjs"; import { setupTestContainer as setupContainer } from "../../src/config/setup-test-container.mjs"; import { reformatFSL } from "../../src/lib/schema.mjs"; -import { commonFetchParams, f } from "../helpers.mjs"; +import { buildUrl, commonFetchParams, f } from "../helpers.mjs"; describe("schema diff", function () { const colorDiffString = @@ -28,7 +28,7 @@ describe("schema diff", function () { gatherFSL.resolves(fsl); }); - it("can display the diff between local and remote schema", async function () { + it("can display the diff between local and staged remote schema", async function () { fetch.resolves( f({ version: 0, @@ -39,14 +39,23 @@ describe("schema diff", function () { await run(`schema diff --secret "secret"`, container); expect(fetch).to.have.been.calledWith( - "https://db.fauna.com/schema/1/validate?force=true&color=ansi&staged=false", + buildUrl("/schema/1/staged/status", { color: "ansi" }), + { ...commonFetchParams, method: "GET" }, + ); + expect(fetch).to.have.been.calledWith( + buildUrl("/schema/1/validate", { + color: "ansi", + diff: "semantic", + force: "true", + staged: "true", + }), { ...commonFetchParams, method: "POST", body: reformatFSL(fsl) }, ); expect(logger.stdout).to.have.been.calledWith(colorDiffString); expect(logger.stderr).to.not.have.been.called; }); - it("can display the diff between local and staged remote schema", async function () { + it("can display the diff between local and active remote schema", async function () { fetch.resolves( f({ version: 0, @@ -54,10 +63,19 @@ describe("schema diff", function () { }), ); - await run(`schema diff --staged --secret "secret"`, container); + await run(`schema diff --active --secret "secret"`, container); expect(fetch).to.have.been.calledWith( - "https://db.fauna.com/schema/1/validate?force=true&color=ansi&staged=true", + buildUrl("/schema/1/staged/status", { color: "ansi" }), + { ...commonFetchParams, method: "GET" }, + ); + expect(fetch).to.have.been.calledWith( + buildUrl("/schema/1/validate", { + color: "ansi", + diff: "semantic", + force: "true", + staged: "false", + }), { ...commonFetchParams, method: "POST", body: reformatFSL(fsl) }, ); expect(logger.stdout).to.have.been.calledWith(colorDiffString); @@ -74,8 +92,16 @@ describe("schema diff", function () { await run(`schema diff --secret "secret" --no-color`, container); + expect(fetch).to.have.been.calledWith(buildUrl("/schema/1/staged/status"), { + ...commonFetchParams, + method: "GET", + }); expect(fetch).to.have.been.calledWith( - "https://db.fauna.com/schema/1/validate?force=true&staged=false", + buildUrl("/schema/1/validate", { + force: "true", + staged: "true", + diff: "semantic", + }), { ...commonFetchParams, method: "POST", body: reformatFSL(fsl) }, ); expect(logger.stdout).to.have.been.calledWith(noColorDiffString); @@ -93,10 +119,19 @@ describe("schema diff", function () { await run(`schema diff --secret "secret"`, container); expect(fetch).to.have.been.calledWith( - "https://db.fauna.com/schema/1/validate?force=true&color=ansi&staged=false", + buildUrl("/schema/1/staged/status", { color: "ansi" }), + { ...commonFetchParams, method: "GET" }, + ); + expect(fetch).to.have.been.calledWith( + buildUrl("/schema/1/validate", { + force: "true", + color: "ansi", + staged: "true", + diff: "semantic", + }), { ...commonFetchParams, method: "POST", body: reformatFSL(fsl) }, ); - expect(logger.stdout).to.have.been.calledWith("No schema differences"); + expect(logger.stdout).to.have.been.calledWith("No schema differences."); expect(logger.stderr).to.not.have.been.called; }); @@ -117,4 +152,8 @@ describe("schema diff", function () { expect(gatherFSL).to.have.been.calledWith("/Users/test-user"); }); + + it.skip("errors if user provides both --staged and --active flags"); + it.skip("works with the --staged flag"); + it.skip("uses the correct intro string 'from ... to ...'"); }); diff --git a/test/schema/pull.mjs b/test/schema/pull.mjs index 1519c3bc..5a1a93a3 100644 --- a/test/schema/pull.mjs +++ b/test/schema/pull.mjs @@ -8,12 +8,9 @@ import { setupTestContainer as setupContainer } from "../../src/config/setup-tes import { deleteUnusedSchemaFiles, getAllSchemaFileContents, - getSchemaFile, - getSchemaFiles, - getStagedSchemaStatus, writeSchemaFiles, } from "../../src/lib/schema.mjs"; -import { commonFetchParams, f } from "../helpers.mjs"; +import { buildUrl, commonFetchParams, f } from "../helpers.mjs"; describe("schema pull", function () { let container, logger, confirm, fetch, fs, fsp, gatherFSL; @@ -24,9 +21,6 @@ describe("schema pull", function () { // this is a funny situation - we actually want the "real" implementations of these. // they end up calling fetch and fs, which is what we'll verify against in the tests. container.register({ - getSchemaFile: awilix.asValue(sinon.spy(getSchemaFile)), - getSchemaFiles: awilix.asValue(sinon.spy(getSchemaFiles)), - getStagedSchemaStatus: awilix.asValue(sinon.spy(getStagedSchemaStatus)), getAllSchemaFileContents: awilix.asValue( sinon.spy(getAllSchemaFileContents), ), @@ -73,7 +67,7 @@ describe("schema pull", function () { fetch.onCall(1).resolves( f({ version: "194838274939473", - status: "none", + status: "ready", }), ); fetch.onCall(2).resolves( @@ -102,24 +96,24 @@ describe("schema pull", function () { expect(gatherFSL).to.have.been.calledWith("."); expect(fetch).to.have.been.calledWith( - "https://db.fauna.com/schema/1/files", + buildUrl("/schema/1/files"), commonFetchParams, ); // the version param in the URL is important - we use it for optimistic locking expect(fetch).to.have.been.calledWith( - "https://db.fauna.com/schema/1/staged/status?version=194838274939473", + buildUrl("/schema/1/staged/status", { version: "194838274939473" }), commonFetchParams, ); expect(fetch).to.have.been.calledWith( - "https://db.fauna.com/schema/1/files/main.fsl", + buildUrl("/schema/1/files/main.fsl"), commonFetchParams, ); expect(fetch).to.have.been.calledWith( - "https://db.fauna.com/schema/1/files/second.fsl", + buildUrl("/schema/1/files/second.fsl"), commonFetchParams, ); expect(fetch).to.have.been.calledWith( - "https://db.fauna.com/schema/1/files/third.fsl", + buildUrl("/schema/1/files/third.fsl"), commonFetchParams, ); @@ -179,7 +173,7 @@ describe("schema pull", function () { fetch.onCall(1).resolves( f({ version: "194838274939473", - status: "none", + status: "ready", }), ); @@ -221,7 +215,7 @@ describe("schema pull", function () { fetch.onCall(1).resolves( f({ version: "194838274939473", - status: "none", + status: "ready", }), ); fetch.onCall(2).resolves( @@ -255,7 +249,7 @@ describe("schema pull", function () { it.skip("does not modify the filesystem if it fails to read file contents", async function () {}); - it("requires the --staged flag if a schema change is staged", async function () { + it("errors if called with the --active flag while a schema change is staged", async function () { fetch.onCall(0).resolves( f({ version: "194838274939473", @@ -269,14 +263,16 @@ describe("schema pull", function () { fetch.onCall(1).resolves( f({ version: "194838274939473", - status: "staged", + status: "ready", }), ); const [error] = await tryToCatch(() => - run(`schema pull --secret "secret"`, container), + run(`schema pull --secret "secret" --active`, container), ); expect(error).to.have.property("code", 1); expect(container.resolve("gatherFSL")).to.not.have.been.called; }); + + it.skip("errors if there are no staged schema changes to pull"); }); diff --git a/test/schema/push.mjs b/test/schema/push.mjs index 82558166..f225c077 100644 --- a/test/schema/push.mjs +++ b/test/schema/push.mjs @@ -4,7 +4,7 @@ import sinon from "sinon"; import { run } from "../../src/cli.mjs"; import { setupTestContainer as setupContainer } from "../../src/config/setup-test-container.mjs"; import { reformatFSL } from "../../src/lib/schema.mjs"; -import { f } from "../helpers.mjs"; +import { buildUrl, f } from "../helpers.mjs"; describe("schema push", function () { const diffString = @@ -35,7 +35,7 @@ describe("schema push", function () { expect(gatherFSL).to.have.been.calledWith("."); expect(fetch).to.have.been.calledWith( - "https://db.fauna.com/schema/1/update?force=true&staged=false", + buildUrl("/schema/1/update", { force: "true", staged: "true" }), { method: "POST", headers: { AUTHORIZATION: "Bearer secret" }, @@ -47,7 +47,7 @@ describe("schema push", function () { expect(logger.stderr).to.not.be.called; }); - it("can push schema by version to active (default)", async function () { + it("can push schema by version to staged (default)", async function () { // user accepts the changes in the interactive prompt confirm.resolves(true); @@ -69,7 +69,11 @@ describe("schema push", function () { await run(`schema push --secret "secret"`, container); expect(fetch).to.have.been.calledWith( - "https://db.fauna.com/schema/1/validate?force=true&staged=false&color=ansi", + buildUrl("/schema/1/validate", { + force: "true", + staged: "true", + color: "ansi", + }), { method: "POST", headers: { AUTHORIZATION: "Bearer secret" }, @@ -78,7 +82,10 @@ describe("schema push", function () { ); expect(fetch).to.have.been.calledWith( - "https://db.fauna.com/schema/1/update?version=1728675598430000&staged=false", + buildUrl("/schema/1/update", { + version: "1728675598430000", + staged: "true", + }), { method: "POST", headers: { AUTHORIZATION: "Bearer secret" }, @@ -91,7 +98,7 @@ describe("schema push", function () { expect(logger.stdout).to.have.been.calledWith(diffString); }); - it("can push schema by version to staging", async function () { + it("can push schema by version to active", async function () { // user accepts the changes in the interactive prompt confirm.resolves(true); @@ -110,10 +117,14 @@ describe("schema push", function () { }), ); - await run(`schema push --secret "secret" --staged`, container); + await run(`schema push --secret "secret" --active`, container); expect(fetch).to.have.been.calledWith( - "https://db.fauna.com/schema/1/validate?force=true&staged=true&color=ansi", + buildUrl("/schema/1/validate", { + force: "true", + staged: "false", + color: "ansi", + }), { method: "POST", headers: { AUTHORIZATION: "Bearer secret" }, @@ -122,7 +133,10 @@ describe("schema push", function () { ); expect(fetch).to.have.been.calledWith( - "https://db.fauna.com/schema/1/update?version=1728675598430000&staged=true", + buildUrl("/schema/1/update", { + version: "1728675598430000", + staged: "false", + }), { method: "POST", headers: { AUTHORIZATION: "Bearer secret" }, @@ -150,7 +164,11 @@ describe("schema push", function () { await run(`schema push --secret "secret"`, container); expect(fetch).to.have.been.calledWith( - "https://db.fauna.com/schema/1/validate?force=true&staged=false&color=ansi", + buildUrl("/schema/1/validate", { + force: "true", + staged: "true", + color: "ansi", + }), { method: "POST", headers: { AUTHORIZATION: "Bearer secret" }, @@ -189,7 +207,11 @@ describe("schema push", function () { await run(`schema push --secret "secret"`, container); expect(fetch).to.have.been.calledWith( - "https://db.fauna.com/schema/1/validate?force=true&staged=false&color=ansi", + buildUrl("/schema/1/validate", { + force: "true", + staged: "true", + color: "ansi", + }), { method: "POST", headers: { AUTHORIZATION: "Bearer secret" }, @@ -198,7 +220,10 @@ describe("schema push", function () { ); expect(fetch).to.have.been.calledWith( - "https://db.fauna.com/schema/1/update?version=1728675598430000&staged=false", + buildUrl("/schema/1/update", { + version: "1728675598430000", + staged: "true", + }), { method: "POST", headers: { AUTHORIZATION: "Bearer secret" }, @@ -212,4 +237,6 @@ describe("schema push", function () { sinon.match.has("message", "Push file contents anyway?"), ); }); + + it.skip("correctly URI encodes file paths"); }); diff --git a/test/schema/status.mjs b/test/schema/status.mjs index 055317b7..9048f63a 100644 --- a/test/schema/status.mjs +++ b/test/schema/status.mjs @@ -1,35 +1,269 @@ +//@ts-check + import { expect } from "chai"; +import chalk from "chalk"; import { run } from "../../src/cli.mjs"; import { setupTestContainer as setupContainer } from "../../src/config/setup-test-container.mjs"; -import { commonFetchParams, f } from "../helpers.mjs"; +import { buildUrl, commonFetchParams, f } from "../helpers.mjs"; describe("schema status", function () { - let container; + let container, fetch, logger; + + let summaryDiff = + "\x1B[1;34m* Adding collection `NewCollection`\x1B[0m to collections.fsl:2:1\n" + + "\x1B[1;34m* Modifying collection `OrderItem`\x1B[0m at collections.fsl:125:1\n" + + "\x1B[1;34m* Modifying function `createOrUpdateCartItem`\x1B[0m at functions.fsl:2:1\n"; + + let textDiff = + "\x1B[1mcollections.fsl\x1B[22m\n" + + "\x1B[36m@ line 1 to 7\x1B[0m\n" + + "\n" + + "\x1B[32m+ collection NewCollection {\x1B[0m\n" + + "\x1B[32m+ }\x1B[0m\n" + + "\x1B[32m+\x1B[0m\n" + + " collection Customer {\n" + + " name: String\n" + + " email: String\n" + + "\x1B[36m@ line 134 to 139\x1B[0m\n" + + " terms [.order]\n" + + " values [.product, .quantity]\n" + + " }\n" + + "\x1B[31m-\x1B[0m\n" + + "\x1B[31m- index byOrderAndProduct {\x1B[0m\n" + + "\x1B[31m- terms [.order, .product]\x1B[0m\n" + + "\x1B[31m- }\x1B[0m\n" + + " }\n" + + "\n" + + "\n" + + "\n" + + "\x1B[1mfunctions.fsl\x1B[22m\n" + + "\x1B[36m@ line 30 to 35\x1B[0m\n" + + " if (product!.stock < quantity) {\n" + + ' abort("Product does not have the requested quantity in stock.")\n' + + " }\n" + + "\x1B[31m-\x1B[0m\n" + + "\x1B[31m- // Attempt to find an existing order item for the order, product pair.\x1B[0m\n" + + "\x1B[31m- // There is a unique constraint on [.order, .product] so this will return at most one result.\x1B[0m\n" + + "\x1B[31m- let orderItem = OrderItem.byOrderAndProduct(customer!.cart, product).first()\x1B[0m\n" + + "\x1B[31m-\x1B[0m\n" + + "\x1B[31m- if (orderItem == null) {\x1B[0m\n" + + "\x1B[31m- // If the order item does not exist, create a new one.\x1B[0m\n" + + "\x1B[31m- OrderItem.create({\x1B[0m\n" + + "\x1B[31m- order: Order(customer!.cart!.id),\x1B[0m\n" + + "\x1B[31m- product: product,\x1B[0m\n" + + "\x1B[31m- quantity: quantity,\x1B[0m\n" + + "\x1B[31m- })\x1B[0m\n" + + "\x1B[31m- } else {\x1B[0m\n" + + "\x1B[31m- // If the order item exists, update the quantity.\x1B[0m\n" + + "\x1B[31m- orderItem!.update({ quantity: quantity })\x1B[0m\n" + + "\x1B[31m- }\x1B[0m\n" + + " }\n" + + "\n" + + " function getOrCreateCart(id) {\n"; beforeEach(() => { container = setupContainer(); + fetch = container.resolve("fetch"); + logger = container.resolve("logger"); }); - it("fetches the current status", async function () { - const fetch = container.resolve("fetch"); - fetch.resolves( + it("fetches the current status when there are no changes", async function () { + fetch.onCall(0).resolves( f({ version: 0, status: "none", diff: "Staged schema: none", + pending_summary: "", + text_diff: "", + }), + ); + fetch.onCall(1).resolves( + f({ + version: 0, + diff: "", }), ); + await run(`schema status --secret "secret"`, container); expect(fetch).to.have.been.calledWith( - "https://db.fauna.com/schema/1/staged/status?diff=true&color=ansi", + buildUrl("/schema/1/staged/status", { diff: "summary", color: "ansi" }), commonFetchParams, ); + expect(fetch).to.have.been.calledWith( + buildUrl("/schema/1/validate", { + diff: "summary", + staged: "true", + version: "0", + }), + { ...commonFetchParams, method: "POST", body: new FormData() }, + ); + expect(logger.stdout).to.have.been.calledWith( + `Staged changes: ${chalk.bold("none")}`, + ); + expect(logger.stdout).to.have.been.calledWith( + `Local changes: ${chalk.bold("none")}\n`, + ); + }); + + it("fetches the current status when there are only local changes", async function () { + fetch.onCall(0).resolves( + f({ + version: 0, + status: "none", + diff: "Staged schema: none", + pending_summary: "", + text_diff: "", + }), + ); + fetch.onCall(1).resolves( + f({ + version: 0, + diff: + "* Adding collection `NewCollection` to collections.fsl:2:1\n" + + "* Modifying collection `OrderItem` at collections.fsl:125:1\n" + + "* Modifying function `createOrUpdateCartItem` at functions.fsl:2:1\n", + }), + ); + + await run(`schema status --secret "secret"`, container); + + expect(fetch).to.have.been.calledWith( + buildUrl("/schema/1/staged/status", { diff: "summary", color: "ansi" }), + commonFetchParams, + ); + expect(fetch).to.have.been.calledWith( + buildUrl("/schema/1/validate", { + diff: "summary", + staged: "true", + version: "0", + }), + { ...commonFetchParams, method: "POST", body: new FormData() }, + ); + expect(logger.stdout).to.have.been.calledWith( + `Staged changes: ${chalk.bold("none")}`, + ); + expect(logger.stdout).to.have.been.calledWith(`Local changes:\n`); + expect(logger.stdout).to.have.been.calledWith( + ` * Adding collection \`NewCollection\` to collections.fsl:2:1\n * Modifying collection \`OrderItem\` at collections.fsl:125:1\n * Modifying function \`createOrUpdateCartItem\` at functions.fsl:2:1\n `, + ); + expect(logger.stdout).to.have.been.calledWith( + "(use `fauna schema diff` to display local changes)", + ); + expect(logger.stdout).to.have.been.calledWith( + "(use `fauna schema push` to stage local changes)", + ); + expect(logger.stderr).not.to.have.been.called; + }); + + it("fetches the current status when there are only staged changes", async function () { + fetch.onCall(0).resolves( + f({ + version: 0, + status: "ready", + diff: summaryDiff, + pending_summary: "", + text_diff: textDiff, + }), + ); + fetch.onCall(1).resolves( + f({ + version: 0, + diff: "", + }), + ); + + await run(`schema status --secret "secret"`, container); + + expect(fetch).to.have.been.calledWith( + buildUrl("/schema/1/staged/status", { diff: "summary", color: "ansi" }), + commonFetchParams, + ); + + expect(fetch).to.have.been.calledWith( + buildUrl("/schema/1/validate", { + diff: "summary", + staged: "true", + version: "0", + }), + { ...commonFetchParams, method: "POST", body: new FormData() }, + ); + expect(fetch).to.have.been.calledWith( + buildUrl("/schema/1/validate", { + diff: "summary", + staged: "true", + version: "0", + }), + { ...commonFetchParams, method: "POST", body: new FormData() }, + ); + expect(logger.stdout).to.have.been.calledWith( + `Staged changes: ${chalk.bold("ready")}`, + ); + expect(logger.stdout).to.have.been.calledWith( + `Local changes: ${chalk.bold("none")}\n`, + ); + expect(logger.stdout).to.have.been.calledWith( + summaryDiff.split("\n").join("\n "), + ); + expect(logger.stderr).not.to.have.been.called; + }); + + it("fetches the current status when there are both local and staged changes", async function () { + fetch.onCall(0).resolves( + f({ + version: 0, + status: "ready", + diff: summaryDiff, + pending_summary: "", + text_diff: "", + }), + ); + fetch.onCall(1).resolves( + f({ + version: 0, + diff: + "* Adding function `newFunction` to functions.fsl:1:1\n" + + "* Modifying function `createOrUpdateCartItem` at functions.fsl:5:1\n", + }), + ); + + await run(`schema status --secret "secret"`, container); + + expect(fetch).to.have.been.calledWith( + buildUrl("/schema/1/staged/status", { diff: "summary", color: "ansi" }), + commonFetchParams, + ); + expect(fetch).to.have.been.calledWith( + buildUrl("/schema/1/validate", { + diff: "summary", + staged: "true", + version: "0", + }), + { ...commonFetchParams, method: "POST", body: new FormData() }, + ); + expect(logger.stdout).to.have.been.calledWith( + `Staged changes: ${chalk.bold("ready")}`, + ); + expect(logger.stdout).to.have.been.calledWith(`Staged changes:\n`); + expect(logger.stdout).to.have.been.calledWith(`Local changes:\n`); + expect(logger.stdout).to.have.been.calledWith( + summaryDiff.split("\n").join("\n "), + ); + expect(logger.stdout).to.have.been.calledWith( + " * Adding function `newFunction` to functions.fsl:1:1\n" + + " * Modifying function `createOrUpdateCartItem` at functions.fsl:5:1\n ", + ); + expect(logger.stdout).to.have.been.calledWith( + "(use `fauna schema diff` to display local changes)", + ); + expect(logger.stdout).to.have.been.calledWith( + "(use `fauna schema push` to stage local changes)", + ); + expect(logger.stderr).not.to.have.been.called; }); it("can fetch status without embedded colors (terminal escape codes)", async function () { - const fetch = container.resolve("fetch"); fetch.resolves( f({ version: 0, @@ -40,8 +274,9 @@ describe("schema status", function () { await run(`schema status --no-color --secret "secret"`, container); expect(fetch).to.have.been.calledWith( - "https://db.fauna.com/schema/1/staged/status?diff=true", + buildUrl("/schema/1/staged/status", { diff: "summary" }), commonFetchParams, ); + expect(logger.stderr).not.to.have.been.called; }); }); From 5a8f4c6402a43ee7c6a2fb00ce5c925a38b37006 Mon Sep 17 00:00:00 2001 From: Ashton Eby Date: Tue, 29 Oct 2024 12:18:17 -0700 Subject: [PATCH 3/3] address PR feedback - support colors on all calls to /validate and /status - fix schema push --active flag description - fix schema status confirmation prompts --- src/commands/schema/abandon.mjs | 9 ++---- src/commands/schema/commit.mjs | 9 ++---- src/commands/schema/diff.mjs | 6 ++-- src/commands/schema/pull.mjs | 11 ++----- src/commands/schema/push.mjs | 21 +++++++------ src/commands/schema/status.mjs | 7 ++--- src/lib/db.mjs | 17 +++++----- src/lib/schema.mjs | 7 ++--- test/general-cli.mjs | 12 ++++++- test/schema/pull.mjs | 5 ++- test/schema/push.mjs | 56 +++++++++++++++++++++++++++++++-- test/schema/status.mjs | 12 +++---- 12 files changed, 111 insertions(+), 61 deletions(-) diff --git a/src/commands/schema/abandon.mjs b/src/commands/schema/abandon.mjs index f6e3ddac..52df7175 100644 --- a/src/commands/schema/abandon.mjs +++ b/src/commands/schema/abandon.mjs @@ -14,9 +14,8 @@ async function doAbandon(argv) { }); await makeFaunaRequest({ - baseUrl: argv.url, + argv, path: `/schema/1/staged/abandon?${params}`, - secret: argv.secret, method: "POST", }); logger.stdout("Schema has been abandoned"); @@ -26,9 +25,8 @@ async function doAbandon(argv) { if (argv.color) params.set("color", "ansi"); const response = await makeFaunaRequest({ - baseUrl: argv.url, + argv, path: `/schema/1/staged/status?${params}`, - secret: argv.secret, method: "GET", }); @@ -46,9 +44,8 @@ async function doAbandon(argv) { const params = new URLSearchParams({ version: response.version }); await makeFaunaRequest({ - baseUrl: argv.url, + argv, path: `/schema/1/staged/abandon?${params}`, - secret: argv.secret, method: "POST", }); diff --git a/src/commands/schema/commit.mjs b/src/commands/schema/commit.mjs index 66d60e42..ae68b0c0 100644 --- a/src/commands/schema/commit.mjs +++ b/src/commands/schema/commit.mjs @@ -14,9 +14,8 @@ async function doCommit(argv) { }); await makeFaunaRequest({ - baseUrl: argv.url, + argv, path: `/schema/1/staged/commit?${params}`, - secret: argv.secret, method: "POST", }); @@ -27,9 +26,8 @@ async function doCommit(argv) { if (argv.color) params.set("color", "ansi"); const response = await makeFaunaRequest({ - baseUrl: argv.url, + argv, path: `/schema/1/staged/status?${params}`, - secret: argv.secret, method: "GET", }); @@ -50,9 +48,8 @@ async function doCommit(argv) { const params = new URLSearchParams({ version: response.version }); await makeFaunaRequest({ - baseUrl: argv.url, + argv, path: `/schema/1/staged/commit?${params}`, - secret: argv.secret, method: "POST", }); diff --git a/src/commands/schema/diff.mjs b/src/commands/schema/diff.mjs index 805cf264..534ea7ba 100644 --- a/src/commands/schema/diff.mjs +++ b/src/commands/schema/diff.mjs @@ -42,10 +42,9 @@ async function doDiff(argv) { if (target === "staged") params.set("diff", diffKind); const { version, status, diff } = await makeFaunaRequest({ - baseUrl: argv.url, + argv, path: "/schema/1/staged/status", params, - secret: argv.secret, method: "GET", }); @@ -71,10 +70,9 @@ async function doDiff(argv) { } const { diff } = await makeFaunaRequest({ - baseUrl: argv.url, + argv, path: "/schema/1/validate", params, - secret: argv.secret, body: files, method: "POST", }); diff --git a/src/commands/schema/pull.mjs b/src/commands/schema/pull.mjs index 48886867..cec54704 100644 --- a/src/commands/schema/pull.mjs +++ b/src/commands/schema/pull.mjs @@ -11,19 +11,17 @@ async function doPull(argv) { // fetch the list of remote FSL files const filesResponse = await makeFaunaRequest({ - baseUrl: argv.url, + argv, path: "/schema/1/files", method: "GET", - secret: argv.secret, }); // check if there's a staged schema const statusResponse = await makeFaunaRequest({ - baseUrl: argv.url, + argv, path: "/schema/1/staged/status", params: new URLSearchParams({ version: filesResponse.version }), method: "GET", - secret: argv.secret, }); // if there's a staged schema, cannot use the --active flag. @@ -88,10 +86,7 @@ async function doPull(argv) { const getAllSchemaFileContents = container.resolve( "getAllSchemaFileContents", ); - const contents = await getAllSchemaFileContents(filenames, { - secret: argv.secret, - baseUrl: argv.url, - }); + const contents = await getAllSchemaFileContents(filenames, argv); // don't start writing or deleting files until we've successfully fetched all // the remote schema files diff --git a/src/commands/schema/push.mjs b/src/commands/schema/push.mjs index d76f4b87..9445e3a4 100644 --- a/src/commands/schema/push.mjs +++ b/src/commands/schema/push.mjs @@ -11,6 +11,8 @@ async function doPush(argv) { const gatherFSL = container.resolve("gatherFSL"); const fsl = reformatFSL(await gatherFSL(argv.dir)); + const isStagedPush = !argv.active; + if (!argv.input) { const params = new URLSearchParams({ force: "true", @@ -18,11 +20,10 @@ async function doPush(argv) { }); await makeFaunaRequest({ - baseUrl: argv.url, + argv, path: "/schema/1/update", params, body: fsl, - secret: argv.secret, method: "POST", }); } else { @@ -35,21 +36,24 @@ async function doPush(argv) { if (argv.color) params.set("color", "ansi"); const response = await makeFaunaRequest({ - baseUrl: argv.url, + argv, path: "/schema/1/validate", params, body: fsl, - secret: argv.secret, method: "POST", }); - let message = "Accept and push changes?"; + let message = isStagedPush + ? "Stage the above changes?" + : "Push the above changes?"; if (response.diff) { logger.stdout(`Proposed diff:\n`); logger.stdout(response.diff); } else { logger.stdout("No logical changes."); - message = "Push file contents anyway?"; + message = isStagedPush + ? "Stage the file contents anyway?" + : "Push the file contents anyway?"; } const confirm = container.resolve("confirm"); const confirmed = await confirm({ @@ -64,11 +68,10 @@ async function doPush(argv) { }); await makeFaunaRequest({ - baseUrl: argv.url, + argv, path: "/schema/1/update", params, body: fsl, - secret: argv.secret, method: "POST", }); } else { @@ -87,7 +90,7 @@ function buildPushCommand(yargs) { }, active: { description: - "Stages the schema change instead of applying it immediately", + "Immediately applies the schema change instead of staging it", type: "boolean", default: false, }, diff --git a/src/commands/schema/status.mjs b/src/commands/schema/status.mjs index ad4c6f78..7211c7c6 100644 --- a/src/commands/schema/status.mjs +++ b/src/commands/schema/status.mjs @@ -16,10 +16,9 @@ async function doStatus(argv) { const fsl = reformatFSL(await gatherFSL(argv.dir)); const statusResponse = await makeFaunaRequest({ - baseUrl: argv.url, + argv, path: "/schema/1/staged/status", params, - secret: argv.secret, method: "GET", }); @@ -28,11 +27,11 @@ async function doStatus(argv) { staged: "true", version: statusResponse.version, }); + if (argv.color) params.set("color", "ansi"); const validationResponse = await makeFaunaRequest({ - baseUrl: argv.url, + argv, path: "/schema/1/validate", params, - secret: argv.secret, method: "POST", body: fsl, }); diff --git a/src/lib/db.mjs b/src/lib/db.mjs index 62b8cf60..75d30ae6 100644 --- a/src/lib/db.mjs +++ b/src/lib/db.mjs @@ -8,10 +8,9 @@ import { container } from "../cli.mjs"; /** * @typedef {Object} fetchParameters - * @property {string} secret - The secret to include in the AUTHORIZATION header of the request. - * @property {string} baseUrl - The base URL from the scheme up through the top level domain and optional port; defaults to "https://db.fauna.com:443". + * @property {Object} argv - The parsed argv from yargs; used to find the base url (`argv.url`), secret (`argv.secret`), and color support (`argv.color`). To overwrite, provided a modified argv to `makeFaunaRequest`. * @property {string} path - The path part of the URL. Added to the baseUrl and params to build the full URL. - * @property {URLSearchParams} [params] - The parameters (and their values) to set in the query string. + * @property {URLSearchParams|undefined} [params] - The parameters (and their values) to set in the query string. * @property {method} method - The HTTP method to use when making the request. * @property {object} [body] - The body to include in the request. * @property {boolean} [shouldThrow=true] - 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. @@ -21,8 +20,7 @@ import { container } from "../cli.mjs"; * @param {fetchParameters} args */ export async function makeFaunaRequest({ - secret, - baseUrl, + argv, path, params = undefined, method, @@ -30,14 +28,17 @@ export async function makeFaunaRequest({ shouldThrow = true, }) { const fetch = container.resolve("fetch"); + const routesWithColor = ["/schema/1/staged/status", "/schema/1/validate"]; + if (params && argv.color && routesWithColor.includes(path)) + params.set("color", "ansi"); if (params && params.sort) params.sort(); const paramsString = params && params.size ? `?${params.toString()}` : ""; let fullUrl; try { - fullUrl = new URL(`${path}${paramsString}`, baseUrl).href; + fullUrl = new URL(`${path}${paramsString}`, argv.url).href; } catch (e) { - e.message = `Could not build valid URL out of base url (${baseUrl}), path (${path}), and params string (${paramsString}) built from params (${JSON.stringify( + e.message = `Could not build valid URL out of base url (${argv.url}), path (${path}), and params string (${paramsString}) built from params (${JSON.stringify( params, )}).`; throw e; @@ -45,7 +46,7 @@ export async function makeFaunaRequest({ const fetchArgs = { method, - headers: { AUTHORIZATION: `Bearer ${secret}` }, + headers: { AUTHORIZATION: `Bearer ${argv.secret}` }, }; if (body) fetchArgs.body = body; diff --git a/src/lib/schema.mjs b/src/lib/schema.mjs index 37619820..87b719c4 100644 --- a/src/lib/schema.mjs +++ b/src/lib/schema.mjs @@ -166,18 +166,17 @@ export async function writeSchemaFiles(dir, filenameToContentsDict) { /** * @param {string[]} filenames - A list of schema file names to fetch - * @param {Omit} overrides + * @param {object} argv * @returns {Promise>} A map of schema file names to their contents. */ -export async function getAllSchemaFileContents(filenames, { ...overrides }) { +export async function getAllSchemaFileContents(filenames, argv) { const promises = []; /** @type Record */ const fileContentCollection = {}; for (const filename of filenames) { promises.push( makeFaunaRequest({ - baseUrl: overrides.baseUrl, - secret: overrides.secret, + argv, path: `/schema/1/files/${encodeURIComponent(filename)}`, method: "GET", }).then(({ content }) => { diff --git a/test/general-cli.mjs b/test/general-cli.mjs index 82b2b041..363f3b86 100644 --- a/test/general-cli.mjs +++ b/test/general-cli.mjs @@ -7,9 +7,9 @@ import { expect } from "chai"; import chalk from "chalk"; import { stub } from "sinon"; -import { f } from "../helpers.mjs"; import { builtYargs, run } from "../src/cli.mjs"; import { setupTestContainer as setupContainer } from "../src/config/setup-test-container.mjs"; +import { f } from "./helpers.mjs"; describe("cli operations", function () { let container; @@ -84,7 +84,17 @@ describe("cli operations", function () { }); it("should check for updates when run", async function () { + const fetch = container.resolve("fetch"); fetch.onCall(0).resolves( + f({ + version: 0, + status: "none", + diff: "Staged schema: none", + pending_summary: "", + text_diff: "", + }), + ); + fetch.onCall(1).resolves( f({ version: 0, diff: "", diff --git a/test/schema/pull.mjs b/test/schema/pull.mjs index 5a1a93a3..526d54ca 100644 --- a/test/schema/pull.mjs +++ b/test/schema/pull.mjs @@ -101,7 +101,10 @@ describe("schema pull", function () { ); // the version param in the URL is important - we use it for optimistic locking expect(fetch).to.have.been.calledWith( - buildUrl("/schema/1/staged/status", { version: "194838274939473" }), + buildUrl("/schema/1/staged/status", { + version: "194838274939473", + color: "ansi", + }), commonFetchParams, ); expect(fetch).to.have.been.calledWith( diff --git a/test/schema/push.mjs b/test/schema/push.mjs index f225c077..3dc63d9b 100644 --- a/test/schema/push.mjs +++ b/test/schema/push.mjs @@ -95,6 +95,9 @@ describe("schema push", function () { expect(logger.stderr).to.not.be.called; expect(logger.stdout).to.have.been.calledWith("Proposed diff:\n"); + expect(confirm).to.have.been.calledWith( + sinon.match.has("message", "Stage the above changes?"), + ); expect(logger.stdout).to.have.been.calledWith(diffString); }); @@ -146,6 +149,9 @@ describe("schema push", function () { expect(logger.stderr).to.not.be.called; expect(logger.stdout).to.have.been.calledWith("Proposed diff:\n"); + expect(confirm).to.have.been.calledWith( + sinon.match.has("message", "Push the above changes?"), + ); expect(logger.stdout).to.have.been.calledWith(diffString); }); @@ -192,7 +198,7 @@ describe("schema push", function () { expect(gatherFSL).to.have.been.calledWith("/absolute/path/elsewhere"); }); - it("warns when attempting to push an empty diff", async function () { + it("warns when attempting to stage an empty diff", async function () { // user accepts the changes in the interactive prompt confirm.resolves(true); @@ -234,7 +240,53 @@ describe("schema push", function () { expect(logger.stderr).to.not.be.called; expect(logger.stdout).to.have.been.calledWith("No logical changes."); expect(confirm).to.have.been.calledWith( - sinon.match.has("message", "Push file contents anyway?"), + sinon.match.has("message", "Stage the file contents anyway?"), + ); + }); + + it("warns when attempting to push an empty diff", async function () { + // user accepts the changes in the interactive prompt + confirm.resolves(true); + + fetch.onCall(0).resolves( + f({ + // this is the version we provide when we mutate the resource + version: 1728675598430000, + // note: no diff + }), + ); + + await run(`schema push --secret "secret" --active`, container); + + expect(fetch).to.have.been.calledWith( + buildUrl("/schema/1/validate", { + force: "true", + staged: "false", + color: "ansi", + }), + { + method: "POST", + headers: { AUTHORIZATION: "Bearer secret" }, + body: reformatFSL(fsl), + }, + ); + + expect(fetch).to.have.been.calledWith( + buildUrl("/schema/1/update", { + version: "1728675598430000", + staged: "false", + }), + { + method: "POST", + headers: { AUTHORIZATION: "Bearer secret" }, + body: reformatFSL(fsl), + }, + ); + + expect(logger.stderr).to.not.be.called; + expect(logger.stdout).to.have.been.calledWith("No logical changes."); + expect(confirm).to.have.been.calledWith( + sinon.match.has("message", "Push the file contents anyway?"), ); }); diff --git a/test/schema/status.mjs b/test/schema/status.mjs index 9048f63a..cf60c878 100644 --- a/test/schema/status.mjs +++ b/test/schema/status.mjs @@ -96,6 +96,7 @@ describe("schema status", function () { diff: "summary", staged: "true", version: "0", + color: "ansi", }), { ...commonFetchParams, method: "POST", body: new FormData() }, ); @@ -138,6 +139,7 @@ describe("schema status", function () { diff: "summary", staged: "true", version: "0", + color: "ansi", }), { ...commonFetchParams, method: "POST", body: new FormData() }, ); @@ -186,14 +188,7 @@ describe("schema status", function () { diff: "summary", staged: "true", version: "0", - }), - { ...commonFetchParams, method: "POST", body: new FormData() }, - ); - expect(fetch).to.have.been.calledWith( - buildUrl("/schema/1/validate", { - diff: "summary", - staged: "true", - version: "0", + color: "ansi", }), { ...commonFetchParams, method: "POST", body: new FormData() }, ); @@ -239,6 +234,7 @@ describe("schema status", function () { diff: "summary", staged: "true", version: "0", + color: "ansi", }), { ...commonFetchParams, method: "POST", body: new FormData() }, );