@iskra-bun/config-kit 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/index.d.ts +77 -1
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/from-env.ts +133 -0
- package/src/index.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @iskra-bun/config-kit
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- e5f41e7: `fromEnv(spec)` builds a kit's section of `app.config` from environment variables with names of your own, such as a legacy service's: each field names its variable, or several (the first one set wins), with `env(names, parser, { default | optional })` converting it by any coercer (`envNumber`, `envPort`…) or Zod schema of a string. Nested sections work, the result is typed from the spec, and every problem is reported at once in a `ConfigError` that names the variables, never their values.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [e86ed55]
|
|
12
|
+
- Updated dependencies [ae7c798]
|
|
13
|
+
- Updated dependencies [ef10372]
|
|
14
|
+
- @iskra-bun/core@0.3.0
|
|
15
|
+
|
|
3
16
|
## 0.1.1
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -39,4 +39,80 @@ declare const envPort: z.ZodPipeline<z.ZodEffects<z.ZodString, number, string>,
|
|
|
39
39
|
*/
|
|
40
40
|
declare function envEnum<T extends string>(values: readonly [T, ...T[]]): z.ZodPipeline<z.ZodEffects<z.ZodString, T, string>, z.ZodEnum<[T, ...T[]]>>;
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
/** What converts a variable's text: a Zod schema of a string (`envNumber`, `envPort`…). */
|
|
43
|
+
interface EnvParser<T> {
|
|
44
|
+
safeParse(value: string): {
|
|
45
|
+
success: true;
|
|
46
|
+
data: T;
|
|
47
|
+
} | {
|
|
48
|
+
success: false;
|
|
49
|
+
error: {
|
|
50
|
+
issues: ReadonlyArray<{
|
|
51
|
+
message: string;
|
|
52
|
+
}>;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** A config field read from environment variables. */
|
|
57
|
+
interface EnvField<T> {
|
|
58
|
+
readonly kind: 'env';
|
|
59
|
+
/** The variables to read, the first one set wins (a new name, then the legacy one). */
|
|
60
|
+
readonly names: readonly string[];
|
|
61
|
+
readonly schema?: EnvParser<T>;
|
|
62
|
+
readonly default?: T;
|
|
63
|
+
readonly optional: boolean;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A field read from `names` (the first set, an empty value counts as unset),
|
|
67
|
+
* converted by `schema` (`envNumber`, `envPort`, `envBool`, `envEnum([...])`
|
|
68
|
+
* or any Zod schema of a string); text by default. A field without a value
|
|
69
|
+
* and without `default` is an error, unless `optional`.
|
|
70
|
+
*/
|
|
71
|
+
declare function env(names: string | readonly string[]): EnvField<string>;
|
|
72
|
+
declare function env<T>(names: string | readonly string[], schema: EnvParser<T>, options?: {
|
|
73
|
+
default?: T;
|
|
74
|
+
}): EnvField<T>;
|
|
75
|
+
declare function env<T>(names: string | readonly string[], schema: EnvParser<T> | undefined, options: {
|
|
76
|
+
optional: true;
|
|
77
|
+
}): EnvField<T | undefined>;
|
|
78
|
+
/** A section: each key a variable name (text), an `env()` field, or a nested section. */
|
|
79
|
+
interface EnvSpec {
|
|
80
|
+
readonly [key: string]: string | readonly string[] | EnvField<unknown> | EnvSpec;
|
|
81
|
+
}
|
|
82
|
+
type Value<E> = E extends EnvField<infer T> ? T : E extends string | readonly string[] ? string : FromEnv<E>;
|
|
83
|
+
/** The keys of optional fields, which may be missing from the config. */
|
|
84
|
+
type OptionalKeys<S> = {
|
|
85
|
+
[K in keyof S]: S[K] extends EnvField<infer T> ? (undefined extends T ? K : never) : never;
|
|
86
|
+
}[keyof S];
|
|
87
|
+
/** The config a spec reads: an optional field's key may be missing. */
|
|
88
|
+
type FromEnv<S> = {
|
|
89
|
+
-readonly [K in Exclude<keyof S, OptionalKeys<S>>]: Value<S[K]>;
|
|
90
|
+
} & {
|
|
91
|
+
-readonly [K in OptionalKeys<S>]?: Exclude<Value<S[K]>, undefined>;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* A config section from environment variables with names of your own (or a
|
|
95
|
+
* legacy service's), validated, with defaults:
|
|
96
|
+
*
|
|
97
|
+
* ```ts
|
|
98
|
+
* // app.config.ts
|
|
99
|
+
* export default {
|
|
100
|
+
* oracle: fromEnv({
|
|
101
|
+
* host: 'DB_HOST_ORACLE',
|
|
102
|
+
* port: env('DB_PORT_ORACLE', envPort, { default: 1521 }),
|
|
103
|
+
* serviceName: env(['DB_SERVICE_ORACLE', 'ORA_SERVICE']),
|
|
104
|
+
* user: 'DB_USER_ORACLE',
|
|
105
|
+
* password: 'DB_PASS_ORACLE',
|
|
106
|
+
* pool: { max: env('DB_POOL_MAX', envNumber, { default: 4 }) },
|
|
107
|
+
* }),
|
|
108
|
+
* };
|
|
109
|
+
* ```
|
|
110
|
+
*
|
|
111
|
+
* Every problem is reported at once in a `ConfigError` that names the
|
|
112
|
+
* variables, never their values.
|
|
113
|
+
*/
|
|
114
|
+
declare function fromEnv<const S extends EnvSpec>(spec: S, options?: {
|
|
115
|
+
source?: Readonly<Record<string, string | undefined>>;
|
|
116
|
+
}): FromEnv<S>;
|
|
117
|
+
|
|
118
|
+
export { type EnvField, type EnvSpec, type FromEnv, type LoadConfigOptions, env, envBool, envEnum, envNumber, envPort, fromEnv, loadConfig };
|
package/dist/index.js
CHANGED
|
@@ -90,13 +90,64 @@ function envEnum(values) {
|
|
|
90
90
|
}).pipe(z2.enum(values));
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
// src/from-env.ts
|
|
94
|
+
import { ConfigError as ConfigError2 } from "@iskra-bun/core";
|
|
95
|
+
function env(names, schema, options = {}) {
|
|
96
|
+
return {
|
|
97
|
+
kind: "env",
|
|
98
|
+
names: typeof names === "string" ? [names] : names,
|
|
99
|
+
schema,
|
|
100
|
+
default: options.default,
|
|
101
|
+
optional: options.optional === true
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
var isField = (value) => typeof value === "object" && value !== null && value.kind === "env";
|
|
105
|
+
function read(spec, source, problems) {
|
|
106
|
+
const out = {};
|
|
107
|
+
for (const [key, entry] of Object.entries(spec)) {
|
|
108
|
+
if (typeof entry === "string" || Array.isArray(entry) || isField(entry)) {
|
|
109
|
+
const field = isField(entry) ? entry : env(entry);
|
|
110
|
+
const name = field.names.find((n) => source[n] !== void 0 && source[n] !== "");
|
|
111
|
+
const label = field.names.join(" or ");
|
|
112
|
+
if (name === void 0) {
|
|
113
|
+
if (field.default !== void 0) out[key] = field.default;
|
|
114
|
+
else if (!field.optional) problems.push(`${label} is not set`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (!field.schema) {
|
|
118
|
+
out[key] = source[name];
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const parsed = field.schema.safeParse(source[name]);
|
|
122
|
+
if (parsed.success) out[key] = parsed.data;
|
|
123
|
+
else problems.push(`${name}: ${parsed.error.issues.map((i) => i.message).join("; ")}`);
|
|
124
|
+
} else {
|
|
125
|
+
out[key] = read(entry, source, problems);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
function fromEnv(spec, options = {}) {
|
|
131
|
+
const problems = [];
|
|
132
|
+
const config = read(spec, options.source ?? process.env, problems);
|
|
133
|
+
if (problems.length > 0) {
|
|
134
|
+
throw new ConfigError2(`Invalid config from the environment:
|
|
135
|
+
${problems.map((p) => ` - ${p}`).join("\n")}`, {
|
|
136
|
+
context: { variables: problems.length }
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return config;
|
|
140
|
+
}
|
|
141
|
+
|
|
93
142
|
// src/index.ts
|
|
94
143
|
import { z as z3 } from "zod";
|
|
95
144
|
export {
|
|
145
|
+
env,
|
|
96
146
|
envBool,
|
|
97
147
|
envEnum,
|
|
98
148
|
envNumber,
|
|
99
149
|
envPort,
|
|
150
|
+
fromEnv,
|
|
100
151
|
loadConfig,
|
|
101
152
|
z3 as z
|
|
102
153
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/loader.ts","../src/coercers.ts","../src/index.ts"],"sourcesContent":["import { z } from 'zod';\nimport { ConfigError } from '@iskra-bun/core';\n\nexport interface LoadConfigOptions<TSchema extends z.ZodTypeAny> {\n /** The Zod schema to validate the source against. */\n schema: TSchema;\n /**\n * The environment source to validate.\n * Defaults to `process.env`.\n */\n source?: Record<string, string | undefined>;\n}\n\n/**\n * Validates `source` (defaults to `process.env`) against the provided Zod schema\n * and returns a deep-frozen, typed config object.\n *\n * On failure, throws a `ConfigError` listing each invalid or missing field by name\n * and reason — never echoing secret values.\n */\nexport function loadConfig<TSchema extends z.ZodTypeAny>(options: LoadConfigOptions<TSchema>): z.output<TSchema> {\n const { schema, source = process.env } = options;\n\n const result = schema.safeParse(source, { errorMap: valueFreeErrors });\n\n if (!result.success) {\n const issues = result.error.issues.map((issue) => {\n const path = issue.path.length > 0 ? issue.path.join('.') : '<root>';\n return ` • ${path}: ${issue.message}`;\n });\n\n throw new ConfigError(`Config validation failed:\\n${issues.join('\\n')}`, {\n context: {\n // Only report field names and messages — never values.\n fields: result.error.issues.map((i) => ({\n path: i.path.join('.') || '<root>',\n message: i.message,\n })),\n },\n });\n }\n\n return deepFreeze(result.data) as z.output<TSchema>;\n}\n\n/**\n * Zod's own messages for enums and literals quote the value received\n * (\"…, received 'hunter2'\"); these say what was expected instead.\n */\nconst valueFreeErrors: z.ZodErrorMap = (issue, ctx) => {\n if (issue.code === z.ZodIssueCode.invalid_enum_value) {\n return { message: `Invalid enum value. Expected ${issue.options.map((o) => `'${String(o)}'`).join(' | ')}` };\n }\n if (issue.code === z.ZodIssueCode.invalid_literal) {\n return { message: `Invalid literal value, expected ${JSON.stringify(issue.expected)}` };\n }\n return { message: ctx.defaultError };\n};\n\n/**\n * Recursively freezes an object so the returned config is truly immutable.\n */\nfunction deepFreeze<T>(obj: T): T {\n if (obj === null || typeof obj !== 'object') return obj;\n\n for (const key of Object.keys(obj as object)) {\n const value = (obj as Record<string, unknown>)[key];\n if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {\n deepFreeze(value);\n }\n }\n\n return Object.freeze(obj);\n}\n","import { z } from 'zod';\n\n// The messages never include the value: it may be a secret (a password put in\n// the wrong variable), and loadConfig() reports them in errors and logs.\n\n/**\n * Coerce a string env var to boolean.\n * Accepts \"true\"/\"1\"/\"yes\" as true, \"false\"/\"0\"/\"no\" as false.\n * Rejects any other value.\n */\nexport const envBool = z\n .string()\n .transform((val, ctx) => {\n const lower = val.toLowerCase().trim();\n if (lower === 'true' || lower === '1' || lower === 'yes') return true;\n if (lower === 'false' || lower === '0' || lower === 'no') return false;\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: 'Expected boolean-like string (\"true\"/\"1\"/\"yes\"/\"false\"/\"0\"/\"no\")',\n });\n return z.NEVER;\n })\n .pipe(z.boolean());\n\n/**\n * Coerce a string env var to a finite number.\n */\nexport const envNumber = z\n .string()\n .transform((val, ctx) => {\n const trimmed = val.trim();\n const n = Number(trimmed);\n if (trimmed === '' || !Number.isFinite(n)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: 'Expected a finite number',\n });\n return z.NEVER;\n }\n return n;\n })\n .pipe(z.number());\n\n/**\n * Coerce a string env var to a valid TCP port (1–65535).\n */\nexport const envPort = z\n .string()\n .transform((val, ctx) => {\n const n = Number(val);\n if (!Number.isInteger(n) || n < 1 || n > 65535) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: 'Expected a port number (1–65535)',\n });\n return z.NEVER;\n }\n return n;\n })\n .pipe(z.number().int().min(1).max(65535));\n\n/**\n * Coerce a string env var to one of the provided literal values.\n * Usage: envEnum(['development', 'production', 'test'])\n */\nexport function envEnum<T extends string>(values: readonly [T, ...T[]]) {\n return z\n .string()\n .transform((val, ctx) => {\n if (!(values as readonly string[]).includes(val)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: `Expected one of [${values.map((v) => `\"${v}\"`).join(', ')}]`,\n });\n return z.NEVER;\n }\n return val as T;\n })\n .pipe(z.enum(values));\n}\n","export { loadConfig } from './loader';\nexport type { LoadConfigOptions } from './loader';\nexport { envBool, envNumber, envPort, envEnum } from './coercers';\nexport { z } from 'zod';\n"],"mappings":"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","names":["z","z"]}
|
|
1
|
+
{"version":3,"sources":["../src/loader.ts","../src/coercers.ts","../src/from-env.ts","../src/index.ts"],"sourcesContent":["import { z } from 'zod';\nimport { ConfigError } from '@iskra-bun/core';\n\nexport interface LoadConfigOptions<TSchema extends z.ZodTypeAny> {\n /** The Zod schema to validate the source against. */\n schema: TSchema;\n /**\n * The environment source to validate.\n * Defaults to `process.env`.\n */\n source?: Record<string, string | undefined>;\n}\n\n/**\n * Validates `source` (defaults to `process.env`) against the provided Zod schema\n * and returns a deep-frozen, typed config object.\n *\n * On failure, throws a `ConfigError` listing each invalid or missing field by name\n * and reason — never echoing secret values.\n */\nexport function loadConfig<TSchema extends z.ZodTypeAny>(options: LoadConfigOptions<TSchema>): z.output<TSchema> {\n const { schema, source = process.env } = options;\n\n const result = schema.safeParse(source, { errorMap: valueFreeErrors });\n\n if (!result.success) {\n const issues = result.error.issues.map((issue) => {\n const path = issue.path.length > 0 ? issue.path.join('.') : '<root>';\n return ` • ${path}: ${issue.message}`;\n });\n\n throw new ConfigError(`Config validation failed:\\n${issues.join('\\n')}`, {\n context: {\n // Only report field names and messages — never values.\n fields: result.error.issues.map((i) => ({\n path: i.path.join('.') || '<root>',\n message: i.message,\n })),\n },\n });\n }\n\n return deepFreeze(result.data) as z.output<TSchema>;\n}\n\n/**\n * Zod's own messages for enums and literals quote the value received\n * (\"…, received 'hunter2'\"); these say what was expected instead.\n */\nconst valueFreeErrors: z.ZodErrorMap = (issue, ctx) => {\n if (issue.code === z.ZodIssueCode.invalid_enum_value) {\n return { message: `Invalid enum value. Expected ${issue.options.map((o) => `'${String(o)}'`).join(' | ')}` };\n }\n if (issue.code === z.ZodIssueCode.invalid_literal) {\n return { message: `Invalid literal value, expected ${JSON.stringify(issue.expected)}` };\n }\n return { message: ctx.defaultError };\n};\n\n/**\n * Recursively freezes an object so the returned config is truly immutable.\n */\nfunction deepFreeze<T>(obj: T): T {\n if (obj === null || typeof obj !== 'object') return obj;\n\n for (const key of Object.keys(obj as object)) {\n const value = (obj as Record<string, unknown>)[key];\n if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {\n deepFreeze(value);\n }\n }\n\n return Object.freeze(obj);\n}\n","import { z } from 'zod';\n\n// The messages never include the value: it may be a secret (a password put in\n// the wrong variable), and loadConfig() reports them in errors and logs.\n\n/**\n * Coerce a string env var to boolean.\n * Accepts \"true\"/\"1\"/\"yes\" as true, \"false\"/\"0\"/\"no\" as false.\n * Rejects any other value.\n */\nexport const envBool = z\n .string()\n .transform((val, ctx) => {\n const lower = val.toLowerCase().trim();\n if (lower === 'true' || lower === '1' || lower === 'yes') return true;\n if (lower === 'false' || lower === '0' || lower === 'no') return false;\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: 'Expected boolean-like string (\"true\"/\"1\"/\"yes\"/\"false\"/\"0\"/\"no\")',\n });\n return z.NEVER;\n })\n .pipe(z.boolean());\n\n/**\n * Coerce a string env var to a finite number.\n */\nexport const envNumber = z\n .string()\n .transform((val, ctx) => {\n const trimmed = val.trim();\n const n = Number(trimmed);\n if (trimmed === '' || !Number.isFinite(n)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: 'Expected a finite number',\n });\n return z.NEVER;\n }\n return n;\n })\n .pipe(z.number());\n\n/**\n * Coerce a string env var to a valid TCP port (1–65535).\n */\nexport const envPort = z\n .string()\n .transform((val, ctx) => {\n const n = Number(val);\n if (!Number.isInteger(n) || n < 1 || n > 65535) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: 'Expected a port number (1–65535)',\n });\n return z.NEVER;\n }\n return n;\n })\n .pipe(z.number().int().min(1).max(65535));\n\n/**\n * Coerce a string env var to one of the provided literal values.\n * Usage: envEnum(['development', 'production', 'test'])\n */\nexport function envEnum<T extends string>(values: readonly [T, ...T[]]) {\n return z\n .string()\n .transform((val, ctx) => {\n if (!(values as readonly string[]).includes(val)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: `Expected one of [${values.map((v) => `\"${v}\"`).join(', ')}]`,\n });\n return z.NEVER;\n }\n return val as T;\n })\n .pipe(z.enum(values));\n}\n","import { ConfigError } from '@iskra-bun/core';\n\n/** What converts a variable's text: a Zod schema of a string (`envNumber`, `envPort`…). */\nexport interface EnvParser<T> {\n safeParse(\n value: string,\n ): { success: true; data: T } | { success: false; error: { issues: ReadonlyArray<{ message: string }> } };\n}\n\n/** A config field read from environment variables. */\nexport interface EnvField<T> {\n readonly kind: 'env';\n /** The variables to read, the first one set wins (a new name, then the legacy one). */\n readonly names: readonly string[];\n readonly schema?: EnvParser<T>;\n readonly default?: T;\n readonly optional: boolean;\n}\n\n/**\n * A field read from `names` (the first set, an empty value counts as unset),\n * converted by `schema` (`envNumber`, `envPort`, `envBool`, `envEnum([...])`\n * or any Zod schema of a string); text by default. A field without a value\n * and without `default` is an error, unless `optional`.\n */\nexport function env(names: string | readonly string[]): EnvField<string>;\nexport function env<T>(names: string | readonly string[], schema: EnvParser<T>, options?: { default?: T }): EnvField<T>;\nexport function env<T>(\n names: string | readonly string[],\n schema: EnvParser<T> | undefined,\n options: { optional: true },\n): EnvField<T | undefined>;\nexport function env<T>(\n names: string | readonly string[],\n schema?: EnvParser<T>,\n options: { default?: T; optional?: boolean } = {},\n): EnvField<T> {\n return {\n kind: 'env',\n names: typeof names === 'string' ? [names] : names,\n schema,\n default: options.default,\n optional: options.optional === true,\n };\n}\n\n/** A section: each key a variable name (text), an `env()` field, or a nested section. */\nexport interface EnvSpec {\n readonly [key: string]: string | readonly string[] | EnvField<unknown> | EnvSpec;\n}\n\ntype Value<E> = E extends EnvField<infer T> ? T : E extends string | readonly string[] ? string : FromEnv<E>;\n\n/** The keys of optional fields, which may be missing from the config. */\ntype OptionalKeys<S> = {\n [K in keyof S]: S[K] extends EnvField<infer T> ? (undefined extends T ? K : never) : never;\n}[keyof S];\n\n/** The config a spec reads: an optional field's key may be missing. */\nexport type FromEnv<S> = {\n -readonly [K in Exclude<keyof S, OptionalKeys<S>>]: Value<S[K]>;\n} & {\n -readonly [K in OptionalKeys<S>]?: Exclude<Value<S[K]>, undefined>;\n};\n\nconst isField = (value: unknown): value is EnvField<unknown> =>\n typeof value === 'object' && value !== null && (value as { kind?: unknown }).kind === 'env';\n\nfunction read(\n spec: EnvSpec,\n source: Readonly<Record<string, string | undefined>>,\n problems: string[],\n): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, entry] of Object.entries(spec)) {\n if (typeof entry === 'string' || Array.isArray(entry) || isField(entry)) {\n const field = isField(entry) ? entry : env(entry as string | readonly string[]);\n const name = field.names.find((n) => source[n] !== undefined && source[n] !== '');\n const label = field.names.join(' or ');\n if (name === undefined) {\n if (field.default !== undefined) out[key] = field.default;\n else if (!field.optional) problems.push(`${label} is not set`);\n continue;\n }\n if (!field.schema) {\n out[key] = source[name];\n continue;\n }\n const parsed = field.schema.safeParse(source[name]!);\n // The message never includes the value: it may be a secret.\n if (parsed.success) out[key] = parsed.data;\n else problems.push(`${name}: ${parsed.error.issues.map((i) => i.message).join('; ')}`);\n } else {\n out[key] = read(entry as EnvSpec, source, problems);\n }\n }\n return out;\n}\n\n/**\n * A config section from environment variables with names of your own (or a\n * legacy service's), validated, with defaults:\n *\n * ```ts\n * // app.config.ts\n * export default {\n * oracle: fromEnv({\n * host: 'DB_HOST_ORACLE',\n * port: env('DB_PORT_ORACLE', envPort, { default: 1521 }),\n * serviceName: env(['DB_SERVICE_ORACLE', 'ORA_SERVICE']),\n * user: 'DB_USER_ORACLE',\n * password: 'DB_PASS_ORACLE',\n * pool: { max: env('DB_POOL_MAX', envNumber, { default: 4 }) },\n * }),\n * };\n * ```\n *\n * Every problem is reported at once in a `ConfigError` that names the\n * variables, never their values.\n */\nexport function fromEnv<const S extends EnvSpec>(\n spec: S,\n options: { source?: Readonly<Record<string, string | undefined>> } = {},\n): FromEnv<S> {\n const problems: string[] = [];\n const config = read(spec, options.source ?? process.env, problems);\n if (problems.length > 0) {\n throw new ConfigError(`Invalid config from the environment:\\n${problems.map((p) => ` - ${p}`).join('\\n')}`, {\n context: { variables: problems.length },\n });\n }\n return config as FromEnv<S>;\n}\n","export { loadConfig } from './loader';\nexport type { LoadConfigOptions } from './loader';\nexport { envBool, envNumber, envPort, envEnum } from './coercers';\nexport { env, fromEnv, type EnvField, type EnvSpec, type FromEnv } from './from-env';\nexport { z } from 'zod';\n"],"mappings":"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","names":["z","ConfigError","z"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iskra-bun/config-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Carga y validacion de configuracion de entorno con Zod para Iskra.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"iskra",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"build": "tsup --config ../../tsup.config.ts"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@iskra-bun/core": "^0.
|
|
53
|
+
"@iskra-bun/core": "^0.3.0",
|
|
54
54
|
"zod": "^3.24.1"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
package/src/from-env.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { ConfigError } from '@iskra-bun/core';
|
|
2
|
+
|
|
3
|
+
/** What converts a variable's text: a Zod schema of a string (`envNumber`, `envPort`…). */
|
|
4
|
+
export interface EnvParser<T> {
|
|
5
|
+
safeParse(
|
|
6
|
+
value: string,
|
|
7
|
+
): { success: true; data: T } | { success: false; error: { issues: ReadonlyArray<{ message: string }> } };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** A config field read from environment variables. */
|
|
11
|
+
export interface EnvField<T> {
|
|
12
|
+
readonly kind: 'env';
|
|
13
|
+
/** The variables to read, the first one set wins (a new name, then the legacy one). */
|
|
14
|
+
readonly names: readonly string[];
|
|
15
|
+
readonly schema?: EnvParser<T>;
|
|
16
|
+
readonly default?: T;
|
|
17
|
+
readonly optional: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A field read from `names` (the first set, an empty value counts as unset),
|
|
22
|
+
* converted by `schema` (`envNumber`, `envPort`, `envBool`, `envEnum([...])`
|
|
23
|
+
* or any Zod schema of a string); text by default. A field without a value
|
|
24
|
+
* and without `default` is an error, unless `optional`.
|
|
25
|
+
*/
|
|
26
|
+
export function env(names: string | readonly string[]): EnvField<string>;
|
|
27
|
+
export function env<T>(names: string | readonly string[], schema: EnvParser<T>, options?: { default?: T }): EnvField<T>;
|
|
28
|
+
export function env<T>(
|
|
29
|
+
names: string | readonly string[],
|
|
30
|
+
schema: EnvParser<T> | undefined,
|
|
31
|
+
options: { optional: true },
|
|
32
|
+
): EnvField<T | undefined>;
|
|
33
|
+
export function env<T>(
|
|
34
|
+
names: string | readonly string[],
|
|
35
|
+
schema?: EnvParser<T>,
|
|
36
|
+
options: { default?: T; optional?: boolean } = {},
|
|
37
|
+
): EnvField<T> {
|
|
38
|
+
return {
|
|
39
|
+
kind: 'env',
|
|
40
|
+
names: typeof names === 'string' ? [names] : names,
|
|
41
|
+
schema,
|
|
42
|
+
default: options.default,
|
|
43
|
+
optional: options.optional === true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A section: each key a variable name (text), an `env()` field, or a nested section. */
|
|
48
|
+
export interface EnvSpec {
|
|
49
|
+
readonly [key: string]: string | readonly string[] | EnvField<unknown> | EnvSpec;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type Value<E> = E extends EnvField<infer T> ? T : E extends string | readonly string[] ? string : FromEnv<E>;
|
|
53
|
+
|
|
54
|
+
/** The keys of optional fields, which may be missing from the config. */
|
|
55
|
+
type OptionalKeys<S> = {
|
|
56
|
+
[K in keyof S]: S[K] extends EnvField<infer T> ? (undefined extends T ? K : never) : never;
|
|
57
|
+
}[keyof S];
|
|
58
|
+
|
|
59
|
+
/** The config a spec reads: an optional field's key may be missing. */
|
|
60
|
+
export type FromEnv<S> = {
|
|
61
|
+
-readonly [K in Exclude<keyof S, OptionalKeys<S>>]: Value<S[K]>;
|
|
62
|
+
} & {
|
|
63
|
+
-readonly [K in OptionalKeys<S>]?: Exclude<Value<S[K]>, undefined>;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const isField = (value: unknown): value is EnvField<unknown> =>
|
|
67
|
+
typeof value === 'object' && value !== null && (value as { kind?: unknown }).kind === 'env';
|
|
68
|
+
|
|
69
|
+
function read(
|
|
70
|
+
spec: EnvSpec,
|
|
71
|
+
source: Readonly<Record<string, string | undefined>>,
|
|
72
|
+
problems: string[],
|
|
73
|
+
): Record<string, unknown> {
|
|
74
|
+
const out: Record<string, unknown> = {};
|
|
75
|
+
for (const [key, entry] of Object.entries(spec)) {
|
|
76
|
+
if (typeof entry === 'string' || Array.isArray(entry) || isField(entry)) {
|
|
77
|
+
const field = isField(entry) ? entry : env(entry as string | readonly string[]);
|
|
78
|
+
const name = field.names.find((n) => source[n] !== undefined && source[n] !== '');
|
|
79
|
+
const label = field.names.join(' or ');
|
|
80
|
+
if (name === undefined) {
|
|
81
|
+
if (field.default !== undefined) out[key] = field.default;
|
|
82
|
+
else if (!field.optional) problems.push(`${label} is not set`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (!field.schema) {
|
|
86
|
+
out[key] = source[name];
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const parsed = field.schema.safeParse(source[name]!);
|
|
90
|
+
// The message never includes the value: it may be a secret.
|
|
91
|
+
if (parsed.success) out[key] = parsed.data;
|
|
92
|
+
else problems.push(`${name}: ${parsed.error.issues.map((i) => i.message).join('; ')}`);
|
|
93
|
+
} else {
|
|
94
|
+
out[key] = read(entry as EnvSpec, source, problems);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A config section from environment variables with names of your own (or a
|
|
102
|
+
* legacy service's), validated, with defaults:
|
|
103
|
+
*
|
|
104
|
+
* ```ts
|
|
105
|
+
* // app.config.ts
|
|
106
|
+
* export default {
|
|
107
|
+
* oracle: fromEnv({
|
|
108
|
+
* host: 'DB_HOST_ORACLE',
|
|
109
|
+
* port: env('DB_PORT_ORACLE', envPort, { default: 1521 }),
|
|
110
|
+
* serviceName: env(['DB_SERVICE_ORACLE', 'ORA_SERVICE']),
|
|
111
|
+
* user: 'DB_USER_ORACLE',
|
|
112
|
+
* password: 'DB_PASS_ORACLE',
|
|
113
|
+
* pool: { max: env('DB_POOL_MAX', envNumber, { default: 4 }) },
|
|
114
|
+
* }),
|
|
115
|
+
* };
|
|
116
|
+
* ```
|
|
117
|
+
*
|
|
118
|
+
* Every problem is reported at once in a `ConfigError` that names the
|
|
119
|
+
* variables, never their values.
|
|
120
|
+
*/
|
|
121
|
+
export function fromEnv<const S extends EnvSpec>(
|
|
122
|
+
spec: S,
|
|
123
|
+
options: { source?: Readonly<Record<string, string | undefined>> } = {},
|
|
124
|
+
): FromEnv<S> {
|
|
125
|
+
const problems: string[] = [];
|
|
126
|
+
const config = read(spec, options.source ?? process.env, problems);
|
|
127
|
+
if (problems.length > 0) {
|
|
128
|
+
throw new ConfigError(`Invalid config from the environment:\n${problems.map((p) => ` - ${p}`).join('\n')}`, {
|
|
129
|
+
context: { variables: problems.length },
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return config as FromEnv<S>;
|
|
133
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { loadConfig } from './loader';
|
|
2
2
|
export type { LoadConfigOptions } from './loader';
|
|
3
3
|
export { envBool, envNumber, envPort, envEnum } from './coercers';
|
|
4
|
+
export { env, fromEnv, type EnvField, type EnvSpec, type FromEnv } from './from-env';
|
|
4
5
|
export { z } from 'zod';
|