Skip to content

Commit 1912747

Browse files
authored
Merge pull request #149 from launchcodedev/env-options-trio
Env options trio type
2 parents 84d299e + da24228 commit 1912747

File tree

10 files changed

+314
-48
lines changed

10 files changed

+314
-48
lines changed

app-config-cli/src/validation.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,19 @@ import { FileSource, EnvironmentAliases, defaultAliases } from '@app-config/node
44
import { loadValidatedConfig } from '@app-config/config';
55

66
export interface Options {
7+
/** Where app-config files are */
78
directory?: string;
9+
/** Override for aliasing of environments */
810
environmentAliases?: EnvironmentAliases;
11+
/** If app-config should be validating in a "no current environment" state */
912
includeNoEnvironment?: boolean;
1013
}
1114

15+
/**
16+
* Loads and validations app-config values in every environment detectable.
17+
*
18+
* Uses a hueristic to find which environments are available, because these are arbitrary.
19+
*/
1220
export async function validateAllConfigVariants({
1321
directory = '.',
1422
environmentAliases = defaultAliases,
@@ -24,9 +32,9 @@ export async function validateAllConfigVariants({
2432
const appConfigEnvironments = new Set<string>();
2533

2634
for (const filename of appConfigFiles) {
27-
const environment = /^\.app-config\.(?:secrets\.)?(.*)\.(?:yml|yaml|json|json5|toml)$/.exec(
28-
filename,
29-
)?.[1];
35+
// extract the environment out, which is the first capture group
36+
const regex = /^\.app-config\.(?:secrets\.)?(.*)\.(?:yml|yaml|json|json5|toml)$/;
37+
const environment = regex.exec(filename)?.[1];
3038

3139
if (environment && environment !== 'meta' && environment !== 'schema') {
3240
appConfigEnvironments.add(environmentAliases[environment] ?? environment);

app-config-config/src/index.ts

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
defaultAliases,
1616
EnvironmentAliases,
1717
EnvironmentSource,
18+
asEnvOptions,
1819
} from '@app-config/node';
1920
import { markAllValuesAsSecret } from '@app-config/extensions';
2021
import { defaultExtensions, defaultEnvExtensions } from '@app-config/default-extensions';
@@ -90,6 +91,12 @@ export async function loadUnvalidatedConfig({
9091
const environmentAliases =
9192
environmentAliasesArg ?? meta.value.environmentAliases ?? defaultAliases;
9293

94+
const environmentOptions = asEnvOptions(
95+
environmentOverride,
96+
environmentAliases,
97+
environmentSourceNames,
98+
);
99+
93100
const parsingExtensions =
94101
parsingExtensionsArg ??
95102
defaultExtensions(environmentAliases, environmentOverride, undefined, environmentSourceNames);
@@ -108,21 +115,11 @@ export async function loadUnvalidatedConfig({
108115
logger.verbose(`Trying to read files for configuration`);
109116

110117
const [mainConfig, secrets] = await Promise.all([
111-
new FlexibleFileSource(
112-
join(directory, fileNameBase),
113-
environmentOverride,
114-
environmentAliases,
115-
undefined,
116-
environmentSourceNames,
117-
).read(parsingExtensions),
118-
119-
new FlexibleFileSource(
120-
join(directory, secretsFileNameBase),
121-
environmentOverride,
122-
environmentAliases,
123-
undefined,
124-
environmentSourceNames,
125-
)
118+
new FlexibleFileSource(join(directory, fileNameBase), undefined, environmentOptions).read(
119+
parsingExtensions,
120+
),
121+
122+
new FlexibleFileSource(join(directory, secretsFileNameBase), undefined, environmentOptions)
126123
.read(secretsFileExtensions)
127124
.catch((error) => {
128125
// NOTE: secrets are optional, so not finding them is normal

app-config-extensions/src/env-directive.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
import type { ParsingExtension } from '@app-config/core';
22
import { AppConfigError } from '@app-config/core';
33
import { named, forKey, keysToPath, validateOptions } from '@app-config/extension-utils';
4-
import { currentEnvironment, defaultAliases, EnvironmentAliases } from '@app-config/node';
4+
import {
5+
currentEnvironment,
6+
defaultAliases,
7+
asEnvOptions,
8+
EnvironmentAliases,
9+
} from '@app-config/node';
510

611
/** Looks up an environment-specific value ($env) */
712
export function envDirective(
813
aliases: EnvironmentAliases = defaultAliases,
914
environmentOverride?: string,
1015
environmentSourceNames?: string[] | string,
1116
): ParsingExtension {
12-
const environment = environmentOverride ?? currentEnvironment(aliases, environmentSourceNames);
1317
const metadata = { shouldOverride: true };
18+
const environment = currentEnvironment(
19+
asEnvOptions(environmentOverride, aliases, environmentSourceNames),
20+
);
1421

1522
return named(
1623
'$env',

app-config-extensions/src/env-var-directive.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
import type { ParsingExtension } from '@app-config/core';
22
import { named, forKey, validationFunction, ValidationFunction } from '@app-config/extension-utils';
33
import { AppConfigError, InObject } from '@app-config/core';
4-
import { currentEnvironment, defaultAliases, EnvironmentAliases } from '@app-config/node';
4+
import {
5+
asEnvOptions,
6+
currentEnvironment,
7+
defaultAliases,
8+
EnvironmentAliases,
9+
} from '@app-config/node';
510

611
/** Substitutes environment variables */
712
export function envVarDirective(
813
aliases: EnvironmentAliases = defaultAliases,
914
environmentOverride?: string,
1015
environmentSourceNames?: string[] | string,
1116
): ParsingExtension {
12-
const envType = environmentOverride ?? currentEnvironment(aliases, environmentSourceNames);
17+
const environment = currentEnvironment(
18+
asEnvOptions(environmentOverride, aliases, environmentSourceNames),
19+
);
1320

1421
return named(
1522
'$envVar',
@@ -72,7 +79,7 @@ export function envVarDirective(
7279
let resolvedValue = process.env[name];
7380

7481
if (!resolvedValue && name === 'APP_CONFIG_ENV') {
75-
resolvedValue = envType;
82+
resolvedValue = environment;
7683
}
7784

7885
if (resolvedValue) {

app-config-extensions/src/substitute-directive.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { named, forKey, validationFunction, ValidationFunction } from '@app-config/extension-utils';
22
import { ParsingExtension, AppConfigError, InObject } from '@app-config/core';
3-
import { currentEnvironment, defaultAliases, EnvironmentAliases } from '@app-config/node';
3+
import {
4+
asEnvOptions,
5+
currentEnvironment,
6+
defaultAliases,
7+
EnvironmentAliases,
8+
} from '@app-config/node';
49
import { logger } from '@app-config/logging';
510

611
/** Substitues environment variables found in strings (similar to bash variable substitution) */
@@ -9,13 +14,15 @@ export function substituteDirective(
914
environmentOverride?: string,
1015
environmentSourceNames?: string[] | string,
1116
): ParsingExtension {
12-
const envType = environmentOverride ?? currentEnvironment(aliases, environmentSourceNames);
17+
const environment = currentEnvironment(
18+
asEnvOptions(environmentOverride, aliases, environmentSourceNames),
19+
);
1320

1421
return named(
1522
'$substitute',
1623
forKey(['$substitute', '$subs'], (value, key, ctx) => async (parse) => {
1724
if (typeof value === 'string') {
18-
return parse(performAllSubstitutions(value, envType), { shouldFlatten: true });
25+
return parse(performAllSubstitutions(value, environment), { shouldFlatten: true });
1926
}
2027

2128
validateObject(value, [...ctx, key]);
@@ -71,7 +78,7 @@ export function substituteDirective(
7178
let resolvedValue = process.env[name];
7279

7380
if (!resolvedValue && name === 'APP_CONFIG_ENV') {
74-
resolvedValue = envType;
81+
resolvedValue = environment;
7582
}
7683

7784
if (resolvedValue) {
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import {
2+
currentEnvironment,
3+
aliasesFor,
4+
asEnvOptions,
5+
defaultAliases,
6+
defaultEnvVarNames,
7+
} from './environment';
8+
9+
describe('currentEnvironment', () => {
10+
describe('deprecated currentEnvironment', () => {
11+
it('uses environmentSourceNames', () => {
12+
process.env.NODE_ENV = 'foo';
13+
process.env.FOO = 'bar';
14+
15+
expect(currentEnvironment(undefined, ['FOO', 'BAR'])).toBe('bar');
16+
expect(currentEnvironment(undefined, ['BAR'])).toBe(undefined);
17+
});
18+
19+
it('uses environmentAliases', () => {
20+
process.env.FOO = 'bar';
21+
process.env.NODE_ENV = 'bar';
22+
23+
expect(currentEnvironment({}, ['FOO'])).toBe('bar');
24+
expect(currentEnvironment({ bar: 'foo' })).toBe('foo');
25+
expect(currentEnvironment({ bar: 'foo' }, ['FOO'])).toBe('foo');
26+
});
27+
});
28+
29+
it('uses envVarNames', () => {
30+
process.env.NODE_ENV = 'foo';
31+
process.env.FOO = 'bar';
32+
33+
expect(currentEnvironment({ envVarNames: ['FOO', 'BAR'], aliases: defaultAliases })).toBe(
34+
'bar',
35+
);
36+
expect(currentEnvironment({ envVarNames: ['BAR'], aliases: defaultAliases })).toBe(undefined);
37+
});
38+
39+
it('uses aliases', () => {
40+
process.env.FOO = 'bar';
41+
process.env.NODE_ENV = 'bar';
42+
43+
expect(currentEnvironment({ envVarNames: ['FOO'], aliases: defaultAliases })).toBe('bar');
44+
expect(currentEnvironment({ aliases: { bar: 'foo' }, envVarNames: defaultEnvVarNames })).toBe(
45+
'foo',
46+
);
47+
expect(currentEnvironment({ aliases: { bar: 'foo' }, envVarNames: ['FOO'] })).toBe('foo');
48+
});
49+
50+
it('uses override', () => {
51+
process.env.NODE_ENV = 'foo';
52+
expect(currentEnvironment({})).toBe('foo');
53+
expect(currentEnvironment({ override: 'bar' })).toBe('bar');
54+
});
55+
});
56+
57+
describe('aliasesFor', () => {
58+
it('reverse lookups', () => {
59+
expect(aliasesFor('foo', { bar: 'foo', baz: 'qux' })).toEqual(['bar']);
60+
expect(aliasesFor('foo', { bar: 'foo', baz: 'foo' })).toEqual(['bar', 'baz']);
61+
});
62+
});
63+
64+
describe('asEnvOptions', () => {
65+
it('reads environmentSourceNames string', () => {
66+
expect(asEnvOptions(undefined, undefined, 'foo')).toEqual({
67+
envVarNames: ['foo'],
68+
aliases: defaultAliases,
69+
});
70+
});
71+
72+
it('reads environmentSourceNames strings', () => {
73+
expect(asEnvOptions(undefined, undefined, ['foo'])).toEqual({
74+
envVarNames: ['foo'],
75+
aliases: defaultAliases,
76+
});
77+
});
78+
});

app-config-node/src/environment.ts

Lines changed: 101 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,109 @@
1+
import { logger } from '@app-config/logging';
2+
3+
/** A mapping for "alias" names of environments, like "dev" => "development" */
14
export interface EnvironmentAliases {
25
[alias: string]: string;
36
}
47

8+
/** Options required for calling {@link currentEnvironment} */
9+
export interface EnvironmentOptions {
10+
/** Absolute override for what the current environment is, still abiding by aliases */
11+
override?: string;
12+
/** A mapping for "alias" names of environments, like "dev" => "development" */
13+
aliases: EnvironmentAliases;
14+
/** What environment variable(s) define the current environment, if override is not defined */
15+
envVarNames: string[];
16+
}
17+
18+
/** Default aliases that app-config will resolve for you */
519
export const defaultAliases: EnvironmentAliases = {
620
prod: 'production',
721
dev: 'development',
822
};
923

24+
/** Default environment variables that app-config will read */
25+
export const defaultEnvVarNames = ['APP_CONFIG_ENV', 'NODE_ENV', 'ENV'];
26+
27+
/** Default options for {@link currentEnvironment} */
28+
export const defaultEnvOptions: EnvironmentOptions = {
29+
aliases: defaultAliases,
30+
envVarNames: defaultEnvVarNames,
31+
};
32+
33+
/** Conversion function useful for old usage of the deprecated {@link currentEnvironment} form */
34+
export function asEnvOptions(
35+
override?: string,
36+
aliases: EnvironmentAliases = defaultAliases,
37+
environmentSourceNames: string[] | string = defaultEnvVarNames,
38+
): EnvironmentOptions {
39+
return {
40+
override,
41+
aliases,
42+
envVarNames:
43+
typeof environmentSourceNames === 'string'
44+
? [environmentSourceNames]
45+
: environmentSourceNames,
46+
};
47+
}
48+
49+
/** Retrieve what app-config thinks the current deployment environment is (ie QA, dev, staging, production) */
50+
export function currentEnvironment(options?: EnvironmentOptions): string | undefined;
51+
52+
/** @deprecated use currentEnvironment(EnvironmentOptions) instead */
1053
export function currentEnvironment(
11-
environmentAliases: EnvironmentAliases = defaultAliases,
12-
environmentSourceNames: string[] | string = ['APP_CONFIG_ENV', 'NODE_ENV', 'ENV'],
13-
) {
54+
environmentAliases?: EnvironmentAliases,
55+
environmentSourceNames?: string[] | string,
56+
): string | undefined;
57+
58+
export function currentEnvironment(...args: any[]): string | undefined {
59+
let environmentSourceNames: string[] = defaultEnvVarNames;
60+
let environmentAliases: EnvironmentAliases = defaultAliases;
61+
let environmentOverride: string | undefined;
62+
63+
if (
64+
args[0] &&
65+
typeof args[0] === 'object' &&
66+
('override' in args[0] || 'aliases' in args[0] || 'envVarNames' in args[0])
67+
) {
68+
const options = args[0] as EnvironmentOptions;
69+
70+
if (options.override) {
71+
environmentOverride = options.override;
72+
}
73+
74+
if (options.aliases) {
75+
environmentAliases = options.aliases;
76+
}
77+
78+
if (options.envVarNames) {
79+
environmentSourceNames = options.envVarNames;
80+
}
81+
} else {
82+
if (args[0]) {
83+
environmentAliases = args[0] as EnvironmentAliases;
84+
logger.warn('Detected deprecated usage of currentEnvironment');
85+
}
86+
87+
if (Array.isArray(args[1])) {
88+
environmentSourceNames = args[1] as string[];
89+
logger.warn('Detected deprecated usage of currentEnvironment');
90+
} else if (typeof args[1] === 'string') {
91+
environmentSourceNames = [args[1]];
92+
logger.warn('Detected deprecated usage of currentEnvironment');
93+
}
94+
}
95+
96+
if (environmentOverride) {
97+
if (environmentAliases[environmentOverride]) {
98+
return environmentAliases[environmentOverride];
99+
}
100+
101+
return environmentOverride;
102+
}
103+
14104
let value: string | undefined;
15105

16-
for (const name of Array.isArray(environmentSourceNames)
17-
? environmentSourceNames
18-
: [environmentSourceNames]) {
106+
for (const name of environmentSourceNames) {
19107
if (process.env[name]?.length) {
20108
value = process.env[name];
21109
break;
@@ -30,3 +118,10 @@ export function currentEnvironment(
30118

31119
return value;
32120
}
121+
122+
/** Reverse lookup of any aliases for some environment */
123+
export function aliasesFor(env: string, aliases: EnvironmentAliases): string[] {
124+
return Object.entries(aliases)
125+
.filter(([, value]) => value === env)
126+
.map(([key]) => key);
127+
}

0 commit comments

Comments
 (0)