@decohere/config 0.0.1-dev.116 → 0.0.1-dev.118
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -0
- package/commonjs/src/yaml-data-source.js +48 -2
- package/es6/src/yaml-data-source.js +48 -2
- package/package.json +4 -4
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Config
|
|
2
|
+
|
|
3
|
+
Application configuration for Decohere: data sources that feed the configuration service, starting with YAML files read from the environment.
|
|
4
|
+
|
|
5
|
+
## YAML files
|
|
6
|
+
|
|
7
|
+
`YamlDataSource` reads two files from `CONFIG_DIR` (default `config`):
|
|
8
|
+
|
|
9
|
+
1. `config.yaml`: production, and the base for every environment.
|
|
10
|
+
2. `config.<NODE_ENV>.yaml`, if it exists, such as `config.dev.yaml`. An unset `NODE_ENV` means `production`, so a missing variable never loads development settings.
|
|
11
|
+
|
|
12
|
+
### How the files merge
|
|
13
|
+
|
|
14
|
+
The environment file only states what it changes:
|
|
15
|
+
|
|
16
|
+
- **Objects** merge key by key.
|
|
17
|
+
- **Lists of named entries** merge by `name`. Every entry is a mapping with a string `name`, as in `database.dataSources`. An entry with a name the base already has merges into that entry, and a new name is added at the end. The base keeps its order. A file that repeats a name in such a list is rejected, because the merge couldn't tell the entries apart.
|
|
18
|
+
- **Any other list**, and a scalar, replaces the earlier value. An empty list (`[]`) clears it.
|
|
19
|
+
|
|
20
|
+
```yaml
|
|
21
|
+
# config.yaml
|
|
22
|
+
database:
|
|
23
|
+
dataSources:
|
|
24
|
+
- name: api
|
|
25
|
+
host: ${API_DB_HOST}
|
|
26
|
+
password: ${API_DB_PASSWORD}
|
|
27
|
+
synchronize: false
|
|
28
|
+
|
|
29
|
+
# config.dev.yaml: only what development changes
|
|
30
|
+
database:
|
|
31
|
+
dataSources:
|
|
32
|
+
- name: api
|
|
33
|
+
synchronize: true
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Placeholders
|
|
37
|
+
|
|
38
|
+
`${NAME}` reads an environment variable, and `${NAME:default}` gives it a default. Placeholders are resolved **after** the merge. That way an environment file can replace a production `${VAR}` without the variable being set. Placeholders still unset after the merge fail together, and the error names every variable and where it was used. A variable set to the text `null` means no value.
|
|
39
|
+
|
|
40
|
+
Secrets come only from the process environment ([ADR 0002](../../docs/adr/0002-secrets-from-the-environment.md)). Nothing here reads a `.env` file; a CLI loads one only with an explicit `--env-file`.
|
|
@@ -38,16 +38,61 @@ function resolveConfigEnv(nodeEnv) {
|
|
|
38
38
|
function isTree(value) {
|
|
39
39
|
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
40
40
|
}
|
|
41
|
-
/**
|
|
41
|
+
/** A non-empty list whose every entry is a mapping with a string `name`, such as `dataSources`. */ function isNamedList(value) {
|
|
42
|
+
return Array.isArray(value) && value.length > 0 && value.every((item)=>isTree(item) && typeof item.name === "string");
|
|
43
|
+
}
|
|
44
|
+
/** Entries with the same name merge in place; new names are added at the end. */ function mergeNamedLists(base, override) {
|
|
45
|
+
const merged = [
|
|
46
|
+
...base
|
|
47
|
+
];
|
|
48
|
+
for (const entry of override){
|
|
49
|
+
const index = merged.findIndex((item)=>item.name === entry.name);
|
|
50
|
+
if (index === -1) {
|
|
51
|
+
merged.push(entry);
|
|
52
|
+
} else {
|
|
53
|
+
merged[index] = merge(merged[index], entry);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return merged;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Objects merge key by key, and so do lists of named entries (matched by `name`), so an
|
|
60
|
+
* environment file only states what it changes. Any other list, and a scalar, in the later file
|
|
61
|
+
* replaces the earlier value.
|
|
62
|
+
*/ function merge(base, override) {
|
|
42
63
|
const merged = {
|
|
43
64
|
...base
|
|
44
65
|
};
|
|
45
66
|
for (const [key, value] of Object.entries(override)){
|
|
46
67
|
const existing = merged[key];
|
|
47
|
-
|
|
68
|
+
if (isTree(existing) && isTree(value)) {
|
|
69
|
+
merged[key] = merge(existing, value);
|
|
70
|
+
} else if (isNamedList(existing) && isNamedList(value)) {
|
|
71
|
+
merged[key] = mergeNamedLists(existing, value);
|
|
72
|
+
} else {
|
|
73
|
+
merged[key] = value;
|
|
74
|
+
}
|
|
48
75
|
}
|
|
49
76
|
return merged;
|
|
50
77
|
}
|
|
78
|
+
/** A named list that repeats a name can't be merged by name, so say which file and where. */ function assertUniqueNames(value, source, path) {
|
|
79
|
+
if (isNamedList(value)) {
|
|
80
|
+
const seen = new Set();
|
|
81
|
+
for (const item of value){
|
|
82
|
+
if (seen.has(item.name)) {
|
|
83
|
+
throw new _errors.ConfigurationError(`Config file ${source}: ${path} lists the name "${String(item.name)}" more than once`);
|
|
84
|
+
}
|
|
85
|
+
seen.add(item.name);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (Array.isArray(value)) {
|
|
89
|
+
value.forEach((item, index)=>assertUniqueNames(item, source, `${path}[${index}]`));
|
|
90
|
+
} else if (isTree(value)) {
|
|
91
|
+
for (const [key, item] of Object.entries(value)){
|
|
92
|
+
assertUniqueNames(item, source, path === "" ? key : `${path}.${key}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
51
96
|
function resolveString(value, path, missing) {
|
|
52
97
|
let hasPlaceholder = false;
|
|
53
98
|
const resolved = value.replace(PLACEHOLDER, (_match, name, fallback)=>{
|
|
@@ -98,6 +143,7 @@ function parseSource({ name, text }) {
|
|
|
98
143
|
if (!isTree(parsed)) {
|
|
99
144
|
throw new _errors.ConfigurationError(`Config file ${name} must contain a mapping at the top level`);
|
|
100
145
|
}
|
|
146
|
+
assertUniqueNames(parsed, name, "");
|
|
101
147
|
return parsed;
|
|
102
148
|
}
|
|
103
149
|
function resolveYamlConfig(sources) {
|
|
@@ -17,16 +17,61 @@ const FILE_EXT = "yaml";
|
|
|
17
17
|
function isTree(value) {
|
|
18
18
|
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
19
19
|
}
|
|
20
|
-
/**
|
|
20
|
+
/** A non-empty list whose every entry is a mapping with a string `name`, such as `dataSources`. */ function isNamedList(value) {
|
|
21
|
+
return Array.isArray(value) && value.length > 0 && value.every((item)=>isTree(item) && typeof item.name === "string");
|
|
22
|
+
}
|
|
23
|
+
/** Entries with the same name merge in place; new names are added at the end. */ function mergeNamedLists(base, override) {
|
|
24
|
+
const merged = [
|
|
25
|
+
...base
|
|
26
|
+
];
|
|
27
|
+
for (const entry of override){
|
|
28
|
+
const index = merged.findIndex((item)=>item.name === entry.name);
|
|
29
|
+
if (index === -1) {
|
|
30
|
+
merged.push(entry);
|
|
31
|
+
} else {
|
|
32
|
+
merged[index] = merge(merged[index], entry);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return merged;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Objects merge key by key, and so do lists of named entries (matched by `name`), so an
|
|
39
|
+
* environment file only states what it changes. Any other list, and a scalar, in the later file
|
|
40
|
+
* replaces the earlier value.
|
|
41
|
+
*/ function merge(base, override) {
|
|
21
42
|
const merged = {
|
|
22
43
|
...base
|
|
23
44
|
};
|
|
24
45
|
for (const [key, value] of Object.entries(override)){
|
|
25
46
|
const existing = merged[key];
|
|
26
|
-
|
|
47
|
+
if (isTree(existing) && isTree(value)) {
|
|
48
|
+
merged[key] = merge(existing, value);
|
|
49
|
+
} else if (isNamedList(existing) && isNamedList(value)) {
|
|
50
|
+
merged[key] = mergeNamedLists(existing, value);
|
|
51
|
+
} else {
|
|
52
|
+
merged[key] = value;
|
|
53
|
+
}
|
|
27
54
|
}
|
|
28
55
|
return merged;
|
|
29
56
|
}
|
|
57
|
+
/** A named list that repeats a name can't be merged by name, so say which file and where. */ function assertUniqueNames(value, source, path) {
|
|
58
|
+
if (isNamedList(value)) {
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
for (const item of value){
|
|
61
|
+
if (seen.has(item.name)) {
|
|
62
|
+
throw new ConfigurationError(`Config file ${source}: ${path} lists the name "${String(item.name)}" more than once`);
|
|
63
|
+
}
|
|
64
|
+
seen.add(item.name);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
value.forEach((item, index)=>assertUniqueNames(item, source, `${path}[${index}]`));
|
|
69
|
+
} else if (isTree(value)) {
|
|
70
|
+
for (const [key, item] of Object.entries(value)){
|
|
71
|
+
assertUniqueNames(item, source, path === "" ? key : `${path}.${key}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
30
75
|
function resolveString(value, path, missing) {
|
|
31
76
|
let hasPlaceholder = false;
|
|
32
77
|
const resolved = value.replace(PLACEHOLDER, (_match, name, fallback)=>{
|
|
@@ -77,6 +122,7 @@ function parseSource({ name, text }) {
|
|
|
77
122
|
if (!isTree(parsed)) {
|
|
78
123
|
throw new ConfigurationError(`Config file ${name} must contain a mapping at the top level`);
|
|
79
124
|
}
|
|
125
|
+
assertUniqueNames(parsed, name, "");
|
|
80
126
|
return parsed;
|
|
81
127
|
}
|
|
82
128
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decohere/config",
|
|
3
|
-
"version": "0.0.1-dev.
|
|
3
|
+
"version": "0.0.1-dev.118",
|
|
4
4
|
"description": "Database ORM abstractions and repository patterns built on TypeORM",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@decohere/core": "0.0.1-dev.
|
|
29
|
-
"@decohere/crucible": "0.0.1-dev.
|
|
30
|
-
"@decohere/http": "0.0.1-dev.
|
|
28
|
+
"@decohere/core": "0.0.1-dev.118",
|
|
29
|
+
"@decohere/crucible": "0.0.1-dev.118",
|
|
30
|
+
"@decohere/http": "0.0.1-dev.118",
|
|
31
31
|
"yaml": "^2.9.1"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {}
|