Skip to content

Commit

Permalink
feat(dotenv): ability to have local and environment-specific dotenv f…
Browse files Browse the repository at this point in the history
…iles (#162)
  • Loading branch information
goldcaddy77 authored Jul 5, 2019
1 parent 90c1ef7 commit 0dcf472
Show file tree
Hide file tree
Showing 22 changed files with 145 additions and 59 deletions.
1 change: 1 addition & 0 deletions examples/1-simple-model/.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
NODE_ENV=development
WARTHOG_APP_HOST=localhost
WARTHOG_APP_PORT=4100
WARTHOG_DB_DATABASE=warthog-example-1
Expand Down
1 change: 1 addition & 0 deletions examples/2-complex-example/.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
NODE_ENV=development
WARTHOG_APP_HOST=localhost
WARTHOG_APP_PORT=4100
WARTHOG_DB_DATABASE=warthog-example-2
Expand Down
10 changes: 6 additions & 4 deletions examples/2-complex-example/tools/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import { getBindingError, logger } from '../../../src';

import { getServer } from '../src/server';

if (process.env.NODE_ENV !== 'development') {
throw 'Seeding only available in development environment';
}

const NUM_USERS = 10;

async function seedDatabase() {
const server = getServer({ openPlayground: false });

// NOTE: this has to be after we instantiate the server, because the server will actually load the environment variables from .env and set process.env.NODE_ENV
if (process.env.NODE_ENV !== 'development') {
throw 'Seeding only available in development environment';
}

await server.start();

let binding;
Expand Down
1 change: 1 addition & 0 deletions examples/3-one-to-many-relationship/.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
NODE_ENV=development
WARTHOG_APP_HOST=localhost
WARTHOG_APP_PORT=4100
WARTHOG_DB_DATABASE=warthog-example-3
Expand Down
1 change: 1 addition & 0 deletions examples/4-many-to-many-relationship/.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
NODE_ENV=development
WARTHOG_APP_HOST=localhost
WARTHOG_APP_PORT=4100
WARTHOG_DB_DATABASE=warthog-example-4
Expand Down
1 change: 1 addition & 0 deletions examples/5-migrations/.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
NODE_ENV=development
WARTHOG_APP_HOST=localhost
WARTHOG_APP_PORT=4100
WARTHOG_DB_CONNECTION=postgres
Expand Down
1 change: 1 addition & 0 deletions examples/6-base-service/.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
NODE_ENV=development
WARTHOG_APP_HOST=localhost
WARTHOG_APP_PORT=4100
WARTHOG_DB_DATABASE=warthog-example-6
Expand Down
10 changes: 6 additions & 4 deletions examples/6-base-service/tools/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import { getBindingError, logger } from '../../../src';

import { getServer } from '../src/server';

if (process.env.NODE_ENV !== 'development') {
throw 'Seeding only available in development environment';
}

const NUM_USERS = 100;

async function seedDatabase() {
const server = getServer({ introspection: true, openPlayground: false });

// NOTE: this has to be after we instantiate the server, because the server will actually load the environment variables from .env and set process.env.NODE_ENV
if (process.env.NODE_ENV !== 'development') {
throw 'Seeding only available in development environment';
}

await server.start();

let binding;
Expand Down
1 change: 1 addition & 0 deletions examples/7-feature-flags/.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
NODE_ENV=development
WARTHOG_APP_HOST=localhost
WARTHOG_APP_PORT=4100
WARTHOG_DB_DATABASE=warthog-example-feature-flag
Expand Down
10 changes: 6 additions & 4 deletions examples/7-feature-flags/tools/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import { Logger } from '../src/logger';
import { Environment, FeatureFlag, FeatureFlagUser, Project, Segment } from '../src/models';
import { getServer } from '../src/server';

if (process.env.NODE_ENV !== 'development') {
throw 'Seeding only available in development environment';
}

async function seedDatabase() {
const server = getServer({ openPlayground: false });

// NOTE: this has to be after we instantiate the server, because the server will actually load the environment variables from .env and set process.env.NODE_ENV
if (process.env.NODE_ENV !== 'development') {
throw 'Seeding only available in development environment';
}

await server.start();

let binding: Binding;
Expand Down
1 change: 1 addition & 0 deletions examples/8-performance/.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
NODE_ENV=development
WARTHOG_APP_HOST=localhost
WARTHOG_APP_PORT=4100
WARTHOG_DB_DATABASE=warthog-example-8
Expand Down
10 changes: 6 additions & 4 deletions examples/8-performance/tools/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ import { Logger } from '../src/logger';
import { User } from '../src/models';
import { getServer } from '../src/server';

if (process.env.NODE_ENV !== 'development') {
throw 'Seeding only available in development environment';
}

async function seedDatabase() {
// Turn off logging to seed database
process.env.WARTHOG_DB_LOGGING = 'none';

const server = getServer({ introspection: true, openPlayground: false });

// NOTE: this has to be after we instantiate the server, because the server will actually load the environment variables from .env and set process.env.NODE_ENV
if (process.env.NODE_ENV !== 'development') {
throw 'Seeding only available in development environment';
}

await server.start();

let binding: Binding;
Expand Down
2 changes: 2 additions & 0 deletions examples/9-production/.env.local
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
WARTHOG_DB_PASSWORD=SUPER_SECRET_PASSWORD
WARTHOG_FOO_BAR=BAZ
10 changes: 6 additions & 4 deletions examples/9-production/tools/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ import { Logger } from '../src/logger';
import { User } from '../src/models';
import { getServer } from '../src/server';

if (process.env.NODE_ENV !== 'development') {
throw 'Seeding only available in development environment';
}

async function seedDatabase() {
// Turn off logging to seed database
process.env.WARTHOG_DB_LOGGING = 'none';

const server = getServer({ introspection: true, openPlayground: false });

// NOTE: this has to be after we instantiate the server, because the server will actually load the environment variables from .env and set process.env.NODE_ENV
if (process.env.NODE_ENV !== 'development' && process.env.WARTHOG_DB_OVERRIDE !== 'true') {
throw 'Seeding only available in development environment';
}

await server.start();

let binding: Binding;
Expand Down
27 changes: 17 additions & 10 deletions src/cli/extensions/db-extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,9 @@ module.exports = (toolbox: GluegunToolbox) => {

toolbox.db = {
create: async function create(database: string) {
if (process.env.NODE_ENV !== 'development' && process.env.WARTHOG_DB_OVERRIDE !== 'true') {
return error(
'Cannot create database without setting WARTHOG_DB_OVERRIDE environment variable to `true`'
);
}
const config = load();
validateNodeEnv(config['NODE_ENV'], toolbox, 'create');

const createDb = util.promisify(pgtools.createdb) as Function;

try {
Expand All @@ -36,12 +33,9 @@ module.exports = (toolbox: GluegunToolbox) => {
info(`Database '${database}' created!`);
},
drop: async function drop() {
if (process.env.NODE_ENV !== 'development' && process.env.WARTHOG_DB_OVERRIDE !== 'true') {
return error(
'Cannot drop database without setting WARTHOG_DB_OVERRIDE environment variable to `true`'
);
}
const config = load();
validateNodeEnv(config['NODE_ENV'], toolbox, 'drop, action: string');

const database = config.get('DB_DATABASE');
const dropDb = util.promisify(pgtools.dropdb) as Function;

Expand Down Expand Up @@ -132,3 +126,16 @@ async function getPgConfig(config: any) {
port: config.get('DB_PORT')
};
}

function validateNodeEnv(env: string, toolbox: Toolbox, action: string) {
if (!env) {
toolbox.print.error('NODE_ENV must be set');
process.exit(1);
}
if (env !== 'development' && process.env.WARTHOG_DB_OVERRIDE !== 'true') {
toolbox.print.error(
`Cannot ${action} database without setting WARTHOG_DB_OVERRIDE environment variable to 'true'`
);
process.exit(1);
}
}
58 changes: 36 additions & 22 deletions src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,33 +49,13 @@ export class Config {
PROJECT_ROOT: string;
container: Container;
logger?: Logger;
NODE_ENV?: string;

// The full config object
config: any;

constructor(private options: ConfigOptions = {}) {
this.PROJECT_ROOT = process.cwd();
const dotenvPath = options.dotenvPath || process.cwd();

// Load .env.local file for super secrets
// With dotenv, the first time an entry ends up in process.env, it stays, so local items should take precedence
let file = path.join(dotenvPath, '.env.local');
// console.log('file', file);
if (fs.existsSync(file)) {
// console.log('1', this.warthogEnvVariables());
dotenv.config({ path: file });
// console.log('2', this.warthogEnvVariables());
}

// Load .env file
file = path.join(dotenvPath, '.env');
// console.log('file', file);
if (fs.existsSync(file)) {
// console.log('3', this.warthogEnvVariables());
dotenv.config({ path: file });
// console.log('4', this.warthogEnvVariables());
}

this.container = options.container || Container;
this.logger = options.logger;

Expand Down Expand Up @@ -111,11 +91,45 @@ export class Config {
WARTHOG_DB_SYNCHRONIZE: 'true'
};

const dotenvPath = options.dotenvPath || this.PROJECT_ROOT;
this.NODE_ENV = this.determineNodeEnv(dotenvPath);
this.loadDotenvFiles(dotenvPath);

return this;
}

// Allow NODE_ENV to be set in the .env file. Check for this first here and then fall back on
// the environment variable. The reason we do this is because using dotenvi will allow us to switch
// between environments. If we require an actual environment variable to be set then we'll have to set
// and unset the value in the current terminal buffer.
determineNodeEnv(dotenvPath: string) {
let nodeEnv = process.env.NODE_ENV;

const filepath = path.join(dotenvPath, '.env');
if (fs.existsSync(filepath)) {
const config = dotenv.parse(fs.readFileSync(filepath));
if (config.NODE_ENV) {
nodeEnv = config.NODE_ENV;
}
}

return (this.NODE_ENV = process.env.NODE_ENV = nodeEnv);
}

loadDotenvFiles(dotenvPath: string) {
// .local files are for secrets, load those first
const files = [`.env.local.${this.NODE_ENV}`, '.env.local', '.env'];

files.forEach((filename: string) => {
const filepath = path.join(dotenvPath, filename);
if (fs.existsSync(filepath)) {
dotenv.config({ path: filepath });
}
});
}

loadSync(): Config {
const devOptions = process.env.NODE_ENV === 'development' ? this.devDefaults : {};
const devOptions = this.NODE_ENV === 'development' ? this.devDefaults : {};
const configFile = this.loadStaticConfigSync();

// Config is loaded as a waterfall. Items at the top of the object are overwritten by those below, so the order is:
Expand Down
11 changes: 4 additions & 7 deletions src/core/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ export class Server<C extends BaseContext> {
config: Config;
apolloConfig?: ApolloServerExpressConfig;
authChecker?: AuthChecker<C>;
autoGenerateFiles: boolean;
connection!: Connection;
container: Container;
graphQLServer!: ApolloServer;
Expand All @@ -64,10 +63,6 @@ export class Server<C extends BaseContext> {
private appOptions: ServerOptions<C>,
private dbOptions: Partial<ConnectionOptions> = {}
) {
if (!process.env.NODE_ENV) {
throw new Error("NODE_ENV must be set - use 'development' locally");
}

if (typeof this.appOptions.host !== 'undefined') {
process.env.WARTHOG_APP_HOST = this.appOptions.host;
// When we move to v2.0 we'll officially deprecate these config values in favor of ENV vars
Expand Down Expand Up @@ -110,7 +105,9 @@ export class Server<C extends BaseContext> {
// module to think they were set by the user
this.config = new Config({ container: this.container, logger: this.logger }).loadSync();

this.autoGenerateFiles = this.config.get('AUTO_GENERATE_FILES') === 'true';
if (!process.env.NODE_ENV) {
throw new Error("NODE_ENV must be set - use 'development' locally");
}
}

getLogger(): Logger {
Expand Down Expand Up @@ -197,7 +194,7 @@ export class Server<C extends BaseContext> {
async start() {
debug('start:start');
await this.establishDBConnection();
if (this.autoGenerateFiles) {
if (this.config.get('AUTO_GENERATE_FILES') === 'true') {
await this.generateFiles();
}
await this.buildGraphQLSchema();
Expand Down
14 changes: 14 additions & 0 deletions src/core/tests/dotenv-files/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
WARTHOG_ENV=111
WARTHOG_A=ENV
WARTHOG_B=ENV
WARTHOG_C=ENV
WARTHOG_D=ENV

# required variables
WARTHOG_APP_HOST=localhost
WARTHOG_APP_PORT=4100
WARTHOG_DB_DATABASE=warthog-example-1
WARTHOG_DB_USERNAME=postgres
WARTHOG_DB_PASSWORD=
WARTHOG_DB_SYNCHRONIZE=true
WARTHOG_DB_HOST=localhost
4 changes: 4 additions & 0 deletions src/core/tests/dotenv-files/.env.local
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
WARTHOG_ENV_LOCAL=222
WARTHOG_B=ENV_LOCAL
WARTHOG_C=ENV_LOCAL
WARTHOG_D=ENV_LOCAL
3 changes: 3 additions & 0 deletions src/core/tests/dotenv-files/.env.local.development
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
WARTHOG_ENV_LOCAL_DEVELOPMENT=333
WARTHOG_C=ENV_LOCAL_DEVELOPMENT
WARTHOG_D=ENV_LOCAL_DEVELOPMENT
6 changes: 6 additions & 0 deletions src/core/tests/dotenv-files/.env.local.production
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
WARTHOG_ENV_LOCAL_PRODUCTION=444

WARTHOG_D=ENV_LOCAL_PRODUCTION

# Make sure we use the actual environment variable
WARTHOG_PROPER_ENV_VARIABLE=ERROR
21 changes: 21 additions & 0 deletions src/core/tests/dotenv-files/dotenv.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Config } from '../../config';

describe('Dotenv files', () => {
it('Pulls config in correct order', async () => {
process.env = {
NODE_ENV: 'development',
WARTHOG_PROPER_ENV_VARIABLE: '12345'
};

new Config({ dotenvPath: __dirname }).loadSync();

expect(process.env.WARTHOG_PROPER_ENV_VARIABLE).toEqual('12345');
expect(process.env.WARTHOG_ENV_LOCAL_DEVELOPMENT).toEqual('333');
expect(process.env.WARTHOG_C).toEqual('ENV_LOCAL_DEVELOPMENT');
expect(process.env.WARTHOG_D).toEqual('ENV_LOCAL_DEVELOPMENT');
expect(process.env.WARTHOG_ENV_LOCAL).toEqual('222');
expect(process.env.WARTHOG_B).toEqual('ENV_LOCAL');
expect(process.env.WARTHOG_ENV).toEqual('111');
expect(process.env.WARTHOG_A).toEqual('ENV');
});
});

0 comments on commit 0dcf472

Please sign in to comment.