diff --git a/PublishNote.md b/PublishNote.md
index ab9eba7..5487136 100644
--- a/PublishNote.md
+++ b/PublishNote.md
@@ -1,4 +1,8 @@
-## v2.0.0 Is Live!
+## v3.0.0
+
+Version 3.0.0 is a full re-write on the TypeScript side of the package. Its Intellisense is now fully accurate for
+almost everything and anything. It also exports the various data source classes to allow inheritance or composition to
+create other data sources.
> Visit the [project's homepage](https://github.com/WJSoftware/wj-config) for the latest version of this README.
---
diff --git a/README.md b/README.md
index 32a1db5..7694eff 100644
--- a/README.md
+++ b/README.md
@@ -2,16 +2,16 @@
[![NPM](https://img.shields.io/npm/v/wj-config?style=plastic)](https://www.npmjs.com/package/wj-config)
![Latest Release](https://img.shields.io/github/v/release/WJSoftware/wj-config?include_prereleases&sort=semver&style=plastic)
-![Lines of code](https://img.shields.io/tokei/lines/github/WJSoftware/wj-config?style=plastic&color=blueviolet)
+![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/WJSoftware/wj-config?style=plastic&color=violet)
![npm bundle size](https://img.shields.io/bundlephobia/min/wj-config?color=red&label=minified&style=plastic)
> JavaScript configuration module for **NodeJS** and **browser frameworks** that works like ASP.net configuration
-> where any number of data sources are merged and environment variables can contribute/overwrite values by following a
->naming convention.
+> where any number of data sources are merged, and environment variables can contribute/overwrite values by following a
+> naming convention.
-Welcome to **wj-config**. This JavaScript configuration library works everywhere, most likely. The table below shows
-the frameworks or libraries that have successful samples in the [examples](https://github.com/WJSoftware/wj-config/tree/main/examples) folder in the left column. The
-right column is pretty much anything else out there that looks like it supports **ES Modules**.
+This JavaScript configuration library works everywhere, most likely. The table below shows the frameworks or libraries
+that have successful samples in the [examples](https://github.com/WJSoftware/wj-config/tree/main/examples) folder in the
+left column. The right column is pretty much anything else out there that looks like it supports **ES Modules**.
@@ -26,7 +26,7 @@ right column is pretty much anything else out there that looks like it supports
JavaScript
TypeScript
NodeJS
- Deno
+ Deno
ReactJS
VueJS
Svelte
@@ -37,11 +37,12 @@ right column is pretty much anything else out there that looks like it supports
Electron
Angular
Remix
- EmberJS
+ EmberJS
SennaJS
MithrilJS
SlingJS
Lit
+ Bun
@@ -145,7 +146,7 @@ Example configuration JSON:
> **NOTE**: The `ws` section is special. See [URL-Building Functions](https://github.com/WJSoftware/wj-config/wiki/English__Theory__URL-Building-Functions)
> in the **Wiki** for the details.
-Now write per-configuration JSON files. Example for development (would be named `config.Development.json`):
+Now write per-environment JSON files. Example for development (would be named `config.Development.json`):
```json
{
@@ -155,8 +156,8 @@ Now write per-configuration JSON files. Example for development (would be named
}
```
-Yes, you only write the overrides, the values that change for the environment. All other configuration is still
-available, but does not have to be repeated.
+Yes, you only write the overrides, the values that change for the environment. All other configuration values will also
+be available, but is not necessary to repeat them: DRY configuration.
### 3. Build Your Configuration Object
@@ -167,10 +168,10 @@ There are two styles available: The *classic* style leaves to you, the programm
a way to select the correct per-environment data source. The *conditional* style leaves the decision to the
configuration builder. Pick whichever pleases you, but know that the latter is safer.
-From now on, any code samples that call the `loadJsonFile()` function are referring to this one:
+From now on, any code samples that call the `loadJsonFile()` function are referring to this function:
```js
-const loadJsonFile = (fileName, isRequired) => {
+function loadJsonFile(fileName, isRequired) {
const fileExists = fs.existsSync(fileName);
if (fileExists) {
const data = fs.readFileSync(fileName);
@@ -184,19 +185,19 @@ const loadJsonFile = (fileName, isRequired) => {
};
```
-If you don't like it, feel free to write your own. I wrote this like a year ago; I had no knowledge of the existence
-of the `fs/promises` module. If you write one yourself using async `fs`, please pull request and share the love. 😁😎
+If you don't like it, feel free to write your own. I wrote this before I knew of the existence of the `fs/promises`
+module. If you write one yourself using async `fs`, please pull request and share the love. 😁😎
#### Classic Style
##### NodeJS ES Modules (Recommended)
```js
-import wjConfig, { Environment } from 'wj-config';
+import wjConfig, { buildEnvironment } from 'wj-config';
import mainConfig from "./config.json" assert {type: 'json'}; // Importing data is a thing in NodeJS.
// Obtain an environment object ahead of time to help setting configuration up.
-const env = new Environment(process.env.NODE_ENV);
+const env = buildEnvironment(process.env.NODE_ENV /*, ['my', 'own', 'environment', 'list'] */);
const configPromise = wjConfig()
.addObject(mainConfig) // Main configuration JSON file.
@@ -205,7 +206,7 @@ const configPromise = wjConfig()
.name(env.current.name)
.addEnvironment(process.env) // Adds a data source that reads the environment variables in process.env.
.includeEnvironment(env) // So the final configuration object has the environment property.
- .createUrlFunctions() // So the final configuration object will contain URL builder functions.
+ .createUrlFunctions('ws') // So the final configuration object will contain URL builder functions.
.build(env.isDevelopment()); // Only trace configuration values in the Development environment.
// This is a top-level await:
@@ -224,8 +225,8 @@ use of [URL-Building Functions](https://github.com/WJSoftware/wj-config/wiki/Eng
// whole thing within a call to .then(), like in one of the examples provided in this project's repository.
// This is why CommonJS is discouraged. It makes things more complex.
module.exports = (async function () {
- const { default: wjConfig, Environment } = await import('wj-config');
- const env = new Environment(process.env.NODE_ENV);
+ const { default: wjConfig, buildEnvironment } = await import('wj-config');
+ const env = buildEnvironment(process.env.NODE_ENV /*, ['my', 'own', 'environment', 'list'] */);
return wjConfig()
.addObject(loadJsonFile('./config.json', true))
.name('Main')
@@ -233,24 +234,24 @@ module.exports = (async function () {
.name(env.current.name)
.addEnvironment(process.env)
.includeEnvironment(env)
- .createUrlFunctions()
+ .createUrlFunctions('ws')
.build(env.isDevelopment());
})();
```
##### Web Projects
-> **IMPORTANT**: If your project is a React project creasted with *Create React App*, the recommendation is to eject
+> **IMPORTANT**: If your project is a React project created with *Create React App*, the recommendation is to eject
> or use the `@craco/craco` package (or similar one) in order to configure webpack to allow top-level awaits. You
> can read the details in the [Top Level Await](https://github.com/WJSoftware/wj-config/wiki/English__JavaScript-Concepts__Top-Level-Await)
> section in the **Wiki**. It can also work without top-level awaits, but in all honesty, I don't like it. The
> **Wiki** also explains how to achieve this for Vite projects (Vue, Svelte, React, etc.).
```js
-import wjConfig, { Environment } from 'wj-config';
+import wjConfig, { buildEnvironment } from 'wj-config';
import mainConfig from './config.json'; // One may import data like this, or fetch it.
-const env = new Environment(window.env.REACT_ENVIRONMENT);
+const env = buildEnvironment(window.env.REACT_ENVIRONMENT /*, ['my', 'own', 'environment', 'list'] */);
const configPromise = wjConfig()
.addObject(mainConfig)
.name('Main') // Give data sources a meaningful name for value tracing purposes.
@@ -258,7 +259,7 @@ const configPromise = wjConfig()
.name(env.current.name)
.addEnvironment(window.env, 'REACT_APP_') // Adds a data source that reads the environment variables in window.env.
.includeEnvironment(env) // So the final configuration object has the environment property.
- .createUrlFunctions() // So the final configuration object will contain URL builder functions.
+ .createUrlFunctions('ws') // So the final configuration object will contain URL builder functions.
.build(env.isDevelopment()); // Only trace configuration values in the Development environment.
export default await configPromise;
@@ -302,17 +303,17 @@ There are two possible ways to do conditional style per-environment configuratio
**Web Projects** sample:
```javascript
-import wjConfig, { Environment } from 'wj-config';
+import wjConfig, { buildEnvironment } from 'wj-config';
import mainConfig from './config.json';
-const env = new Environment(window.env.REACT_ENVIRONMENT);
+const env = buildEnvironment(window.env.REACT_ENVIRONMENT /*, ['my', 'own', 'environment', 'list'] */);
const config = wjConfig()
.addObject(mainConfig)
.name('Main')
.includeEnvironment(env)
.addPerEnvironment((b, envName) => b.addFetched(`/config.${envName}.json`, false))
.addEnvironment(window.env, 'REACT_APP_')
- .createUrlFunctions()
+ .createUrlFunctions('ws')
.build(env.isDevelopment());
export default await config;
@@ -325,10 +326,10 @@ It looks almost identical to the classic. This one has a few advantages:
3. Makes sure there's at least one data source per defined environment.
**IMPORTANT**: This conditional style requires the call to `includeEnvironment()` and to be made *before* calling
-`addPerEnvironment()`. Make sure you define your environment names when creating the `Environment` object:
+`addPerEnvironment()`. Make sure you define your environment names when creating the environment object:
```javascript
-const env = new Environment(window.env.REACT_ENVIRONMENT, ['myDev', 'myTest', 'myProd']);
+const env = buildEnvironment(window.env.REACT_ENVIRONMENT, ['myDev', 'myTest', 'myProd']);
```
This way `addPerEnvironment()` knows your environment names.
@@ -336,10 +337,10 @@ This way `addPerEnvironment()` knows your environment names.
The longer way of the conditional style looks like this:
```javascript
-import wjConfig, { Environment } from 'wj-config';
+import wjConfig, { buildEnvironment } from 'wj-config';
import mainConfig from './config.json';
-const env = new Environment(window.env.REACT_ENVIRONMENT);
+const env = buildEnvironment(window.env.REACT_ENVIRONMENT);
const config = wjConfig()
.addObject(mainConfig)
.name('Main')
@@ -351,12 +352,14 @@ const config = wjConfig()
.forEnvironment('Production')
.addEnvironment(window.env, 'REACT_APP_')
.includeEnvironment(env)
- .createUrlFunctions()
+ .createUrlFunctions('ws')
.build(env.isDevelopment());
export default await config;
```
+> When not specified, the list of environments is `'Development'`, `'PreProduction'`, and `'Production'`.
+
This one has advantages 2 and 3 above, plus allows for the possiblity of having completely different data source types
per environment. Furthermore, this allows you to add more environment-specific data sources if, for example, a
particular environment requires 2 or more data sources. 95% of the time you'll need the short one only.
@@ -368,10 +371,10 @@ This works in **NodeJS** too. There is a performance catch, though: If in Node
ones. To avoid this performance hit, pass a function to `addObject()` that, in turn, calls `loadJsonFile()`:
```js
-import wjConfig, { Environment } from 'wj-config';
+import wjConfig, { buildEnvironment } from 'wj-config';
import mainConfig from "./config.json" assert {type: 'json'};
-const env = new Environment(process.env.NODE_ENV);
+const env = buildEnvironment(process.env.NODE_ENV);
const config = wjConfig()
.addObject(mainConfig)
@@ -380,7 +383,7 @@ const config = wjConfig()
// Using a function that calls loadJsonFile() instead of calling loadJsonFile directly.
.addPerEnvironment((b, envName) => b.addObject(() => loadJsonFile(`./config.${envName}.json`)))
.addEnvironment(process.env)
- .createUrlFunctions()
+ .createUrlFunctions('ws')
.build(env.isDevelopment());
export default await config;
@@ -391,8 +394,7 @@ Now you know how to do per-environment configuration in the *classic* and *condi
## Documentation
This README was already too long, so all documentation has been re-written and placed in this repository's
-[wiki](https://github.com/WJSoftware/wj-config/wiki). It is in English only for now; it should be available in
-Spanish too within the next few months.
+[wiki](https://github.com/WJSoftware/wj-config/wiki). It is in English only for now.
Be sure to stop by because this not-so-quick start tutorial only scratched the surface of what is possible with
**wj-config**.
diff --git a/package-lock.json b/package-lock.json
index 34b99a6..3e5a8e3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,18 +1,213 @@
{
"name": "wj-config",
- "lockfileVersion": 2,
+ "lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"devDependencies": {
- "typescript": "^5.2.2"
+ "publint": "^0.2.11",
+ "typescript": "^5.6.2"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/glob": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
+ "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^5.0.1",
+ "once": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/ignore-walk": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-5.0.1.tgz",
+ "integrity": "sha512-yemi4pMf51WKT7khInJqAvsIGzoqYXblnsz0ql8tM+yi1EKYTY1evX4NAbJrLL/Aanr2HyZeluqU+Oi7MGHokw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minimatch": "^5.0.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/mri": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
+ "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm-bundled": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-2.0.1.tgz",
+ "integrity": "sha512-gZLxXdjEzE/+mOstGDqR6b0EkhJ+kM6fxM6vUuckuctuVPh80Q6pw/rSZj9s4Gex9GxWtIicO1pc8DB9KZWudw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "npm-normalize-package-bin": "^2.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/npm-normalize-package-bin": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-2.0.0.tgz",
+ "integrity": "sha512-awzfKUO7v0FscrSpRoogyNm0sajikhBWpU0QMrW09AMi9n1PoKU6WaIqUzuJSQnpciZZmJ/jMZ2Egfmb/9LiWQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/npm-packlist": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-5.1.3.tgz",
+ "integrity": "sha512-263/0NGrn32YFYi4J533qzrQ/krmmrWwhKkzwTuM4f/07ug51odoaNjUexxO4vxlzURHcmYMH1QjvHjsNDKLVg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^8.0.1",
+ "ignore-walk": "^5.0.1",
+ "npm-bundled": "^2.0.0",
+ "npm-normalize-package-bin": "^2.0.0"
+ },
+ "bin": {
+ "npm-packlist": "bin/index.js"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/publint": {
+ "version": "0.2.12",
+ "resolved": "https://registry.npmjs.org/publint/-/publint-0.2.12.tgz",
+ "integrity": "sha512-YNeUtCVeM4j9nDiTT2OPczmlyzOkIXNtdDZnSuajAxS/nZ6j3t7Vs9SUB4euQNddiltIwu7Tdd3s+hr08fAsMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "npm-packlist": "^5.1.3",
+ "picocolors": "^1.1.1",
+ "sade": "^1.8.1"
+ },
+ "bin": {
+ "publint": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://bjornlu.com/sponsor"
+ }
+ },
+ "node_modules/sade": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
+ "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mri": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=6"
}
},
"node_modules/typescript": {
- "version": "5.2.2",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
- "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
+ "version": "5.6.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
+ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
"dev": true,
+ "license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -20,14 +215,13 @@
"engines": {
"node": ">=14.17"
}
- }
- },
- "dependencies": {
- "typescript": {
- "version": "5.2.2",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
- "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
- "dev": true
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true,
+ "license": "ISC"
}
}
}
diff --git a/package.json b/package.json
index 04263dc..36c6646 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,6 @@
{
"devDependencies": {
- "typescript": "^5.2.2"
+ "publint": "^0.2.11",
+ "typescript": "^5.6.2"
}
}
diff --git a/src/Builder.ts b/src/Builder.ts
deleted file mode 100644
index 7fade03..0000000
--- a/src/Builder.ts
+++ /dev/null
@@ -1,276 +0,0 @@
-import type { ConfigurationValue, IBuilder, IConfig, ICoreConfig, IDataSource, IEnvironment, Predicate, ProcessFetchResponse, Traits } from "wj-config";
-import DictionaryDataSource from "./DictionaryDataSource.js"
-import { Environment } from "./Environment.js"
-import EnvironmentDataSource from "./EnvironmentDataSource.js"
-import FetchedDataSource from "./FetchedDataSource.js";
-import { isConfig } from "./helpers.js"
-import JsonDataSource from "./JsonDataSource.js";
-import makeWsUrlFunctions from "./makeWsUrlFunctions.js"
-import merge from "./Merge.js"
-import { ObjectDataSource } from "./ObjectDataSource.js"
-import SingleValueDataSource from "./SingleValueDataSource.js";
-
-interface IEnvironmentSource {
- name?: string,
- environment: IEnvironment
-}
-
-interface IUrlData {
- wsPropertyNames: string[];
- routeValuesRegExp: RegExp;
-}
-
-interface IDataSourceDef {
- dataSource: IDataSource,
- predicate?: Predicate
-}
-
-export default class Builder implements IBuilder {
- /**
- * Default list of property names that undergo the URL functions transformation.
- */
- static readonly defaultWsPropertyNames = ['ws'];
-
- /**
- * Collection of data sources added to the builder.
- */
- private _dsDefs: IDataSourceDef[] = [];
-
- /**
- * Environment source.
- */
- private _envSource?: IEnvironmentSource;
-
- /**
- * Boolean flag used to raise an error if there was no call to includeEnvironment() when it is known to be needed.
- */
- private _envIsRequired: boolean = false;
-
- /**
- * Dictionary of environment names that have been configured with a data source using the addPerEnvironment()
- * helper function. The value is the number of times the environment name has been used.
- */
- private _perEnvDsCount: { [x: string]: number } | null = null;
-
- /**
- * URL data used to create URL functions out of specific property values in the resulting configuration object.
- */
- private _urlData?: IUrlData;
-
- /**
- * Flag to determine if the last call in the builder was the addition of a data source.
- */
- private _lastCallWasDsAdd: boolean = false;
-
- add(dataSource: IDataSource): IBuilder {
- this._dsDefs.push({
- dataSource: dataSource
- });
- dataSource.index = this._dsDefs.length - 1;
- this._lastCallWasDsAdd = true;
- return this;
- }
-
- addObject(obj: ICoreConfig | (() => Promise)): IBuilder {
- return this.add(new ObjectDataSource(obj));
- }
-
- addDictionary(dictionary: ICoreConfig | (() => Promise), hierarchySeparator: string = ':', prefixOrPredicate?: string | Predicate): IBuilder {
- return this.add(new DictionaryDataSource(dictionary, hierarchySeparator, prefixOrPredicate));
- }
-
- addEnvironment(env: ICoreConfig | (() => Promise), prefix: string = 'OPT_'): IBuilder {
- return this.add(new EnvironmentDataSource(env, prefix));
- }
-
- addFetched(input: URL | RequestInfo | (() => Promise), required: boolean = true, init?: RequestInit, procesFn?: ProcessFetchResponse): IBuilder {
- return this.add(new FetchedDataSource(input, required, init, procesFn));
- }
-
- addJson(json: string | (() => Promise), jsonParser?: JSON, reviver?: (this: any, key: string, value: any) => any) {
- return this.add(new JsonDataSource(json, jsonParser, reviver));
- }
-
- addSingleValue(path: string | (() => Promise<[string, ConfigurationValue]>), valueOrHierarchySeparator?: ConfigurationValue | string, hierarchySeparator?: string): IBuilder {
- return this.add(new SingleValueDataSource(path, valueOrHierarchySeparator, typeof path === 'function' ? valueOrHierarchySeparator as string : hierarchySeparator));
- }
-
- addPerEnvironment(addDs: (builder: IBuilder, envName: string) => boolean | string): IBuilder {
- if (!this._envSource) {
- throw new Error('Using addPerEnvironment() requires a prior call to includeEnvironment().');
- }
- this._envSource.environment.all.forEach(n => {
- const result = addDs(this, n);
- if (result !== false) {
- this.forEnvironment(n, typeof result === 'string' ? result : undefined);
- }
- });
- return this;
- }
-
- name(name: string): IBuilder {
- if (!this._lastCallWasDsAdd) {
- throw new Error('Names for data sources must be set immediately after adding the data source or setting its conditional.');
- }
- this._dsDefs[this._dsDefs.length - 1].dataSource.name = name;
- return this;
- }
-
- when(predicate: Predicate, dataSourceName?: string): IBuilder {
- if (!this._lastCallWasDsAdd) {
- throw new Error('Conditionals for data sources must be set immediately after adding the data source or setting its name.');
- }
- if (this._dsDefs[this._dsDefs.length - 1].predicate) {
- throw new Error('Cannot set more than one predicate (conditional) per data source, and the last-added data source already has a predicate.');
- }
- const dsDef = this._dsDefs[this._dsDefs.length - 1];
- dsDef.predicate = predicate;
- if (dataSourceName != undefined) {
- this.name(dataSourceName);
- }
- return this;
- }
-
- whenAllTraits(traits: Traits, dataSourceName?: string): IBuilder {
- this._envIsRequired = true;
- return this.when(env => {
- return (env as IEnvironment).hasTraits(traits);
- }, dataSourceName);
- }
-
- whenAnyTrait(traits: Traits, dataSourceName?: string): IBuilder {
- this._envIsRequired = true;
- return this.when(env => {
- return (env as IEnvironment).hasAnyTrait(traits);
- }, dataSourceName);
- }
-
- forEnvironment(envName: string, dataSourceName?: string): IBuilder {
- this._envIsRequired = true;
- this._perEnvDsCount = this._perEnvDsCount ?? {};
- let count = this._perEnvDsCount[envName] ?? 0;
- this._perEnvDsCount[envName] = ++count;
- dataSourceName =
- dataSourceName ??
- (count === 1 ? `${envName} (environment-specific)` : `${envName} #${count} (environment-specific)`);
- return this.when(e => e?.current.name === envName, dataSourceName);
- }
-
- includeEnvironment(valueOrEnv: string | IEnvironment, propNameOrEnvNames?: string[] | string, propertyName?: string): IBuilder {
- this._lastCallWasDsAdd = false;
- const propName = typeof propNameOrEnvNames === 'string' ? propNameOrEnvNames : propertyName;
- const envNames = propNameOrEnvNames && typeof propNameOrEnvNames !== 'string' ? propNameOrEnvNames : Environment.defaultNames;
- let env: IEnvironment | undefined = undefined;
- if (typeof valueOrEnv === 'string') {
- env = new Environment(valueOrEnv, envNames);
- }
- else {
- env = valueOrEnv;
- }
- this._envSource = {
- name: propName,
- environment: env
- };
- return this;
- }
-
- createUrlFunctions(wsPropertyNames?: string | string[], routeValuesRegExp?: RegExp): IBuilder {
- this._lastCallWasDsAdd = false;
- let propNames = null;
- if (typeof wsPropertyNames === 'string') {
- if (wsPropertyNames !== '') {
- propNames = [wsPropertyNames];
- }
- }
- else if (wsPropertyNames && wsPropertyNames.length > 0) {
- propNames = wsPropertyNames
- }
- else {
- propNames = Builder.defaultWsPropertyNames;
- }
- this._urlData = {
- wsPropertyNames: propNames as string[],
- routeValuesRegExp: routeValuesRegExp ?? /\{(\w+)\}/g
- };
- return this;
- }
-
- async build(traceValueSources: boolean = false, enforcePerEnvironmentCoverage: boolean = true): Promise {
- this._lastCallWasDsAdd = false;
- // See if environment is required.
- if (this._envIsRequired && !this._envSource) {
- throw new Error('The used build steps include at least one step that requires environment information. Ensure you are using "includeEnvironment()" as part of the build chain.');
- }
- // See if forEnvironment was used.
- if (this._perEnvDsCount) {
- // Ensure all specified environments are part of the possible list of environments.
- let envCount = 0;
- for (const e in this._perEnvDsCount) {
- if (!(this._envSource as IEnvironmentSource).environment.all.includes(e)) {
- throw new Error(`The environment name "${e}" was used in a call to forEnvironment(), but said name is not part of the list of possible environment names.`);
- }
- ++envCount;
- }
- if (enforcePerEnvironmentCoverage) {
- // Ensure all possible environment names were included.
- const totalEnvs = (this._envSource as IEnvironmentSource).environment.all.length;
- if (envCount !== totalEnvs) {
- throw new Error(`Only ${envCount} environment(s) were configured using forEnvironment() out of a total of ${totalEnvs} environment(s). Either complete the list or disable this check when calling build().`);
- }
- }
- }
- const qualifyingDs: IDataSource[] = [];
- let wjConfig: ICoreConfig;
- if (this._dsDefs.length > 0) {
- // Prepare a list of qualifying data sources. A DS qualifies if it has no predicate or
- // the predicate returns true.
- this._dsDefs.forEach(ds => {
- if (!ds.predicate || ds.predicate(this._envSource?.environment)) {
- qualifyingDs.push(ds.dataSource);
- }
- });
- if (qualifyingDs.length > 0) {
- const dsTasks: Promise[] = [];
- qualifyingDs.forEach(ds => {
- dsTasks.push(ds.getObject());
- });
- const sources = await Promise.all(dsTasks);
- wjConfig = merge(sources, traceValueSources ? qualifyingDs : undefined);
- }
- else {
- wjConfig = {};
- }
- }
- else {
- wjConfig = {};
- }
- if (this._envSource) {
- const envPropertyName = this._envSource.name ?? 'environment';
- if (wjConfig[envPropertyName] !== undefined) {
- throw new Error(`Cannot use property name "${envPropertyName}" for the environment object because it was defined for something else.`);
- }
- wjConfig[envPropertyName] = this._envSource.environment;
- }
- const urlData = this._urlData;
- if (urlData) {
- urlData.wsPropertyNames.forEach((value) => {
- const obj = wjConfig[value];
- if (isConfig(obj)) {
- makeWsUrlFunctions(obj, urlData.routeValuesRegExp, globalThis.window && globalThis.window.location !== undefined);
- }
- else {
- throw new Error(`The level 1 property "${value}" is not a node value (object), but it was specified as being an object containing URL-building information.`);
- }
- });
- }
- if (traceValueSources) {
- if (qualifyingDs.length > 0) {
- wjConfig._qualifiedDs = qualifyingDs.map(ds => ds.trace());
- }
- else {
- wjConfig._qualifiedDs = [];
- }
- }
- return wjConfig;
- }
-};
diff --git a/src/DictionaryDataSource.ts b/src/DictionaryDataSource.ts
deleted file mode 100644
index 2a8bcf0..0000000
--- a/src/DictionaryDataSource.ts
+++ /dev/null
@@ -1,117 +0,0 @@
-import type { ConfigurationValue, ICoreConfig, IDataSource, Predicate } from "wj-config";
-import { DataSource } from "./DataSource.js";
-import { attemptParse, forEachProperty, isConfig } from "./helpers.js";
-
-const processKey = (key: string, hierarchySeparator: string, prefix?: string) => {
- if (prefix) {
- key = key.substring(prefix.length);
- }
- return key.split(hierarchySeparator);
-};
-
-const ensurePropertyValue = (obj: ICoreConfig, name: string) => {
- if (obj[name] === undefined) {
- obj[name] = {};
- }
- return obj[name];
-}
-
-export default class DictionaryDataSource extends DataSource implements IDataSource {
- private _dictionary: ICoreConfig | (() => Promise);
- private _hierarchySeparator: string;
- private _prefixOrPredicate?: string | Predicate;
-
- #buildPredicate(): [Predicate, string] {
- let predicateFn: Predicate = name => true;
- let prefix: string = '';
- if (this._prefixOrPredicate) {
- if (typeof this._prefixOrPredicate === "string") {
- prefix = this._prefixOrPredicate;
- predicateFn = name => name.startsWith(prefix);
- }
- else {
- predicateFn = this._prefixOrPredicate;
- }
- }
- return [predicateFn, prefix];
- }
-
- #validateDictionary(dic: ICoreConfig) {
- if (!isConfig(dic)) {
- throw new Error('The provided dictionary must be a flat object.');
- }
- const [predicateFn, prefix] = this.#buildPredicate();
- forEachProperty(dic, (k, v) => {
- if (!predicateFn(k)) {
- // This property does not qualify, so skip its validation.
- return false;
- }
- if (isConfig(v)) {
- throw new Error(`The provided dictionary must be a flat object: Property ${k} has a non-scalar value.`);
- }
- });
- }
-
- #inflateDictionary(dic: ICoreConfig) {
- const result: ICoreConfig = {};
- if (!dic || !isConfig(dic)) {
- return result;
- }
- const [predicateFn, prefix] = this.#buildPredicate();
- forEachProperty(dic, (key, value) => {
- if (predicateFn(key)) {
- // Object values are disallowed because a dictionary's source is assumed to be flat.
- if (isConfig(value)) {
- throw new Error(`Dictionary data sources cannot hold object values. Key: ${key}`);
- }
- const keyParts = processKey(key, this._hierarchySeparator, prefix);
- let obj: ConfigurationValue = result;
- for (let i = 0; i < keyParts.length - 1; ++i) {
- obj = ensurePropertyValue(obj as ICoreConfig, keyParts[i]);
- if (!isConfig(obj)) {
- throw new Error(`Cannot set the value of variable "${key}" because "${keyParts[i]}" has already been created as a leaf value.`);
- }
- }
- // Ensure there is no value override.
- if ((obj as ICoreConfig)[keyParts[keyParts.length - 1]]) {
- throw new Error(`Cannot set the value of variable "${key}" because "${keyParts[keyParts.length - 1]}" has already been created as an object to hold other values.`);
- }
- // If the value is a string, attempt parsing. This is to support data sources that can only hold strings
- // as values, such as enumerating actual system environment variables.
- if (typeof value === 'string') {
- value = attemptParse(value);
- }
- (obj as ICoreConfig)[keyParts[keyParts.length - 1]] = value;
- }
- });
- return result;
- }
-
- constructor(dictionary: ICoreConfig | (() => Promise), hierarchySeparator: string, prefixOrPredicate?: string | Predicate) {
- super('Dictionary');
- if (!hierarchySeparator) {
- throw new Error('Dictionaries must specify a hierarchy separator.');
- }
- if (typeof hierarchySeparator !== 'string') {
- throw new Error('The hierarchy separator must be a string.');
- }
- this._hierarchySeparator = hierarchySeparator;
- if (typeof prefixOrPredicate === 'string' && prefixOrPredicate.length === 0) {
- throw new Error('The provided prefix value cannot be an empty string.');
- }
- this._prefixOrPredicate = prefixOrPredicate;
- if (dictionary && typeof dictionary !== 'function') {
- this.#validateDictionary(dictionary);
- }
- this._dictionary = dictionary;
- }
-
- async getObject(): Promise {
- let dic = this._dictionary;
- if (dic && typeof dic === 'function') {
- dic = await dic();
- }
- const inflatedObject = this.#inflateDictionary(dic);
- return Promise.resolve(inflatedObject);
- }
-}
diff --git a/src/Environment.ts b/src/Environment.ts
deleted file mode 100644
index 868db1a..0000000
--- a/src/Environment.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import type { EnvironmentTest, IEnvironment, IEnvironmentDefinition, Trait, Traits } from "wj-config";
-import { InvalidEnvNameError } from "./EnvConfigError.js";
-import { EnvironmentDefinition } from "./EnvironmentDefinition.js";
-import { isArray } from "./helpers.js";
-
-function ensureEnvDefinition(env: string | IEnvironmentDefinition): IEnvironmentDefinition {
- if (typeof env === 'string') {
- return new EnvironmentDefinition(env);
- }
- return env;
-}
-
-export class Environment implements IEnvironment {
- /**
- * Default list of environment names.
- */
- static defaultNames: string[] = ['Development', 'PreProduction', 'Production'];
-
- readonly current: IEnvironmentDefinition;
- readonly all: string[];
- [x: string | 'current' | 'all' | 'hasTraits' | 'hasAnyTrait']: EnvironmentTest | IEnvironmentDefinition | string[] | ((traits: Traits) => boolean)
-
- constructor(currentEnvironment: string | IEnvironmentDefinition, possibleEnvironments?: string[]) {
- this.all = possibleEnvironments ?? Environment.defaultNames;
- this.current = ensureEnvDefinition(currentEnvironment);
- let validCurrentEnvironment = false;
- this.all.forEach((envName) => {
- (this as unknown as { [x: string]: () => boolean })[`is${envName}`] = function () { return (this as unknown as Environment).current.name === envName; };
- validCurrentEnvironment = validCurrentEnvironment || this.current.name === envName;
- });
- // Throw if the current environment name was not found among the possible environment names..
- if (!validCurrentEnvironment) {
- throw new InvalidEnvNameError(this.current.name);
- }
- }
-
- #normalizeTestTraits(traits: Trait | Traits): Trait | Traits {
- if (typeof traits === 'number' && typeof this.current.traits !== 'number') {
- throw new TypeError('Cannot test a numeric trait against string traits.');
- }
- if ((typeof traits === 'string' || (isArray(traits) && typeof traits[0] === 'string')) && typeof this.current.traits === 'number') {
- throw new TypeError('Cannot test string traits against a numeric trait.');
- }
- if (typeof traits === 'string') {
- traits = [traits];
- }
- return traits;
- }
-
- hasTraits(traits: Trait | Traits): boolean {
- traits = this.#normalizeTestTraits(traits);
- const hasBitwiseTraits = (t: number) => ((this.current.traits as number) & t) === t && t > 0;
- const hasStringTraits = (t: string[]) => {
- let has = true;
- t.forEach(it => {
- has = has && (this.current.traits as string[]).includes(it);
- });
- return has;
- };
- if (typeof traits === "number") {
- return hasBitwiseTraits(traits);
- }
- return hasStringTraits(traits as string[]);
- }
-
- hasAnyTrait(traits: Trait | Traits): boolean {
- traits = this.#normalizeTestTraits(traits);
- const hasAnyBitwiseTrait = (t: number) => ((this.current.traits as number) & t) > 0;
- const hasAnyStringTrait = (t: string[]) => {
- for (let x of t) {
- if ((this.current.traits as string[]).includes(x)) {
- return true;
- }
- }
- return false;
- };
- if (typeof traits === "number") {
- return hasAnyBitwiseTrait(traits);
- }
- return hasAnyStringTrait(traits as string[]);
- }
-}
diff --git a/src/EnvironmentDataSource.ts b/src/EnvironmentDataSource.ts
deleted file mode 100644
index 1f3c7c1..0000000
--- a/src/EnvironmentDataSource.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import type { ICoreConfig } from "wj-config";
-import DictionaryDataSource from "./DictionaryDataSource.js";
-
-export default class EnvironmentDataSource extends DictionaryDataSource {
- constructor(env: ICoreConfig | (() => Promise), prefix?: string) {
- super(env, '__', prefix);
- if (!prefix) {
- throw new Error('The prefix is mandatory to avoid accidental imports of sensitive data from environment variable values.');
- }
- this.name = 'Environment';
- }
-}
diff --git a/src/EnvironmentDefinition.ts b/src/EnvironmentDefinition.ts
index 7c954e1..c417b1c 100644
--- a/src/EnvironmentDefinition.ts
+++ b/src/EnvironmentDefinition.ts
@@ -1,10 +1,24 @@
-import type { IEnvironmentDefinition, Traits } from "wj-config";
+import type { IEnvironmentDefinition, Traits } from "./wj-config.js";
-export class EnvironmentDefinition implements IEnvironmentDefinition {
- public readonly name: string;
+/**
+ * Environment definition class used to specify the current environment as an object.
+ */
+export class EnvironmentDefinition implements IEnvironmentDefinition {
+ /**
+ * Gets the environment's name.
+ */
+ public readonly name: TEnvironments;
+ /**
+ * Gets the environment's assigned traits.
+ */
public readonly traits: Traits;
- constructor(name: string, traits?: Traits) {
+ /**
+ * Initializes a new instance of this class.
+ * @param name The name of the current environment.
+ * @param traits The traits assigned to the current environment.
+ */
+ constructor(name: TEnvironments, traits?: Traits) {
this.name = name;
this.traits = traits ?? 0;
}
diff --git a/src/Merge.ts b/src/Merge.ts
index 10eb403..83c4f74 100644
--- a/src/Merge.ts
+++ b/src/Merge.ts
@@ -1,12 +1,13 @@
-import type { ICoreConfig, IDataSource } from "wj-config";
-import { forEachProperty, isArray, isConfig } from "./helpers.js";
+import { forEachProperty, isArray, isConfigNode } from "./helpers.js";
+import type { ConfigurationNode, IDataSource, Trace } from "./wj-config.js";
+
type TraceRequest = {
- trace: ICoreConfig,
+ trace: Trace,
dataSource: IDataSource
}
-function mergeTwo(obj1: ICoreConfig, obj2: ICoreConfig, trace?: TraceRequest) {
+function mergeTwo(obj1: ConfigurationNode, obj2: ConfigurationNode, trace?: TraceRequest) {
let recursiveTrace: TraceRequest | undefined;
// Add the properties of obj2.
forEachProperty(obj2, (key, value) => {
@@ -14,17 +15,17 @@ function mergeTwo(obj1: ICoreConfig, obj2: ICoreConfig, trace?: TraceRequest) {
if (value1 !== undefined) {
// If it is a scalar/array value, the value in object 2 must also be a scalar or array.
// If it is an object value, then value in object 2 must also be an object.
- if (isConfig(value1) && !isConfig(value)) {
+ if (isConfigNode(value1) && !isConfigNode(value)) {
throw new Error(`The destination value of property "${key}" is an object, but the second object is not providing an object value.`);
}
- if (!isConfig(value1) && isConfig(value)) {
+ if (!isConfigNode(value1) && isConfigNode(value)) {
throw new Error(`The destination value of property "${key}" is a scalar/array value, but the second object is not providing a scalar/array value.`);
}
- if (isConfig(value1)) {
+ if (isConfigNode(value1)) {
// Recursively merge obj2 into obj1.
if (trace) {
recursiveTrace = {
- trace: trace.trace[key] = (trace.trace[key] as ICoreConfig) ?? {},
+ trace: (trace.trace[key] = trace.trace[key] ?? {}) as Trace,
dataSource: trace.dataSource
}
}
@@ -38,18 +39,18 @@ function mergeTwo(obj1: ICoreConfig, obj2: ICoreConfig, trace?: TraceRequest) {
}
}
else {
- if (trace && isConfig(value)) {
+ if (trace && isConfigNode(value)) {
// Must trace, so merge.
obj1[key] = {};
recursiveTrace = {
- trace: trace.trace[key] = (trace.trace[key] as ICoreConfig) ?? {},
+ trace: (trace.trace[key] = trace.trace[key] ?? {}) as Trace,
dataSource: trace.dataSource
};
- mergeTwo((obj1[key] as ICoreConfig), value, recursiveTrace);
+ mergeTwo((obj1[key] as ConfigurationNode), value, recursiveTrace);
}
else {
obj1[key] = value;
- if (!isConfig(value) && trace) {
+ if (!isConfigNode(value) && trace) {
// Update the trace.
trace.trace[key] = trace.dataSource.trace();
}
@@ -59,19 +60,19 @@ function mergeTwo(obj1: ICoreConfig, obj2: ICoreConfig, trace?: TraceRequest) {
return obj1;
}
-export default function merge(objects: ICoreConfig[], dataSources?: IDataSource[]): ICoreConfig {
+export default function merge(objects: ConfigurationNode[], dataSources?: IDataSource[]): ConfigurationNode & { _trace?: Trace; } {
if (!isArray(objects)) {
throw new Error('The provided value is not an array of objects.');
}
// There must be at least one object.
- if (objects.length === 0 || !isConfig(objects[0]) || objects[0] === null || objects[0] === undefined) {
+ if (objects.length === 0 || !isConfigNode(objects[0]) || objects[0] === null || objects[0] === undefined) {
throw new Error('The first element of the array is required and must be a suitable configuration object.');
}
// If there are data sources, the number of these must match the number of provided objects.
if (dataSources && objects.length !== dataSources?.length) {
throw new Error('The number of provided objects differs from the number of provided data sources.');
}
- let result: ICoreConfig = objects[0];
+ let result: ConfigurationNode = objects[0];
let initialIndex = 1;
let trace: TraceRequest | undefined;
if (dataSources) {
@@ -88,7 +89,7 @@ export default function merge(objects: ICoreConfig[], dataSources?: IDataSource[
if (nextObject === null || nextObject === undefined) {
nextObject = {};
}
- if (!isConfig(nextObject)) {
+ if (!isConfigNode(nextObject)) {
throw new Error(`Configuration object at index ${idx} is not of the appropriate type.`);
}
if (trace) {
@@ -97,7 +98,7 @@ export default function merge(objects: ICoreConfig[], dataSources?: IDataSource[
mergeTwo(result, nextObject, trace);
}
if (trace) {
- result._trace = trace.trace;
+ (result as Trace)._trace = trace.trace;
}
return result;
}
\ No newline at end of file
diff --git a/src/buildEnvironment.ts b/src/buildEnvironment.ts
new file mode 100644
index 0000000..8f1714d
--- /dev/null
+++ b/src/buildEnvironment.ts
@@ -0,0 +1,92 @@
+import { InvalidEnvNameError } from "./EnvConfigError.js";
+import { EnvironmentDefinition } from "./EnvironmentDefinition.js";
+import { isArray } from "./helpers.js";
+import type { EnvironmentTestFn, IEnvironment, IEnvironmentDefinition, Trait, Traits } from "./wj-config.js";
+
+function ensureEnvDefinition(env: string | IEnvironmentDefinition): IEnvironmentDefinition {
+ if (typeof env === 'string') {
+ return new EnvironmentDefinition(env) as IEnvironmentDefinition;
+ }
+ return env;
+}
+
+function capitalize(text: string) {
+ return text[0].toLocaleUpperCase() + text.slice(1);
+}
+
+/**
+ * Builds an environment object with the provided environment information.
+ * @param currentEnvironment The application's current environment.
+ * @param possibleEnvironments The complete list of all possible environments.
+ * @returns The newly created `IEnvironment` object.
+ */
+export function buildEnvironment(
+ currentEnvironment: TEnvironments | IEnvironmentDefinition,
+ possibleEnvironments?: TEnvironments[]
+): IEnvironment {
+ const defaultNames: string[] = ['Development', 'PreProduction', 'Production'];
+ const env = {
+ all: possibleEnvironments ?? defaultNames,
+ current: ensureEnvDefinition(currentEnvironment),
+ } as IEnvironment;
+ env.hasAnyTrait = hasAnyTrait.bind(env);
+ env.hasTraits = hasTraits.bind(env);
+ let validCurrentEnvironment = false;
+ env.all.forEach((envName) => {
+ (env as Record)[`is${capitalize(envName)}`] = function () {
+ return env.current.name === envName;
+ };
+ validCurrentEnvironment = validCurrentEnvironment || env.current.name === envName;
+ });
+ // Throw if the current environment name was not found among the possible environment names..
+ if (!validCurrentEnvironment) {
+ throw new InvalidEnvNameError(env.current.name);
+ }
+ return env;
+
+ function normalizeTestTraits(this: IEnvironment, traits: Trait | Traits): Trait | Traits {
+ if (typeof traits === 'number' && typeof this.current.traits !== 'number') {
+ throw new TypeError('Cannot test a numeric trait against string traits.');
+ }
+ if ((typeof traits === 'string' || (isArray(traits) && typeof traits[0] === 'string')) && typeof this.current.traits === 'number') {
+ throw new TypeError('Cannot test string traits against a numeric trait.');
+ }
+ if (typeof traits === 'string') {
+ traits = [traits];
+ }
+ return traits;
+ }
+
+ function hasTraits(this: IEnvironment, traits: Trait | Traits): boolean {
+ traits = normalizeTestTraits.call(this, traits);
+ const hasBitwiseTraits = (t: number) => ((this.current.traits as number) & t) === t && t > 0;
+ const hasStringTraits = (t: string[]) => {
+ let has = true;
+ t.forEach(it => {
+ has = has && (this.current.traits as string[]).includes(it);
+ });
+ return has;
+ };
+ if (typeof traits === "number") {
+ return hasBitwiseTraits(traits);
+ }
+ return hasStringTraits(traits as string[]);
+ }
+
+ function hasAnyTrait(this: IEnvironment, traits: Trait | Traits): boolean {
+ traits = normalizeTestTraits.call(this, traits);
+ const hasAnyBitwiseTrait = (t: number) => ((this.current.traits as number) & t) > 0;
+ const hasAnyStringTrait = (t: string[]) => {
+ for (let x of t) {
+ if ((this.current.traits as string[]).includes(x)) {
+ return true;
+ }
+ }
+ return false;
+ };
+ if (typeof traits === "number") {
+ return hasAnyBitwiseTrait(traits);
+ }
+ return hasAnyStringTrait(traits as string[]);
+ }
+}
diff --git a/src/builders/Builder.ts b/src/builders/Builder.ts
new file mode 100644
index 0000000..47fda39
--- /dev/null
+++ b/src/builders/Builder.ts
@@ -0,0 +1,84 @@
+import { buildEnvironment } from "../buildEnvironment.js";
+import { DictionaryDataSource } from "../dataSources/DictionaryDataSource.js";
+import { EnvironmentDataSource } from "../dataSources/EnvironmentDataSource.js";
+import { FetchedDataSource } from "../dataSources/FetchedDataSource.js";
+import { JsonDataSource } from "../dataSources/JsonDataSource.js";
+import { ObjectDataSource } from "../dataSources/ObjectDataSource.js";
+import { SingleValueDataSource } from "../dataSources/SingleValueDataSource.js";
+import type { ConfigurationValue, IBuilder, IDataSource, IEnvironment, IncludeEnvironment, InflateDictionary, InflateKey, MergeResult, Predicate, ProcessFetchResponse, UrlBuilderSectionWithCheck } from "../wj-config.js";
+import { BuilderImpl } from "./BuilderImpl.js";
+import { EnvAwareBuilder, type IEnvironmentSource } from "./EnvAwareBuilder.js";
+
+export class Builder = {}> implements IBuilder {
+ #impl: BuilderImpl = new BuilderImpl();
+ add>(dataSource: IDataSource) {
+ this.#impl.add(dataSource);
+ return this as unknown as IBuilder>;
+ }
+
+ addObject>(obj: NewT | (() => Promise)) {
+ return this.add(new ObjectDataSource(obj));
+ }
+
+ addDictionary, TSep extends string = ':'>(dictionary: TDic | (() => Promise), hierarchySeparator?: TSep, prefixOrPredicate?: string | Predicate) {
+ return this.add, unknown>>(new DictionaryDataSource(dictionary, hierarchySeparator ?? ':', prefixOrPredicate));
+ }
+
+ addEnvironment, TPrefix extends string = 'OPT_'>(env: TDic | (() => Promise), prefix: string = 'OPT_') {
+ // @ts-expect-error InflateDictionary's resulting type, for some reason, always asserts true against "unknown". TS bug?
+ return this.add>(new EnvironmentDataSource(env, prefix));
+ }
+
+ addFetched>(input: URL | RequestInfo | (() => Promise), required: boolean = true, init?: RequestInit, procesFn?: ProcessFetchResponse) {
+ return this.add(new FetchedDataSource(input, required, init, procesFn));
+ }
+
+ addJson>(json: string | (() => Promise), jsonParser?: JSON, reviver?: (this: any, key: string, value: any) => any) {
+ return this.add(new JsonDataSource(json, jsonParser, reviver));
+ }
+
+ addSingleValue(path: TKey | (() => Promise<[TKey, TValue]>), valueOrHierarchySeparator?: TValue | TSep, hierarchySeparator?: TSep) {
+ return this.add>(new SingleValueDataSource>(path, valueOrHierarchySeparator, typeof path === 'function' ? valueOrHierarchySeparator as string : hierarchySeparator));
+ }
+
+ name(name: string) {
+ this.#impl.name(name);
+ return this;
+ }
+
+ when(predicate: () => boolean, dataSourceName?: string) {
+ this.#impl.when(predicate, dataSourceName);
+ return this;
+ }
+
+ includeEnvironment(
+ valueOrEnv: TEnvironments | IEnvironment,
+ propNameOrEnvNames?: TEnvironments[] | TEnvironmentKey,
+ propertyName?: TEnvironmentKey
+ ) {
+ this.#impl._lastCallWasDsAdd = false;
+ const propName = (typeof propNameOrEnvNames === 'string' ? propNameOrEnvNames : propertyName) ?? 'environment';
+ const envNames = (propNameOrEnvNames && typeof propNameOrEnvNames !== 'string') ? propNameOrEnvNames : undefined;
+ let env: IEnvironment;
+ if (typeof valueOrEnv === 'object') {
+ env = valueOrEnv;
+ }
+ else {
+ env = buildEnvironment(valueOrEnv, envNames);
+ }
+ const envSource: IEnvironmentSource = {
+ name: propName,
+ environment: env
+ };
+ return new EnvAwareBuilder & IncludeEnvironment>(envSource, this.#impl);
+ }
+
+ createUrlFunctions(wsPropertyNames: TUrl | TUrl[], routeValuesRegExp?: RegExp) {
+ this.#impl.createUrlFunctions(wsPropertyNames, routeValuesRegExp);
+ return this as unknown as IBuilder & UrlBuilderSectionWithCheck>;
+ }
+
+ build(traceValueSources: boolean = false) {
+ return this.#impl.build(traceValueSources, p => p()) as unknown as Promise;
+ }
+};
diff --git a/src/builders/BuilderImpl.ts b/src/builders/BuilderImpl.ts
new file mode 100644
index 0000000..90f050c
--- /dev/null
+++ b/src/builders/BuilderImpl.ts
@@ -0,0 +1,127 @@
+import { isConfigNode } from "../helpers.js";
+import makeWsUrlFunctions from "../makeWsUrlFunctions.js";
+import merge from "../Merge.js";
+import { IDataSource, Predicate } from "../wj-config.js";
+
+interface IUrlData {
+ wsPropertyNames: string[];
+ routeValuesRegExp: RegExp;
+}
+
+interface IDataSourceDef {
+ dataSource: IDataSource>,
+ predicate?: (env?: any) => boolean;
+}
+
+export class BuilderImpl {
+ /**
+ * Collection of data sources added to the builder.
+ */
+ _dsDefs: IDataSourceDef[] = [];
+ /**
+ * URL data used to create URL functions out of specific property values in the resulting configuration object.
+ */
+ _urlData?: IUrlData;
+ /**
+ * Flag to determine if the last call in the builder was the addition of a data source.
+ */
+ _lastCallWasDsAdd: boolean = false;
+
+ add(dataSource: IDataSource>) {
+ this._dsDefs.push({
+ dataSource: dataSource
+ });
+ dataSource.index = this._dsDefs.length - 1;
+ this._lastCallWasDsAdd = true;
+ }
+ name(name: string) {
+ if (!this._lastCallWasDsAdd) {
+ throw new Error('Names for data sources must be set immediately after adding the data source or setting its conditional.');
+ }
+ this._dsDefs[this._dsDefs.length - 1].dataSource.name = name;
+ }
+
+ when(predicate: Predicate, dataSourceName?: string) {
+ if (!this._lastCallWasDsAdd) {
+ throw new Error('Conditionals for data sources must be set immediately after adding the data source or setting its name.');
+ }
+ if (this._dsDefs[this._dsDefs.length - 1].predicate) {
+ throw new Error('Cannot set more than one predicate (conditional) per data source, and the last-added data source already has a predicate.');
+ }
+ const dsDef = this._dsDefs[this._dsDefs.length - 1];
+ dsDef.predicate = predicate;
+ if (dataSourceName != undefined) {
+ this.name(dataSourceName);
+ }
+ }
+
+ createUrlFunctions(wsPropertyNames: string | number | symbol | (string | number | symbol)[], routeValuesRegExp?: RegExp) {
+ this._lastCallWasDsAdd = false;
+ let propNames: (string | number | symbol)[];
+ if (typeof wsPropertyNames === 'string') {
+ if (wsPropertyNames !== '') {
+ propNames = [wsPropertyNames];
+ }
+ }
+ else if (Array.isArray(wsPropertyNames) && wsPropertyNames.length > 0) {
+ propNames = wsPropertyNames
+ }
+ else {
+ throw new Error("The 'wsPropertyNames' property now has no default value and must be provided.");
+ }
+ this._urlData = {
+ wsPropertyNames: propNames! as string[],
+ routeValuesRegExp: routeValuesRegExp ?? /\{(\w+)\}/g
+ };
+ }
+
+ async build(traceValueSources: boolean = false, evaluatePredicate: Predicate) {
+ this._lastCallWasDsAdd = false;
+ const qualifyingDs: IDataSource>[] = [];
+ let wjConfig: Record;
+ if (this._dsDefs.length > 0) {
+ // Prepare a list of qualifying data sources. A DS qualifies if it has no predicate or
+ // the predicate returns true.
+ this._dsDefs.forEach(ds => {
+ if (!ds.predicate || evaluatePredicate(ds.predicate)) {
+ qualifyingDs.push(ds.dataSource);
+ }
+ });
+ if (qualifyingDs.length > 0) {
+ const dsTasks: Promise>[] = [];
+ qualifyingDs.forEach(ds => {
+ dsTasks.push(ds.getObject());
+ });
+ const sources = await Promise.all(dsTasks);
+ wjConfig = merge(sources, traceValueSources ? qualifyingDs : undefined);
+ }
+ else {
+ wjConfig = {};
+ }
+ }
+ else {
+ wjConfig = {};
+ }
+ const urlData = this._urlData;
+ if (urlData) {
+ urlData.wsPropertyNames.forEach((value) => {
+ const obj = wjConfig[value];
+ if (isConfigNode(obj)) {
+ makeWsUrlFunctions(obj, urlData.routeValuesRegExp, globalThis.window && globalThis.window.location !== undefined);
+ }
+ else {
+ throw new Error(`The level 1 property "${value}" is not a node value (object), but it was specified as being an object containing URL-building information.`);
+ }
+ });
+ }
+ if (traceValueSources) {
+ if (qualifyingDs.length > 0) {
+ wjConfig._qualifiedDs = qualifyingDs.map(ds => ds.trace());
+ }
+ else {
+ wjConfig._qualifiedDs = [];
+ }
+ }
+ return wjConfig;
+ }
+}
diff --git a/src/builders/EnvAwareBuilder.ts b/src/builders/EnvAwareBuilder.ts
new file mode 100644
index 0000000..3de64fb
--- /dev/null
+++ b/src/builders/EnvAwareBuilder.ts
@@ -0,0 +1,153 @@
+import { DictionaryDataSource } from "../dataSources/DictionaryDataSource.js";
+import { EnvironmentDataSource } from "../dataSources/EnvironmentDataSource.js";
+import { FetchedDataSource } from "../dataSources/FetchedDataSource.js";
+import { JsonDataSource } from "../dataSources/JsonDataSource.js";
+import { ObjectDataSource } from "../dataSources/ObjectDataSource.js";
+import { SingleValueDataSource } from "../dataSources/SingleValueDataSource.js";
+import type { ConfigurationValue, IDataSource, IEnvAwareBuilder, IEnvironment, InflateDictionary, InflateKey, MergeResult, Predicate, ProcessFetchResponse, Traits, UrlBuilderSectionWithCheck } from "../wj-config.js";
+import { BuilderImpl } from "./BuilderImpl.js";
+
+export interface IEnvironmentSource {
+ name?: string,
+ environment: IEnvironment;
+}
+
+export class EnvAwareBuilder = {}> implements IEnvAwareBuilder {
+ /**
+ * Environment source.
+ */
+ private _envSource: IEnvironmentSource;
+ #impl: BuilderImpl;
+
+ constructor(envSource: IEnvironmentSource, impl: BuilderImpl) {
+ this._envSource = envSource;
+ this.#impl = impl;
+ }
+
+ add>(dataSource: IDataSource) {
+ this.#impl.add(dataSource);
+ return this as unknown as IEnvAwareBuilder>;
+ }
+
+ addObject>(obj: NewT | (() => Promise)) {
+ return this.add(new ObjectDataSource(obj));
+ }
+
+ addDictionary, TSep extends string = ':'>(dictionary: Record | (() => Promise>), hierarchySeparator: string = ':', prefixOrPredicate?: string | Predicate) {
+ // @ts-expect-error
+ return this.add>(new DictionaryDataSource(dictionary, hierarchySeparator, prefixOrPredicate));
+ }
+
+ addEnvironment, TPrefix extends string = 'OPT_'>(env: Record | (() => Promise>), prefix: string = 'OPT_') {
+ return this.add>>(new EnvironmentDataSource(env, prefix));
+ }
+
+ addFetched>(input: URL | RequestInfo | (() => Promise), required: boolean = true, init?: RequestInit, procesFn?: ProcessFetchResponse) {
+ return this.add(new FetchedDataSource(input, required, init, procesFn));
+ }
+
+ addJson>(json: string | (() => Promise), jsonParser?: JSON, reviver?: (this: any, key: string, value: any) => any) {
+ return this.add(new JsonDataSource(json, jsonParser, reviver));
+ }
+
+ addSingleValue(path: TKey | (() => Promise<[TKey, TValue]>), valueOrHierarchySeparator?: TValue | TSep, hierarchySeparator?: TSep) {
+ return this.add(new SingleValueDataSource>>(path, valueOrHierarchySeparator, typeof path === 'function' ? valueOrHierarchySeparator as string : hierarchySeparator));
+ }
+
+ name(name: string) {
+ this.#impl.name(name);
+ return this;
+ }
+
+ createUrlFunctions(wsPropertyNames: TUrl | TUrl[], routeValuesRegExp?: RegExp) {
+ this.#impl.createUrlFunctions(wsPropertyNames, routeValuesRegExp);
+ return this as unknown as IEnvAwareBuilder & UrlBuilderSectionWithCheck>;
+ }
+
+ /**
+ * Boolean flag used to raise an error if there was no call to includeEnvironment() when it is known to be needed.
+ */
+ private _envIsRequired: boolean = false;
+
+ /**
+ * Dictionary of environment names that have been configured with a data source using the addPerEnvironment()
+ * helper function. The value is the number of times the environment name has been used.
+ */
+ private _perEnvDsCount: Record, number> | null = null;
+
+ addPerEnvironment>(addDs: (builder: IEnvAwareBuilder, envName: TEnvironments) => boolean | string) {
+ if (!this._envSource) {
+ throw new Error('Using addPerEnvironment() requires a prior call to includeEnvironment().');
+ }
+ this._envSource.environment.all.forEach(n => {
+ const result = addDs(this, n);
+ if (result !== false) {
+ this.forEnvironment(n, typeof result === 'string' ? result : undefined);
+ }
+ });
+ return this as unknown as IEnvAwareBuilder>;
+ }
+
+ when(predicate: Predicate | undefined>, dataSourceName?: string) {
+ this.#impl.when(predicate, dataSourceName);
+ return this;
+ }
+
+ forEnvironment(envName: Exclude, dataSourceName?: string) {
+ this._envIsRequired = true;
+ this._perEnvDsCount = this._perEnvDsCount ?? {} as Record, number>;
+ let count = this._perEnvDsCount[envName] ?? 0;
+ this._perEnvDsCount[envName] = ++count;
+ dataSourceName =
+ dataSourceName ??
+ (count === 1 ? `${envName} (environment-specific)` : `${envName} #${count} (environment-specific)`);
+ return this.when(e => e?.current.name === envName, dataSourceName);
+ }
+
+ whenAllTraits(traits: Traits, dataSourceName?: string) {
+ this._envIsRequired = true;
+ return this.when(env => {
+ return env!.hasTraits(traits) ?? false;
+ }, dataSourceName);
+ }
+
+ whenAnyTrait(traits: Traits, dataSourceName?: string) {
+ this._envIsRequired = true;
+ return this.when(env => {
+ return env!.hasAnyTrait(traits);
+ }, dataSourceName);
+ }
+
+ async build(traceValueSources: boolean = false, enforcePerEnvironmentCoverage: boolean = true) {
+ this.#impl._lastCallWasDsAdd = false;
+ // See if environment is required.
+ if (this._envIsRequired && !this._envSource) {
+ throw new Error('The used build steps include at least one step that requires environment information. Ensure you are using "includeEnvironment()" as part of the build chain.');
+ }
+ // See if forEnvironment was used.
+ if (this._perEnvDsCount) {
+ // Ensure all specified environments are part of the possible list of environments.
+ let envCount = 0;
+ for (const e in this._perEnvDsCount) {
+ if (!this._envSource!.environment.all.includes(e as Exclude)) {
+ throw new Error(`The environment name "${e}" was used in a call to forEnvironment(), but said name is not part of the list of possible environment names.`);
+ }
+ ++envCount;
+ }
+ if (enforcePerEnvironmentCoverage) {
+ // Ensure all possible environment names were included.
+ const totalEnvs = (this._envSource as IEnvironmentSource).environment.all.length;
+ if (envCount !== totalEnvs) {
+ throw new Error(`Only ${envCount} environment(s) were configured using forEnvironment() out of a total of ${totalEnvs} environment(s). Either complete the list or disable this check when calling build().`);
+ }
+ }
+ }
+ const result = await this.#impl.build(traceValueSources, p => p(this._envSource?.environment));
+ const envPropertyName = this._envSource.name ?? 'environment';
+ if (result[envPropertyName] !== undefined) {
+ throw new Error(`Cannot use property name "${envPropertyName}" for the environment object because it was defined for something else.`);
+ }
+ result[envPropertyName] = this._envSource.environment;
+ return result as T;
+ }
+}
\ No newline at end of file
diff --git a/src/DataSource.ts b/src/dataSources/DataSource.ts
similarity index 85%
rename from src/DataSource.ts
rename to src/dataSources/DataSource.ts
index e2b1699..25531c6 100644
--- a/src/DataSource.ts
+++ b/src/dataSources/DataSource.ts
@@ -1,4 +1,4 @@
-import type { IDataSourceInfo } from "wj-config";
+import type { IDataSourceInfo } from "../wj-config.js";
export class DataSource {
name: string;
diff --git a/src/dataSources/DictionaryDataSource.ts b/src/dataSources/DictionaryDataSource.ts
new file mode 100644
index 0000000..9755c00
--- /dev/null
+++ b/src/dataSources/DictionaryDataSource.ts
@@ -0,0 +1,128 @@
+import { attemptParse, forEachProperty, isConfigNode } from "../helpers.js";
+import type { ConfigurationNode, ConfigurationValue, Dictionary, IDataSource, Predicate } from "../wj-config.js";
+import { DataSource } from "./DataSource.js";
+
+const processKey = (key: string, hierarchySeparator: string, prefix?: string) => {
+ if (prefix) {
+ key = key.substring(prefix.length);
+ }
+ return key.split(hierarchySeparator);
+};
+
+const ensurePropertyValue = (obj: ConfigurationNode, name: string) => {
+ if (obj[name] === undefined) {
+ obj[name] = {};
+ }
+ return obj[name];
+}
+
+export class DictionaryDataSource> extends DataSource implements IDataSource {
+ #dictionary: Record | (() => Promise>);
+ #hierarchySeparator: string;
+ #prefixOrPredicate?: string | Predicate;
+
+ #buildPredicate(): [Predicate, string] {
+ let predicateFn: Predicate = _ => true;
+ let prefix: string = '';
+ if (this.#prefixOrPredicate) {
+ if (typeof this.#prefixOrPredicate === "string") {
+ prefix = this.#prefixOrPredicate;
+ predicateFn = name => name.startsWith(prefix);
+ }
+ else {
+ predicateFn = this.#prefixOrPredicate;
+ }
+ }
+ return [predicateFn, prefix];
+ }
+
+ #validateDictionary(dic: unknown) {
+ if (!isConfigNode(dic)) {
+ throw new Error('The provided dictionary must be a flat object.');
+ }
+ const [predicateFn, prefix] = this.#buildPredicate();
+ forEachProperty(dic, (k, v) => {
+ if (!predicateFn(k)) {
+ // This property does not qualify, so skip its validation.
+ return false;
+ }
+ if (isConfigNode(v)) {
+ throw new Error(`The provided dictionary must be a flat object: Property ${k} has a non-scalar value.`);
+ }
+ });
+ }
+
+ #inflateDictionary(dic: Dictionary) {
+ const result = {} as T;
+ if (!dic) {
+ return result;
+ }
+ const [predicateFn, prefix] = this.#buildPredicate();
+ forEachProperty(dic, (key, value) => {
+ if (predicateFn(key)) {
+ // Object values are disallowed because a dictionary's source is assumed to be flat.
+ // if (isConfigNode(value)) {
+ // throw new Error(`Dictionary data sources cannot hold object values. Key: ${key}`);
+ // }
+ const keyParts = processKey(key, this.#hierarchySeparator, prefix);
+ let obj: ConfigurationValue | ConfigurationNode = result;
+ let keyPath = '';
+ for (let i = 0; i < keyParts.length - 1; ++i) {
+ keyPath += (keyPath.length ? '.' : '') + keyParts[i];
+ obj = ensurePropertyValue(obj, keyParts[i]);
+ if (!isConfigNode(obj)) {
+ throw new Error(`Cannot set the value of property "${key}" because "${keyPath}" has already been created as a leaf value.`);
+ }
+ }
+ // Ensure there is no value override.
+ if (obj[keyParts[keyParts.length - 1]]) {
+ throw new Error(`Cannot set the value of variable "${key}" because "${keyParts[keyParts.length - 1]}" has already been created as an object to hold other values.`);
+ }
+ // If the value is a string, attempt parsing. This is to support data sources that can only hold strings
+ // as values, such as enumerating actual system environment variables.
+ if (typeof value === 'string') {
+ value = attemptParse(value);
+ }
+ obj[keyParts[keyParts.length - 1]] = value;
+ }
+ });
+ return result;
+ }
+
+ constructor(dictionary: Dictionary | (() => Promise), hierarchySeparator: string, prefixOrPredicate?: string | Predicate) {
+ super('Dictionary');
+ if (!hierarchySeparator) {
+ throw new Error('Dictionaries must specify a hierarchy separator.');
+ }
+ if (typeof hierarchySeparator !== 'string') {
+ throw new Error('The hierarchy separator must be a string.');
+ }
+ this.#hierarchySeparator = hierarchySeparator;
+ if (prefixOrPredicate !== undefined) {
+ if (typeof prefixOrPredicate === 'string' && prefixOrPredicate.length === 0) {
+ throw new Error('The provided prefix value cannot be an empty string.');
+ }
+ if (typeof prefixOrPredicate !== 'string' && typeof prefixOrPredicate !== 'function') {
+ throw new Error('The prefix argument can only be a string or a function.');
+ }
+ if (typeof prefixOrPredicate === 'string' && prefixOrPredicate.length === 0) {
+ throw new Error('An empty string cannot be used as prefix.');
+ }
+ }
+ this.#prefixOrPredicate = prefixOrPredicate;
+ if (typeof dictionary !== 'function') {
+ this.#validateDictionary(dictionary);
+ }
+ this.#dictionary = dictionary;
+ }
+
+ async getObject(): Promise {
+ let dic = this.#dictionary;
+ if (dic && typeof dic === 'function') {
+ dic = await dic();
+ this.#validateDictionary(dic);
+ }
+ const inflatedObject = this.#inflateDictionary(dic);
+ return Promise.resolve(inflatedObject);
+ }
+}
diff --git a/src/dataSources/EnvironmentDataSource.ts b/src/dataSources/EnvironmentDataSource.ts
new file mode 100644
index 0000000..1725735
--- /dev/null
+++ b/src/dataSources/EnvironmentDataSource.ts
@@ -0,0 +1,12 @@
+import type { Dictionary } from "../wj-config.js";
+import { DictionaryDataSource } from "./DictionaryDataSource.js";
+
+export class EnvironmentDataSource> extends DictionaryDataSource {
+ constructor(env: Dictionary | (() => Promise), prefix?: string) {
+ super(env, '__', prefix);
+ if (!prefix) {
+ throw new Error('The prefix is mandatory to avoid accidental imports of sensitive data from environment variable values.');
+ }
+ this.name = 'Environment';
+ }
+}
diff --git a/src/FetchedDataSource.ts b/src/dataSources/FetchedDataSource.ts
similarity index 89%
rename from src/FetchedDataSource.ts
rename to src/dataSources/FetchedDataSource.ts
index cb35121..712d6b0 100644
--- a/src/FetchedDataSource.ts
+++ b/src/dataSources/FetchedDataSource.ts
@@ -1,12 +1,12 @@
-import type { ICoreConfig, ProcessFetchResponse } from "wj-config";
+import type { ProcessFetchResponse } from "../wj-config.js";
import { DataSource } from "./DataSource.js";
-export default class FetchedDataSource extends DataSource {
+export class FetchedDataSource> extends DataSource {
private _input: URL | RequestInfo | (() => Promise);
private _required: boolean;
private _init?: RequestInit;
- private _processFn: ProcessFetchResponse;
- constructor(input: URL | RequestInfo | (() => Promise), required: boolean = true, init?: RequestInit, processFn?: ProcessFetchResponse) {
+ private _processFn: ProcessFetchResponse;
+ constructor(input: URL | RequestInfo | (() => Promise), required: boolean = true, init?: RequestInit, processFn?: ProcessFetchResponse) {
super(typeof input === 'string' ? `Fetch ${input}` : 'Fetched Configuration');
this._input = input;
this._required = required;
@@ -28,12 +28,12 @@ export default class FetchedDataSource extends DataSource {
});
}
- async getObject(): Promise {
+ async getObject(): Promise {
let input = this._input;
if (typeof input === 'function') {
input = await input();
}
- let data: ICoreConfig = {};
+ let data = {} as T;
try {
const response = await fetch(input, this._init);
try {
diff --git a/src/JsonDataSource.ts b/src/dataSources/JsonDataSource.ts
similarity index 81%
rename from src/JsonDataSource.ts
rename to src/dataSources/JsonDataSource.ts
index 3bbef44..49b8960 100644
--- a/src/JsonDataSource.ts
+++ b/src/dataSources/JsonDataSource.ts
@@ -1,6 +1,6 @@
import { DataSource } from "./DataSource.js";
-export default class JsonDataSource extends DataSource {
+export class JsonDataSource> extends DataSource {
private _json: string | (() => Promise);
private _jsonParser: JSON;
private _reviver?: (this: any, key: string, value: any) => any;
@@ -16,6 +16,6 @@ export default class JsonDataSource extends DataSource {
if (typeof json === 'function') {
json = await json();
}
- return this._jsonParser.parse(json, this._reviver);
+ return this._jsonParser.parse(json, this._reviver) as T;
}
}
diff --git a/src/ObjectDataSource.ts b/src/dataSources/ObjectDataSource.ts
similarity index 65%
rename from src/ObjectDataSource.ts
rename to src/dataSources/ObjectDataSource.ts
index 5ab9b4f..499330e 100644
--- a/src/ObjectDataSource.ts
+++ b/src/dataSources/ObjectDataSource.ts
@@ -1,18 +1,18 @@
-import type { ICoreConfig, IDataSource } from "wj-config";
+import { isConfigNode } from "../helpers.js";
+import type { IDataSource } from "../wj-config.js";
import { DataSource } from "./DataSource.js";
-import { isConfig } from "./helpers.js";
/**
* Configuration data source class that injects a pre-build JavaScript object into the configuration build chain.
*/
-export class ObjectDataSource extends DataSource implements IDataSource {
+export class ObjectDataSource> extends DataSource implements IDataSource {
/**
* The object to inject.
*/
- private _obj: ICoreConfig | (() => Promise);
+ private _obj: T | (() => Promise);
- #validateObject(obj: ICoreConfig) {
- if (!isConfig(obj)) {
+ #validateObject(obj: T) {
+ if (!isConfigNode(obj)) {
throw new Error('The provided object is not suitable as configuration data source.');
}
}
@@ -21,7 +21,7 @@ export class ObjectDataSource extends DataSource implements IDataSource {
* Initializes a new instance of this class.
* @param obj Data object to inject into the configuration build chain.
*/
- constructor(obj: ICoreConfig | (() => Promise)) {
+ constructor(obj: T | (() => Promise)) {
super('Object');
if (typeof obj !== 'function') {
this.#validateObject(obj);
@@ -29,7 +29,7 @@ export class ObjectDataSource extends DataSource implements IDataSource {
this._obj = obj;
}
- async getObject(): Promise {
+ async getObject(): Promise {
let obj = this._obj;
if (typeof obj === 'function') {
obj = await obj();
diff --git a/src/SingleValueDataSource.ts b/src/dataSources/SingleValueDataSource.ts
similarity index 54%
rename from src/SingleValueDataSource.ts
rename to src/dataSources/SingleValueDataSource.ts
index 89ca2e3..6dab2e3 100644
--- a/src/SingleValueDataSource.ts
+++ b/src/dataSources/SingleValueDataSource.ts
@@ -1,12 +1,12 @@
-import type { ConfigurationValue, ICoreConfig } from "wj-config";
-import DictionaryDataSource from "./DictionaryDataSource.js";
+import type { ConfigurationValue, Dictionary } from "../wj-config.js";
+import { DictionaryDataSource } from "./DictionaryDataSource.js";
-const buildDictionary = (key: string | (() => Promise<[string, ConfigurationValue]>), value?: ConfigurationValue): ICoreConfig | (() => Promise) => {
+function buildDictionary(key: string | (() => Promise<[string, ConfigurationValue]>), value?: ConfigurationValue) {
if (!key) {
throw new Error('No valid path was provided.');
}
const dicFn = (k: string, v: ConfigurationValue) => {
- const dic: ICoreConfig = {};
+ const dic: Dictionary = {};
dic[k] = v;
return dic;
};
@@ -19,8 +19,12 @@ const buildDictionary = (key: string | (() => Promise<[string, ConfigurationValu
return dicFn(key, value);
}
-export default class SingleValueDataSource extends DictionaryDataSource {
- constructor(path: string | (() => Promise<[string, ConfigurationValue]>), value?: ConfigurationValue, hierarchySeparator: string = ':') {
+export class SingleValueDataSource> extends DictionaryDataSource {
+ constructor(
+ path: string | (() => Promise<[string, ConfigurationValue]>),
+ value?: ConfigurationValue,
+ hierarchySeparator: string = ':'
+ ) {
super(buildDictionary(path, value), hierarchySeparator);
if (typeof path === 'string') {
this.name = `Single Value: ${path}`;
diff --git a/src/dataSources/index.ts b/src/dataSources/index.ts
new file mode 100644
index 0000000..1e8b5d9
--- /dev/null
+++ b/src/dataSources/index.ts
@@ -0,0 +1,8 @@
+export { DataSource } from "./DataSource.js";
+export { DictionaryDataSource } from "./DictionaryDataSource.js";
+export { EnvironmentDataSource } from "./EnvironmentDataSource.js";
+export { FetchedDataSource } from "./FetchedDataSource.js";
+export { JsonDataSource } from "./JsonDataSource.js";
+export { ObjectDataSource } from "./ObjectDataSource.js";
+export { SingleValueDataSource } from "./SingleValueDataSource.js";
+
diff --git a/src/helpers.ts b/src/helpers.ts
index 04ccbe0..baaee7e 100644
--- a/src/helpers.ts
+++ b/src/helpers.ts
@@ -1,6 +1,6 @@
'use strict';
-import { IConfig } from "wj-config";
+import { ConfigurationNode, Dictionary } from "./wj-config.js";
/**
* Tests the provided object to determine if it is an array.
@@ -15,8 +15,34 @@ export function isArray(obj: unknown): obj is any[] { return Array.isArray(obj);
* @param obj Object to test.
* @returns True if the object is a non-leaf object; false otherwise.
*/
-export function isConfig(obj: unknown): obj is IConfig {
- return typeof obj === 'object' && !isArray(obj) && !(obj instanceof Date);
+export function isConfigNode(obj: unknown): obj is ConfigurationNode {
+ return typeof obj === 'object'
+ && obj !== null
+ && !isArray(obj)
+ && !(obj instanceof Date)
+ && !(obj instanceof Set)
+ && !(obj instanceof Map)
+ && !(obj instanceof WeakMap)
+ && !(obj instanceof WeakRef)
+ && !(obj instanceof WeakSet)
+ ;
+}
+
+/**
+ * Tests a particular object to determine if it is a dictionary.
+ * @param obj Object to test.
+ * @returns `true` if the object is a dictionary, or `false` otherwise.
+ */
+export function isDictionary(obj: unknown): obj is Dictionary {
+ if (typeof obj === 'object' && obj !== null && !isArray(obj) && !(obj instanceof Date)) {
+ for (let key in obj) {
+ if (isConfigNode((obj as Record)[key])) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
}
/**
@@ -79,7 +105,7 @@ export const attemptParse = (value: (string | undefined | null)) => {
else if (isFloat.test(value)) {
parsedValue = Number.parseFloat(value);
}
- if (parsedValue !== NaN && parsedValue !== null) {
+ if (parsedValue !== null && !isNaN(parsedValue)) {
return parsedValue;
}
// Return as string.
diff --git a/src/index.ts b/src/index.ts
index 9c84e5a..51b040a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,9 +1,9 @@
-import { IBuilder } from "wj-config";
-import Builder from "./Builder.js";
+import { Builder } from "./builders/Builder.js";
+import { type IBuilder } from "./wj-config.js";
-export * from "./Environment.js";
+export * from "./buildEnvironment.js";
export * from "./EnvironmentDefinition.js";
-export * from "./DataSource.js";
+export type * from "./wj-config.js";
export default function wjConfig(): IBuilder {
return new Builder();
}
diff --git a/src/makeWsUrlFunctions.ts b/src/makeWsUrlFunctions.ts
index 93545b2..0c66815 100644
--- a/src/makeWsUrlFunctions.ts
+++ b/src/makeWsUrlFunctions.ts
@@ -1,5 +1,5 @@
-import type { ICoreConfig, IWsParent, IWsPath, QueryString, RouteValues, RouteValuesFunction } from "wj-config";
-import { forEachProperty, isArray, isFunction, isConfig } from "./helpers.js";
+import { forEachProperty, isArray, isConfigNode } from "./helpers.js";
+import type { ConfigurationNode, QueryStringArg, RouteReplacementArg, RouteValuesFn, UrlNode, UrlRoot } from "./wj-config.js";
const noop = (x?: any) => '';
@@ -8,17 +8,17 @@ const rootUrlObjectProps = ['host', 'rootPath'];
const rootUrlObjectPropsForBrowser = ['host', 'rootPath', 'scheme', 'port'];
function buildUrlImpl(
- this: IWsPath,
+ this: UrlNode,
path: string,
- routeValues?: RouteValues,
+ routeValues?: RouteReplacementArg,
routeRegex?: RegExp,
- queryString?: QueryString
+ queryString?: QueryStringArg
) {
- let routeValuesFn: RouteValuesFunction | undefined = undefined;
+ let routeValuesFn: RouteValuesFn | undefined = undefined;
let index = 0;
if (routeValues) {
- if (isFunction(routeValues)) {
- routeValuesFn = routeValues;
+ if (typeof routeValues === 'function') {
+ routeValuesFn = routeValues as RouteValuesFn;
}
else if (isArray(routeValues)) {
routeValuesFn = n => routeValues[index++];
@@ -41,7 +41,7 @@ function buildUrlImpl(
} else if (url.length - qmPos - 1) {
url += '&'
}
- let qsValue: string | ICoreConfig | undefined;
+ let qsValue: string | Record | undefined;
if (typeof queryString === 'function') {
qsValue = queryString();
}
@@ -65,7 +65,14 @@ function buildUrlImpl(
return url;
}
-function parentRootPath(this: IWsParent, isBrowser: boolean) {
+/**
+ * Builds a relative, absolute or full URL using the information found in the root node and taking into account if the
+ * code is running in the browser.
+ * @param this URL root node holding the URL information.
+ * @param isBrowser A Boolean value that indicates if code is running in the browser.
+ * @returns The relative, absolute or full URL, product of the information in the root node.
+ */
+function parentRootPath(this: UrlRoot, isBrowser: boolean) {
if ((!this.host && !isBrowser) || (!this.host && !this.port && !this.scheme)) {
// When no host outside the browser, or no host, port or scheme in the browser,
// build a relative URL starting with the root path.
@@ -74,16 +81,22 @@ function parentRootPath(this: IWsParent, isBrowser: boolean) {
return `${(this.scheme ?? 'http')}://${(this.host ?? globalThis.window?.location?.hostname ?? '')}${(this.port ? `:${this.port}` : '')}${(this.rootPath ?? '')}`;
}
-function pathRootPath(this: IWsPath, parent: IWsPath) {
+/**
+ * Builds a relative, absolute or full URL by appending path information to the generated URL from the parent node.
+ * @param this URL node holding path information.
+ * @param parent Parent node.
+ * @returns The relative, absolute or full URL built by appending path information to the parent's generated URL.
+ */
+function pathRootPath(this: UrlNode, parent: UrlNode) {
const rp = (parent[rootPathFn] ?? noop)();
return `${rp}${(this.rootPath ?? '')}`;
}
-function makeWsUrlFunctions(ws: IWsParent | ICoreConfig, routeValuesRegExp: RegExp, isBrowser: boolean, parent?: IWsParent) {
+function makeWsUrlFunctions(ws: UrlRoot | ConfigurationNode, routeValuesRegExp: RegExp, isBrowser: boolean, parent?: UrlRoot) {
if (!ws) {
return;
}
- if (!isConfig(ws)) {
+ if (!isConfigNode(ws)) {
throw new Error(`Cannot operate on a non-object value (provided value is of type ${typeof ws}).`);
}
const shouldConvert = (name: string) => {
@@ -97,46 +110,46 @@ function makeWsUrlFunctions(ws: IWsParent | ICoreConfig, routeValuesRegExp: RegE
];
return !name.startsWith('_') && !exceptions.includes(name);
};
- const isRoot = (obj: object) => {
+ const isUrlRoot = (obj: object): obj is UrlRoot => {
// An object is a root object if it has host or rootPath, or if code is running in a browser, an object is a
// root object if it has any of the reserved properties.
let yes = false;
forEachProperty(obj, k => yes = rootUrlObjectProps.includes(k) || (isBrowser && rootUrlObjectPropsForBrowser.includes(k)));
return yes;
};
+ const isUrlNode = (obj: unknown, parent: UrlNode | undefined): obj is UrlNode => {
+ return !!parent?.[rootPathFn] || !!(obj as UrlNode)._rootPath;
+ }
// Add the _rootPath() function.
- let canBuildUrl = true;
- if (isRoot(ws) && (!parent?.buildUrl)) {
+ if (isUrlRoot(ws) && (!parent?.buildUrl)) {
ws[rootPathFn] = function () {
- return parentRootPath.bind((ws as IWsPath))(isBrowser);
+ return parentRootPath.bind(ws)(isBrowser);
};
}
- else if (parent !== undefined && parent[rootPathFn] !== undefined) {
+ else if (isUrlNode(ws, parent)) {
ws[rootPathFn] = function () {
- return pathRootPath.bind((ws as IWsPath))(parent);
+ return pathRootPath.bind(ws)(parent!);
};
}
- else {
- canBuildUrl = false;
- }
- if (canBuildUrl) {
+ if (isUrlNode(ws, parent)) {
// Add the buildUrl function.
- ws.buildUrl = function (path: string, routeValues?: RouteValues, queryString?: QueryString) {
- return buildUrlImpl.bind((ws as IWsPath))(path, routeValues, routeValuesRegExp, queryString);
+ ws.buildUrl = function (path: string, routeValues?: RouteReplacementArg, queryString?: QueryStringArg) {
+ return buildUrlImpl.bind((ws as UrlNode))(path, routeValues, routeValuesRegExp, queryString);
};
}
+ const canServeAsParent = isUrlNode(ws, undefined);
// For every non-object property in the object, make it a function.
// Properties that have an object are recursively configured.
forEachProperty(ws, (key, value) => {
const sc = shouldConvert(key);
- if (sc && canBuildUrl && typeof value === 'string') {
- ws[key] = function (routeValues?: RouteValues, queryString?: QueryString) {
- return ((ws as IWsPath).buildUrl ?? noop)(value, routeValues, queryString);
+ if (sc && canServeAsParent && typeof value === 'string') {
+ (ws as UrlNode)[key] = function (routeValues?: RouteReplacementArg, queryString?: QueryStringArg) {
+ return (ws.buildUrl ?? noop)(value, routeValues, queryString);
};
}
- else if (sc && isConfig(value)) {
+ else if (sc && isConfigNode(value)) {
// Object value.
- makeWsUrlFunctions(value as IWsParent, routeValuesRegExp, isBrowser, (ws as IWsParent));
+ makeWsUrlFunctions(value, routeValuesRegExp, isBrowser, canServeAsParent ? ws : undefined);
}
});
};
diff --git a/src/package-lock.json b/src/package-lock.json
index 50ac97f..2139094 100644
--- a/src/package-lock.json
+++ b/src/package-lock.json
@@ -1,7 +1,7 @@
{
"name": "wj-config",
"version": "2.0.2",
- "lockfileVersion": 2,
+ "lockfileVersion": 3,
"requires": true,
"packages": {
"": {
@@ -9,29 +9,25 @@
"version": "2.0.2",
"license": "MIT-open-group",
"devDependencies": {
- "typescript": "^4.7.4"
+ "typescript": "^5.6.2"
+ },
+ "engines": {
+ "node": ">=16.9.0"
}
},
"node_modules/typescript": {
- "version": "4.7.4",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
- "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
+ "version": "5.6.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
+ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
"dev": true,
+ "license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
- "node": ">=4.2.0"
+ "node": ">=14.17"
}
}
- },
- "dependencies": {
- "typescript": {
- "version": "4.7.4",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
- "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
- "dev": true
- }
}
}
diff --git a/src/package.json b/src/package.json
index ee6cace..32a420e 100644
--- a/src/package.json
+++ b/src/package.json
@@ -1,10 +1,13 @@
{
"name": "wj-config",
- "version": "2.0.2",
+ "version": "3.0.0-beta.1",
"main": "index.js",
"type": "module",
"description": "Javascript configuration module for NodeJS and browser frameworks such as React that works like ASP.net configuration where data sources are specified (usually JSON files) and environment variables can contribute/overwrite values by following a naming convention.",
- "author": "José Pablo Ramírez Vargas ",
+ "author": {
+ "email": "webJose@gmail.com",
+ "name": "José Pablo Ramírez Vargas"
+ },
"keywords": [
"config",
"wj",
@@ -33,9 +36,21 @@
"license": "MIT-open-group",
"homepage": "https://github.com/WJSoftware/wj-config#readme",
"devDependencies": {
- "typescript": "^4.7.4"
+ "typescript": "^5.6.2"
},
"types": "wj-config.d.ts",
+ "exports": {
+ ".": {
+ "types": "./index.d.ts",
+ "import": "./index.js",
+ "default": "./index.js"
+ },
+ "./sources": {
+ "types": "./dataSources/index.d.ts",
+ "import": "./dataSources/index.js",
+ "default": "./dataSources/index.js"
+ }
+ },
"scripts": {
"build": "npx tsc"
},
diff --git a/src/wj-config.d.ts b/src/wj-config.d.ts
index 790e00f..1dd8a7f 100644
--- a/src/wj-config.d.ts
+++ b/src/wj-config.d.ts
@@ -1,422 +1,554 @@
-declare module 'wj-config' {
+/**
+ * Possible value types in properties of a configuration object.
+ */
+export type SingleConfigurationValue = string | number | Date | boolean | undefined | null;
+
+/**
+ * Defines the type of configuration leaf properties.
+ */
+export type ConfigurationValue = SingleConfigurationValue | SingleConfigurationValue[];
+
+/**
+ * A configuration node.
+ */
+export interface ConfigurationNode {
+ [x: string]: ConfigurationValue | ConfigurationNode
+}
+
+/**
+ * Types an object-merging operation's result.
+ */
+export type MergeResult, NewT> = (Omit & {
+ [K in keyof NewT]-?: K extends keyof T ?
+ (
+ T[K] extends Record ?
+ (NewT[K] extends Record ? MergeResult : never) :
+ (NewT[K] extends Record ? never : NewT[K])
+ ) : NewT[K]
+}) extends infer R ? { [K in keyof R]: R[K] } : never;
+
+/**
+ * Types individual dictionary values and inflates them.
+ */
+export type InflateKey = TKey extends `${TPrefix}${infer FullKey}` ?
+ FullKey extends `${infer Key}${TSep}${infer Rest}` ?
+ {
+ [K in Key]: InflateKey
+ } :
+ {
+ [K in TKey]: TValue;
+ } : never;
+
+/**
+ * Inflates entire dictionaries into their corresponding final objects.
+ */
+export type InflateDictionary, TSep extends string, TPrefix extends string = ""> = {
+ [K in keyof TDic]: (x: InflateKey) => void
+} extends Record void> ? I : never;
+
+/**
+ * Defines the shape of dictionaries.
+ */
+export type Dictionary = Record;
+
+/**
+ * WJ-Config module's entry point. Creates a builder object that is used to specify the various configuration
+ * sources and settings.
+ */
+export default function wjConfig(): IBuilder;
+
+/**
+ * Predicate function that evaluates an arbitrary number of criteria against the data and returns a judgment in terms
+ * of a Boolean value.
+ */
+export type Predicate = (data: T) => boolean;
+
+/**
+ * Defines functions that process a fetched response object and returns configuration data.
+ */
+export type ProcessFetchResponse> = (response: Response) => Promise;
+
+/**
+ * Defines the capabilities of a configurtion object that contains an environment object in the environment property.
+ */
+export type IncludeEnvironment = {
+ [K in Key as `${K}`]: IEnvironment;
+}
+
+/**
+ * Defines the requirements of objects that wish to provide JSON-parsing services.
+ */
+export interface IJsonParser> {
/**
- * WJ-Config module's entry point. Creates a builder object that is used to specify the various configuration
- * sources and settings.
+ * Parses the provided JSON string data and returns a JavaScript object.
+ * @param json The JSON string to parse.
*/
- export default function wjConfig(): IBuilder;
+ parse(json: string): T;
+}
+/**
+ * Defines the capabilities required from data source information objects used in value tracing.
+ */
+export interface IDataSourceInfo {
/**
- * Predicate function that evaluates an arbitrary number of criteria against the data and returns a judgment in terms
- * of a Boolean value.
+ * Provides the name of the data source instance that can be used in messages and logs.
*/
- export type Predicate = (data: T) => boolean;
+ name: string;
/**
- * Possible values in a configuration object.
+ * Index (position) of the data source object in the builder's list of data sources.
*/
- export type ConfigurationValue = string | number | Date | boolean | IEnvironment | IDefaultEnvironment | undefined | null | Function | ICoreConfig | IDataSourceInfo | IDataSourceInfo[];
+ index?: number;
+}
+/**
+ * Defines the capabilities required from data sources.
+ */
+export interface IDataSource = Record> extends IDataSourceInfo {
/**
- * Type alias that describes the data type of interim or final configuration objects.
+ * Asynchronously obtains the object that will be used as building block in the creation of the final
+ * configuration object.
*/
- export type IConfig = ICoreConfig | IEnvConfig | IDefaultEnvConfig;
-
- export type ProcessFetchResponse = (response: Response) => Promise;
+ getObject(): Promise;
/**
- * Defines the core and most basic capabilities found in configuration objects.
+ * Returns a data source information object on demand. This is used when building a configuration object with
+ * value tracing.
*/
- export interface ICoreConfig {
- [x: string | symbol]: ConfigurationValue;
- }
+ trace(): IDataSourceInfo;
+}
+
+/**
+ * Defines the shape of configuration-tracing objects.
+ */
+export interface Trace {
+ [x: string]: IDataSourceInfo | Trace;
+}
+/**
+ * Defines the capabilities required from configuration builders.
+ */
+export interface IBuilder = {}> {
/**
- * Defines the capabilities of a configurtion object that contains an environment object in the environment property.
+ * Adds the provided data source to the collection of data sources that will be used to build the
+ * configuration object.
+ * @param dataSource The data source to include.
*/
- export interface IEnvConfig extends ICoreConfig {
- environment: IEnvironment;
- }
-
+ add>(dataSource: IDataSource): IBuilder>;
/**
- * Defines the capabilities of a configurtion object that contains an environment object in the environment property
- * created using the default list of environment names.
+ * Adds the specified object to the collection of data sources that will be used to build the configuration
+ * object.
+ * @param obj Data object to include as part of the final configuration data, or a function that returns said
+ * object.
*/
- export interface IDefaultEnvConfig extends ICoreConfig {
- environment: IDefaultEnvironment
- }
-
- export interface IJsonParser {
- parse(json: string): ICoreConfig
- }
-
+ addObject>(obj: NewT | (() => Promise)): IBuilder>;
/**
- * Defines the capabilities required from data source information objects used in value tracing.
+ * Adds the specified dictionary to the collection of data sources that will be used to build the configuration
+ * object.
+ * @param dictionary Dictionary object to include (after processing) as part of the final configuration data,
+ * or a function that returns said object.
+ * @param hierarchySeparator Optional hierarchy separator. If none is specified, a colon (:) is assumed.
+ * @param prefix Optional prefix. Only properties that start with the specified prefix are included, and the
+ * prefix is always removed after the dictionary is processed. If no prefix is provided, then all dictionary
+ * entries will contribute to the configuration data.
*/
- export interface IDataSourceInfo {
- /**
- * Provides the name of the data source instance that can be used in messages and logs.
- */
- name: string;
-
- /**
- * Index (position) of the data source object in the builder's list of data sources.
- */
- index?: number;
- }
-
+ addDictionary, TSep extends string = ':'>(dictionary: TDic | (() => Promise), hierarchySeparator?: TSep, prefix?: string): IBuilder>>;
/**
- * Defines the capabilities required from data sources.
+ * Adds the specified dictionary to the collection of data sources that will be used to build the configuration
+ * object.
+ * @param dictionary Dictionary object to include (after processing) as part of the final configuration data,
+ * or a function that returns said object.
+ * @param hierarchySeparator Optional hierarchy separator. If none is specified, a colon (:) is assumed.
+ * @param predicate Optional predicate function that is called for every property in the dictionary. Only when
+ * the return value of the predicate is true the property is included in configuration.
*/
- export interface IDataSource extends IDataSourceInfo {
- /**
- * Asynchronously obtains the object that will be used as building block in the creation of the final
- * configuration object.
- */
- getObject(): Promise;
-
- /**
- * Returns a data source information object on demand. This is used when building a configuration object with
- * value tracing.
- */
- trace(): IDataSourceInfo;
- }
-
+ addDictionary, TSep extends string = ':'>(dictionary: TDic | (() => Promise), hierarchySeparator?: TSep, predicate?: Predicate): IBuilder>>;
/**
- * Defines the capabilities required from configuration builders.
- */
- export interface IBuilder {
- /**
- * Adds the provided data source to the collection of data sources that will be used to build the
- * configuration object.
- * @param dataSource The data source to include.
- */
- add(dataSource: IDataSource): IBuilder;
- /**
- * Adds the specified object to the collection of data sources that will be used to build the configuration
- * object.
- * @param obj Data object to include as part of the final configuration data, or a function that returns said
- * object.
- */
- addObject(obj: ICoreConfig | (() => Promise)): IBuilder;
-
- /**
- * Adds the specified dictionary to the collection of data sources that will be used to build the configuration
- * object.
- * @param dictionary Dictionary object to include (after processing) as part of the final configuration data,
- * or a function that returns said object.
- * @param hierarchySeparator Optional hierarchy separator. If none is specified, a colon (:) is assumed.
- * @param prefix Optional prefix. Only properties that start with the specified prefix are included, and the
- * prefix is always removed after the dictionary is processed. If no prefix is provided, then all dictionary
- * entries will contribute to the configuration data.
- */
- addDictionary(dictionary: ICoreConfig | (() => Promise), hierarchySeparator?: string, prefix?: string): IBuilder;
-
- /**
- * Adds the specified dictionary to the collection of data sources that will be used to build the configuration
- * object.
- * @param dictionary Dictionary object to include (after processing) as part of the final configuration data,
- * or a function that returns said object.
- * @param hierarchySeparator Optional hierarchy separator. If none is specified, a colon (:) is assumed.
- * @param predicate Optional predicate function that is called for every property in the dictionary. Only when
- * the return value of the predicate is true the property is included in configuration.
- */
- addDictionary(dictionary: ICoreConfig | (() => Promise), hierarchySeparator?: string, predicate?: Predicate): IBuilder;
-
- /**
- * Adds the qualifying environment variables to the collection of data sources that will be used to build the
- * configuration object.
- * @param env Environment object containing the environment variables to include in the configuration, or a
- * function that returns said object.
- * @param prefix Optional prefix. Only properties that start with the specified prefix are included, and the
- * prefix is always removed after processing. To avoid exposing non-application data as configuration, a prefix
- * is always used. If none is specified, the default prefix is OPT_.
- */
- addEnvironment(env: ICoreConfig | (() => Promise), prefix?: string): IBuilder;
-
- /**
- * Adds a fetch operation to the collection of data sources that will be used to build the configuration
- * object.
- * @param url URL to fetch.
- * @param required Optional Boolean value indicating if the fetch must produce an object.
- * @param init Optional fetch init data. Refer to the fecth() documentation for information.
- * @param processFn Optional processing function that must return the configuration data as an object.
- */
- addFetched(url: URL | (() => Promise), required: boolean = true, init?: RequestInit, processFn?: ProcessFetchResponse): IBuilder;
-
- /**
- * Adds a fetch operation to the collection of data sources that will be used to build the configuration
- * object.
- * @param request Request object to use when fetching. Refer to the fetch() documentation for information.
- * @param required Optional Boolean value indicating if the fetch must produce an object.
- * @param init Optional fetch init data. Refer to the fecth() documentation for information.
- * @param processFn Optional processing function that must return the configuration data as an object.
- */
- addFetched(request: RequestInfo | (() => Promise), required: boolean = true, init?: RequestInit, processFn?: ProcessFetchResponse): IBuilder;
-
- /**
- * Adds the specified JSON string to the collection of data sources that will be used to build the
- * configuration object.
- * @param json The JSON string to parse into a JavaScript object, or a function that returns said string.
- * @param jsonParser Optional JSON parser. If not specified, the built-in JSON object will be used.
- * @param reviver Optional reviver function. For more information see the JSON.parse() documentation.
- */
- addJson(json: string | (() => Promise), jsonParser?: JSON, reviver?: (this: any, key: string, value: any) => any): IBuilder;
-
- /**
- * Adds a single value to the collection of data sources that will be used to build the configuration object.
- * @param path Key comprised of names that determine the hierarchy of the value.
- * @param value Value of the property.
- * @param hierarchySeparator Optional hierarchy separator. If not specified, colon (:) is assumed.
- */
- addSingleValue(path: string, value?: ConfigurationValue, hierarchySeparator: string = ':'): IBuilder;
-
- /**
- * Adds a single value to the collection of data sources that will be used to build the configuration object.
- * @param dataFn Function that returns the [key, value] tuple that needs to be added.
- * @param hierarchySeparator Optional hierarchy separator. If not specified, colon (:) is assumed.
- */
- addSingleValue(dataFn: () => Promise<[string, ConfigurationValue]>, hierarchySeparator: string = ':'): IBuilder;
-
- /**
- * Special function that allows the developer the opportunity to add one data source per defined environment.
- *
- * The function iterates through all possible environments and calls the addDs function for each one. It is
- * assumed that the addDs function will add zero or one data source. To signal no data source was added,
- * addDs must return the boolean value "false".
- * @param addDs Function that is meant to add a single data source of any type that is associated to the
- * provided environment name.
- */
- addPerEnvironment(addDs: (builder: IBuilder, envName: string) => boolean | string): IBuilder;
-
- /**
- * Sets the data source name of the last data source added to the builder.
- * @param name Name for the data source.
- */
- name(name: string): IBuilder;
-
- /**
- * Makes the last-added data source conditionally inclusive.
- * @param predicate Predicate function that is run whenever the build function runs. If the predicate returns
- * true, then the data source will be included; if it returns false, then the data source is skipped.
- * @param dataSourceName Optional data source name. Provided to simplify the build chain and is merely a
- * shortcut to include a call to the name() function. Equivalent to when().name().
- */
- when(predicate: Predicate, dataSourceName?: string): IBuilder;
-
- /**
- * Makes the last-added data source conditionally included only if the current environment possesses all of
- * the listed traits.
- * @param traits The traits the current environment must have for the data source to be added.
- * @param dataSourceName Optional data source name. Provided to simplify the build chain and is merely a
- * shortcut to include a call to the name() function. Equivalent to whenAllTraits().name().
- */
- whenAllTraits(traits: Traits, dataSourceName?: string): IBuilder;
-
- /**
- * Makes the last-added data source conditionally included if the current environment possesses any of the
- * listed traits. Only one coincidence is necessary.
- * @param traits The list of possible traits the current environment may have in order for the data source to
- * be included.
- * @param dataSourceName Optional data source name. Provided to simplify the build chain and is merely a
- * shortcut to include a call to the name() function. Equivalent to whenAnyTrait().name().
- */
- whenAnyTrait(traits: Traits, dataSourceName?: string): IBuilder;
-
- /**
- * Makes the last-added data source conditionally included if the current environment's name is equal to the
- * provided environment name.
- * @param envName The environment name to use to conditionally include the last-added data source.
- * @param dataSourceName Optional data source name. Provided to simplify the build chain and is
- * merely a shortcut to include a call to the name() function.
- */
- forEnvironment(envName: string, dataSourceName?: string): IBuilder;
-
- /**
- * Adds the provided environment object as a property of the final configuration object.
- * @param env Previously created environment object.
- * @param propertyName Optional property name for the environment object.
- */
- includeEnvironment(env: IEnvironment, propertyName?: string): IBuilder;
-
- /**
- * Adds an environment object created with the provided value and names as a property of the final configuration
- * object.
- * @param value Current environment name.
- * @param envNames List of possible environment names.
- * @param propertyName Optional property name for the environment object.
- */
- includeEnvironment(value: string, envNames?: string[], propertyName?: string): IBuilder;
-
- /**
- * Creates URL functions in the final configuration object for URL's defined according to the wj-config standard.
- * @param wsPropertyNames Optional list of property names whose values are expected to be objects that contain
- * host, port, scheme or root path data at some point in their child hierarchy. If not provided, then the default
- * list will be used. It can also be a single string, which is the same as a 1-element array.
- * @param routeValueRegExp Optional regular expression used to identify replaceable route values. If this is not
- * provided, then the default regular expression will match route values of the form {}, such as
- * {code} or {id}.
- */
- createUrlFunctions(wsPropertyNames?: string | string[], routeValueRegExp?: RegExp): IBuilder;
-
- /**
- * Asynchronously builds the final configuration object.
- */
- build(traceValueSources: boolean = false, enforcePerEnvironmentCoverage: boolean = true): Promise;
- }
-
+ * Adds the qualifying environment variables to the collection of data sources that will be used to build the
+ * configuration object.
+ * @param env Environment object containing the environment variables to include in the configuration, or a
+ * function that returns said object.
+ * @param prefix Optional prefix. Only properties that start with the specified prefix are included, and the
+ * prefix is always removed after processing. To avoid exposing non-application data as configuration, a prefix
+ * is always used. If none is specified, the default prefix is OPT_.
+ */
+ addEnvironment, TPrefix extends string = 'OPT_'>(env: TDic | (() => Promise), prefix?: TPrefix): IBuilder>>;
/**
- * Function type that allows environment objects to declare testing functions, such as isProduction().
+ * Adds a fetch operation to the collection of data sources that will be used to build the configuration object.
+ * @param url URL to fetch.
+ * @param required Optional Boolean value indicating if the fetch must produce an object.
+ * @param init Optional fetch init data. Refer to the fecth() documentation for information.
+ * @param processFn Optional processing function that must return the configuration data as an object.
*/
- export type EnvironmentTest = () => boolean;
-
+ addFetched>(url: URL | (() => Promise), required?: boolean, init?: RequestInit, processFn?: ProcessFetchResponse): IBuilder>;
/**
- * Defines the capabilities required from environment objects.
+ * Adds a fetch operation to the collection of data sources that will be used to build the configuration
+ * object.
+ * @param request Request object to use when fetching. Refer to the fetch() documentation for information.
+ * @param required Optional Boolean value indicating if the fetch must produce an object.
+ * @param init Optional fetch init data. Refer to the fecth() documentation for information.
+ * @param processFn Optional processing function that must return the configuration data as an object.
*/
- export interface IEnvironment {
- /**
- * The current environment (represented by an environment definition).
- */
- readonly current: IEnvironmentDefinition;
-
- /**
- * The list of known environments (represented by a list of environment definitions).
- */
- readonly all: string[];
-
- /**
- * Tests the current environment definition for the presence of the specified traits. It will return true
- * only if all specified traits are present; othewise it will return false.
- * @param traits The environment traits expected to be found in the current environment definition.
- */
- hasTraits(traits: Traits): boolean;
-
- /**
- * Tests the current environment definition for the presence of any of the specified traits. It will return
- * true if any of the specified traits is present. If none of the specified traits are present, then the
- * return value will be false.
- * @param traits The environments traits of which at least on of them is expected to be found in the current
- * environment definition.
- */
- hasAnyTrait(traits: Traits): boolean;
- [x: string | 'current' | 'all' | 'hasTraits' | 'hasAnyTrait']: EnvironmentTest | IEnvironmentDefinition | string[] | ((traits: Traits) => boolean)
- }
-
+ addFetched>(request: RequestInfo | (() => Promise), required?: boolean, init?: RequestInit, processFn?: ProcessFetchResponse): IBuilder>;
/**
- * Environment interface that describes the environment object created with default environment names.
+ * Adds the specified JSON string to the collection of data sources that will be used to build the
+ * configuration object.
+ * @param json The JSON string to parse into a JavaScript object, or a function that returns said string.
+ * @param jsonParser Optional JSON parser. If not specified, the built-in JSON object will be used.
+ * @param reviver Optional reviver function. For more information see the JSON.parse() documentation.
*/
- export interface IDefaultEnvironment extends IEnvironment {
- /**
- * Tests if the current environment is the Development environment.
- */
- isDevelopment: EnvironmentTest;
-
- /**
- * Tests if the current environment is the PreProduction environment.
- */
- isPreProduction: EnvironmentTest;
-
- /**
- * Tests if the current environment is the Production environment.
- */
- isProduction: EnvironmentTest
- }
-
+ addJson>(json: string | (() => Promise), jsonParser?: IJsonParser, reviver?: (this: any, key: string, value: any) => any): IBuilder>;
/**
- * Type of function used as route values when calling a URL-building function.
+ * Adds a single value to the collection of data sources that will be used to build the configuration object.
+ * @param path Key comprised of names that determine the hierarchy of the value.
+ * @param value Value of the property.
+ * @param hierarchySeparator Optional hierarchy separator. If not specified, colon (:) is assumed.
*/
- export type RouteValuesFunction = (name: string) => string
-
+ addSingleValue(path: TKey, value?: TValue, hierarchySeparator?: TSep): IBuilder>>;
/**
- * Type that describes the possible ways of passing route values when calling a URL-building function.
+ * Adds a single value to the collection of data sources that will be used to build the configuration object.
+ * @param dataFn Function that returns the [key, value] tuple that needs to be added.
+ * @param hierarchySeparator Optional hierarchy separator. If not specified, colon (:) is assumed.
*/
- export type RouteValues = RouteValuesFunction | { [x: string]: string }
-
+ addSingleValue(dataFn: () => Promise, hierarchySeparator?: TSep): IBuilder>>;
+ /**
+ * Sets the data source name of the last data source added to the builder.
+ * @param name Name for the data source.
+ */
+ name(name: string): IBuilder;
+ /**
+ * Makes the last-added data source conditionally inclusive.
+ * @param predicate Predicate function that is run whenever the build function runs. If the predicate returns
+ * true, then the data source will be included; if it returns false, then the data source is skipped.
+ * @param dataSourceName Optional data source name. Provided to simplify the build chain and is merely a
+ * shortcut to include a call to the name() function. Equivalent to when().name().
+ */
+ when(predicate: () => boolean, dataSourceName?: string): IBuilder;
+ /**
+ * Adds the provided environment object as a property of the final configuration object.
+ * @param env Previously created environment object.
+ * @param propertyName Optional property name for the environment object.
+ */
+ includeEnvironment(env: IEnvironment, propertyName?: TEnvironmentKey): IEnvAwareBuilder & IncludeEnvironment>;
/**
- * Type that describes the possible ways of specifing a query string when calling a URL-building function.
+ * Creates URL functions in the final configuration object for URL's defined according to the wj-config standard.
+ * @param wsPropertyNames Optional list of property names whose values are expected to be objects that contain
+ * host, port, scheme or root path data at some point in their child hierarchy. If not provided, then the default
+ * list will be used. It can also be a single string, which is the same as a 1-element array.
+ * @param routeValueRegExp Optional regular expression used to identify replaceable route values. If this is not
+ * provided, then the default regular expression will match route values of the form {}, such as
+ * {code} or {id}.
*/
- export type QueryString = (() => string | ICoreConfig) | string | ICoreConfig
+ createUrlFunctions(wsPropertyNames: TUrl | TUrl[], routeValueRegExp?: RegExp): IBuilder & UrlBuilderSectionWithCheck>;
+ /**
+ * Asynchronously builds the final configuration object.
+ */
+ build(traceValueSources?: boolean): Promise;
+}
+export interface IEnvAwareBuilder = {}> {
/**
- * Defines the capabilities exposed by non-leaf nodes in a webservices hierarchy.
+ * Adds the provided data source to the collection of data sources that will be used to build the
+ * configuration object.
+ * @param dataSource The data source to include.
*/
- export interface IWsPath extends ICoreConfig {
- rootPath?: string;
- _rootPath: () => string;
- buildUrl: (url: string, replaceValues?: RouteValues, queryString?: QueryString) => string;
- }
+ add>(dataSource: IDataSource): IEnvAwareBuilder>;
+ /**
+ * Adds the specified object to the collection of data sources that will be used to build the configuration object.
+ * @param obj Data object to include as part of the final configuration data, or a function that returns said
+ * object.
+ */
+ addObject>(obj: NewT | (() => Promise)): IEnvAwareBuilder>;
+ /**
+ * Adds the specified dictionary to the collection of data sources that will be used to build the configuration
+ * object.
+ * @param dictionary Dictionary object to include (after processing) as part of the final configuration data,
+ * or a function that returns said object.
+ * @param hierarchySeparator Optional hierarchy separator. If none is specified, a colon (:) is assumed.
+ * @param prefix Optional prefix. Only properties that start with the specified prefix are included, and the
+ * prefix is always removed after the dictionary is processed. If no prefix is provided, then all dictionary
+ * entries will contribute to the configuration data.
+ */
+ addDictionary, TSep extends string = ':'>(dictionary: Record | (() => Promise>), hierarchySeparator?: string, prefix?: string): IEnvAwareBuilder>>;
+ /**
+ * Adds the specified dictionary to the collection of data sources that will be used to build the configuration
+ * object.
+ * @param dictionary Dictionary object to include (after processing) as part of the final configuration data,
+ * or a function that returns said object.
+ * @param hierarchySeparator Optional hierarchy separator. If none is specified, a colon (:) is assumed.
+ * @param predicate Optional predicate function that is called for every property in the dictionary. Only when
+ * the return value of the predicate is true the property is included in configuration.
+ */
+ addDictionary, TSep extends string = ':'>(dictionary: Record | (() => Promise>), hierarchySeparator?: string, predicate?: Predicate): IEnvAwareBuilder>>;
+ /**
+ * Adds the qualifying environment variables to the collection of data sources that will be used to build the
+ * configuration object.
+ * @param env Environment object containing the environment variables to include in the configuration, or a
+ * function that returns said object.
+ * @param prefix Optional prefix. Only properties that start with the specified prefix are included, and the
+ * prefix is always removed after processing. To avoid exposing non-application data as configuration, a prefix
+ * is always used. If none is specified, the default prefix is OPT_.
+ */
+ addEnvironment, TPrefix extends string = 'OPT_'>(env: Record | (() => Promise>), prefix?: string): IEnvAwareBuilder