Skip to content

Commit 594f7a5

Browse files
benjieben-pr-p
andauthored
feat: add --config option for custom .grmc(.js) path (#113)
Co-authored-by: ben-pr-p <[email protected]>
1 parent a4f56bf commit 594f7a5

File tree

13 files changed

+233
-101
lines changed

13 files changed

+233
-101
lines changed

README.md

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,8 @@ Commands:
223223
graphile-migrate completion Generate shell completion script.
224224
225225
Options:
226-
--help Show help [boolean]
226+
--help Show help [boolean]
227+
--config, -c Optional path to gmrc file [string] [default: .gmrc[.js]]
227228
228229
You are running graphile-migrate v1.0.2.
229230
```
@@ -238,8 +239,9 @@ Initializes a graphile-migrate project by creating a `.gmrc` file and
238239
`migrations` folder.
239240
240241
Options:
241-
--help Show help [boolean]
242-
--folder Use a folder rather than a file for the current migration.
242+
--help Show help [boolean]
243+
--config, -c Optional path to gmrc file [string] [default: .gmrc[.js]]
244+
--folder Use a folder rather than a file for the current migration.
243245
[boolean] [default: false]
244246
```
245247

@@ -254,6 +256,7 @@ For use in production and development.
254256
255257
Options:
256258
--help Show help [boolean]
259+
--config, -c Optional path to gmrc file [string] [default: .gmrc[.js]]
257260
--shadow Apply migrations to the shadow DB (for development).
258261
[boolean] [default: false]
259262
--forceActions Run beforeAllMigrations and afterAllMigrations actions even if
@@ -270,9 +273,11 @@ Runs any un-executed committed migrations and then runs and watches the current
270273
migration, re-running it on any change. For development.
271274
272275
Options:
273-
--help Show help [boolean]
274-
--once Runs the current migration and then exits.[boolean] [default: false]
275-
--shadow Applies changes to shadow DB. [boolean] [default: false]
276+
--help Show help [boolean]
277+
--config, -c Optional path to gmrc file [string] [default: .gmrc[.js]]
278+
--once Runs the current migration and then exits.
279+
[boolean] [default: false]
280+
--shadow Applies changes to shadow DB. [boolean] [default: false]
276281
```
277282

278283

@@ -286,6 +291,7 @@ current migration. Resets the shadow database.
286291
287292
Options:
288293
--help Show help [boolean]
294+
--config, -c Optional path to gmrc file [string] [default: .gmrc[.js]]
289295
--message, -m Optional commit message to label migration, must not contain
290296
newlines. [string]
291297
```
@@ -308,7 +314,8 @@ should result in the exact same hash. Development only, and liable to cause
308314
conflicts with other developers - be careful.
309315
310316
Options:
311-
--help Show help [boolean]
317+
--help Show help [boolean]
318+
--config, -c Optional path to gmrc file [string] [default: .gmrc[.js]]
312319
```
313320

314321

@@ -321,10 +328,11 @@ Drops and re-creates the database, re-running all committed migrations from the
321328
start. **HIGHLY DESTRUCTIVE**.
322329
323330
Options:
324-
--help Show help [boolean]
325-
--shadow Applies migrations to shadow DB. [boolean] [default: false]
326-
--erase This is your double opt-in to make it clear this DELETES EVERYTHING.
327-
[boolean] [default: false]
331+
--help Show help [boolean]
332+
--config, -c Optional path to gmrc file [string] [default: .gmrc[.js]]
333+
--shadow Applies migrations to shadow DB. [boolean] [default: false]
334+
--erase This is your double opt-in to make it clear this DELETES
335+
EVERYTHING. [boolean] [default: false]
328336
```
329337

330338

@@ -345,6 +353,7 @@ output.
345353
346354
Options:
347355
--help Show help [boolean]
356+
--config, -c Optional path to gmrc file [string] [default: .gmrc[.js]]
348357
--skipDatabase Skip checks that require a database connection.
349358
[boolean] [default: false]
350359
```
@@ -359,8 +368,9 @@ Compiles a SQL file, inserting all the placeholders and returning the result to
359368
STDOUT
360369
361370
Options:
362-
--help Show help [boolean]
363-
--shadow Apply shadow DB placeholders (for development).
371+
--help Show help [boolean]
372+
--config, -c Optional path to gmrc file [string] [default: .gmrc[.js]]
373+
--shadow Apply shadow DB placeholders (for development).
364374
[boolean] [default: false]
365375
```
366376

@@ -377,6 +387,7 @@ run against the same database (via GM_DBURL envvar) unless --shadow or
377387
378388
Options:
379389
--help Show help [boolean]
390+
--config, -c Optional path to gmrc file [string] [default: .gmrc[.js]]
380391
--shadow Apply to the shadow database (for development).
381392
[boolean] [default: false]
382393
--root Run the file using the root user (but application database).
@@ -490,6 +501,10 @@ opening brace `{` would be prepended with `module.exports =`:
490501
module.exports = {
491502
```
492503
504+
All commands accept an optional `--config` parameter with a custom path to a
505+
`.gmrc(.js)` file. This is useful if, for example, you have a monorepo or other
506+
project with multiple interacting databases.
507+
493508
### Windows
494509
495510
Since committed migrations utilize hashes to verify file integrity, the

__tests__/settings.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import "./helpers"; // Side effects - must come first
33
import * as mockFs from "mock-fs";
44
import * as path from "path";
55

6+
import { DEFAULT_GMRC_PATH, getSettings } from "../src/commands/_common";
67
import {
78
makeRootDatabaseConnectionString,
89
ParsedSettings,
@@ -278,3 +279,36 @@ describe("actions", () => {
278279
`);
279280
});
280281
});
282+
283+
describe("gmrc path", () => {
284+
it("defaults to .gmrc", async () => {
285+
mockFs.restore();
286+
mockFs({
287+
[DEFAULT_GMRC_PATH]: `
288+
{ "connectionString": "postgres://appuser:apppassword@host:5432/defaultdb" }
289+
`,
290+
});
291+
const settings = await getSettings();
292+
expect(settings.connectionString).toEqual(
293+
"postgres://appuser:apppassword@host:5432/defaultdb",
294+
);
295+
mockFs.restore();
296+
});
297+
298+
it("accepts an override and follows it", async () => {
299+
mockFs.restore();
300+
mockFs({
301+
[DEFAULT_GMRC_PATH]: `
302+
{ "connectionString": "postgres://appuser:apppassword@host:5432/defaultdb" }
303+
`,
304+
".other-gmrc": `
305+
{ "connectionString": "postgres://appuser:apppassword@host:5432/otherdb" }
306+
`,
307+
});
308+
const settings = await getSettings({ configFile: ".other-gmrc" });
309+
expect(settings.connectionString).toEqual(
310+
"postgres://appuser:apppassword@host:5432/otherdb",
311+
);
312+
mockFs.restore();
313+
});
314+
});

