@geekmidas/envkit 9.0.2 → 10.0.0-alpha.1
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/README.md +2 -2
- package/dist/{EnvironmentBuilder-DDgLJAAo.cjs → EnvironmentBuilder-15SdFJdK.cjs} +2 -2
- package/dist/EnvironmentBuilder-15SdFJdK.cjs.map +1 -0
- package/dist/{EnvironmentBuilder-CFen3oIg.mjs → EnvironmentBuilder-C-2fViCT.mjs} +2 -2
- package/dist/EnvironmentBuilder-C-2fViCT.mjs.map +1 -0
- package/dist/{EnvironmentBuilder-DHfDXJUm.d.mts → EnvironmentBuilder-CoBQ9Xp2.d.mts} +2 -2
- package/dist/{EnvironmentBuilder-CNgcdzSR.d.cts.map → EnvironmentBuilder-CoBQ9Xp2.d.mts.map} +1 -1
- package/dist/{EnvironmentBuilder-CNgcdzSR.d.cts → EnvironmentBuilder-DeIle4QN.d.cts} +2 -2
- package/dist/{EnvironmentBuilder-DHfDXJUm.d.mts.map → EnvironmentBuilder-DeIle4QN.d.cts.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/sst.cjs +13 -6
- package/dist/sst.cjs.map +1 -1
- package/dist/sst.d.cts +15 -4
- package/dist/sst.d.cts.map +1 -1
- package/dist/sst.d.mts +15 -4
- package/dist/sst.d.mts.map +1 -1
- package/dist/sst.mjs +13 -6
- package/dist/sst.mjs.map +1 -1
- package/package.json +6 -2
- package/CHANGELOG.md +0 -115
- package/dist/EnvironmentBuilder-CFen3oIg.mjs.map +0 -1
- package/dist/EnvironmentBuilder-DDgLJAAo.cjs.map +0 -1
- package/docs/api-reference.md +0 -302
- package/docs/async-secrets-design.md +0 -355
- package/examples/basic-usage.ts +0 -386
- package/src/EnvironmentBuilder.ts +0 -192
- package/src/EnvironmentParser.ts +0 -330
- package/src/SnifferEnvironmentParser.ts +0 -334
- package/src/SstEnvValidator.ts +0 -369
- package/src/SstEnvironmentBuilder.ts +0 -343
- package/src/__tests__/ConfigParser.spec.ts +0 -394
- package/src/__tests__/EnvironmentBuilder.spec.ts +0 -254
- package/src/__tests__/EnvironmentParser.spec.ts +0 -839
- package/src/__tests__/SnifferEnvironmentParser.spec.ts +0 -644
- package/src/__tests__/SstEnvValidator.spec.ts +0 -236
- package/src/__tests__/SstEnvironmentBuilder.spec.ts +0 -397
- package/src/__tests__/credentials.integration.spec.ts +0 -239
- package/src/__tests__/credentials.spec.ts +0 -136
- package/src/__tests__/formatter.spec.ts +0 -268
- package/src/__tests__/sst.spec.ts +0 -437
- package/src/credentials.ts +0 -112
- package/src/formatter.ts +0 -146
- package/src/index.ts +0 -24
- package/src/sst.ts +0 -76
- package/sst-env.d.ts +0 -8
- package/tsconfig.json +0 -9
- package/tsdown.config.ts +0 -18
package/examples/basic-usage.ts
DELETED
|
@@ -1,386 +0,0 @@
|
|
|
1
|
-
import { EnvironmentParser } from '@geekmidas/envkit';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
|
|
4
|
-
const parser = new EnvironmentParser(process.env as {});
|
|
5
|
-
// Example 1: Basic configuration
|
|
6
|
-
export function basicExample() {
|
|
7
|
-
const config = parser.create((get) => ({
|
|
8
|
-
appName: get('APP_NAME').string().default('My App'),
|
|
9
|
-
port: get('PORT').string().transform(Number).default(3000),
|
|
10
|
-
isDevelopment: get('NODE_ENV')
|
|
11
|
-
.string()
|
|
12
|
-
.transform((env) => env === 'development'),
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
const result = config.parse();
|
|
16
|
-
|
|
17
|
-
return result;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Example 2: Database configuration
|
|
21
|
-
export function databaseExample() {
|
|
22
|
-
const config = parser.create((get) => ({
|
|
23
|
-
database: {
|
|
24
|
-
host: get('DB_HOST').string().default('localhost'),
|
|
25
|
-
port: get('DB_PORT').string().transform(Number).default(5432),
|
|
26
|
-
name: get('DB_NAME').string(),
|
|
27
|
-
user: get('DB_USER').string(),
|
|
28
|
-
password: get('DB_PASSWORD').string(),
|
|
29
|
-
ssl: get('DB_SSL')
|
|
30
|
-
.string()
|
|
31
|
-
.transform((v) => v === 'true')
|
|
32
|
-
.default(false),
|
|
33
|
-
poolSize: get('DB_POOL_SIZE')
|
|
34
|
-
.string()
|
|
35
|
-
.transform(Number)
|
|
36
|
-
.int()
|
|
37
|
-
.min(1)
|
|
38
|
-
.max(100)
|
|
39
|
-
.default(10),
|
|
40
|
-
},
|
|
41
|
-
}));
|
|
42
|
-
|
|
43
|
-
return config.parse();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Example 3: API configuration with validation
|
|
47
|
-
export function apiConfigExample() {
|
|
48
|
-
const config = parser.create((get) => ({
|
|
49
|
-
api: {
|
|
50
|
-
baseUrl: get('API_BASE_URL').url(),
|
|
51
|
-
key: get('API_KEY').string().min(32),
|
|
52
|
-
secret: get('API_SECRET').string().min(64),
|
|
53
|
-
timeout: get('API_TIMEOUT').string().transform(Number).default(5000),
|
|
54
|
-
retries: get('API_RETRIES').string().transform(Number).default(3),
|
|
55
|
-
endpoints: {
|
|
56
|
-
users: get('API_ENDPOINT_USERS').string().default('/api/v1/users'),
|
|
57
|
-
auth: get('API_ENDPOINT_AUTH').string().default('/api/v1/auth'),
|
|
58
|
-
products: get('API_ENDPOINT_PRODUCTS')
|
|
59
|
-
.string()
|
|
60
|
-
.default('/api/v1/products'),
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
}));
|
|
64
|
-
|
|
65
|
-
return config.parse();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Example 4: Feature flags and complex validation
|
|
69
|
-
export function featureFlagsExample() {
|
|
70
|
-
const config = parser.create((get) => ({
|
|
71
|
-
features: {
|
|
72
|
-
authentication: get('FEATURE_AUTH')
|
|
73
|
-
.string()
|
|
74
|
-
.transform((v) => v === 'true')
|
|
75
|
-
.default(true),
|
|
76
|
-
rateLimit: get('FEATURE_RATE_LIMIT')
|
|
77
|
-
.string()
|
|
78
|
-
.transform((v) => v === 'true')
|
|
79
|
-
.default(true),
|
|
80
|
-
cache: get('FEATURE_CACHE')
|
|
81
|
-
.string()
|
|
82
|
-
.transform((v) => v === 'true')
|
|
83
|
-
.default(false),
|
|
84
|
-
beta: {
|
|
85
|
-
enabled: get('FEATURE_BETA')
|
|
86
|
-
.string()
|
|
87
|
-
.transform((v) => v === 'true')
|
|
88
|
-
.default(false),
|
|
89
|
-
allowedUsers: get('FEATURE_BETA_USERS')
|
|
90
|
-
.string()
|
|
91
|
-
.transform((users) =>
|
|
92
|
-
users ? users.split(',').map((u) => u.trim()) : [],
|
|
93
|
-
)
|
|
94
|
-
.default([]),
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
rateLimit: {
|
|
98
|
-
windowMs: get('RATE_LIMIT_WINDOW_MS')
|
|
99
|
-
.string()
|
|
100
|
-
.transform(Number)
|
|
101
|
-
.default(60000),
|
|
102
|
-
maxRequests: get('RATE_LIMIT_MAX_REQUESTS')
|
|
103
|
-
.string()
|
|
104
|
-
.transform(Number)
|
|
105
|
-
.default(100),
|
|
106
|
-
},
|
|
107
|
-
}));
|
|
108
|
-
|
|
109
|
-
return config.parse();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Example 5: Email configuration with refinements
|
|
113
|
-
export function emailConfigExample() {
|
|
114
|
-
const config = parser.create((get) => ({
|
|
115
|
-
email: {
|
|
116
|
-
provider: get('EMAIL_PROVIDER').enum(['sendgrid', 'mailgun', 'ses']),
|
|
117
|
-
apiKey: get('EMAIL_API_KEY').string().min(20),
|
|
118
|
-
from: {
|
|
119
|
-
name: get('EMAIL_FROM_NAME').string().default('Support Team'),
|
|
120
|
-
address: get('EMAIL_FROM_ADDRESS').string().email(),
|
|
121
|
-
},
|
|
122
|
-
replyTo: get('EMAIL_REPLY_TO').string().email().optional(),
|
|
123
|
-
templates: {
|
|
124
|
-
welcome: get('EMAIL_TEMPLATE_WELCOME').string().uuid(),
|
|
125
|
-
resetPassword: get('EMAIL_TEMPLATE_RESET_PASSWORD').string().uuid(),
|
|
126
|
-
invoice: get('EMAIL_TEMPLATE_INVOICE').string().uuid().optional(),
|
|
127
|
-
},
|
|
128
|
-
smtp: {
|
|
129
|
-
host: get('SMTP_HOST').string().optional(),
|
|
130
|
-
port: get('SMTP_PORT').string().transform(Number).optional(),
|
|
131
|
-
secure: get('SMTP_SECURE')
|
|
132
|
-
.string()
|
|
133
|
-
.transform((v) => v === 'true')
|
|
134
|
-
.default(true),
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
}));
|
|
138
|
-
|
|
139
|
-
return config.parse();
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Example 6: Multi-environment configuration
|
|
143
|
-
export function multiEnvironmentExample() {
|
|
144
|
-
const env = process.env.NODE_ENV || 'development';
|
|
145
|
-
|
|
146
|
-
// Different config sources based on environment
|
|
147
|
-
const configSource = {
|
|
148
|
-
...process.env,
|
|
149
|
-
// Override with environment-specific values
|
|
150
|
-
...(env === 'production'
|
|
151
|
-
? {
|
|
152
|
-
LOG_LEVEL: 'error',
|
|
153
|
-
DEBUG: 'false',
|
|
154
|
-
}
|
|
155
|
-
: {
|
|
156
|
-
LOG_LEVEL: 'debug',
|
|
157
|
-
DEBUG: 'true',
|
|
158
|
-
}),
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
const parser = new EnvironmentParser(configSource);
|
|
162
|
-
|
|
163
|
-
const config = parser.create((get) => ({
|
|
164
|
-
env: get('NODE_ENV')
|
|
165
|
-
.enum(['development', 'staging', 'production'])
|
|
166
|
-
.default('development'),
|
|
167
|
-
logging: {
|
|
168
|
-
level: get('LOG_LEVEL').enum([
|
|
169
|
-
'trace',
|
|
170
|
-
'debug',
|
|
171
|
-
'info',
|
|
172
|
-
'warn',
|
|
173
|
-
'error',
|
|
174
|
-
'fatal',
|
|
175
|
-
]),
|
|
176
|
-
pretty: get('LOG_PRETTY')
|
|
177
|
-
.string()
|
|
178
|
-
.transform((v) => v === 'true')
|
|
179
|
-
.default(env !== 'production'),
|
|
180
|
-
debug: get('DEBUG')
|
|
181
|
-
.string()
|
|
182
|
-
.transform((v) => v === 'true'),
|
|
183
|
-
},
|
|
184
|
-
server: {
|
|
185
|
-
host: get('HOST').string().default('0.0.0.0'),
|
|
186
|
-
port: get('PORT')
|
|
187
|
-
.string()
|
|
188
|
-
.transform(Number)
|
|
189
|
-
.default(env === 'production' ? 80 : 3000),
|
|
190
|
-
cors: {
|
|
191
|
-
enabled: get('CORS_ENABLED')
|
|
192
|
-
.string()
|
|
193
|
-
.transform((v) => v === 'true')
|
|
194
|
-
.default(true),
|
|
195
|
-
origins: get('CORS_ORIGINS')
|
|
196
|
-
.string()
|
|
197
|
-
.transform((origins) => origins.split(',').map((o) => o.trim()))
|
|
198
|
-
.refine((origins) => origins.every((o) => o.startsWith('http')), {
|
|
199
|
-
message: 'All CORS origins must be valid URLs',
|
|
200
|
-
})
|
|
201
|
-
.default(['http://localhost:3000']),
|
|
202
|
-
},
|
|
203
|
-
},
|
|
204
|
-
}));
|
|
205
|
-
|
|
206
|
-
return config.parse();
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Example 7: Error handling
|
|
210
|
-
export function errorHandlingExample() {
|
|
211
|
-
try {
|
|
212
|
-
const config = parser
|
|
213
|
-
.create((get) => ({
|
|
214
|
-
required: {
|
|
215
|
-
apiKey: get('API_KEY').string().min(32),
|
|
216
|
-
databaseUrl: get('DATABASE_URL').string().url(),
|
|
217
|
-
adminEmail: get('ADMIN_EMAIL').string().email(),
|
|
218
|
-
},
|
|
219
|
-
optional: {
|
|
220
|
-
sentryDsn: get('SENTRY_DSN').string().url().optional(),
|
|
221
|
-
slackWebhook: get('SLACK_WEBHOOK').string().url().optional(),
|
|
222
|
-
},
|
|
223
|
-
}))
|
|
224
|
-
.parse();
|
|
225
|
-
|
|
226
|
-
return config;
|
|
227
|
-
} catch (error) {
|
|
228
|
-
if (error instanceof z.ZodError) {
|
|
229
|
-
error.errors.forEach((err) => {
|
|
230
|
-
const _path = err.path.join('.');
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
// In a real app, you might want to exit
|
|
234
|
-
process.exit(1);
|
|
235
|
-
}
|
|
236
|
-
throw error;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Example 8: Using with dotenv
|
|
241
|
-
export function dotenvExample() {
|
|
242
|
-
// Load .env file
|
|
243
|
-
require('dotenv').config();
|
|
244
|
-
|
|
245
|
-
const config = parser.create((get) => ({
|
|
246
|
-
app: {
|
|
247
|
-
name: get('APP_NAME').string(),
|
|
248
|
-
version: get('APP_VERSION')
|
|
249
|
-
.string()
|
|
250
|
-
.regex(/^\d+\.\d+\.\d+$/),
|
|
251
|
-
description: get('APP_DESCRIPTION').string().optional(),
|
|
252
|
-
},
|
|
253
|
-
secrets: {
|
|
254
|
-
jwtSecret: get('JWT_SECRET').string().min(64),
|
|
255
|
-
encryptionKey: get('ENCRYPTION_KEY').string().length(32),
|
|
256
|
-
apiKeys: get('API_KEYS')
|
|
257
|
-
.string()
|
|
258
|
-
.transform((keys) => keys.split(',').map((k) => k.trim()))
|
|
259
|
-
.pipe(z.array(z.string().min(32))),
|
|
260
|
-
},
|
|
261
|
-
}));
|
|
262
|
-
|
|
263
|
-
return config.parse();
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Example 9: Custom transformations
|
|
267
|
-
export function customTransformationsExample() {
|
|
268
|
-
const config = parser.create((get) => ({
|
|
269
|
-
// Parse JSON
|
|
270
|
-
features: get('FEATURES_JSON')
|
|
271
|
-
.string()
|
|
272
|
-
.transform((str) => JSON.parse(str))
|
|
273
|
-
.pipe(z.record(z.boolean())),
|
|
274
|
-
|
|
275
|
-
// Parse duration strings
|
|
276
|
-
timeouts: {
|
|
277
|
-
request: get('TIMEOUT_REQUEST')
|
|
278
|
-
.string()
|
|
279
|
-
.transform(parseDuration)
|
|
280
|
-
.default('30s'),
|
|
281
|
-
idle: get('TIMEOUT_IDLE').string().transform(parseDuration).default('5m'),
|
|
282
|
-
},
|
|
283
|
-
|
|
284
|
-
// Parse memory sizes
|
|
285
|
-
limits: {
|
|
286
|
-
memory: get('MEMORY_LIMIT')
|
|
287
|
-
.string()
|
|
288
|
-
.transform(parseMemorySize)
|
|
289
|
-
.default('512MB'),
|
|
290
|
-
upload: get('UPLOAD_LIMIT')
|
|
291
|
-
.string()
|
|
292
|
-
.transform(parseMemorySize)
|
|
293
|
-
.default('10MB'),
|
|
294
|
-
},
|
|
295
|
-
|
|
296
|
-
// Complex array parsing
|
|
297
|
-
allowedDomains: get('ALLOWED_DOMAINS')
|
|
298
|
-
.string()
|
|
299
|
-
.transform((domains) =>
|
|
300
|
-
domains
|
|
301
|
-
.split(',')
|
|
302
|
-
.map((d) => d.trim())
|
|
303
|
-
.filter(Boolean),
|
|
304
|
-
)
|
|
305
|
-
.pipe(z.array(z.string().regex(/^[a-z0-9.-]+$/i))),
|
|
306
|
-
}));
|
|
307
|
-
|
|
308
|
-
return config.parse();
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Helper functions for custom transformations
|
|
312
|
-
function parseDuration(duration: string): number {
|
|
313
|
-
const match = duration.match(/^(\d+)(ms|s|m|h)$/);
|
|
314
|
-
if (!match) throw new Error(`Invalid duration: ${duration}`);
|
|
315
|
-
|
|
316
|
-
const [, value, unit] = match;
|
|
317
|
-
const multipliers = { ms: 1, s: 1000, m: 60000, h: 3600000 };
|
|
318
|
-
return parseInt(value, 10) * multipliers[unit as keyof typeof multipliers];
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function parseMemorySize(size: string): number {
|
|
322
|
-
const match = size.match(/^(\d+)(B|KB|MB|GB)$/i);
|
|
323
|
-
if (!match) throw new Error(`Invalid memory size: ${size}`);
|
|
324
|
-
|
|
325
|
-
const [, value, unit] = match;
|
|
326
|
-
const multipliers = { B: 1, KB: 1024, MB: 1048576, GB: 1073741824 };
|
|
327
|
-
return (
|
|
328
|
-
parseInt(value, 10) *
|
|
329
|
-
multipliers[unit.toUpperCase() as keyof typeof multipliers]
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Example 10: Type-safe configuration module
|
|
334
|
-
// config.ts - This is how you'd typically use it in a real app
|
|
335
|
-
export const loadConfig = () => {
|
|
336
|
-
const parser = new EnvironmentParser(process.env as any);
|
|
337
|
-
|
|
338
|
-
return parser
|
|
339
|
-
.create((get) => ({
|
|
340
|
-
app: {
|
|
341
|
-
name: get('APP_NAME').string().default('My Application'),
|
|
342
|
-
env: get('NODE_ENV')
|
|
343
|
-
.enum(['development', 'staging', 'production'])
|
|
344
|
-
.default('development'),
|
|
345
|
-
port: get('PORT').string().transform(Number).default(3000),
|
|
346
|
-
host: get('HOST').string().default('localhost'),
|
|
347
|
-
},
|
|
348
|
-
database: {
|
|
349
|
-
url: get('DATABASE_URL').string().url(),
|
|
350
|
-
maxConnections: get('DB_MAX_CONNECTIONS')
|
|
351
|
-
.string()
|
|
352
|
-
.transform(Number)
|
|
353
|
-
.default(10),
|
|
354
|
-
ssl: get('DB_SSL')
|
|
355
|
-
.string()
|
|
356
|
-
.transform((v) => v === 'true')
|
|
357
|
-
.default(false),
|
|
358
|
-
},
|
|
359
|
-
redis: {
|
|
360
|
-
url: get('REDIS_URL').string().url().optional(),
|
|
361
|
-
ttl: get('REDIS_TTL').string().transform(Number).default(3600),
|
|
362
|
-
},
|
|
363
|
-
auth: {
|
|
364
|
-
jwtSecret: get('JWT_SECRET').string().min(32),
|
|
365
|
-
jwtExpiry: get('JWT_EXPIRY').string().default('7d'),
|
|
366
|
-
bcryptRounds: get('BCRYPT_ROUNDS')
|
|
367
|
-
.string()
|
|
368
|
-
.transform(Number)
|
|
369
|
-
.default(10),
|
|
370
|
-
},
|
|
371
|
-
features: {
|
|
372
|
-
signups: get('FEATURE_SIGNUPS')
|
|
373
|
-
.string()
|
|
374
|
-
.transform((v) => v === 'true')
|
|
375
|
-
.default(true),
|
|
376
|
-
subscriptions: get('FEATURE_SUBSCRIPTIONS')
|
|
377
|
-
.string()
|
|
378
|
-
.transform((v) => v === 'true')
|
|
379
|
-
.default(false),
|
|
380
|
-
},
|
|
381
|
-
}))
|
|
382
|
-
.parse();
|
|
383
|
-
};
|
|
384
|
-
|
|
385
|
-
// Export the config for use throughout the app
|
|
386
|
-
export const config = loadConfig();
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import snakecase from 'lodash.snakecase';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Converts a string to environment variable case format (UPPER_SNAKE_CASE).
|
|
5
|
-
* Numbers following underscores are preserved without the underscore.
|
|
6
|
-
*
|
|
7
|
-
* @param name - The string to convert
|
|
8
|
-
* @returns The converted string in environment variable format
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* environmentCase('myVariable') // 'MY_VARIABLE'
|
|
12
|
-
* environmentCase('apiV2') // 'APIV2'
|
|
13
|
-
*/
|
|
14
|
-
export function environmentCase(name: string): string {
|
|
15
|
-
return snakecase(name)
|
|
16
|
-
.toUpperCase()
|
|
17
|
-
.replace(/_\d+/g, (r) => {
|
|
18
|
-
return r.replace('_', '');
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* A record of environment variable names to their values.
|
|
24
|
-
* Values can be primitives or nested records.
|
|
25
|
-
*/
|
|
26
|
-
export interface EnvRecord {
|
|
27
|
-
[key: string]: EnvValue;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Represents a value that can be stored in an environment record.
|
|
32
|
-
* Can be a primitive value or a nested record of environment values.
|
|
33
|
-
*/
|
|
34
|
-
export type EnvValue = string | number | boolean | EnvRecord;
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* A resolver function that converts a typed value into environment variables.
|
|
38
|
-
*
|
|
39
|
-
* @template T - The type of value this resolver handles (without the `type` key)
|
|
40
|
-
* @param key - The key name from the input record
|
|
41
|
-
* @param value - The value to resolve (without the `type` key)
|
|
42
|
-
* @returns A record of environment variable names to their values
|
|
43
|
-
*/
|
|
44
|
-
export type EnvironmentResolver<T = any> = (key: string, value: T) => EnvRecord;
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* A map of type discriminator strings to their resolver functions.
|
|
48
|
-
*/
|
|
49
|
-
export type Resolvers = Record<string, EnvironmentResolver<any>>;
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Options for configuring the EnvironmentBuilder.
|
|
53
|
-
*/
|
|
54
|
-
export interface EnvironmentBuilderOptions {
|
|
55
|
-
/**
|
|
56
|
-
* Handler called when a value's type doesn't match any registered resolver.
|
|
57
|
-
* Defaults to console.warn.
|
|
58
|
-
*/
|
|
59
|
-
onUnmatchedValue?: (key: string, value: unknown) => void;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Input value type - either a string or an object with a `type` discriminator.
|
|
64
|
-
*/
|
|
65
|
-
export type InputValue = string | { type: string; [key: string]: unknown };
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Base type for typed input values with a specific type discriminator.
|
|
69
|
-
*/
|
|
70
|
-
export type TypedInputValue<TType extends string = string> = {
|
|
71
|
-
type: TType;
|
|
72
|
-
[key: string]: unknown;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Extracts the `type` string value from an input value.
|
|
77
|
-
*/
|
|
78
|
-
type ExtractType<T> = T extends { type: infer U extends string } ? U : never;
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Removes the `type` key from an object type.
|
|
82
|
-
*/
|
|
83
|
-
type OmitType<T> = T extends { type: string } ? Omit<T, 'type'> : never;
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Extracts all unique `type` values from a record (excluding plain strings).
|
|
87
|
-
*/
|
|
88
|
-
type AllTypeValues<TRecord extends Record<string, InputValue>> = {
|
|
89
|
-
[K in keyof TRecord]: ExtractType<TRecord[K]>;
|
|
90
|
-
}[keyof TRecord];
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* For a given type value, finds the corresponding value type (without `type` key).
|
|
94
|
-
*/
|
|
95
|
-
type ValueForType<
|
|
96
|
-
TRecord extends Record<string, InputValue>,
|
|
97
|
-
TType extends string,
|
|
98
|
-
> = {
|
|
99
|
-
[K in keyof TRecord]: TRecord[K] extends { type: TType }
|
|
100
|
-
? OmitType<TRecord[K]>
|
|
101
|
-
: never;
|
|
102
|
-
}[keyof TRecord];
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Generates typed resolvers based on the input record.
|
|
106
|
-
* Keys are the `type` values, values are resolver functions receiving the value without `type`.
|
|
107
|
-
*/
|
|
108
|
-
export type TypedResolvers<TRecord extends Record<string, InputValue>> = {
|
|
109
|
-
[TType in AllTypeValues<TRecord>]: EnvironmentResolver<
|
|
110
|
-
ValueForType<TRecord, TType>
|
|
111
|
-
>;
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* A generic, extensible class for building environment variables from
|
|
116
|
-
* objects with type-discriminated values.
|
|
117
|
-
*
|
|
118
|
-
* @template TRecord - The input record type for type inference
|
|
119
|
-
* @template TResolvers - The resolvers type (defaults to TypedResolvers<TRecord>)
|
|
120
|
-
*
|
|
121
|
-
* @example
|
|
122
|
-
* ```typescript
|
|
123
|
-
* const env = new EnvironmentBuilder(
|
|
124
|
-
* {
|
|
125
|
-
* apiKey: { type: 'secret', value: 'xyz' },
|
|
126
|
-
* appName: 'my-app'
|
|
127
|
-
* },
|
|
128
|
-
* {
|
|
129
|
-
* // `value` is typed as { value: string } (without `type`)
|
|
130
|
-
* secret: (key, value) => ({ [key]: value.value }),
|
|
131
|
-
* }
|
|
132
|
-
* ).build();
|
|
133
|
-
* // { API_KEY: 'xyz', APP_NAME: 'my-app' }
|
|
134
|
-
* ```
|
|
135
|
-
*/
|
|
136
|
-
export class EnvironmentBuilder<
|
|
137
|
-
TRecord extends Record<string, InputValue> = Record<string, InputValue>,
|
|
138
|
-
TResolvers extends Resolvers = TypedResolvers<TRecord>,
|
|
139
|
-
> {
|
|
140
|
-
private readonly record: TRecord;
|
|
141
|
-
private readonly resolvers: TResolvers;
|
|
142
|
-
private readonly options: Required<EnvironmentBuilderOptions>;
|
|
143
|
-
|
|
144
|
-
constructor(
|
|
145
|
-
record: TRecord,
|
|
146
|
-
resolvers: TResolvers,
|
|
147
|
-
options: EnvironmentBuilderOptions = {},
|
|
148
|
-
) {
|
|
149
|
-
this.record = record;
|
|
150
|
-
this.resolvers = resolvers;
|
|
151
|
-
this.options = {
|
|
152
|
-
onUnmatchedValue: options.onUnmatchedValue ?? ((_key, _value) => {}),
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Build environment variables from the input record.
|
|
158
|
-
*
|
|
159
|
-
* - Plain string values are passed through with key transformation
|
|
160
|
-
* - Object values with a `type` property are matched against resolvers
|
|
161
|
-
* - Resolvers receive values without the `type` key
|
|
162
|
-
* - Only root-level keys are transformed to UPPER_SNAKE_CASE
|
|
163
|
-
*
|
|
164
|
-
* @returns A record of environment variables
|
|
165
|
-
*/
|
|
166
|
-
build(): EnvRecord {
|
|
167
|
-
const env: EnvRecord = {};
|
|
168
|
-
|
|
169
|
-
for (const [key, value] of Object.entries(this.record)) {
|
|
170
|
-
// Handle plain string values
|
|
171
|
-
if (typeof value === 'string') {
|
|
172
|
-
env[environmentCase(key)] = value;
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Handle objects with type discriminator
|
|
177
|
-
const { type, ...rest } = value;
|
|
178
|
-
const resolver = this.resolvers[type];
|
|
179
|
-
if (resolver) {
|
|
180
|
-
const resolved = resolver(key, rest);
|
|
181
|
-
// Transform only root-level keys from resolver output
|
|
182
|
-
for (const [resolvedKey, resolvedValue] of Object.entries(resolved)) {
|
|
183
|
-
env[environmentCase(resolvedKey)] = resolvedValue;
|
|
184
|
-
}
|
|
185
|
-
} else {
|
|
186
|
-
this.options.onUnmatchedValue(key, value);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return env;
|
|
191
|
-
}
|
|
192
|
-
}
|