Skip to content

Commit

Permalink
feat: add --config option for custom .grmc(.js) path (#113)
Browse files Browse the repository at this point in the history
Co-authored-by: ben-pr-p <[email protected]>
  • Loading branch information
benjie and ben-pr-p authored Apr 16, 2021
1 parent a4f56bf commit 594f7a5
Show file tree
Hide file tree
Showing 13 changed files with 233 additions and 101 deletions.
41 changes: 28 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,8 @@ Commands:
graphile-migrate completion Generate shell completion script.
Options:
--help Show help [boolean]
--help Show help [boolean]
--config, -c Optional path to gmrc file [string] [default: .gmrc[.js]]
You are running graphile-migrate v1.0.2.
```
Expand All @@ -238,8 +239,9 @@ Initializes a graphile-migrate project by creating a `.gmrc` file and
`migrations` folder.
Options:
--help Show help [boolean]
--folder Use a folder rather than a file for the current migration.
--help Show help [boolean]
--config, -c Optional path to gmrc file [string] [default: .gmrc[.js]]
--folder Use a folder rather than a file for the current migration.
[boolean] [default: false]
```

Expand All @@ -254,6 +256,7 @@ For use in production and development.
Options:
--help Show help [boolean]
--config, -c Optional path to gmrc file [string] [default: .gmrc[.js]]
--shadow Apply migrations to the shadow DB (for development).
[boolean] [default: false]
--forceActions Run beforeAllMigrations and afterAllMigrations actions even if
Expand All @@ -270,9 +273,11 @@ Runs any un-executed committed migrations and then runs and watches the current
migration, re-running it on any change. For development.
Options:
--help Show help [boolean]
--once Runs the current migration and then exits.[boolean] [default: false]
--shadow Applies changes to shadow DB. [boolean] [default: false]
--help Show help [boolean]
--config, -c Optional path to gmrc file [string] [default: .gmrc[.js]]
--once Runs the current migration and then exits.
[boolean] [default: false]
--shadow Applies changes to shadow DB. [boolean] [default: false]
```


Expand All @@ -286,6 +291,7 @@ current migration. Resets the shadow database.
Options:
--help Show help [boolean]
--config, -c Optional path to gmrc file [string] [default: .gmrc[.js]]
--message, -m Optional commit message to label migration, must not contain
newlines. [string]
```
Expand All @@ -308,7 +314,8 @@ should result in the exact same hash. Development only, and liable to cause
conflicts with other developers - be careful.
Options:
--help Show help [boolean]
--help Show help [boolean]
--config, -c Optional path to gmrc file [string] [default: .gmrc[.js]]
```


Expand All @@ -321,10 +328,11 @@ Drops and re-creates the database, re-running all committed migrations from the
start. **HIGHLY DESTRUCTIVE**.
Options:
--help Show help [boolean]
--shadow Applies migrations to shadow DB. [boolean] [default: false]
--erase This is your double opt-in to make it clear this DELETES EVERYTHING.
[boolean] [default: false]
--help Show help [boolean]
--config, -c Optional path to gmrc file [string] [default: .gmrc[.js]]
--shadow Applies migrations to shadow DB. [boolean] [default: false]
--erase This is your double opt-in to make it clear this DELETES
EVERYTHING. [boolean] [default: false]
```


Expand All @@ -345,6 +353,7 @@ output.
Options:
--help Show help [boolean]
--config, -c Optional path to gmrc file [string] [default: .gmrc[.js]]
--skipDatabase Skip checks that require a database connection.
[boolean] [default: false]
```
Expand All @@ -359,8 +368,9 @@ Compiles a SQL file, inserting all the placeholders and returning the result to
STDOUT
Options:
--help Show help [boolean]
--shadow Apply shadow DB placeholders (for development).
--help Show help [boolean]
--config, -c Optional path to gmrc file [string] [default: .gmrc[.js]]
--shadow Apply shadow DB placeholders (for development).
[boolean] [default: false]
```

Expand All @@ -377,6 +387,7 @@ run against the same database (via GM_DBURL envvar) unless --shadow or
Options:
--help Show help [boolean]
--config, -c Optional path to gmrc file [string] [default: .gmrc[.js]]
--shadow Apply to the shadow database (for development).
[boolean] [default: false]
--root Run the file using the root user (but application database).
Expand Down Expand Up @@ -490,6 +501,10 @@ opening brace `{` would be prepended with `module.exports =`:
module.exports = {
```
All commands accept an optional `--config` parameter with a custom path to a
`.gmrc(.js)` file. This is useful if, for example, you have a monorepo or other
project with multiple interacting databases.
### Windows
Since committed migrations utilize hashes to verify file integrity, the
Expand Down
34 changes: 34 additions & 0 deletions __tests__/settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import "./helpers"; // Side effects - must come first
import * as mockFs from "mock-fs";
import * as path from "path";

import { DEFAULT_GMRC_PATH, getSettings } from "../src/commands/_common";
import {
makeRootDatabaseConnectionString,
ParsedSettings,
Expand Down Expand Up @@ -278,3 +279,36 @@ describe("actions", () => {
`);
});
});