src/cli.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,14 @@ yargs
7878
.command(wrapHandler(compileCommand))
7979
.command(wrapHandler(runCommand))
8080

81+
// Make sure options added here are represented in CommonArgv
82+
.option("config", {
83+
alias: "c",
84+
type: "string",
85+
description: "Optional path to gmrc file",
86+
defaultDescription: ".gmrc[.js]",
87+
})
88+
8189
.completion("completion", "Generate shell completion script.")
8290
.epilogue(
8391
process.env.GRAPHILE_SPONSOR

src/commands/_common.ts

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
11
import { constants, promises as fsp } from "fs";
22
import * as JSON5 from "json5";
3+
import { resolve } from "path";
34
import { parse } from "pg-connection-string";
45

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

7-
export const GMRC_PATH = `${process.cwd()}/.gmrc`;
8-
export const GMRCJS_PATH = `${GMRC_PATH}.js`;
8+
export const DEFAULT_GMRC_PATH = `${process.cwd()}/.gmrc`;
9+
export const DEFAULT_GMRCJS_PATH = `${DEFAULT_GMRC_PATH}.js`;
10+
11+
/**
12+
* Represents the option flags that are valid for all commands (see
13+
* src/cli.ts).
14+
*/
15+
export interface CommonArgv {
16+
/**
17+
* Optional path to the gmrc file.
18+
*/
19+
config?: string;
20+
}
921

1022
export async function exists(path: string): Promise<boolean> {
1123
try {
@@ -30,20 +42,59 @@ export async function getSettingsFromJSON(path: string): Promise<Settings> {
3042
}
3143
}
3244

33-
export async function getSettings(): Promise<Settings> {
34-
if (await exists(GMRC_PATH)) {
35-
return getSettingsFromJSON(GMRC_PATH);
36-
} else if (await exists(GMRCJS_PATH)) {
45+
/**
46+
* Options passed to the getSettings function.
47+
*/
48+
interface Options {
49+
/**
50+
* Optional path to the gmrc config path to use; if not provided we'll fall
51+
* back to `./.gmrc` and `./.gmrc.js`.
52+
*
53+
* This must be the full path, including extension. If the extension is `.js`
54+
* then we'll use `require` to import it, otherwise we'll read it as JSON5.
55+
*/
56+
configFile?: string;
57+
}
58+
59+
/**
60+
* Gets the raw settings from the relevant .gmrc file. Does *not* validate the
61+
* settings - the result of this call should not be trusted. Pass the result of
62+
* this function to `parseSettings` to get validated settings.
63+
*/
64+
export async function getSettings(options: Options = {}): Promise<Settings> {
65+
const { configFile } = options;
66+
const tryRequire = (path: string): Settings => {
67+
// If the file is e.g. `foo.js` then Node `require('foo.js')` would look in
68+
// `node_modules`; we don't want this - instead force it to be a relative
69+
// path.
70+
const relativePath = resolve(process.cwd(), path);
71+
3772
try {
38-
return require(GMRCJS_PATH);
73+
return require(relativePath);
3974
} catch (e) {
4075
throw new Error(
41-
`Failed to import '${GMRCJS_PATH}'; error:\n ${e.stack.replace(
76+
`Failed to import '${relativePath}'; error:\n ${e.stack.replace(
4277
/\n/g,
4378
"\n ",
4479
)}`,
4580
);
4681
}
82+
};
83+
84+
if (configFile != null) {
85+
if (!(await exists(configFile))) {
86+
throw new Error(`Failed to import '${configFile}': file not found`);
87+
}
88+
89+
if (configFile.endsWith(".js")) {
90+
return tryRequire(configFile);
91+
} else {
92+
return await getSettingsFromJSON(configFile);
93+
}
94+
} else if (await exists(DEFAULT_GMRC_PATH)) {
95+
return await getSettingsFromJSON(DEFAULT_GMRC_PATH);
96+
} else if (await exists(DEFAULT_GMRCJS_PATH)) {
97+
return tryRequire(DEFAULT_GMRCJS_PATH);
4798
} else {
4899
throw new Error(
49100
"No .gmrc file found; please run `graphile-migrate init` first.",

src/commands/commit.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,14 @@ import {
1717
} from "../migration";
1818
import { ParsedSettings, parseSettings, Settings } from "../settings";
1919
import { sluggify } from "../sluggify";
20-
import { getSettings } from "./_common";
20+
import { CommonArgv, getSettings } from "./_common";
2121
import { _migrate } from "./migrate";
2222
import { _reset } from "./reset";
2323

24+
interface CommitArgv extends CommonArgv {
25+
message?: string;
26+
}
27+
2428
function omit<T extends object, K extends keyof T>(
2529
obj: T,
2630
keys: K[],
@@ -128,12 +132,7 @@ export async function commit(
128132
return _commit(parsedSettings, message);
129133
}
130134

131-
export const commitCommand: CommandModule<
132-
never,
133-
{
134-
message?: string;
135-
}
136-
> = {
135+
export const commitCommand: CommandModule<never, CommitArgv> = {
137136
command: "commit",
138137
aliases: [],
139138
describe:
@@ -151,6 +150,6 @@ export const commitCommand: CommandModule<
151150
if (argv.message !== undefined && !argv.message) {
152151
throw new Error("Missing or empty commit message after --message flag");
153152
}
154-
await commit(await getSettings(), argv.message);
153+
await commit(await getSettings({ configFile: argv.config }), argv.message);
155154
},
156155
};

src/commands/compile.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { CommandModule } from "yargs";
33

44
import { compilePlaceholders } from "../migration";
55
import { parseSettings, Settings } from "../settings";
6-
import { getSettings, readStdin } from "./_common";
6+
import { CommonArgv, getSettings, readStdin } from "./_common";
7+
8+
interface CompileArgv extends CommonArgv {
9+
shadow?: boolean;
10+
}
711

812
export async function compile(
913
settings: Settings,
@@ -14,12 +18,7 @@ export async function compile(
1418
return compilePlaceholders(parsedSettings, content, shadow);
1519
}
1620

17-
export const compileCommand: CommandModule<
18-
{},
19-
{
20-
shadow?: boolean;
21-
}
22-
> = {
21+
export const compileCommand: CommandModule<{}, CompileArgv> = {
2322
command: "compile [file]",
2423
aliases: [],
2524
describe: `\
@@ -32,7 +31,7 @@ Compiles a SQL file, inserting all the placeholders and returning the result to
3231
},
3332
},
3433
handler: async argv => {
35-
const settings = await getSettings();
34+
const settings = await getSettings({ configFile: argv.config });
3635
const content =
3736
typeof argv.file === "string"
3837
? await fsp.readFile(argv.file, "utf8")

0 commit comments

Comments
 (0)