Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add schema push --stage #355

Merged
merged 1 commit into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 42 additions & 16 deletions src/commands/schema/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,19 @@ export default class PushSchemaCommand extends SchemaCommand {
description: "Push the change without a diff or schema version check",
default: false,
}),
stage: Flags.boolean({
description:
"Stages the schema change, instead of applying it immediately",
default: false,
}),
};

static description = "Push the current project's .fsl files to Fauna.";

static examples = [
"$ fauna schema push",
"$ fauna schema push --dir schemas/myschema",
"$ fauna schema push --stage",
];

async run() {
Expand All @@ -24,8 +30,14 @@ export default class PushSchemaCommand extends SchemaCommand {
try {
const { url, secret } = await this.fetchsetup();
if (this.flags?.force) {
// Just push.
const res = await fetch(new URL("/schema/1/update?force=true", url), {
const params = new URLSearchParams({
force: "true", // Just push.
staged: this.flags?.stage ? "true" : "false",
});

// This is how MDN says to do it for some reason.
const path = new URL(`/schema/1/update?${params}`, url);
const res = await fetch(path, {
method: "POST",
headers: { AUTHORIZATION: `Bearer ${secret}` },
body: this.body(files),
Expand All @@ -34,21 +46,30 @@ export default class PushSchemaCommand extends SchemaCommand {
// @ts-expect-error-next-line
duplex: "half",
});

const json = await res.json();
if (json.error) {
this.error(json.error.message);
this.error(json.error?.message ?? json.error);
}
} else {
// Confirm diff, then push it.
const res = await fetch(new URL("/schema/1/validate?force=true", url), {
// Confirm diff, then push it. `force` is set on `validate` so we don't
// need to pass the last known schema version through.
const params = new URLSearchParams({
force: "true",
});
const path = new URL(`/schema/1/validate?${params}`, url);
Comment on lines +55 to +60
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we just drop the "force" param for validate?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can pass a version instead, so we just require force or version to be set on all the schema endpoints. we could probably drop it, it just keeps the validation simple in the core endpoint.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, just thinking it might make using the API itself simpler. I forget/lost track if we're actually passing version to validate at any point

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to double check, I think we might be using it in the dashboard to check if a schema version is stale. If we're not using it there, I'll go ahead and remove it from the core endpoint.

const res = await fetch(path, {
method: "POST",
headers: { AUTHORIZATION: `Bearer ${secret}` },
body: this.body(files),
// @ts-expect-error-next-line
duplex: "half",
});
const json = await res.json();
if (json.error) {
this.error(json.error.message);
this.error(json.error?.message ?? json.error);
}

let message = "Accept and push changes?";
if (json.diff) {
this.log(`Proposed diff:\n`);
Expand All @@ -61,17 +82,22 @@ export default class PushSchemaCommand extends SchemaCommand {
message,
default: false,
});

if (confirmed) {
const res = await fetch(
new URL(`/schema/1/update?version=${json.version}`, url),
{
method: "POST",
headers: { AUTHORIZATION: `Bearer ${secret}` },
body: this.body(files),
// @ts-expect-error-next-line
duplex: "half",
}
);
const params = new URLSearchParams({
version: json.version,
staged: this.flags?.stage ? "true" : "false",
});

const path = new URL(`/schema/1/update?${params}`, url);
const res = await fetch(path, {
method: "POST",
headers: { AUTHORIZATION: `Bearer ${secret}` },
body: this.body(files),
// @ts-expect-error-next-line
duplex: "half",
});

const json0 = await res.json();
if (json0.error) {
this.error(json0.error.message);
Expand Down
24 changes: 24 additions & 0 deletions test/commands/schema.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ describe("fauna schema diff test", () => {
});

describe("fauna schema push test", () => {
afterEach(() => {
nock.cleanAll();
});

it("runs schema push", async () => {
nock(getEndpoint(), { allowUnmocked: false })
.persist()
Expand All @@ -75,6 +79,26 @@ describe("fauna schema push test", () => {
// Restore the stub after the test
stubConfirm.restore();
});

it("runs schema push --stage", async () => {
nock(getEndpoint(), { allowUnmocked: false })
.persist()
.post("/", matchFqlReq(q.Now()))
.reply(200, new Date())
.post("/schema/1/validate?force=true")
.reply(200, diff)
.post("/schema/1/update?version=0&stage=true")
.reply(200, updated);

// Stubbing the confirmation to always return true
const stubConfirm = sinon.stub(inquirer, "confirm").resolves(true);
const { stdout } = await runCommand(
withOpts(["schema push", "--dir=test/testdata", "--stage"])
);
expect(stdout).to.contain(`${diff.diff}`);
// Restore the stub after the test
stubConfirm.restore();
});
});

const testdir = "test/testdata";
Expand Down
130 changes: 130 additions & 0 deletions test/integ/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { runCommand } from "@oclif/test";
import { fail } from "assert";
import { env } from "process";

export type ShellResult = { stdout: string; stderr: string; ok: boolean };

const TEST_PREFIX = "fauna_shell_integ_test_";

export const newDB = async (secret?: string): Promise<string> => {
const name = TEST_PREFIX + Math.floor(Math.random() * 1000000000);

return evalOk<string>(
stripMargin(
`|if (Database.byName('${name}').exists()) {
| Database.byName('${name}').delete()
|}
|Database.create({ name: '${name}', typechecked: true })
|Key.create({ role: 'admin', database: '${name}' }).secret
|`
),
secret
);
};

export const cleanupDBs = async (): Promise<void> => {
const { url, secret } = endpoint();

const query = stripMargin(
`|Database.all().forEach((db) => {
| if (db.name.startsWith('${TEST_PREFIX}')) {
| db.delete()
| }
|})
|`
);

const res = await fetch(new URL("/query/1", url), {
method: "POST",
headers: { AUTHORIZATION: `Bearer ${secret}` },
body: JSON.stringify({ query }),
// @ts-expect-error-next-line
duplex: "half",
});

if (res.status !== 200) {
fail(`Cleanup failed: ${await res.text()}`);
}
};

export const evalOk = async <T>(code: string, secret?: string): Promise<T> => {
const res = JSON.parse(
await shellOk(`fauna eval "${code}" --format json`, secret)
);
// FIXME: This should really fail `shellOk`, but error handling is hard.
if (res?.error) {
fail(`Eval failed: ${res.summary}`);
}

return res;
};

export const shellOk = async (
cmd: string,
secret?: string
): Promise<string> => {
const res = await shell(cmd, secret);
if (!res.ok) {
fail(`Command unexpectedly failed:\n${res.stderr}`);
}

return res.stdout;
};

export const shellErr = async (cmd: string): Promise<string> => {
const res = await shell(cmd);
if (res.ok) {
fail(`Command should not have exitted succesfully:\n${res.stdout}`);
}

return res.stderr;
};

export const stripMargin = (str: string): string => {
return str
.split("\n")
.map((line) => {
const trimmed = line.trimStart();
if (trimmed.startsWith("|")) {
return trimmed.slice(1);
} else {
return trimmed;
}
})
.join("\n");
};

export const shell = async (
cmd: string,
secret?: string
): Promise<ShellResult> => {
const parts = cmd.split(" ");
if (parts[0] !== "fauna") {
fail("Command must start with fauna");
}

const { url, secret: s } = endpoint();

const opts = [
parts.slice(1).join(" "),
`--url ${url}`,
`--secret ${secret ?? s}`,
];

const out = await runCommand(opts);

return {
stdout: out.stdout,
stderr: out.stderr + out.error?.message,
ok: out.error === undefined,
};
};

const endpoint = () => {
return {
url: `${env.FAUNA_SCHEME ?? "http"}://${env.FAUNA_DOMAIN ?? "127.0.0.1"}:${
env.FAUNA_PORT ?? 8443
}`,
secret: env.FAUNA_SECRET ?? "secret",
};
};
54 changes: 54 additions & 0 deletions test/integ/schema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { expect } from "chai";
import { cleanupDBs, evalOk, newDB, 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.
describe.skip("fauna schema staged commands", () => {
// Cleanup after ourselves.
after(async function () {
await cleanupDBs();
});

it("fauna schema push --stage --force works", async () => {
const secret = await newDB();

await shellOk(
"fauna schema push --dir test/integ/schema/start --force",
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
);

// Index should be in the FQL definition.
expect(
await evalOk("Collection.byName('User')!.indexes.byName", secret)
).to.deep.equal({
terms: [
{
field: ".name",
mva: false,
},
],
queryable: true,
status: "complete",
});

// But, the index should not be visible on the companion object.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for comments with intent

expect(
await evalOk(
stripMargin(
`|let user: Any = User
|user.byName`
),
secret
)
).to.deep.equal(null);
});
});
8 changes: 8 additions & 0 deletions test/integ/schema/staged_index/main.fsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
collection User {
name: String
email: String

index byName {
terms [.name]
}
}
4 changes: 4 additions & 0 deletions test/integ/schema/start/main.fsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
collection User {
name: String
email: String
}