@decohere/config 0.0.1-dev.116
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/commonjs/src/configuration-data-source.js +12 -0
- package/commonjs/src/configuration.extensions.js +45 -0
- package/commonjs/src/configuration.service.js +98 -0
- package/commonjs/src/const.js +20 -0
- package/commonjs/src/errors.js +12 -0
- package/commonjs/src/index.js +22 -0
- package/commonjs/src/yaml-data-source.js +139 -0
- package/es6/src/configuration-data-source.js +2 -0
- package/es6/src/configuration.extensions.js +35 -0
- package/es6/src/configuration.service.js +88 -0
- package/es6/src/const.js +2 -0
- package/es6/src/errors.js +2 -0
- package/es6/src/index.js +5 -0
- package/es6/src/yaml-data-source.js +126 -0
- package/package.json +34 -0
- package/types/src/configuration-data-source.d.ts +8 -0
- package/types/src/configuration.extensions.d.ts +16 -0
- package/types/src/configuration.service.d.ts +15 -0
- package/types/src/const.d.ts +2 -0
- package/types/src/errors.d.ts +2 -0
- package/types/src/index.d.ts +5 -0
- package/types/src/yaml-data-source.d.ts +24 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "ConfigurationDataSource", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return ConfigurationDataSource;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
class ConfigurationDataSource {
|
|
12
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "BuilderExtensions", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return BuilderExtensions;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _http = require("@decohere/http");
|
|
12
|
+
const _configuration = require("@decohere/core/configuration");
|
|
13
|
+
const _extensibility = require("@decohere/core/extensibility");
|
|
14
|
+
const _configurationservice = require("./configuration.service.js");
|
|
15
|
+
const _const = require("./const.js");
|
|
16
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
17
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
18
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
19
|
+
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
20
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
21
|
+
}
|
|
22
|
+
function _ts_metadata(k, v) {
|
|
23
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
24
|
+
}
|
|
25
|
+
class BuilderExtensions {
|
|
26
|
+
static withConfiguration(builder, ...dataSourceArgs) {
|
|
27
|
+
const dataSources = dataSourceArgs ?? [];
|
|
28
|
+
builder.withServices((builder)=>{
|
|
29
|
+
builder.registerSingleton(_configuration.ConfigurationServiceToken, _configurationservice.ConfigurationService);
|
|
30
|
+
for (const dataSource of dataSources){
|
|
31
|
+
builder.registerSingleton(_const.ConfigurationDataSourceKey, dataSource);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
return builder;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
_ts_decorate([
|
|
38
|
+
(0, _extensibility.extension)(_http.Builder),
|
|
39
|
+
_ts_metadata("design:type", Function),
|
|
40
|
+
_ts_metadata("design:paramtypes", [
|
|
41
|
+
typeof _http.Builder === "undefined" ? Object : _http.Builder,
|
|
42
|
+
Array
|
|
43
|
+
]),
|
|
44
|
+
_ts_metadata("design:returntype", void 0)
|
|
45
|
+
], BuilderExtensions, "withConfiguration", null);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "ConfigurationService", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return ConfigurationService;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _decorators = require("@decohere/crucible/decorators");
|
|
12
|
+
const _caching = require("@decohere/core/caching");
|
|
13
|
+
const _system = require("@decohere/core/system");
|
|
14
|
+
const _const = require("./const.js");
|
|
15
|
+
const _errors = require("./errors.js");
|
|
16
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
17
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
18
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
19
|
+
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
20
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
21
|
+
}
|
|
22
|
+
function _ts_metadata(k, v) {
|
|
23
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
24
|
+
}
|
|
25
|
+
function _ts_param(paramIndex, decorator) {
|
|
26
|
+
return function(target, key) {
|
|
27
|
+
decorator(target, key, paramIndex);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
class ConfigurationService {
|
|
31
|
+
data = {};
|
|
32
|
+
dataSources = [];
|
|
33
|
+
isInitialized = false;
|
|
34
|
+
constructor(dataSources = []){
|
|
35
|
+
this.dataSources = dataSources;
|
|
36
|
+
}
|
|
37
|
+
async initialize() {
|
|
38
|
+
if (this.isInitialized) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const records = await Promise.all(this.dataSources.map(async (d)=>await d.getData()));
|
|
42
|
+
this.validateDataSources(records);
|
|
43
|
+
const entries = records.flatMap((r)=>Object.entries(r));
|
|
44
|
+
this.data = Object.fromEntries(entries);
|
|
45
|
+
this.isInitialized = true;
|
|
46
|
+
}
|
|
47
|
+
async get(typeOrString) {
|
|
48
|
+
await this.ensureInitialized();
|
|
49
|
+
const obj = typeof typeOrString === "string" ? this.getStringKey(typeOrString) : this.getConstructable(typeOrString);
|
|
50
|
+
return obj;
|
|
51
|
+
}
|
|
52
|
+
async ensureInitialized() {
|
|
53
|
+
if (this.isInitialized) return;
|
|
54
|
+
await this.initialize();
|
|
55
|
+
}
|
|
56
|
+
validateDataSources(records) {
|
|
57
|
+
const keys = records.flatMap((record)=>Object.keys(record));
|
|
58
|
+
const uniqueKeys = new Set(keys);
|
|
59
|
+
if (uniqueKeys.size < keys.length) {
|
|
60
|
+
throw new _errors.ConfigurationError(`Cannot have duplicate data source names. The data sources are: ${keys.join(",")}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
getConstructable(type) {
|
|
64
|
+
const key = (0, _system.lowerCamelCase)(type?.name.replace(/configuration|config/i, ""));
|
|
65
|
+
const data = this.data[key];
|
|
66
|
+
const configObject = new type();
|
|
67
|
+
Object.assign(configObject, data);
|
|
68
|
+
return configObject;
|
|
69
|
+
}
|
|
70
|
+
getStringKey(key) {
|
|
71
|
+
const data = this.data[key];
|
|
72
|
+
return data;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
_ts_decorate([
|
|
76
|
+
(0, _caching.memoize)(),
|
|
77
|
+
_ts_metadata("design:type", Function),
|
|
78
|
+
_ts_metadata("design:paramtypes", [
|
|
79
|
+
typeof Constructable === "undefined" ? Object : Constructable
|
|
80
|
+
]),
|
|
81
|
+
_ts_metadata("design:returntype", typeof T === "undefined" ? Object : T)
|
|
82
|
+
], ConfigurationService.prototype, "getConstructable", null);
|
|
83
|
+
_ts_decorate([
|
|
84
|
+
(0, _caching.cache)(),
|
|
85
|
+
_ts_metadata("design:type", Function),
|
|
86
|
+
_ts_metadata("design:paramtypes", [
|
|
87
|
+
String
|
|
88
|
+
]),
|
|
89
|
+
_ts_metadata("design:returntype", typeof T === "undefined" ? Object : T)
|
|
90
|
+
], ConfigurationService.prototype, "getStringKey", null);
|
|
91
|
+
ConfigurationService = _ts_decorate([
|
|
92
|
+
(0, _decorators.injectable)(),
|
|
93
|
+
_ts_param(0, (0, _decorators.inject)(_const.ConfigurationDataSourceKey)),
|
|
94
|
+
_ts_metadata("design:type", Function),
|
|
95
|
+
_ts_metadata("design:paramtypes", [
|
|
96
|
+
Array
|
|
97
|
+
])
|
|
98
|
+
], ConfigurationService);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
function _export(target, all) {
|
|
6
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: Object.getOwnPropertyDescriptor(all, name).get
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
_export(exports, {
|
|
12
|
+
get ConfigurationDataSourceKey () {
|
|
13
|
+
return ConfigurationDataSourceKey;
|
|
14
|
+
},
|
|
15
|
+
get DEFAULT_CONFIG_DIR () {
|
|
16
|
+
return DEFAULT_CONFIG_DIR;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
const ConfigurationDataSourceKey = Symbol("ConfigurationDataSourceKey");
|
|
20
|
+
const DEFAULT_CONFIG_DIR = "config";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "ConfigurationError", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return ConfigurationError;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
class ConfigurationError extends Error {
|
|
12
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
_export_star(require("./configuration.extensions.js"), exports);
|
|
6
|
+
_export_star(require("./configuration.service.js"), exports);
|
|
7
|
+
_export_star(require("./configuration-data-source.js"), exports);
|
|
8
|
+
_export_star(require("./const.js"), exports);
|
|
9
|
+
_export_star(require("./yaml-data-source.js"), exports);
|
|
10
|
+
function _export_star(from, to) {
|
|
11
|
+
Object.keys(from).forEach(function(k) {
|
|
12
|
+
if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) {
|
|
13
|
+
Object.defineProperty(to, k, {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
get: function() {
|
|
16
|
+
return from[k];
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
return from;
|
|
22
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
function _export(target, all) {
|
|
6
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: Object.getOwnPropertyDescriptor(all, name).get
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
_export(exports, {
|
|
12
|
+
get YamlDataSource () {
|
|
13
|
+
return YamlDataSource;
|
|
14
|
+
},
|
|
15
|
+
get resolveConfigEnv () {
|
|
16
|
+
return resolveConfigEnv;
|
|
17
|
+
},
|
|
18
|
+
get resolveYamlConfig () {
|
|
19
|
+
return resolveYamlConfig;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
const _io = require("@decohere/core/io");
|
|
23
|
+
const _yaml = require("yaml");
|
|
24
|
+
const _configurationdatasource = require("./configuration-data-source.js");
|
|
25
|
+
const _const = require("./const.js");
|
|
26
|
+
const _errors = require("./errors.js");
|
|
27
|
+
const CONFIG_FILE_NAME = "config";
|
|
28
|
+
/** Unset NODE_ENV must not silently load config.dev.yaml (TypeORM synchronize, etc.). */ const DEFAULT_ENV = "production";
|
|
29
|
+
const FILE_EXT = "yaml";
|
|
30
|
+
function resolveConfigEnv(nodeEnv) {
|
|
31
|
+
const env = nodeEnv ?? process.env.NODE_ENV;
|
|
32
|
+
if (env == null || env.trim() === "") {
|
|
33
|
+
return DEFAULT_ENV;
|
|
34
|
+
}
|
|
35
|
+
return env;
|
|
36
|
+
}
|
|
37
|
+
/** `${NAME}` or `${NAME:default}`. The default may be empty, and runs up to the first `}`. */ const PLACEHOLDER = /\$\{(\w+)(?::([^}]*))?\}/g;
|
|
38
|
+
function isTree(value) {
|
|
39
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
40
|
+
}
|
|
41
|
+
/** Objects merge key by key; a list or a scalar in the later file replaces the earlier value. */ function merge(base, override) {
|
|
42
|
+
const merged = {
|
|
43
|
+
...base
|
|
44
|
+
};
|
|
45
|
+
for (const [key, value] of Object.entries(override)){
|
|
46
|
+
const existing = merged[key];
|
|
47
|
+
merged[key] = isTree(existing) && isTree(value) ? merge(existing, value) : value;
|
|
48
|
+
}
|
|
49
|
+
return merged;
|
|
50
|
+
}
|
|
51
|
+
function resolveString(value, path, missing) {
|
|
52
|
+
let hasPlaceholder = false;
|
|
53
|
+
const resolved = value.replace(PLACEHOLDER, (_match, name, fallback)=>{
|
|
54
|
+
hasPlaceholder = true;
|
|
55
|
+
const fromEnv = process.env[name];
|
|
56
|
+
if (fromEnv !== undefined) {
|
|
57
|
+
return fromEnv;
|
|
58
|
+
}
|
|
59
|
+
if (fallback !== undefined) {
|
|
60
|
+
return fallback;
|
|
61
|
+
}
|
|
62
|
+
missing.push({
|
|
63
|
+
name,
|
|
64
|
+
path
|
|
65
|
+
});
|
|
66
|
+
return "";
|
|
67
|
+
});
|
|
68
|
+
// A variable set to the text "null" means no value.
|
|
69
|
+
return hasPlaceholder && resolved === "null" ? null : resolved;
|
|
70
|
+
}
|
|
71
|
+
function resolveValue(value, path, missing) {
|
|
72
|
+
if (typeof value === "string") {
|
|
73
|
+
return resolveString(value, path, missing);
|
|
74
|
+
}
|
|
75
|
+
if (Array.isArray(value)) {
|
|
76
|
+
return value.map((item, index)=>resolveValue(item, `${path}[${index}]`, missing));
|
|
77
|
+
}
|
|
78
|
+
if (isTree(value)) {
|
|
79
|
+
const resolved = {};
|
|
80
|
+
for (const [key, item] of Object.entries(value)){
|
|
81
|
+
resolved[key] = resolveValue(item, path === "" ? key : `${path}.${key}`, missing);
|
|
82
|
+
}
|
|
83
|
+
return resolved;
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
function parseSource({ name, text }) {
|
|
88
|
+
let parsed;
|
|
89
|
+
try {
|
|
90
|
+
parsed = (0, _yaml.parse)(text);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
93
|
+
throw new _errors.ConfigurationError(`Cannot parse config file ${name}: ${reason}`);
|
|
94
|
+
}
|
|
95
|
+
if (parsed == null) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
if (!isTree(parsed)) {
|
|
99
|
+
throw new _errors.ConfigurationError(`Config file ${name} must contain a mapping at the top level`);
|
|
100
|
+
}
|
|
101
|
+
return parsed;
|
|
102
|
+
}
|
|
103
|
+
function resolveYamlConfig(sources) {
|
|
104
|
+
let merged = {};
|
|
105
|
+
for (const source of sources){
|
|
106
|
+
const tree = parseSource(source);
|
|
107
|
+
if (tree !== undefined) {
|
|
108
|
+
merged = merge(merged, tree);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const missing = [];
|
|
112
|
+
const resolved = resolveValue(merged, "", missing);
|
|
113
|
+
if (missing.length > 0) {
|
|
114
|
+
const lines = missing.map(({ name, path })=>` - ${name} (used at ${path})`);
|
|
115
|
+
throw new _errors.ConfigurationError(`Config references ${missing.length} environment variable(s) that are not set:\n${lines.join("\n")}\n` + "Set them, give the value a default with ${NAME:default}, or replace it in the environment's config file.");
|
|
116
|
+
}
|
|
117
|
+
return resolved;
|
|
118
|
+
}
|
|
119
|
+
class YamlDataSource extends _configurationdatasource.ConfigurationDataSource {
|
|
120
|
+
name = YamlDataSource.name;
|
|
121
|
+
async getData() {
|
|
122
|
+
const configDir = process.env.CONFIG_DIR ?? _const.DEFAULT_CONFIG_DIR;
|
|
123
|
+
const env = resolveConfigEnv();
|
|
124
|
+
const candidates = [
|
|
125
|
+
`${configDir}/${CONFIG_FILE_NAME}.${FILE_EXT}`,
|
|
126
|
+
`${configDir}/${CONFIG_FILE_NAME}.${env}.${FILE_EXT}`
|
|
127
|
+
];
|
|
128
|
+
const sources = [];
|
|
129
|
+
for (const filePath of candidates){
|
|
130
|
+
if (await _io.io.fileExists(filePath)) {
|
|
131
|
+
sources.push({
|
|
132
|
+
name: filePath,
|
|
133
|
+
text: await _io.io.readFile(filePath)
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return resolveYamlConfig(sources);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
}
|
|
7
|
+
function _ts_metadata(k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
}
|
|
10
|
+
import { Builder } from "@decohere/http";
|
|
11
|
+
import { ConfigurationServiceToken } from "@decohere/core/configuration";
|
|
12
|
+
import { extension } from "@decohere/core/extensibility";
|
|
13
|
+
import { ConfigurationService } from "./configuration.service.js";
|
|
14
|
+
import { ConfigurationDataSourceKey } from "./const.js";
|
|
15
|
+
export class BuilderExtensions {
|
|
16
|
+
static withConfiguration(builder, ...dataSourceArgs) {
|
|
17
|
+
const dataSources = dataSourceArgs ?? [];
|
|
18
|
+
builder.withServices((builder)=>{
|
|
19
|
+
builder.registerSingleton(ConfigurationServiceToken, ConfigurationService);
|
|
20
|
+
for (const dataSource of dataSources){
|
|
21
|
+
builder.registerSingleton(ConfigurationDataSourceKey, dataSource);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
return builder;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
_ts_decorate([
|
|
28
|
+
extension(Builder),
|
|
29
|
+
_ts_metadata("design:type", Function),
|
|
30
|
+
_ts_metadata("design:paramtypes", [
|
|
31
|
+
typeof Builder === "undefined" ? Object : Builder,
|
|
32
|
+
Array
|
|
33
|
+
]),
|
|
34
|
+
_ts_metadata("design:returntype", void 0)
|
|
35
|
+
], BuilderExtensions, "withConfiguration", null);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
}
|
|
7
|
+
function _ts_metadata(k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
}
|
|
10
|
+
function _ts_param(paramIndex, decorator) {
|
|
11
|
+
return function(target, key) {
|
|
12
|
+
decorator(target, key, paramIndex);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
import { inject, injectable } from "@decohere/crucible/decorators";
|
|
16
|
+
import { cache, memoize } from "@decohere/core/caching";
|
|
17
|
+
import { lowerCamelCase } from "@decohere/core/system";
|
|
18
|
+
import { ConfigurationDataSourceKey } from "./const.js";
|
|
19
|
+
import { ConfigurationError } from "./errors.js";
|
|
20
|
+
export class ConfigurationService {
|
|
21
|
+
data = {};
|
|
22
|
+
dataSources = [];
|
|
23
|
+
isInitialized = false;
|
|
24
|
+
constructor(dataSources = []){
|
|
25
|
+
this.dataSources = dataSources;
|
|
26
|
+
}
|
|
27
|
+
async initialize() {
|
|
28
|
+
if (this.isInitialized) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const records = await Promise.all(this.dataSources.map(async (d)=>await d.getData()));
|
|
32
|
+
this.validateDataSources(records);
|
|
33
|
+
const entries = records.flatMap((r)=>Object.entries(r));
|
|
34
|
+
this.data = Object.fromEntries(entries);
|
|
35
|
+
this.isInitialized = true;
|
|
36
|
+
}
|
|
37
|
+
async get(typeOrString) {
|
|
38
|
+
await this.ensureInitialized();
|
|
39
|
+
const obj = typeof typeOrString === "string" ? this.getStringKey(typeOrString) : this.getConstructable(typeOrString);
|
|
40
|
+
return obj;
|
|
41
|
+
}
|
|
42
|
+
async ensureInitialized() {
|
|
43
|
+
if (this.isInitialized) return;
|
|
44
|
+
await this.initialize();
|
|
45
|
+
}
|
|
46
|
+
validateDataSources(records) {
|
|
47
|
+
const keys = records.flatMap((record)=>Object.keys(record));
|
|
48
|
+
const uniqueKeys = new Set(keys);
|
|
49
|
+
if (uniqueKeys.size < keys.length) {
|
|
50
|
+
throw new ConfigurationError(`Cannot have duplicate data source names. The data sources are: ${keys.join(",")}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
getConstructable(type) {
|
|
54
|
+
const key = lowerCamelCase(type?.name.replace(/configuration|config/i, ""));
|
|
55
|
+
const data = this.data[key];
|
|
56
|
+
const configObject = new type();
|
|
57
|
+
Object.assign(configObject, data);
|
|
58
|
+
return configObject;
|
|
59
|
+
}
|
|
60
|
+
getStringKey(key) {
|
|
61
|
+
const data = this.data[key];
|
|
62
|
+
return data;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
_ts_decorate([
|
|
66
|
+
memoize(),
|
|
67
|
+
_ts_metadata("design:type", Function),
|
|
68
|
+
_ts_metadata("design:paramtypes", [
|
|
69
|
+
typeof Constructable === "undefined" ? Object : Constructable
|
|
70
|
+
]),
|
|
71
|
+
_ts_metadata("design:returntype", typeof T === "undefined" ? Object : T)
|
|
72
|
+
], ConfigurationService.prototype, "getConstructable", null);
|
|
73
|
+
_ts_decorate([
|
|
74
|
+
cache(),
|
|
75
|
+
_ts_metadata("design:type", Function),
|
|
76
|
+
_ts_metadata("design:paramtypes", [
|
|
77
|
+
String
|
|
78
|
+
]),
|
|
79
|
+
_ts_metadata("design:returntype", typeof T === "undefined" ? Object : T)
|
|
80
|
+
], ConfigurationService.prototype, "getStringKey", null);
|
|
81
|
+
ConfigurationService = _ts_decorate([
|
|
82
|
+
injectable(),
|
|
83
|
+
_ts_param(0, inject(ConfigurationDataSourceKey)),
|
|
84
|
+
_ts_metadata("design:type", Function),
|
|
85
|
+
_ts_metadata("design:paramtypes", [
|
|
86
|
+
Array
|
|
87
|
+
])
|
|
88
|
+
], ConfigurationService);
|
package/es6/src/const.js
ADDED
package/es6/src/index.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { io } from "@decohere/core/io";
|
|
2
|
+
import { parse } from "yaml";
|
|
3
|
+
import { ConfigurationDataSource } from "./configuration-data-source.js";
|
|
4
|
+
import { DEFAULT_CONFIG_DIR } from "./const.js";
|
|
5
|
+
import { ConfigurationError } from "./errors.js";
|
|
6
|
+
const CONFIG_FILE_NAME = "config";
|
|
7
|
+
/** Unset NODE_ENV must not silently load config.dev.yaml (TypeORM synchronize, etc.). */ const DEFAULT_ENV = "production";
|
|
8
|
+
const FILE_EXT = "yaml";
|
|
9
|
+
/** Resolve which env-specific config file to merge (e.g. config.dev.yaml). */ export function resolveConfigEnv(nodeEnv) {
|
|
10
|
+
const env = nodeEnv ?? process.env.NODE_ENV;
|
|
11
|
+
if (env == null || env.trim() === "") {
|
|
12
|
+
return DEFAULT_ENV;
|
|
13
|
+
}
|
|
14
|
+
return env;
|
|
15
|
+
}
|
|
16
|
+
/** `${NAME}` or `${NAME:default}`. The default may be empty, and runs up to the first `}`. */ const PLACEHOLDER = /\$\{(\w+)(?::([^}]*))?\}/g;
|
|
17
|
+
function isTree(value) {
|
|
18
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
/** Objects merge key by key; a list or a scalar in the later file replaces the earlier value. */ function merge(base, override) {
|
|
21
|
+
const merged = {
|
|
22
|
+
...base
|
|
23
|
+
};
|
|
24
|
+
for (const [key, value] of Object.entries(override)){
|
|
25
|
+
const existing = merged[key];
|
|
26
|
+
merged[key] = isTree(existing) && isTree(value) ? merge(existing, value) : value;
|
|
27
|
+
}
|
|
28
|
+
return merged;
|
|
29
|
+
}
|
|
30
|
+
function resolveString(value, path, missing) {
|
|
31
|
+
let hasPlaceholder = false;
|
|
32
|
+
const resolved = value.replace(PLACEHOLDER, (_match, name, fallback)=>{
|
|
33
|
+
hasPlaceholder = true;
|
|
34
|
+
const fromEnv = process.env[name];
|
|
35
|
+
if (fromEnv !== undefined) {
|
|
36
|
+
return fromEnv;
|
|
37
|
+
}
|
|
38
|
+
if (fallback !== undefined) {
|
|
39
|
+
return fallback;
|
|
40
|
+
}
|
|
41
|
+
missing.push({
|
|
42
|
+
name,
|
|
43
|
+
path
|
|
44
|
+
});
|
|
45
|
+
return "";
|
|
46
|
+
});
|
|
47
|
+
// A variable set to the text "null" means no value.
|
|
48
|
+
return hasPlaceholder && resolved === "null" ? null : resolved;
|
|
49
|
+
}
|
|
50
|
+
function resolveValue(value, path, missing) {
|
|
51
|
+
if (typeof value === "string") {
|
|
52
|
+
return resolveString(value, path, missing);
|
|
53
|
+
}
|
|
54
|
+
if (Array.isArray(value)) {
|
|
55
|
+
return value.map((item, index)=>resolveValue(item, `${path}[${index}]`, missing));
|
|
56
|
+
}
|
|
57
|
+
if (isTree(value)) {
|
|
58
|
+
const resolved = {};
|
|
59
|
+
for (const [key, item] of Object.entries(value)){
|
|
60
|
+
resolved[key] = resolveValue(item, path === "" ? key : `${path}.${key}`, missing);
|
|
61
|
+
}
|
|
62
|
+
return resolved;
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
function parseSource({ name, text }) {
|
|
67
|
+
let parsed;
|
|
68
|
+
try {
|
|
69
|
+
parsed = parse(text);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
72
|
+
throw new ConfigurationError(`Cannot parse config file ${name}: ${reason}`);
|
|
73
|
+
}
|
|
74
|
+
if (parsed == null) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
if (!isTree(parsed)) {
|
|
78
|
+
throw new ConfigurationError(`Config file ${name} must contain a mapping at the top level`);
|
|
79
|
+
}
|
|
80
|
+
return parsed;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Merge config files in order (later files win), then resolve `${VAR}` placeholders on the
|
|
84
|
+
* merged result.
|
|
85
|
+
*
|
|
86
|
+
* Resolving after the merge means an environment file such as `config.dev.yaml` can replace a
|
|
87
|
+
* production `${VAR}` without that variable being set: a placeholder that was replaced is never
|
|
88
|
+
* looked up. Placeholders still unset after the merge fail together, naming every variable and
|
|
89
|
+
* the config path that used it. `${VAR:default}` keeps a default for optional values.
|
|
90
|
+
*/ export function resolveYamlConfig(sources) {
|
|
91
|
+
let merged = {};
|
|
92
|
+
for (const source of sources){
|
|
93
|
+
const tree = parseSource(source);
|
|
94
|
+
if (tree !== undefined) {
|
|
95
|
+
merged = merge(merged, tree);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const missing = [];
|
|
99
|
+
const resolved = resolveValue(merged, "", missing);
|
|
100
|
+
if (missing.length > 0) {
|
|
101
|
+
const lines = missing.map(({ name, path })=>` - ${name} (used at ${path})`);
|
|
102
|
+
throw new ConfigurationError(`Config references ${missing.length} environment variable(s) that are not set:\n${lines.join("\n")}\n` + "Set them, give the value a default with ${NAME:default}, or replace it in the environment's config file.");
|
|
103
|
+
}
|
|
104
|
+
return resolved;
|
|
105
|
+
}
|
|
106
|
+
export class YamlDataSource extends ConfigurationDataSource {
|
|
107
|
+
name = YamlDataSource.name;
|
|
108
|
+
async getData() {
|
|
109
|
+
const configDir = process.env.CONFIG_DIR ?? DEFAULT_CONFIG_DIR;
|
|
110
|
+
const env = resolveConfigEnv();
|
|
111
|
+
const candidates = [
|
|
112
|
+
`${configDir}/${CONFIG_FILE_NAME}.${FILE_EXT}`,
|
|
113
|
+
`${configDir}/${CONFIG_FILE_NAME}.${env}.${FILE_EXT}`
|
|
114
|
+
];
|
|
115
|
+
const sources = [];
|
|
116
|
+
for (const filePath of candidates){
|
|
117
|
+
if (await io.fileExists(filePath)) {
|
|
118
|
+
sources.push({
|
|
119
|
+
name: filePath,
|
|
120
|
+
text: await io.readFile(filePath)
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return resolveYamlConfig(sources);
|
|
125
|
+
}
|
|
126
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@decohere/config",
|
|
3
|
+
"version": "0.0.1-dev.116",
|
|
4
|
+
"description": "Database ORM abstractions and repository patterns built on TypeORM",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"typescript",
|
|
7
|
+
"typeorm",
|
|
8
|
+
"orm",
|
|
9
|
+
"database",
|
|
10
|
+
"repository",
|
|
11
|
+
"decohere"
|
|
12
|
+
],
|
|
13
|
+
"license": "ISC",
|
|
14
|
+
"author": "Typesmiths",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "index.js",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./types/src/index.d.ts",
|
|
20
|
+
"import": "./es6/src/index.js",
|
|
21
|
+
"require": "./commonjs/src/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@decohere/core": "0.0.1-dev.116",
|
|
29
|
+
"@decohere/crucible": "0.0.1-dev.116",
|
|
30
|
+
"@decohere/http": "0.0.1-dev.116",
|
|
31
|
+
"yaml": "^2.9.1"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {}
|
|
34
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface IConfigurationDataSource {
|
|
2
|
+
readonly name: string;
|
|
3
|
+
getData(): Promise<Record<string, unknown>>;
|
|
4
|
+
}
|
|
5
|
+
export declare abstract class ConfigurationDataSource implements IConfigurationDataSource {
|
|
6
|
+
abstract readonly name: string;
|
|
7
|
+
abstract getData(): Promise<Record<string, unknown>>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Builder } from "@decohere/http";
|
|
2
|
+
import { type IConfigurationService } from "@decohere/core/configuration";
|
|
3
|
+
import type { Constructable } from "@decohere/core/reflection";
|
|
4
|
+
import type { ConfigurationDataSource } from "./configuration-data-source.js";
|
|
5
|
+
export type ConfigurationOptions = {
|
|
6
|
+
configurationService?: Constructable<IConfigurationService>;
|
|
7
|
+
dataSources?: Constructable<ConfigurationDataSource>[];
|
|
8
|
+
};
|
|
9
|
+
declare module "@decohere/http" {
|
|
10
|
+
interface Builder {
|
|
11
|
+
withConfiguration(...dataSources: Constructable<ConfigurationDataSource>[]): Builder;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export declare class BuilderExtensions {
|
|
15
|
+
static withConfiguration(builder: Builder, ...dataSourceArgs: Constructable<ConfigurationDataSource>[]): Builder<import("@decohere/http").App>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { IConfigurationService } from "@decohere/core/configuration";
|
|
2
|
+
import type { Constructable } from "@decohere/core/reflection";
|
|
3
|
+
import type { ConfigurationDataSource } from "./configuration-data-source.js";
|
|
4
|
+
export declare class ConfigurationService implements IConfigurationService {
|
|
5
|
+
protected data: Record<string, unknown>;
|
|
6
|
+
readonly dataSources: readonly ConfigurationDataSource[];
|
|
7
|
+
protected isInitialized: boolean;
|
|
8
|
+
constructor(dataSources?: ConfigurationDataSource[]);
|
|
9
|
+
private initialize;
|
|
10
|
+
get<T extends object>(typeOrString: Constructable<T> | string): Promise<T>;
|
|
11
|
+
private ensureInitialized;
|
|
12
|
+
private validateDataSources;
|
|
13
|
+
private getConstructable;
|
|
14
|
+
private getStringKey;
|
|
15
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ConfigurationDataSource } from "./configuration-data-source.js";
|
|
2
|
+
/** Resolve which env-specific config file to merge (e.g. config.dev.yaml). */
|
|
3
|
+
export declare function resolveConfigEnv(nodeEnv?: string): string;
|
|
4
|
+
export type ConfigSource = {
|
|
5
|
+
/** Shown in errors, e.g. the file name. */
|
|
6
|
+
name: string;
|
|
7
|
+
text: string;
|
|
8
|
+
};
|
|
9
|
+
type Tree = Record<string, unknown>;
|
|
10
|
+
/**
|
|
11
|
+
* Merge config files in order (later files win), then resolve `${VAR}` placeholders on the
|
|
12
|
+
* merged result.
|
|
13
|
+
*
|
|
14
|
+
* Resolving after the merge means an environment file such as `config.dev.yaml` can replace a
|
|
15
|
+
* production `${VAR}` without that variable being set: a placeholder that was replaced is never
|
|
16
|
+
* looked up. Placeholders still unset after the merge fail together, naming every variable and
|
|
17
|
+
* the config path that used it. `${VAR:default}` keeps a default for optional values.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveYamlConfig(sources: ConfigSource[]): Tree;
|
|
20
|
+
export declare class YamlDataSource extends ConfigurationDataSource {
|
|
21
|
+
name: string;
|
|
22
|
+
getData(): Promise<Record<string, unknown>>;
|
|
23
|
+
}
|
|
24
|
+
export {};
|