@decohere/config 0.0.1-dev.125 → 0.0.1-dev.5

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.
@@ -1,172 +0,0 @@
1
- import { io } from "@decohere/core/io";
2
- import { parse } from "yaml";
3
- import { ConfigurationDataSource } from "./configuration-data-source.js";
4
- import { DEFAULT_CONFIG_DIR } from "./const.js";
5
- import { ConfigurationError } from "./errors.js";
6
- const CONFIG_FILE_NAME = "config";
7
- /** Unset NODE_ENV must not silently load config.dev.yaml (TypeORM synchronize, etc.). */ const DEFAULT_ENV = "production";
8
- const FILE_EXT = "yaml";
9
- /** Resolve which env-specific config file to merge (e.g. config.dev.yaml). */ export function resolveConfigEnv(nodeEnv) {
10
- const env = nodeEnv ?? process.env.NODE_ENV;
11
- if (env == null || env.trim() === "") {
12
- return DEFAULT_ENV;
13
- }
14
- return env;
15
- }
16
- /** `${NAME}` or `${NAME:default}`. The default may be empty, and runs up to the first `}`. */ const PLACEHOLDER = /\$\{(\w+)(?::([^}]*))?\}/g;
17
- function isTree(value) {
18
- return value != null && typeof value === "object" && !Array.isArray(value);
19
- }
20
- /** A non-empty list whose every entry is a mapping with a string `name`, such as `dataSources`. */ function isNamedList(value) {
21
- return Array.isArray(value) && value.length > 0 && value.every((item)=>isTree(item) && typeof item.name === "string");
22
- }
23
- /** Entries with the same name merge in place; new names are added at the end. */ function mergeNamedLists(base, override) {
24
- const merged = [
25
- ...base
26
- ];
27
- for (const entry of override){
28
- const index = merged.findIndex((item)=>item.name === entry.name);
29
- if (index === -1) {
30
- merged.push(entry);
31
- } else {
32
- merged[index] = merge(merged[index], entry);
33
- }
34
- }
35
- return merged;
36
- }
37
- /**
38
- * Objects merge key by key, and so do lists of named entries (matched by `name`), so an
39
- * environment file only states what it changes. Any other list, and a scalar, in the later file
40
- * replaces the earlier value.
41
- */ function merge(base, override) {
42
- const merged = {
43
- ...base
44
- };
45
- for (const [key, value] of Object.entries(override)){
46
- const existing = merged[key];
47
- if (isTree(existing) && isTree(value)) {
48
- merged[key] = merge(existing, value);
49
- } else if (isNamedList(existing) && isNamedList(value)) {
50
- merged[key] = mergeNamedLists(existing, value);
51
- } else {
52
- merged[key] = value;
53
- }
54
- }
55
- return merged;
56
- }
57
- /** A named list that repeats a name can't be merged by name, so say which file and where. */ function assertUniqueNames(value, source, path) {
58
- if (isNamedList(value)) {
59
- const seen = new Set();
60
- for (const item of value){
61
- if (seen.has(item.name)) {
62
- throw new ConfigurationError(`Config file ${source}: ${path} lists the name "${String(item.name)}" more than once`);
63
- }
64
- seen.add(item.name);
65
- }
66
- }
67
- if (Array.isArray(value)) {
68
- value.forEach((item, index)=>assertUniqueNames(item, source, `${path}[${index}]`));
69
- } else if (isTree(value)) {
70
- for (const [key, item] of Object.entries(value)){
71
- assertUniqueNames(item, source, path === "" ? key : `${path}.${key}`);
72
- }
73
- }
74
- }
75
- function resolveString(value, path, missing) {
76
- let hasPlaceholder = false;
77
- const resolved = value.replace(PLACEHOLDER, (_match, name, fallback)=>{
78
- hasPlaceholder = true;
79
- const fromEnv = process.env[name];
80
- if (fromEnv !== undefined) {
81
- return fromEnv;
82
- }
83
- if (fallback !== undefined) {
84
- return fallback;
85
- }
86
- missing.push({
87
- name,
88
- path
89
- });
90
- return "";
91
- });
92
- // A variable set to the text "null" means no value.
93
- return hasPlaceholder && resolved === "null" ? null : resolved;
94
- }
95
- function resolveValue(value, path, missing) {
96
- if (typeof value === "string") {
97
- return resolveString(value, path, missing);
98
- }
99
- if (Array.isArray(value)) {
100
- return value.map((item, index)=>resolveValue(item, `${path}[${index}]`, missing));
101
- }
102
- if (isTree(value)) {
103
- const resolved = {};
104
- for (const [key, item] of Object.entries(value)){
105
- resolved[key] = resolveValue(item, path === "" ? key : `${path}.${key}`, missing);
106
- }
107
- return resolved;
108
- }
109
- return value;
110
- }
111
- function parseSource({ name, text }) {
112
- let parsed;
113
- try {
114
- parsed = parse(text);
115
- } catch (error) {
116
- const reason = error instanceof Error ? error.message : String(error);
117
- throw new ConfigurationError(`Cannot parse config file ${name}: ${reason}`);
118
- }
119
- if (parsed == null) {
120
- return undefined;
121
- }
122
- if (!isTree(parsed)) {
123
- throw new ConfigurationError(`Config file ${name} must contain a mapping at the top level`);
124
- }
125
- assertUniqueNames(parsed, name, "");
126
- return parsed;
127
- }
128
- /**
129
- * Merge config files in order (later files win), then resolve `${VAR}` placeholders on the
130
- * merged result.
131
- *
132
- * Resolving after the merge means an environment file such as `config.dev.yaml` can replace a
133
- * production `${VAR}` without that variable being set: a placeholder that was replaced is never
134
- * looked up. Placeholders still unset after the merge fail together, naming every variable and
135
- * the config path that used it. `${VAR:default}` keeps a default for optional values.
136
- */ export function resolveYamlConfig(sources) {
137
- let merged = {};
138
- for (const source of sources){
139
- const tree = parseSource(source);
140
- if (tree !== undefined) {
141
- merged = merge(merged, tree);
142
- }
143
- }
144
- const missing = [];
145
- const resolved = resolveValue(merged, "", missing);
146
- if (missing.length > 0) {
147
- const lines = missing.map(({ name, path })=>` - ${name} (used at ${path})`);
148
- throw new ConfigurationError(`Config references ${missing.length} environment variable(s) that are not set:\n${lines.join("\n")}\n` + "Set them, give the value a default with ${NAME:default}, or replace it in the environment's config file.");
149
- }
150
- return resolved;
151
- }
152
- export class YamlDataSource extends ConfigurationDataSource {
153
- name = YamlDataSource.name;
154
- async getData() {
155
- const configDir = process.env.CONFIG_DIR ?? DEFAULT_CONFIG_DIR;
156
- const env = resolveConfigEnv();
157
- const candidates = [
158
- `${configDir}/${CONFIG_FILE_NAME}.${FILE_EXT}`,
159
- `${configDir}/${CONFIG_FILE_NAME}.${env}.${FILE_EXT}`
160
- ];
161
- const sources = [];
162
- for (const filePath of candidates){
163
- if (await io.fileExists(filePath)) {
164
- sources.push({
165
- name: filePath,
166
- text: await io.readFile(filePath)
167
- });
168
- }
169
- }
170
- return resolveYamlConfig(sources);
171
- }
172
- }
@@ -1,8 +0,0 @@
1
- export interface IConfigurationDataSource {
2
- readonly name: string;
3
- getData(): Promise<Record<string, unknown>>;
4
- }
5
- export declare abstract class ConfigurationDataSource implements IConfigurationDataSource {
6
- abstract readonly name: string;
7
- abstract getData(): Promise<Record<string, unknown>>;
8
- }
@@ -1,16 +0,0 @@
1
- import { Builder } from "@decohere/http";
2
- import { type IConfigurationService } from "@decohere/core/configuration";
3
- import type { Constructable } from "@decohere/core/reflection";
4
- import type { ConfigurationDataSource } from "./configuration-data-source.js";
5
- export type ConfigurationOptions = {
6
- configurationService?: Constructable<IConfigurationService>;
7
- dataSources?: Constructable<ConfigurationDataSource>[];
8
- };
9
- declare module "@decohere/http" {
10
- interface Builder {
11
- withConfiguration(...dataSources: Constructable<ConfigurationDataSource>[]): Builder;
12
- }
13
- }
14
- export declare class BuilderExtensions {
15
- static withConfiguration(builder: Builder, ...dataSourceArgs: Constructable<ConfigurationDataSource>[]): Builder<import("@decohere/http").App>;
16
- }
@@ -1,15 +0,0 @@
1
- import type { IConfigurationService } from "@decohere/core/configuration";
2
- import type { Constructable } from "@decohere/core/reflection";
3
- import type { ConfigurationDataSource } from "./configuration-data-source.js";
4
- export declare class ConfigurationService implements IConfigurationService {
5
- protected data: Record<string, unknown>;
6
- readonly dataSources: readonly ConfigurationDataSource[];
7
- protected isInitialized: boolean;
8
- constructor(dataSources?: ConfigurationDataSource[]);
9
- private initialize;
10
- get<T extends object>(typeOrString: Constructable<T> | string): Promise<T>;
11
- private ensureInitialized;
12
- private validateDataSources;
13
- private getConstructable;
14
- private getStringKey;
15
- }
@@ -1,2 +0,0 @@
1
- export declare const ConfigurationDataSourceKey: unique symbol;
2
- export declare const DEFAULT_CONFIG_DIR = "config";
@@ -1,2 +0,0 @@
1
- export declare class ConfigurationError extends Error {
2
- }
@@ -1,5 +0,0 @@
1
- export * from "./configuration.extensions.js";
2
- export * from "./configuration.service.js";
3
- export * from "./configuration-data-source.js";
4
- export * from "./const.js";
5
- export * from "./yaml-data-source.js";
@@ -1,24 +0,0 @@
1
- import { ConfigurationDataSource } from "./configuration-data-source.js";
2
- /** Resolve which env-specific config file to merge (e.g. config.dev.yaml). */
3
- export declare function resolveConfigEnv(nodeEnv?: string): string;
4
- export type ConfigSource = {
5
- /** Shown in errors, e.g. the file name. */
6
- name: string;
7
- text: string;
8
- };
9
- type Tree = Record<string, unknown>;
10
- /**
11
- * Merge config files in order (later files win), then resolve `${VAR}` placeholders on the
12
- * merged result.
13
- *
14
- * Resolving after the merge means an environment file such as `config.dev.yaml` can replace a
15
- * production `${VAR}` without that variable being set: a placeholder that was replaced is never
16
- * looked up. Placeholders still unset after the merge fail together, naming every variable and
17
- * the config path that used it. `${VAR:default}` keeps a default for optional values.
18
- */
19
- export declare function resolveYamlConfig(sources: ConfigSource[]): Tree;
20
- export declare class YamlDataSource extends ConfigurationDataSource {
21
- name: string;
22
- getData(): Promise<Record<string, unknown>>;
23
- }
24
- export {};
File without changes
File without changes