diff --git a/src/commands/stack/add.ts b/src/commands/stack/add.ts index 4aca0217..6464b817 100644 --- a/src/commands/stack/add.ts +++ b/src/commands/stack/add.ts @@ -31,7 +31,7 @@ export default class AddStackCommand extends Command { ]; async run() { - const config = ShellConfig.read({}); + const config = ShellConfig.read({}, this); await this.execute(config); } diff --git a/src/commands/stack/list.ts b/src/commands/stack/list.ts index 96bb63eb..3457acba 100644 --- a/src/commands/stack/list.ts +++ b/src/commands/stack/list.ts @@ -10,7 +10,7 @@ export default class ListStackCommand extends Command { static examples = ["$ fauna stack list"]; async run() { - const config = ShellConfig.read({}); + const config = ShellConfig.read({}, this); await this.execute(config); } diff --git a/src/commands/stack/select.ts b/src/commands/stack/select.ts index 907d3067..2f7775d1 100644 --- a/src/commands/stack/select.ts +++ b/src/commands/stack/select.ts @@ -14,7 +14,7 @@ export default class SelectStackCommand extends Command { static examples = ["$ fauna stack select my-stack"]; async run() { - const config = ShellConfig.read({}); + const config = ShellConfig.read({}, this); await this.execute(config); } diff --git a/src/lib/config/index.ts b/src/lib/config/index.ts index 97cd08b9..802647ab 100644 --- a/src/lib/config/index.ts +++ b/src/lib/config/index.ts @@ -129,6 +129,11 @@ export type EndpointConfig = { graphqlPort: number; }; +export interface LogChannel { + warn(_: string): void; + log(_: string): void; +} + export class ShellConfig { // fields from CLI and files flags: Config; @@ -144,14 +149,14 @@ export class ShellConfig { // The path to the project config. projectPath: string | undefined; - static read(flags: any) { + static read(flags: any, log?: LogChannel) { const rootConfig = ini.parse(readFileOpt(getRootConfigPath())); const projectConfigPath = getProjectConfigPath(); const projectConfig = projectConfigPath ? ini.parse(readFile(projectConfigPath)) : undefined; - return new ShellConfig({ + const shellConfig = new ShellConfig({ flags, rootConfig, projectPath: @@ -160,6 +165,12 @@ export class ShellConfig { : undefined, projectConfig, }); + + if (log !== undefined) { + shellConfig.configErrors().forEach((err) => log.warn(err)); + } + + return shellConfig; } static readWithOverrides(opts?: ShellOpts): ShellConfig { @@ -284,6 +295,18 @@ export class ShellConfig { return this.endpoint!.makeScopedEndpoint(database, opts.role); }; + configErrors(): string[] { + if (this.rootConfig.invalidEndpoints.length > 0) { + return [ + `The following endpoint definitions in ${getRootConfigPath()} are invalid:\n ${this.rootConfig.invalidEndpoints.join( + "\n" + )}\n Resolve them by ensuring they have a secret defined or remove them if they are not needed.`, + ]; + } else { + return []; + } + } + /** * Saves the project config, if present. */ @@ -295,6 +318,13 @@ export class ShellConfig { * Saves the root config. */ saveRootConfig() { + if (this.rootConfig.invalidEndpoints.length > 0) { + throw new Error( + `The following endpoint definitions in ${getRootConfigPath()} are invalid:\n ${this.rootConfig.invalidEndpoints.join( + "\n" + )}\n Resolve them by ensuring they have a secret defined or remove them if they are not needed.` + ); + } this.rootConfig.save(getRootConfigPath()); } diff --git a/src/lib/config/root-config.ts b/src/lib/config/root-config.ts index 28b7cc61..373235ee 100644 --- a/src/lib/config/root-config.ts +++ b/src/lib/config/root-config.ts @@ -6,6 +6,7 @@ const ini = require("ini"); export class RootConfig { defaultEndpoint?: string; endpoints: { [key: string]: Endpoint }; + invalidEndpoints: string[]; constructor(config: Config) { this.defaultEndpoint = config.strOpt("default"); @@ -23,14 +24,24 @@ export class RootConfig { this.endpoints = Object.fromEntries( config .objectsIn("endpoint") - .map(([k, v]) => [k, Endpoint.fromConfig(k, v)]) + .filter(([k, v]) => Endpoint.fromConfig(k, v) !== undefined) + .map(([k, v]) => [k, Endpoint.fromConfig(k, v)!]) ); + this.invalidEndpoints = config + .objectsIn("endpoint") + .filter(([k, v]) => Endpoint.fromConfig(k, v) === undefined) + .map(([k, _]) => k); } else { this.endpoints = Object.fromEntries( config .allObjectsWhere((k) => k !== "default") - .map(([k, v]) => [k, Endpoint.fromConfig(k, v)]) + .filter(([k, v]) => Endpoint.fromConfig(k, v) !== undefined) + .map(([k, v]) => [k, Endpoint.fromConfig(k, v)!]) ); + this.invalidEndpoints = config + .allObjectsWhere((k) => k !== "default") + .filter(([k, v]) => Endpoint.fromConfig(k, v) === undefined) + .map(([k, _]) => k); } if (this.defaultEndpoint === "default") { @@ -108,15 +119,20 @@ export class Endpoint { graphqlHost: string; graphqlPort: number; - static fromConfig(name: string, config: Config) { - return new Endpoint({ - name: name, - secret: config.str("secret"), - url: Endpoint.getURLFromConfig(config), + static fromConfig(name: string, config: Config): Endpoint | undefined { + const secOpt = config.strOpt("secret"); + if (secOpt === undefined) { + return undefined; + } else { + return new Endpoint({ + name: name, + secret: secOpt, + url: Endpoint.getURLFromConfig(config), - graphqlHost: config.strOpt("graphqlHost"), - graphqlPort: config.numberOpt("graphqlPort"), - }); + graphqlHost: config.strOpt("graphqlHost"), + graphqlPort: config.numberOpt("graphqlPort"), + }); + } } constructor(opts: { diff --git a/src/lib/fauna-command.js b/src/lib/fauna-command.js index 7016ed58..2814b6bc 100644 --- a/src/lib/fauna-command.js +++ b/src/lib/fauna-command.js @@ -37,7 +37,7 @@ class FaunaCommand extends Command { const { flags: f, args: a } = await this.parse(this.constructor); this.flags = f; this.args = a; - this.shellConfig = ShellConfig.read(this.flags); + this.shellConfig = ShellConfig.read(this.flags, this); } success(msg) { diff --git a/test/commands/endpoint.test.ts b/test/commands/endpoint.test.ts index 95572c42..ff0d3c75 100644 --- a/test/commands/endpoint.test.ts +++ b/test/commands/endpoint.test.ts @@ -69,6 +69,7 @@ describe("endpoint:add", () => { graphqlPort: 443, }, }, + invalidEndpoints: [], }); expect(ctx.config.saveRootConfig.calledOnce).to.be.true; }); @@ -122,6 +123,7 @@ describe("endpoint:add", () => { graphqlPort: 443, }, }, + invalidEndpoints: [], }); expect(ctx.config.saveRootConfig.calledOnce).to.be.true; }); @@ -191,6 +193,7 @@ describe("endpoint:remove", () => { graphqlPort: 443, }, }, + invalidEndpoints: [], }); expect(ctx.config.saveRootConfig.calledOnce).to.be.true; }); @@ -230,6 +233,7 @@ describe("endpoint:remove", () => { graphqlPort: 443, }, }, + invalidEndpoints: [], }); expect(ctx.config.saveRootConfig.calledOnce).to.be.true; }); diff --git a/test/lib/config.test.ts b/test/lib/config.test.ts index 17bdd9ad..51cff81f 100644 --- a/test/lib/config.test.ts +++ b/test/lib/config.test.ts @@ -4,6 +4,7 @@ import { ShellConfig, ShellOpts, getProjectConfigPath, + getRootConfigPath, } from "../../src/lib/config"; import sinon from "sinon"; @@ -105,6 +106,23 @@ describe("root config", () => { url: "https://db.fauna.com", }); }); + + it("fails to save if the root config has invalid endpoints", () => { + const invalidConfig = new ShellConfig({ + rootConfig: { + default: "my-endpoint", + "my-endpoint": { + secret: "fn1234", + url: "http://localhost:8443", + }, + invalidEndpoints: ["invalid-endpoint"], + }, + }); + const expectedMsg = `The following endpoint definitions in ${getRootConfigPath()} are invalid:\n ${invalidConfig.rootConfig.invalidEndpoints.join( + "\n" + )}\n Resolve them by ensuring they have a secret defined or remove them if they are not needed.`; + expect(() => invalidConfig.saveRootConfig()).to.throw(expectedMsg); + }); }); describe("root config with flags", () => {