From 0ea82fc03eef2dd02b47933d8000eb3021b2777f Mon Sep 17 00:00:00 2001 From: Ashton Eby Date: Mon, 21 Oct 2024 15:26:10 -0700 Subject: [PATCH] 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/diff.mjs | 106 +++++++++++++++--- src/commands/schema/pull.mjs | 37 ++++--- src/commands/schema/push.mjs | 19 ++-- src/commands/schema/status.mjs | 45 +++++++- src/config/setup-container.mjs | 6 - src/lib/db.mjs | 5 +- src/lib/schema.mjs | 47 +------- test/helpers.mjs | 19 ++++ test/schema/diff.mjs | 38 +++++-- test/schema/pull.mjs | 32 +++--- test/schema/push.mjs | 26 +++-- test/schema/status.mjs | 196 +++++++++++++++++++++++++++++++-- 12 files changed, 434 insertions(+), 142 deletions(-) diff --git a/src/commands/schema/diff.mjs b/src/commands/schema/diff.mjs index 821e17d8..e57d3ac3 100644 --- a/src/commands/schema/diff.mjs +++ b/src/commands/schema/diff.mjs @@ -6,47 +6,119 @@ 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) { return yargs .options({ staged: { - description: - "Compare the local schema to the staged schema instead of the active schema.", + description: "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..aabb8b07 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, - secret: argv.secret, - }); - - // if there's a staged schema, require the --staged flag. - // getting unstaged FSL while staged FSL exists is not yet + 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. + // 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..c9cdc98a 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", @@ -82,7 +85,7 @@ function buildPushCommand(yargs) { default: true, 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..b22cf8de 100644 --- a/src/commands/schema/status.mjs +++ b/src/commands/schema/status.mjs @@ -1,23 +1,60 @@ //@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", }); + // console.log('statusResponse', statusResponse) + + 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 + }); + // console.log('validationResponse', validationResponse) + + 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 ")); + } - logger.stdout(response.diff); + 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 956cb08c..14ca22cb 100644 --- a/src/config/setup-container.mjs +++ b/src/config/setup-container.mjs @@ -9,9 +9,6 @@ import { gatherFSL, gatherRelativeFSLFilePaths, getAllSchemaFileContents, - getStagedSchemaStatus, - getSchemaFile, - getSchemaFiles, deleteUnusedSchemaFiles, writeSchemaFiles, } from "../lib/schema.mjs"; @@ -81,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..8f6761ea 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 0fb3527e..023ace54 100644 --- a/src/lib/schema.mjs +++ b/src/lib/schema.mjs @@ -174,7 +174,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; }), ); @@ -184,43 +189,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/helpers.mjs b/test/helpers.mjs index eddb5472..d7289715 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 20a390bf..4f260b64 100644 --- a/test/schema/diff.mjs +++ b/test/schema/diff.mjs @@ -1,6 +1,6 @@ import { expect } from "chai"; -import { f, commonFetchParams } from "../helpers.mjs"; +import { buildUrl, f, commonFetchParams } from "../helpers.mjs"; import { run } from "../../src/cli.mjs"; import { setupTestContainer as setupContainer } from "../../src/config/setup-test-container.mjs"; @@ -30,7 +30,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, @@ -41,14 +41,18 @@ 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, @@ -56,10 +60,14 @@ 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); @@ -77,7 +85,11 @@ describe("schema diff", function () { await run(`schema diff --secret "secret" --no-color`, container); expect(fetch).to.have.been.calledWith( - "https://db.fauna.com/schema/1/validate?force=true&staged=false", + buildUrl("/schema/1/staged/status"), + { ...commonFetchParams, method: "GET" }, + ); + expect(fetch).to.have.been.calledWith( + buildUrl("/schema/1/validate", { force: "true", staged: "true", diff: "semantic" }), { ...commonFetchParams, method: "POST", body: reformatFSL(fsl) }, ); expect(logger.stdout).to.have.been.calledWith(noColorDiffString); @@ -95,10 +107,14 @@ 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; }); @@ -119,4 +135,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 689acf79..a65c928d 100644 --- a/test/schema/pull.mjs +++ b/test/schema/pull.mjs @@ -3,15 +3,12 @@ import sinon from "sinon"; import * as awilix from "awilix"; -import { f, commonFetchParams } from "../helpers.mjs"; +import { buildUrl, f, commonFetchParams } from "../helpers.mjs"; import tryToCatch from "try-to-catch"; import { run } from "../../src/cli.mjs"; import { setupTestContainer as setupContainer } from "../../src/config/setup-test-container.mjs"; import { - getSchemaFile, - getSchemaFiles, - getStagedSchemaStatus, getAllSchemaFileContents, writeSchemaFiles, deleteUnusedSchemaFiles, @@ -26,9 +23,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), ), @@ -75,7 +69,7 @@ describe("schema pull", function () { fetch.onCall(1).resolves( f({ version: "194838274939473", - status: "none", + status: "ready", }), ); fetch.onCall(2).resolves( @@ -108,24 +102,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, ); @@ -185,7 +179,7 @@ describe("schema pull", function () { fetch.onCall(1).resolves( f({ version: "194838274939473", - status: "none", + status: "ready", }), ); @@ -227,7 +221,7 @@ describe("schema pull", function () { fetch.onCall(1).resolves( f({ version: "194838274939473", - status: "none", + status: "ready", }), ); fetch.onCall(2).resolves( @@ -261,7 +255,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", @@ -275,14 +269,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 a88e837e..bea143a0 100644 --- a/test/schema/push.mjs +++ b/test/schema/push.mjs @@ -2,7 +2,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 { f } from "../helpers.mjs"; +import { buildUrl, f } from "../helpers.mjs"; import sinon from "sinon"; describe("schema push", function () { @@ -34,7 +34,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" }, @@ -46,7 +46,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); @@ -68,7 +68,7 @@ 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" }, @@ -77,7 +77,7 @@ 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" }, @@ -90,7 +90,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); @@ -109,10 +109,10 @@ 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" }, @@ -121,7 +121,7 @@ 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" }, @@ -149,7 +149,7 @@ 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" }, @@ -188,7 +188,7 @@ 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" }, @@ -197,7 +197,7 @@ 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" }, @@ -211,4 +211,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 3918c74f..bb5bd484 100644 --- a/test/schema/status.mjs +++ b/test/schema/status.mjs @@ -1,36 +1,215 @@ +import chalk from "chalk"; import { expect } from "chai"; -import { f, commonFetchParams } from "../helpers.mjs"; +import { f, commonFetchParams, buildUrl } from "../helpers.mjs"; import { run } from "../../src/cli.mjs"; import { setupTestContainer as setupContainer } from "../../src/config/setup-test-container.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, @@ -41,8 +220,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; }); });