configuru
Version:
Manage the configuration of your Nodejs application with multiple environments and custom preferences, utilizing Configuru in CI and development as well!
206 lines (159 loc) • 6.31 kB
Markdown
## Advanced usage
### Constant parameters
Sometimes it makes sense to have a static parameter in configuration. For example, when connecting to third party service, you want to have api key configurable per environment, but connection options not. You can insert in your configuration schema constant values of any type without loader and use Configuru for application parameters as well.
```typescript
import { createLoader, schema } from 'configuru'
const loader = createLoader()
const configSchema = loader({
configurable: schema.string('KEY'),
static: 'foo',
})
// { configurable: <loaded value>, static: 'foo' }
const { configurable, static } = loader.values()
```
### Default variable names
You don't need to provide variable name to schema if you named the config key same as the key in the configuration.
This can be helpful if you drive your whole config file only by environment variables.
> ⚠️ For nested config keys, the last object key is used for the variable name
```typescript
import { createLoader, schema } from 'configuru'
const loader = createLoader()
const configSchema = loader({
SERVER_PORT: schema.string(), // Loads from SERVER_PORT env var
SQL_CONNECTION_STRING: schema.string(), // Loaded from SQL_CONNECTION_STRING env var
apiKey: schema.string(), // ⚠️ Loaded from apiKey env var
api: {
apiKey: schema.string(), // ⚠️ Loaded from apiKey env var
key: schema.string('apiKey'), // ⚠️ Loaded from apiKey env var
},
})
const {
SERVER_PORT,
SQL_CONNECTION_STRING,
apiKey,
api: { apiKey, key },
} = loader.values()
```
### Reloading the config
If you are working in an environment that is able to change the configuration during runtime, `reload`
function can be helpful to load a new configuration based on same settings and schema as the previous one.
```typescript
import { createLoader, schema } from 'configuru'
const loader = createLoader()
const configSchema = loader({
canChange: schema.string('CHANGEABLE'),
})
const { canChange } = configSchema.values() // old value
// value changed in config file or env variables during runtime
const { canChange } = configSchema.reload().values() // new value
```
### Hidden variables, secrets and logging
`config.ts`
```typescript
import { createLoader, schema } from 'configuru'
const loader = createLoader()
// create `buildConfig` function, we will use two loaders
const configSchema = loader({
// add hidden flag
apiKey: schema.string.hidden('SECRET_KEY'),
})
export default configSchema.values()
export const maskedConfig = configSchema.maskedValues()
```
`foo.ts`
```typescript
import config, { maskedConfig } from './config'
// use in your app, never log
config.apiKey // szvor4VYgS79z3QSBtmN0dJeyXbg1Xip
// don't use, is truncated (or hidden for shorter vars) but okay-ish to log
maskedConfig.apiKey // [redacted]
```
### Options
```typescript
import { createLoader } from 'configuru'
const loader = createLoader({
defaultConfigPath: 'default-config', // defaults to ".env"
userConfigPath: process.env.USER_CONFIG, // defaults to process.env.CFG_JSON_PATH
envMode: 'all', // defaults to "default"
})
```
1. `defaultConfigPath`: Where to look for your default config JSON file (provide null to skip)
2. `userConfigPath`: Where to look for your user config JSON file (provide null to skip)
3. `envMode`: How to handle process.env variables
1. `all` - Load (override) all vars available in process.env to the store
2. `default` - Load (override) only vars with keys from default config
3. `merged` - Load (override) only vars with keys from either (user or default) config
4. `none` - Don't use env variables
🦉 When configuring configuru, you can always use paths with or without extension, it will try to find any of the supported formats via replacing/adding valid extensions.
### Mismatch types
Configuru is type safe, but you can run into problems, having an enum value parsed as a string.
Since there is no enum loader, thus no validation, the value can be anything.
```typescript
const loader = createLoader()
type Level = 'info' | 'warn' | 'unknown'
const createLogger = (opts: { defaultLevel: Level }) => {
/*...*/
}
const config1 = loader({
defaultLevel: loader.string('LOGGER_DEFAULT_LEVEL'),
}).values() // type -> { defaultLevel: string }
createLogger(config1) // error: string not assignable to Level
```
To solve it, you can create a custom loader
```typescript
// create custom loader
const levelLoader = schema.custom(x => {
let level: Level = 'unknown'
if (x === 'info' || x === 'warn') {
level = x
}
return level
})
// or just
const optimisticLevelLoader = schema.custom(x => x as Level)
const config2 = loader({
defaultLevel: levelLoader('LOGGER_DEFAULT_LEVEL'),
}).values() // type -> { defaultLevel: Level }
createLogger(config2) // OK
```
### Custom loaders
Default loaders not enough? You can use custom loaders to create any transformation you want and types will be inferred from the return type of you function.
```typescript
// 'PHOTO-2019-04-01' => { type: 'PHOTO', date: Date }
const stampLoader = schema.custom(x => {
const [type, y, m, d] = x.split('-')
return { type, date: new Date(y, m, d) }
})
const config = loader({
stamp: stampLoader('STAMP'),
}).values() // type -> { stamp: { type: any, date: Date } };
```
Custom loaders can also return an object that contains nested loaders. Meaning you can have a custom loader that
loads a value from storage and based on that value returns another objects (or an array of objects) that themselves may contain loaders.
This can be particularly useful for arrays of connection strings that may contain sensitive information like passwords. This will give you type-safe array of objects, while still supporting hidden loaders.
```typescript
// env.json
// {
// "PASSWORD": "pwd",
// "HOSTS": "host1,host2"
// }
const configSchema = loader({
connections: schema.custom(x => {
return x.split(',').map(host => ({
host,
password: schema.string.hidden('PASSWORD'),
}))
})('HOSTS'),
})
console.log(configSchema.maskedValues())
// [
// {
// "host": "host1",
// "foo": "[redacted]"
// },
// {
// "host": "host2",
// "foo": "[redacted]"
// }
// ]
```