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

FE-6254 Setting schema on 'fauna local --database Foo' #533

Merged
merged 13 commits into from
Dec 17, 2024
52 changes: 52 additions & 0 deletions src/commands/local.mjs
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ import { container } from "../cli.mjs";
import { ensureContainerRunning } from "../lib/docker-containers.mjs";
import { CommandError, ValidationError } from "../lib/errors.mjs";
import { colorize, Format } from "../lib/formatting/colorize.mjs";
import { pushSchema } from "../lib/schema.mjs";

/**
* Starts the local Fauna container
@@ -28,6 +29,34 @@ async function startLocal(argv) {
if (argv.database) {
await createDatabase(argv);
}
if (argv.directory) {
await createDatabaseSchema(argv);
}
}

async function createDatabaseSchema(argv) {
const logger = container.resolve("logger");
logger.stderr(
colorize(
`[CreateDatabaseSchema] Creating schema for database '${argv.database}' from directory '${argv.directory}'...`,
{
format: Format.LOG,
color: argv.color,
},
),
);
// hack to let us push schema to the local database
argv.secret = `secret:${argv.database}:admin`;
await pushSchema(argv);
logger.stderr(
colorize(
`[CreateDatabaseSchema] Schema for database '${argv.database}' created from directory '${argv.directory}'.`,
{
format: Format.LOG,
color: argv.color,
},
),
);
}

async function createDatabase(argv) {
@@ -152,6 +181,24 @@ function buildLocalCommand(yargs) {
description:
"User-defined priority for the database. Valid only if --database is set.",
},
"project-directory": {
type: "string",
alias: ["dir", "directory"],
description:
"Path to a local directory containing `.fsl` files for the database. Valid only if --database is set.",
},
active: {
description:
"Immediately apply the local schema to the database's active schema. Skips staging the schema. Can result in temporarily unavailable indexes.",
type: "boolean",
default: false,
},
input: {
description:
"Prompt for schema input, such as confirmation. To disable prompts, use `--no-input` or `--input=false`. Disabled prompts are useful for scripts, CI/CD, and automation workflows.",
default: true,
type: "boolean",
},
})
.check((argv) => {
if (argv.maxAttempts < 1) {
@@ -177,6 +224,11 @@ function buildLocalCommand(yargs) {
"--priority can only be set if --database is set.",
);
}
if (argv.directory && !argv.database) {
throw new ValidationError(
"--directory,--dir can only be set if --database is set.",
);
}
return true;
});
}
92 changes: 2 additions & 90 deletions src/commands/schema/push.mjs
Original file line number Diff line number Diff line change
@@ -1,97 +1,9 @@
//@ts-check

import path from "path";

import { container } from "../../cli.mjs";
import { yargsWithCommonQueryOptions } from "../../lib/command-helpers.mjs";
import { ValidationError } from "../../lib/errors.mjs";
import { getSecret } from "../../lib/fauna-client.mjs";
import { reformatFSL } from "../../lib/schema.mjs";
import { pushSchema } from "../../lib/schema.mjs";
ecooper marked this conversation as resolved.
Show resolved Hide resolved
import { localSchemaOptions } from "./schema.mjs";

async function doPush(argv) {
const logger = container.resolve("logger");
const makeFaunaRequest = container.resolve("makeFaunaRequest");
const gatherFSL = container.resolve("gatherFSL");

const isStagedPush = !argv.active;
const secret = await getSecret();
const fslFiles = await gatherFSL(argv.dir);
const hasLocalSchema = fslFiles.length > 0;
const absoluteDirPath = path.resolve(argv.dir);
const fsl = reformatFSL(fslFiles);

if (!hasLocalSchema) {
throw new ValidationError(
`No schema files (*.fsl) found in '${absoluteDirPath}'. Use '--dir' to specify a different directory, or create new .fsl files in this location.`,
);
} else if (!argv.input) {
const params = new URLSearchParams({
force: "true",
staged: argv.active ? "false" : "true",
});

await makeFaunaRequest({
argv,
path: "/schema/1/update",
params,
body: fsl,
method: "POST",
secret,
});
} else {
// Confirm diff, then push it.
const params = new URLSearchParams({
staged: argv.active ? "false" : "true",
});

const response = await makeFaunaRequest({
argv,
path: "/schema/1/diff",
params,
body: fsl,
method: "POST",
secret,
});

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 = isStagedPush
? "Stage the file contents anyway?"
: "Push the file contents anyway?";
}
const confirm = container.resolve("confirm");
const confirmed = await confirm({
message,
default: false,
});

if (confirmed) {
const params = new URLSearchParams({
version: response.version,
staged: argv.active ? "false" : "true",
});

await makeFaunaRequest({
argv,
path: "/schema/1/update",
params,
body: fsl,
method: "POST",
secret,
});
} else {
logger.stdout("Push cancelled.");
}
}
}

function buildPushCommand(yargs) {
return yargsWithCommonQueryOptions(yargs)
.options({
@@ -133,5 +45,5 @@ export default {
command: "push",
description: "Push local .fsl schema files to Fauna.",
builder: buildPushCommand,
handler: doPush,
handler: pushSchema,
};
9 changes: 5 additions & 4 deletions src/lib/auth/credentials.mjs
Original file line number Diff line number Diff line change
@@ -3,18 +3,19 @@ import { asValue, Lifetime } from "awilix";
import { container } from "../../cli.mjs";
import { ValidationError } from "../errors.mjs";
import { FaunaAccountClient } from "../fauna-account-client.mjs";
import { isLocal } from "../middleware.mjs";
import { AccountKeys } from "./accountKeys.mjs";
import { DatabaseKeys } from "./databaseKeys.mjs";

const validateCredentialArgs = (argv) => {
const logger = container.resolve("logger");
const illegalArgCombos = [
["accountKey", "secret", "local"],
["secret", "database", "local"],
["secret", "role", "local"],
["accountKey", "secret", isLocal],
["secret", "database", isLocal],
["secret", "role", isLocal],
];
for (const [first, second, conditional] of illegalArgCombos) {
if (argv[first] && argv[second] && !argv[conditional]) {
if (argv[first] && argv[second] && !conditional(argv)) {
throw new ValidationError(
`Cannot use both the '--${first}' and '--${second}' options together. Please specify only one.`,
);
15 changes: 12 additions & 3 deletions src/lib/middleware.mjs
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ import { container } from "../cli.mjs";
import { fixPath } from "../lib/file-util.mjs";
import { redactedStringify } from "./formatting/redact.mjs";

const LOCAL_URL = "http://localhost:8443";
const LOCAL_URL = "http://0.0.0.0:8443";
const LOCAL_SECRET = "secret";
const DEFAULT_URL = "https://db.fauna.com";

@@ -79,6 +79,15 @@ export function applyLocalArg(argv) {
applyLocalToSecret(argv);
}

/**
* @param {import('yargs').Arguments} argv
* @returns {boolean} true if this command acts on a local
* container, false otherwise.
*/
export function isLocal(argv) {
return argv.local || argv._[0] === "local";
}