describe("gmrc path", () => {
it("defaults to .gmrc", async () => {
mockFs.restore();
mockFs({
[DEFAULT_GMRC_PATH]: `
{ "connectionString": "postgres://appuser:apppassword@host:5432/defaultdb" }
`,
});
const settings = await getSettings();
expect(settings.connectionString).toEqual(
"postgres://appuser:apppassword@host:5432/defaultdb",
);
mockFs.restore();
});

it("accepts an override and follows it", async () => {
mockFs.restore();
mockFs({
[DEFAULT_GMRC_PATH]: `
{ "connectionString": "postgres://appuser:apppassword@host:5432/defaultdb" }
`,
".other-gmrc": `
{ "connectionString": "postgres://appuser:apppassword@host:5432/otherdb" }
`,
});
const settings = await getSettings({ configFile: ".other-gmrc" });
expect(settings.connectionString).toEqual(
"postgres://appuser:apppassword@host:5432/otherdb",
);
mockFs.restore();
});
});
8 changes: 8 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ yargs
.command(wrapHandler(compileCommand))
.command(wrapHandler(runCommand))

// Make sure options added here are represented in CommonArgv
.option("config", {
alias: "c",
type: "string",
description: "Optional path to gmrc file",
defaultDescription: ".gmrc[.js]",
})

