Implement the whole thing
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Config
|
||||
Parse a configuration from env values using a description scheme (using Zod).
|
||||
|
||||
Keys are the name given in the TypeBox scheme, in capital case, separated with an underscore. Sub-objects' names will start with parent's name, followed by an underscore then the key.
|
||||
|
||||
**Any** key can be followed by `__FILE`. In this case, the given value is expected to be a path and the content of the file will be used to be the content of the config. The content of the file will be trimmed from any whitespace character. The file is read using UTF-8 encoding.
|
||||
@@ -0,0 +1,27 @@
|
||||
import {type Static, type TObject, type TProperties} from '@sinclair/typebox'
|
||||
import {Value} from '@sinclair/typebox/value'
|
||||
import logger from 'log'
|
||||
|
||||
import {read_object} from './src/read_type'
|
||||
|
||||
const log = logger('config')
|
||||
export {Type} from '@sinclair/typebox' // Re-export Type so users can describe config
|
||||
|
||||
export async function parse<T extends TProperties>(scheme: TObject<T>): Promise<Static<TObject<T>> | null> {
|
||||
log.info("Parse configuration from env")
|
||||
|
||||
log.debug("Read configuration from env")
|
||||
const config = await read_object(scheme)
|
||||
if (!config.ok) return null
|
||||
|
||||
// This check is kind of a duplicate, is it useful ?
|
||||
log.debug("Validate config against scheme")
|
||||
const config_parsed = Value.Check(scheme, config.data)
|
||||
if (config_parsed) {
|
||||
log.trace("Config is valid")
|
||||
return config.data
|
||||
} else {
|
||||
log.warn("Invalid config, check failed")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -1,13 +1,15 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@sinclair/typebox": "^0.34.33",
|
||||
"dotenv": "^16.5.0",
|
||||
"log": "git+https://git.pband.ch/typescript/log.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.15",
|
||||
"@types/node": "^22.15.30",
|
||||
"@types/tmp": "^0.2.6",
|
||||
"bun": "^1.2.15",
|
||||
"logger-console": "git+https://git.pband.ch/typescript/logger-console.git",
|
||||
"tmp": "^0.2.3",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export type Ok<T> = {ok: true, data: T} | {ok: false}
|
||||
@@ -0,0 +1,38 @@
|
||||
import logger from 'log'
|
||||
import fs from 'node:fs/promises'
|
||||
|
||||
import type {Ok} from './helpers'
|
||||
|
||||
const log = logger('config:read_env')
|
||||
const FILE_EXT = "__FILE"
|
||||
|
||||
// Read from env or file
|
||||
export async function read_env(key: string): Promise<Ok<string | undefined>> {
|
||||
const path = process.env[key + FILE_EXT]
|
||||
if (path !== undefined) return from_file(path)
|
||||
else return {ok: true, data: process.env[key]}
|
||||
}
|
||||
async function from_file(path: string): Promise<Ok<string>> {
|
||||
log.debug("Read a key from a file")
|
||||
log.trace("Path :", path)
|
||||
|
||||
let content = await get_file_content(path)
|
||||
if (!content.ok) return content
|
||||
|
||||
content.data = content.data.trim()
|
||||
|
||||
return content
|
||||
}
|
||||
async function get_file_content(path: string) : Promise<Ok<string>> {
|
||||
log.debug('Read file content')
|
||||
log.trace('Path :', path)
|
||||
|
||||
try {
|
||||
const data = await fs.readFile(path, {encoding: 'utf-8'})
|
||||
return {ok: true, data}
|
||||
} catch (e) {
|
||||
log.warn('Failed to read file', path)
|
||||
log.debug('Error :', e)
|
||||
return {ok: false}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
OptionalKind,
|
||||
Type,
|
||||
type Static,
|
||||
type TDate,
|
||||
type TObject,
|
||||
type TProperties,
|
||||
type TSchema, type TString
|
||||
} from '@sinclair/typebox'
|
||||
import {Value} from '@sinclair/typebox/value'
|
||||
import logger from 'log'
|
||||
|
||||
import type {Ok} from './helpers'
|
||||
import {read_env} from './read_env'
|
||||
|
||||
|
||||
const log = logger('config:read_type')
|
||||
|
||||
// Read each TypeBox type from env
|
||||
export async function read_object<T extends TProperties>(scheme: TObject<T>, base_name: string = "") : Promise<Ok<Static<TObject<T>>>> {
|
||||
log.debug("Read object from env")
|
||||
if (base_name.length !== 0) base_name = base_name + "_"
|
||||
|
||||
const data: Static<TObject<T>> = Value.Create(scheme)
|
||||
|
||||
for (const key in scheme.properties) {
|
||||
const sub_key = base_name + key.toUpperCase()
|
||||
|
||||
// Get sub-scheme
|
||||
const sub_scheme = scheme.properties[key]
|
||||
|
||||
// Read value
|
||||
let value: Ok<unknown>;
|
||||
if (sub_scheme.type === 'object') {
|
||||
value = await read_object(sub_scheme as unknown as TObject<T>, sub_key)
|
||||
} else if (sub_scheme.type === 'Date') {
|
||||
value = await read_date(sub_scheme as unknown as TDate, sub_key)
|
||||
} else {
|
||||
value = await read_type(sub_scheme, sub_key)
|
||||
}
|
||||
// If read failed, early return the error
|
||||
if (!value.ok) return value
|
||||
|
||||
// If optional, don't store useless data
|
||||
if (value.data === undefined) continue
|
||||
|
||||
// Set value
|
||||
// @ts-expect-error : it would be nice to fix this :)
|
||||
data[key] = value.data
|
||||
}
|
||||
|
||||
return {ok: true, data}
|
||||
}
|
||||
|
||||
export async function read_type<T extends TSchema>(scheme: T, name: string) : Promise<Ok<Static<T> | undefined>> {
|
||||
let value = await read_env(name)
|
||||
if (!value.ok) return value
|
||||
|
||||
if (value.data === undefined && scheme[OptionalKind] !== undefined) return {ok: true, data: undefined}
|
||||
|
||||
try {
|
||||
const data = Value.Parse(scheme, value.data)
|
||||
return {ok: true, data}
|
||||
} catch (e) {
|
||||
log.warn(`Parsing of key '${name}' failed (invalid value : '${value.data}')`)
|
||||
log.debug('Error :', e)
|
||||
return {ok: false}
|
||||
}
|
||||
}
|
||||
|
||||
export async function read_date(scheme: TDate, name: string): Promise<Ok<Static<TDate> | undefined>> {
|
||||
let string_scheme: TString;
|
||||
if (scheme[OptionalKind] !== undefined) string_scheme = Type.Optional(Type.String())
|
||||
else string_scheme = Type.String()
|
||||
|
||||
const value = await read_type(string_scheme, name)
|
||||
if (!value.ok) return value
|
||||
if (value.data === undefined) return {ok: true, data: undefined}
|
||||
|
||||
const data = new Date(value.data)
|
||||
const checked = Value.Check(scheme, data)
|
||||
if (!checked) {
|
||||
log.warn(`Invalid date for '${name}' : ${value.data}`)
|
||||
return {ok: false}
|
||||
}
|
||||
|
||||
return {ok: true, data}
|
||||
}
|
||||
Reference in New Issue
Block a user