/**
* Mutates argv.url appropriately for local Fauna usage
* (i.e. local container usage). If --local is provided
@@ -89,7 +98,7 @@ export function applyLocalArg(argv) {
function applyLocalToUrl(argv) {
const logger = container.resolve("logger");
if (!argv.url) {
if (argv.local) {
if (isLocal(argv)) {
argv.url = LOCAL_URL;
logger.debug(
`Set url to '${LOCAL_URL}' as --local was given and --url was not`,
@@ -120,7 +129,7 @@ function applyLocalToUrl(argv) {
*/
function applyLocalToSecret(argv) {
const logger = container.resolve("logger");
if (!argv.secret && argv.local) {
if (!argv.secret && isLocal(argv)) {
if (argv.role && argv.database) {
argv.secret = `${LOCAL_SECRET}:${argv.database}:${argv.role}`;
} else if (argv.role) {
88 changes: 88 additions & 0 deletions src/lib/schema.mjs
Original file line number Diff line number Diff line change
@@ -4,9 +4,97 @@ import * as path from "path";

import { container } from "../cli.mjs";
import { makeFaunaRequest } from "../lib/db.mjs";
import { ValidationError } from "./errors.mjs";
import { getSecret } from "./fauna-client.mjs";
import { dirExists, dirIsWriteable } from "./file-util.mjs";

/**
* Pushes a schema (FSL) based on argv.
* @param {import("yargs").Argv & {dir: string, active: boolean, input: boolean}} argv
*/
export async function pushSchema(argv) {
const logger = container.resolve("logger");
const makeFaunaRequest = container.resolve("makeFaunaRequest");
const gatherFSL = container.resolve("gatherFSL");

const isStagedPush = !argv.active;
const secret = await getSecret();
const fslFiles = await gatherFSL(argv.dir);
const hasLocalSchema = fslFiles.length > 0;
const absoluteDirPath = path.resolve(argv.dir);
const fsl = reformatFSL(fslFiles);

if (!hasLocalSchema) {
throw new ValidationError(
`No schema files (*.fsl) found in '${absoluteDirPath}'. Use '--dir' to specify a different directory, or create new .fsl files in this location.`,
);
} else if (!argv.input) {
const params = new URLSearchParams({
force: "true",
staged: argv.active ? "false" : "true",
});

await makeFaunaRequest({
argv,
path: "/schema/1/update",
params,
body: fsl,
method: "POST",
secret,
});
} else {
// Confirm diff, then push it.
const params = new URLSearchParams({
staged: argv.active ? "false" : "true",
});

const response = await makeFaunaRequest({
argv,
path: "/schema/1/diff",
params,
body: fsl,
method: "POST",
secret,
});

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 = isStagedPush
? "Stage the file contents anyway?"
: "Push the file contents anyway?";
}
const confirm = container.resolve("confirm");
const confirmed = await confirm({
message,
default: false,
});

if (confirmed) {
const params = new URLSearchParams({
version: response.version,
staged: argv.active ? "false" : "true",
});

await makeFaunaRequest({
argv,
path: "/schema/1/update",
params,
body: fsl,
method: "POST",
secret,
});
} else {
logger.stdout("Push cancelled.");
}
}
}

/**
* @param {string} dir - The directory path to check for existence and write access
*/
8 changes: 4 additions & 4 deletions test/config.mjs
Original file line number Diff line number Diff line change
@@ -177,13 +177,13 @@ describe("configuration file", function () {
});
});

it("--local arg sets the argv.url to http://localhost:8443 if no --url is given", async function () {
it("--local arg sets the argv.url to http://0.0.0.0:8443 if no --url is given", async function () {
fs.readdirSync.withArgs(process.cwd()).returns([]);
await runArgvTest({
cmd: `argv --secret "no-config" --local`,
argvMatcher: sinon.match({
secret: "no-config",
url: "http://localhost:8443",
url: "http://0.0.0.0:8443",
}),
});
});
@@ -205,7 +205,7 @@ describe("configuration file", function () {
cmd: `argv --local`,
argvMatcher: sinon.match({
secret: "secret",
url: "http://localhost:8443",
url: "http://0.0.0.0:8443",
}),
});
});
@@ -216,7 +216,7 @@ describe("configuration file", function () {
cmd: `argv --local --secret "sauce"`,
argvMatcher: sinon.match({
secret: "sauce",
url: "http://localhost:8443",
url: "http://0.0.0.0:8443",
}),
});
});
Loading