-
Notifications
You must be signed in to change notification settings - Fork 0
/
mod.ts
333 lines (307 loc) · 10.8 KB
/
mod.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
/**
* @module
*
* This module provides functions to set up environment variables in a Deno application.
*
* The main function in this module is {@link initVariable}, which sets up an environment variable.
* The variable can have three different sources:
* 1. The environment variable itself;
* 2. A file specified by another environment variable with the name `[variable]_FILE`; or
* 3. A default value.
*
* The environment variable takes precedence over the file, which takes precedence over the default value.
*
* The function also validates the environment variable's value using a Zod schema.
*
* After this is set up, you can simply read values synchronously from the environment using `Deno.env.get`.
*
* If, at that point, the environment variable is not set, the application shall throw an {@link EnvNotSetError}.
*/
import { getLogger } from "@std/log";
/**
* Whereas `envVariable` is the name of an environment variable used in the application,
* this error shall be thrown when the application tries to access an environment variable
* that is not set.
*
* Note that such variables should be set using {@link initVariable} at the beginning
* of the application's execution.
*/
export class EnvNotSetError extends Error {
/**
* Create a new {@link EnvNotSetError}.
*
* Use this when an environment variable is not set and the application tries to access it.
*
* @param envVariable name of the environment variable
* @param cause the cause of the error, if any
*/
constructor(public readonly envVariable: string, cause?: unknown) {
super(
`Environment variable ${envVariable} is not set.\n` +
`You can also set it by specifying a path to a file ` +
`with its value using ${envVariable}_FILE.`,
);
this.cause = cause;
}
}
/**
* Whereas:
* 1. `envVariable` is the name of an environment variable used in the application,
* 2. `defaultValue` is the default value for the environment variable (can be undefined), and
* 3. `validator` is a Zod schema used to validate the environment variable's value,
*
* this function will:
*
* 1. Look for the environment variable `envVariable`;
* 2. If it is not set, try to set it by reading the file specified by `${envVariable}_FILE`;
* 3. If `${envVariable}_FILE` is not set, set the environment variable to `defaultValue`;
* 4. If `defaultValue` is undefined, delete the environment variable;
* 5. Validate the environment variable's value using `validator`.
*
* @param envVariable name of the environment variable
* @param validator Zod schema used to validate the environment variable's value. Defaults to {@link OPTIONAL}.
* @param defaultValue default value for the environment variable
*
* @throws {ConfigParseError} If the final environment variable's value cannot be parsed using `validator`,
* this function will throw a `ConfigParseError`.
* @throws {ConfigFileReadError} If the file at the path specified by `${envVariable}_FILE` cannot be read,
* this function will throw a `ConfigFileReadError`.
*/
export async function initVariable(
envVariable: string,
validator: ZodSchemaCompat = OPTIONAL,
defaultValue?: string,
): Promise<void> {
logger().debug(`(${envVariable}) Setting up environment variable.`, {
envVariable,
defaultValue,
});
let source = `Environment variable ${envVariable}`;
if (!Deno.env.get(envVariable)) {
source = `File from ${envVariable}_FILE`;
await setFromFile(envVariable);
}
if (!Deno.env.get(envVariable)) {
source = `Default value ${defaultValue ? `"${defaultValue}"` : "[none]"}`;
setFromDefault(envVariable, defaultValue);
}
const { error: parseError } = validator.safeParse(
Deno.env.get(envVariable) || defaultValue,
);
if (parseError) {
logger().error(`Could not parse variable ${envVariable}.`, {
error: parseError,
value: Deno.env.get(envVariable),
});
throw new ConfigParseError(
`Could not parse variable ${envVariable}. Details:\n${parseError}`,
);
}
logger().info(`Variable: ${envVariable} (using ${source})`, {
envVariable,
value: Deno.env.get(envVariable),
source,
required: validator.isOptional(),
});
}
/**
* Whereas
* 1. `envVariable` is the name of an environment variable used in the application not set,
* 2. `pathVariable` is the name of the environment variable that specifies the path to the
* file containing the desiredvalue of `envVariable`, and
* 3. `cause` is the error that occurred while trying to read the file at the path specified by `pathVariable`,
*
* this error shall be thrown when reading the file at the path specified by `pathVariable` fails.
*/
export class ConfigFileReadError extends Error {
/**
* Create a new {@link ConfigFileReadError}.
* @param envVariable the name of the environment variable
* @param pathVariable the name of the environment variable that specifies the path to the file
* @param cause the cause of the error
*/
constructor(
public readonly envVariable: string,
public readonly pathVariable: string,
cause: Error,
) {
super(
`Could not read file "${
Deno.env.get(pathVariable)
}" to set ${envVariable} (based on ${pathVariable}). Details:\n${cause}`,
);
this.cause = cause;
}
}
/**
* An error that gets thrown by {@link initVariable} when the environment variable
* cannot be parsed using the Zod schema.
*/
export class ConfigParseError extends Error {}
/**
* Set the environment variable from the default value.
* If the default value is not set, the environment variable is deleted.
* @param envVariable name of the environment variable
* @param defaultValue default value for the environment variable
*/
function setFromDefault(envVariable: string, defaultValue?: string) {
if (defaultValue === undefined) {
logger().debug(
`(${envVariable}) No default value set for environment variable.`,
{
envVariable,
},
);
return Deno.env.delete(envVariable);
}
logger().debug(
`(${envVariable}) Setting environment variable from default.`,
{
envVariable,
defaultValue,
},
);
Deno.env.set(envVariable, defaultValue);
}
/**
* Whereas `envVariable` is the name of an environment variable currently not set, this function will:
*
* 1. Look for an environment variable named `${envVariable}_FILE`.
* 2. If it exists, read the file at the path specified by `${envVariable}_FILE`.
* 3. Set the environment variable `envVariable` to the contents of the file.
*
* If `${envVariable}_FILE` is not set, this function will do nothing.
* @throws {ConfigFileReadError} If the file at the path specified by `${envVariable}_FILE` cannot be read,
* this function will throw a `ConfigFileReadError`.
*
* @param envVariable name of the environment variable
*/
async function setFromFile(envVariable: string): Promise<void> {
const pathVariable = `${envVariable}_FILE`;
logger().debug(
`(${envVariable}) Trying to read environment variable from file.`,
{
envVariable,
pathVariable,
},
);
const configValuePath = Deno.env.get(`${envVariable}_FILE`);
if (!configValuePath) {
logger().debug(
`(${envVariable}) No ${envVariable}_FILE environment variable set. Skipping.`,
);
return; // No file to read
}
const [fileContent, readFileError] = await Deno.readTextFile(configValuePath)
.then(
(content) => [content, null] as const,
).catch((error) => [null, error as unknown as Error] as const);
if (readFileError) {
logger().error(`Could not read file.`, {
envVariable,
pathVariable,
configValuePath,
readFileError,
});
throw new ConfigFileReadError(envVariable, pathVariable, readFileError);
}
logger().debug(`(${envVariable}) Setting environment variable from file.`, {
envVariable,
pathVariable,
configValuePath,
});
Deno.env.set(envVariable, fileContent);
}
/**
* Get the logger for this module.
*/
function logger() {
return getLogger("@wuespace/envar");
}
/**
* A type that is compatible with Zod schemas.
*/
export type ZodSchemaCompat = {
safeParse: (value?: string) => { error: Error | null | undefined };
isOptional: () => boolean;
};
/**
* A `ZodSchemaCompat` validator that represents a required variable.
*
* Useful for projects where you don't need full-blown Zod schemas.
*
* @example
* ```typescript
* import { initVariable, REQUIRED } from "@wuespace/envar/";
* // This will throw an error if MY_ENV_VAR is not set in one of the sources.
* await initVariable("MY_ENV_VAR", REQUIRED);
* ```
*/
export const REQUIRED: ZodSchemaCompat = {
isOptional: () => false,
safeParse: (val) => ({
error: typeof val === "string"
? undefined
: new Error(`Expected value to be a string, but got ${typeof val}`),
}),
};
/**
* A `ZodSchemaCompat` validator that represents an optional variable.
*
* Useful for projects where you don't need full-blown Zod schemas.
*
* @example
* ```typescript
* import { initVariable, OPTIONAL } from "@wuespace/envar/";
* // This will not throw an error if MY_ENV_VAR is not set in one of the sources.
* await initVariable("MY_ENV_VAR", OPTIONAL);
* ```
*/
export const OPTIONAL: ZodSchemaCompat = {
isOptional: () => true,
safeParse: (val) => ({
error: typeof val === "string" || val === undefined
? undefined
: new Error(`Expected value to be a string, but got ${typeof val}`),
}),
};
/**
* A `ZodSchemaCompat` validator that represents a required, non-empty variable.
*
* Useful for projects where you don't need full-blown Zod schemas.
*
* @example
* ```typescript
* import { initVariable, REQUIRED_NON_EMPTY } from "@wuespace/envar/";
* // This will throw an error if MY_ENV_VAR is not set in one of the sources or if it is an empty string.
* await initVariable("MY_ENV_VAR", REQUIRED_NON_EMPTY);
* ```
*/
export const REQUIRED_NON_EMPTY: ZodSchemaCompat = {
isOptional: () => false,
safeParse: (val) => ({
error: typeof val === "string" && val.length > 0
? undefined
: new Error(`Expected value to be a non-empty string, but got "${val?.toString()}"`),
}),
};
/**
* A `ZodSchemaCompat` validator that represents an optional, non-empty variable.
* Valid values are non-empty strings or undefined. Any other value is invalid.
*
* Useful for projects where you don't need full-blown Zod schemas.
*
* @example
* ```typescript
* import { initVariable, OPTIONAL_NON_EMPTY } from "@wuespace/envar/";
* // This will not throw an error if MY_ENV_VAR is an empty string.
* await initVariable("MY_ENV_VAR", OPTIONAL_NON_EMPTY);
*/
export const OPTIONAL_NON_EMPTY: ZodSchemaCompat = {
isOptional: () => true,
safeParse: (val) => ({
error: typeof val === "string" && val.length > 0 || val === undefined
? undefined
: new Error(`Expected value to be a non-empty string or unset, but got "${val?.toString()}"`),
}),
};