.completion("completion", "Generate shell completion script.")
.epilogue(
process.env.GRAPHILE_SPONSOR
Expand Down
67 changes: 59 additions & 8 deletions src/commands/_common.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
import { constants, promises as fsp } from "fs";
import * as JSON5 from "json5";
import { resolve } from "path";
import { parse } from "pg-connection-string";

import { Settings } from "../settings";

export const GMRC_PATH = `${process.cwd()}/.gmrc`;
export const GMRCJS_PATH = `${GMRC_PATH}.js`;
export const DEFAULT_GMRC_PATH = `${process.cwd()}/.gmrc`;
export const DEFAULT_GMRCJS_PATH = `${DEFAULT_GMRC_PATH}.js`;

/**
* Represents the option flags that are valid for all commands (see
* src/cli.ts).
*/
export interface CommonArgv {
/**
* Optional path to the gmrc file.
*/
config?: string;
}

export async function exists(path: string): Promise<boolean> {
try {
Expand All @@ -30,20 +42,59 @@ export async function getSettingsFromJSON(path: string): Promise<Settings> {
}
}

export async function getSettings(): Promise<Settings> {
if (await exists(GMRC_PATH)) {
return getSettingsFromJSON(GMRC_PATH);
} else if (await exists(GMRCJS_PATH)) {
/**
* Options passed to the getSettings function.
*/
interface Options {
/**
* Optional path to the gmrc config path to use; if not provided we'll fall
* back to `./.gmrc` and `./.gmrc.js`.
*
* This must be the full path, including extension. If the extension is `.js`
* then we'll use `require` to import it, otherwise we'll read it as JSON5.
*/
configFile?: string;
}

/**
* Gets the raw settings from the relevant .gmrc file. Does *not* validate the
* settings - the result of this call should not be trusted. Pass the result of
* this function to `parseSettings` to get validated settings.
*/
export async function getSettings(options: Options = {}): Promise<Settings> {
const { configFile } = options;
const tryRequire = (path: string): Settings => {
// If the file is e.g. `foo.js` then Node `require('foo.js')` would look in
// `node_modules`; we don't want this - instead force it to be a relative
// path.
const relativePath = resolve(process.cwd(), path);

try {
return require(GMRCJS_PATH);
return require(relativePath);
} catch (e) {
throw new Error(
`Failed to import '${GMRCJS_PATH}'; error:\n ${e.stack.replace(
`Failed to import '${relativePath}'; error:\n ${e.stack.replace(
/\n/g,
"\n ",
)}`,
);
}
};

if (configFile != null) {
if (!(await exists(configFile))) {
throw new Error(`Failed to import '${configFile}': file not found`);
}

if (configFile.endsWith(".js")) {
return tryRequire(configFile);
} else {
return await getSettingsFromJSON(configFile);
}
} else if (await exists(DEFAULT_GMRC_PATH)) {
return await getSettingsFromJSON(DEFAULT_GMRC_PATH);
} else if (await exists(DEFAULT_GMRCJS_PATH)) {
return tryRequire(DEFAULT_GMRCJS_PATH);
} else {
throw new Error(
"No .gmrc file found; please run `graphile-migrate init` first.",
Expand Down
15 changes: 7 additions & 8 deletions src/commands/commit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@ import {
} from "../migration";
import { ParsedSettings, parseSettings, Settings } from "../settings";
import { sluggify } from "../sluggify";
import { getSettings } from "./_common";
import { CommonArgv, getSettings } from "./_common";
import { _migrate } from "./migrate";
import { _reset } from "./reset";

interface CommitArgv extends CommonArgv {
message?: string;
}

function omit<T extends object, K extends keyof T>(
obj: T,
keys: K[],
Expand Down Expand Up @@ -128,12 +132,7 @@ export async function commit(
return _commit(parsedSettings, message);
}

export const commitCommand: CommandModule<
never,
{
message?: string;
}
> = {
export const commitCommand: CommandModule<never, CommitArgv> = {
command: "commit",
aliases: [],
describe:
Expand All @@ -151,6 +150,6 @@ export const commitCommand: CommandModule<
if (argv.message !== undefined && !argv.message) {
throw new Error("Missing or empty commit message after --message flag");
}
await commit(await getSettings(), argv.message);
await commit(await getSettings({ configFile: argv.config }), argv.message);
},
};
15 changes: 7 additions & 8 deletions src/commands/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { CommandModule } from "yargs";

import { compilePlaceholders } from "../migration";
import { parseSettings, Settings } from "../settings";
import { getSettings, readStdin } from "./_common";
import { CommonArgv, getSettings, readStdin } from "./_common";

interface CompileArgv extends CommonArgv {
shadow?: boolean;
}

export async function compile(
settings: Settings,
Expand All @@ -14,12 +18,7 @@ export async function compile(
return compilePlaceholders(parsedSettings, content, shadow);
}

export const compileCommand: CommandModule<
{},
{
shadow?: boolean;
}
> = {
export const compileCommand: CommandModule<{}, CompileArgv> = {
command: "compile [file]",
aliases: [],
describe: `\
Expand All @@ -32,7 +31,7 @@ Compiles a SQL file, inserting all the placeholders and returning the result to
},
},
handler: async argv => {
const settings = await getSettings();
const settings = await getSettings({ configFile: argv.config });
const content =
typeof argv.file === "string"
? await fsp.readFile(argv.file, "utf8")
Expand Down
Loading

0 comments on commit 594f7a5

Please sign in to comment.