diff --git a/src/commands/schema/abandon.ts b/src/commands/schema/abandon.ts new file mode 100644 index 00000000..f585ced9 --- /dev/null +++ b/src/commands/schema/abandon.ts @@ -0,0 +1,96 @@ +import { confirm } from "@inquirer/prompts"; +import SchemaCommand from "../../lib/schema-command"; +import { Flags } from "@oclif/core"; + +export default class CommitSchemaCommand extends SchemaCommand { + static flags = { + ...SchemaCommand.flags, + force: Flags.boolean({ + description: "Push the change without a diff or schema version check", + default: false, + }), + }; + + static description = "Abandons the currently staged schema."; + + static examples = ["$ fauna schema abandon"]; + + async run() { + try { + const { url, secret } = await this.fetchsetup(); + if (this.flags?.force) { + const params = new URLSearchParams({ + force: "true", // Just abandon, don't pass a schema version through. + }); + + const path = new URL(`/schema/1/staged/abandon?${params}`, url); + const res = await fetch(path, { + method: "POST", + headers: { AUTHORIZATION: `Bearer ${secret}` }, + // https://github.com/nodejs/node/issues/46221 + // https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1483 + // @ts-expect-error-next-line + duplex: "half", + }); + + const json = await res.json(); + if (json.error) { + this.error(json.error?.message ?? json.error); + } + + this.log("Schema has been abandonded"); + } else { + // Show status to confirm. + const { url, secret } = await this.fetchsetup(); + const res = await fetch( + new URL("/schema/1/staged/status?diff=true", url), + { + method: "GET", + headers: { AUTHORIZATION: `Bearer ${secret}` }, + // @ts-expect-error-next-line + duplex: "half", + } + ); + + const json = await res.json(); + if (json.error) { + this.error(json.error.message); + } + + if (json.status === "none") { + this.error("There is no staged schema to abandon"); + } + + this.log(json.diff); + + const confirmed = await confirm({ + message: "Abandon these changes?", + default: false, + }); + + if (confirmed) { + const params = new URLSearchParams({ version: json.version }); + + const path = new URL(`/schema/1/staged/abandon?${params}`, url); + const res = await fetch(path, { + method: "POST", + headers: { AUTHORIZATION: `Bearer ${secret}` }, + // @ts-expect-error-next-line + duplex: "half", + }); + + const json0 = await res.json(); + if (json0.error) { + this.error(json0.error.message); + } + + this.log("Schema has been abandoned"); + } else { + this.log("Abandon cancelled"); + } + } + } catch (err) { + this.error(err); + } + } +} diff --git a/src/commands/schema/commit.ts b/src/commands/schema/commit.ts new file mode 100644 index 00000000..4d344fcc --- /dev/null +++ b/src/commands/schema/commit.ts @@ -0,0 +1,100 @@ +import { confirm } from "@inquirer/prompts"; +import SchemaCommand from "../../lib/schema-command"; +import { Flags } from "@oclif/core"; + +export default class CommitSchemaCommand extends SchemaCommand { + static flags = { + ...SchemaCommand.flags, + force: Flags.boolean({ + description: "Push the change without a diff or schema version check", + default: false, + }), + }; + + static description = "Commits the currently staged schema."; + + static examples = ["$ fauna schema commit"]; + + async run() { + try { + const { url, secret } = await this.fetchsetup(); + if (this.flags?.force) { + const params = new URLSearchParams({ + force: "true", // Just commit, don't pass a schema version through. + }); + + const path = new URL(`/schema/1/staged/commit?${params}`, url); + const res = await fetch(path, { + method: "POST", + headers: { AUTHORIZATION: `Bearer ${secret}` }, + // https://github.com/nodejs/node/issues/46221 + // https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1483 + // @ts-expect-error-next-line + duplex: "half", + }); + + const json = await res.json(); + if (json.error) { + this.error(json.error?.message ?? json.error); + } + + this.log("Schema has been committed"); + } else { + // Show status to confirm. + const { url, secret } = await this.fetchsetup(); + const res = await fetch( + new URL("/schema/1/staged/status?diff=true", url), + { + method: "GET", + headers: { AUTHORIZATION: `Bearer ${secret}` }, + // @ts-expect-error-next-line + duplex: "half", + } + ); + + const json = await res.json(); + if (json.error) { + this.error(json.error.message); + } + + if (json.status === "none") { + this.error("There is no staged schema to commit"); + } + + this.log(json.diff); + + if (json.status !== "ready") { + this.error("Schema is not ready to be committed"); + } + + const confirmed = await confirm({ + message: "Accept and commit these changes?", + default: false, + }); + + if (confirmed) { + const params = new URLSearchParams({ version: json.version }); + + const path = new URL(`/schema/1/staged/commit?${params}`, url); + const res = await fetch(path, { + method: "POST", + headers: { AUTHORIZATION: `Bearer ${secret}` }, + // @ts-expect-error-next-line + duplex: "half", + }); + + const json0 = await res.json(); + if (json0.error) { + this.error(json0.error.message); + } + + this.log("Schema has been committed"); + } else { + this.log("Commit cancelled"); + } + } + } catch (err) { + this.error(err); + } + } +} diff --git a/test/commands/schema.test.js b/test/commands/schema.test.js index 1cc593d7..2bd2290d 100644 --- a/test/commands/schema.test.js +++ b/test/commands/schema.test.js @@ -118,6 +118,101 @@ describe("fauna schema push test", () => { ); expect(stdout).to.contain(`${diff.diff}`); }); + + it("runs schema commit", async () => { + nock(getEndpoint(), { allowUnmocked: false }) + .persist() + .post("/", matchFqlReq(q.Now())) + .reply(200, new Date()) + .get("/schema/1/staged/status?diff=true") + .reply(200, { + version: 3, + status: "ready", + diff: diff.diff, + }) + .post("/schema/1/staged/commit?version=3") + .reply(200, { version: 0 }); + + // Stubbing the confirmation to always return true + const stubConfirm = sinon.stub(inquirer, "confirm").resolves(true); + const { stdout } = await runCommand( + withOpts(["schema commit", "--dir=test/testdata"]) + ); + expect(stdout).to.contain(`Schema has been committed`); + // Restore the stub after the test + stubConfirm.restore(); + }); + + it("won't commit when schema isn't ready", async () => { + nock(getEndpoint(), { allowUnmocked: false }) + .persist() + .post("/", matchFqlReq(q.Now())) + .reply(200, new Date()) + .get("/schema/1/staged/status?diff=true") + .reply(200, { + version: 3, + status: "pending", + diff: diff.diff, + }); + + // Stubbing the confirmation to always return true + const stubConfirm = sinon.stub(inquirer, "confirm").resolves(true); + const { stdout, error } = await runCommand( + withOpts(["schema commit", "--dir=test/testdata"]) + ); + expect(stdout).to.contain(diff.diff); + expect(error.message).to.equal("Schema is not ready to be committed"); + // Restore the stub after the test + stubConfirm.restore(); + }); + + it("runs schema abandon", async () => { + nock(getEndpoint(), { allowUnmocked: false }) + .persist() + .post("/", matchFqlReq(q.Now())) + .reply(200, new Date()) + .get("/schema/1/staged/status?diff=true") + .reply(200, { + version: 3, + status: "ready", + diff: diff.diff, + }) + .post("/schema/1/staged/abandon?version=3") + .reply(200, { version: 0 }); + + // Stubbing the confirmation to always return true + const stubConfirm = sinon.stub(inquirer, "confirm").resolves(true); + const { stdout } = await runCommand( + withOpts(["schema abandon", "--dir=test/testdata"]) + ); + expect(stdout).to.contain(`Schema has been abandoned`); + // Restore the stub after the test + stubConfirm.restore(); + }); + + it("will abandon even when schema isn't ready", async () => { + nock(getEndpoint(), { allowUnmocked: false }) + .persist() + .post("/", matchFqlReq(q.Now())) + .reply(200, new Date()) + .get("/schema/1/staged/status?diff=true") + .reply(200, { + version: 3, + status: "pending", + diff: diff.diff, + }) + .post("/schema/1/staged/abandon?version=3") + .reply(200, { version: 0 }); + + // Stubbing the confirmation to always return true + const stubConfirm = sinon.stub(inquirer, "confirm").resolves(true); + const { stdout } = await runCommand( + withOpts(["schema abandon", "--dir=test/testdata"]) + ); + expect(stdout).to.contain(`Schema has been abandoned`); + // Restore the stub after the test + stubConfirm.restore(); + }); }); const testdir = "test/testdata"; diff --git a/test/integ/base.ts b/test/integ/base.ts index 50a0c0e7..12b1ec74 100644 --- a/test/integ/base.ts +++ b/test/integ/base.ts @@ -71,8 +71,11 @@ export const shellOk = async ( return res.stdout; }; -export const shellErr = async (cmd: string): Promise => { - const res = await shell(cmd); +export const shellErr = async ( + cmd: string, + secret?: string +): Promise => { + const res = await shell(cmd, secret); if (res.ok) { fail(`Command should not have exitted succesfully:\n${res.stdout}`); } diff --git a/test/integ/schema.test.ts b/test/integ/schema.test.ts index 7a016cbb..316c5dc9 100644 --- a/test/integ/schema.test.ts +++ b/test/integ/schema.test.ts @@ -1,5 +1,12 @@ import { expect } from "chai"; -import { cleanupDBs, evalOk, newDB, shellOk, stripMargin } from "./base"; +import { + cleanupDBs, + evalOk, + newDB, + shellErr, + shellOk, + stripMargin, +} from "./base"; // FIXME: Once we get `nock` out of here, we need to revive this test. It works // fine locally, but it causes the entire test run to freeze in CI. @@ -104,4 +111,140 @@ describe.skip("fauna schema staged commands", () => { ) ); }); + + it("fauna schema commit --force works", async () => { + const secret = await newDB(); + + await shellOk( + "fauna schema push --dir test/integ/schema/start --force", + secret + ); + + await evalOk( + "User.create({ id: 0, name: 'Alice', email: 'alice@example.com' })", + secret + ); + + expect( + await evalOk("Collection.all().map(.name).toArray()", secret) + ).to.deep.equal(["User"]); + + await shellOk( + "fauna schema push --dir test/integ/schema/staged_index --force --stage", + secret + ); + + // The index should not be visible on the companion object. + expect( + await evalOk( + stripMargin( + `|let user: Any = User + |user.byName` + ), + secret + ) + ).to.deep.equal(null); + + // Commit the schema + await shellOk("fauna schema commit --dir . --force", secret); + + // Index should now be available on the companion object. + expect( + await evalOk( + stripMargin(`User.byName('Alice').toArray().map(.id)`), + secret + ) + ).to.deep.equal(["0"]); + + // Status should be blank now. + expect(await status(secret)).to.equal( + stripMargin( + `|Status: none + |` + ) + ); + + // Comitting when there is nothing staged should return an error. + expect( + await shellErr("fauna schema commit --dir . --force", secret) + ).to.equal("There is no staged schema to commit"); + }); + + it("fauna schema abandon --force works", async () => { + const secret = await newDB(); + + await shellOk( + "fauna schema push --dir test/integ/schema/start --force", + secret + ); + + await evalOk( + "User.create({ id: 0, name: 'Alice', email: 'alice@example.com' })", + secret + ); + + expect( + await evalOk("Collection.all().map(.name).toArray()", secret) + ).to.deep.equal(["User"]); + + await shellOk( + "fauna schema push --dir test/integ/schema/staged_index --force --stage", + secret + ); + + // The index should be visible on the definition object. + expect( + await evalOk("Collection.byName('User')!.indexes.byName", secret) + ).to.deep.equal({ + terms: [ + { + field: ".name", + mva: false, + }, + ], + queryable: true, + status: "complete", + }); + + // But not visible on the companion object. + expect( + await evalOk( + stripMargin( + `|let user: Any = User + |user.byName` + ), + secret + ) + ).to.deep.equal(null); + + // Abandon the schema + await shellOk("fauna schema abandon --dir . --force", secret); + + // Index should no longer be in the definition object. + expect( + await evalOk("Collection.byName('User')!.indexes.byName", secret) + ).to.deep.equal(null); + expect( + await evalOk( + stripMargin( + `|let user: Any = User + |user.byName` + ), + secret + ) + ).to.deep.equal(null); + + // Status should be blank now. + expect(await status(secret)).to.equal( + stripMargin( + `|Status: none + |` + ) + ); + + // Abandoning when there is no staged schema should return an error. + expect( + await shellErr("fauna schema abandon --dir . --force", secret) + ).to.equal("There is no staged schema to abandon"); + }); });