zero-http 0.2.5 → 0.3.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 +1250 -283
- package/documentation/config/db.js +25 -0
- package/documentation/config/middleware.js +44 -0
- package/documentation/config/tls.js +12 -0
- package/documentation/controllers/cookies.js +34 -0
- package/documentation/controllers/tasks.js +108 -0
- package/documentation/full-server.js +25 -184
- package/documentation/models/Task.js +21 -0
- package/documentation/public/data/api.json +404 -24
- package/documentation/public/data/docs.json +1139 -0
- package/documentation/public/data/examples.json +80 -2
- package/documentation/public/data/options.json +23 -8
- package/documentation/public/index.html +138 -99
- package/documentation/public/scripts/app.js +1 -3
- package/documentation/public/scripts/custom-select.js +189 -0
- package/documentation/public/scripts/data-sections.js +233 -250
- package/documentation/public/scripts/playground.js +270 -0
- package/documentation/public/scripts/ui.js +4 -3
- package/documentation/public/styles.css +56 -5
- package/documentation/public/vendor/icons/compress.svg +17 -17
- package/documentation/public/vendor/icons/database.svg +21 -0
- package/documentation/public/vendor/icons/env.svg +21 -0
- package/documentation/public/vendor/icons/fetch.svg +11 -14
- package/documentation/public/vendor/icons/security.svg +15 -0
- package/documentation/public/vendor/icons/sse.svg +12 -13
- package/documentation/public/vendor/icons/static.svg +12 -26
- package/documentation/public/vendor/icons/stream.svg +7 -13
- package/documentation/public/vendor/icons/validate.svg +17 -0
- package/documentation/routes/api.js +41 -0
- package/documentation/routes/core.js +20 -0
- package/documentation/routes/playground.js +29 -0
- package/documentation/routes/realtime.js +49 -0
- package/documentation/routes/uploads.js +71 -0
- package/index.js +62 -1
- package/lib/app.js +200 -8
- package/lib/body/json.js +28 -5
- package/lib/body/multipart.js +29 -1
- package/lib/body/raw.js +1 -1
- package/lib/body/sendError.js +1 -0
- package/lib/body/text.js +1 -1
- package/lib/body/typeMatch.js +6 -2
- package/lib/body/urlencoded.js +5 -2
- package/lib/debug.js +345 -0
- package/lib/env/index.js +440 -0
- package/lib/errors.js +231 -0
- package/lib/http/request.js +219 -1
- package/lib/http/response.js +410 -6
- package/lib/middleware/compress.js +39 -6
- package/lib/middleware/cookieParser.js +237 -0
- package/lib/middleware/cors.js +13 -2
- package/lib/middleware/csrf.js +135 -0
- package/lib/middleware/errorHandler.js +90 -0
- package/lib/middleware/helmet.js +176 -0
- package/lib/middleware/index.js +7 -2
- package/lib/middleware/rateLimit.js +12 -1
- package/lib/middleware/requestId.js +54 -0
- package/lib/middleware/static.js +95 -11
- package/lib/middleware/timeout.js +72 -0
- package/lib/middleware/validator.js +257 -0
- package/lib/orm/adapters/json.js +215 -0
- package/lib/orm/adapters/memory.js +383 -0
- package/lib/orm/adapters/mongo.js +444 -0
- package/lib/orm/adapters/mysql.js +272 -0
- package/lib/orm/adapters/postgres.js +394 -0
- package/lib/orm/adapters/sql-base.js +142 -0
- package/lib/orm/adapters/sqlite.js +311 -0
- package/lib/orm/index.js +276 -0
- package/lib/orm/model.js +895 -0
- package/lib/orm/query.js +807 -0
- package/lib/orm/schema.js +172 -0
- package/lib/router/index.js +136 -47
- package/lib/sse/stream.js +15 -3
- package/lib/ws/connection.js +19 -3
- package/lib/ws/handshake.js +3 -0
- package/lib/ws/index.js +3 -1
- package/lib/ws/room.js +222 -0
- package/package.json +15 -5
- package/types/app.d.ts +120 -0
- package/types/env.d.ts +80 -0
- package/types/errors.d.ts +147 -0
- package/types/fetch.d.ts +43 -0
- package/types/index.d.ts +135 -0
- package/types/middleware.d.ts +292 -0
- package/types/orm.d.ts +610 -0
- package/types/request.d.ts +99 -0
- package/types/response.d.ts +142 -0
- package/types/router.d.ts +78 -0
- package/types/sse.d.ts +78 -0
- package/types/websocket.d.ts +119 -0
package/lib/env/index.js
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module env
|
|
3
|
+
* @description Zero-dependency typed environment variable system.
|
|
4
|
+
* Loads `.env` files, validates against a typed schema, and
|
|
5
|
+
* exposes a fast accessor with built-in type coercion.
|
|
6
|
+
*
|
|
7
|
+
* Supports: string, number, boolean, integer, array, json, url, port, enum.
|
|
8
|
+
* Multi-environment: `.env`, `.env.local`, `.env.{NODE_ENV}`, `.env.{NODE_ENV}.local`.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* const { env } = require('zero-http');
|
|
12
|
+
*
|
|
13
|
+
* env.load({
|
|
14
|
+
* PORT: { type: 'port', default: 3000 },
|
|
15
|
+
* DATABASE_URL: { type: 'string', required: true },
|
|
16
|
+
* DEBUG: { type: 'boolean', default: false },
|
|
17
|
+
* ALLOWED_ORIGINS: { type: 'array', separator: ',' },
|
|
18
|
+
* LOG_LEVEL: { type: 'enum', values: ['debug','info','warn','error'], default: 'info' },
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* env.PORT // => 3000 (number)
|
|
22
|
+
* env('PORT') // => 3000
|
|
23
|
+
* env.DEBUG // => false (boolean)
|
|
24
|
+
* env.require('DATABASE_URL') // throws if missing
|
|
25
|
+
*/
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
|
|
29
|
+
// ═══════════════════════════════════════════════════════════
|
|
30
|
+
// .env file parser
|
|
31
|
+
// ═══════════════════════════════════════════════════════════
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse a `.env` file string into key-value pairs.
|
|
35
|
+
* Supports `#` comments, single/double/backtick quotes, multiline values,
|
|
36
|
+
* inline comments, interpolation `${VAR}`, and `export` prefix.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} src - Raw file contents.
|
|
39
|
+
* @returns {Object<string, string>} Parsed key-value pairs.
|
|
40
|
+
*/
|
|
41
|
+
function parse(src)
|
|
42
|
+
{
|
|
43
|
+
const result = {};
|
|
44
|
+
const lines = src.replace(/\r\n?/g, '\n').split('\n');
|
|
45
|
+
|
|
46
|
+
let i = 0;
|
|
47
|
+
while (i < lines.length)
|
|
48
|
+
{
|
|
49
|
+
let line = lines[i].trim();
|
|
50
|
+
i++;
|
|
51
|
+
|
|
52
|
+
// Skip comments and blank lines
|
|
53
|
+
if (!line || line.startsWith('#')) continue;
|
|
54
|
+
|
|
55
|
+
// Strip optional `export ` prefix
|
|
56
|
+
if (line.startsWith('export ')) line = line.slice(7).trim();
|
|
57
|
+
|
|
58
|
+
const eqIdx = line.indexOf('=');
|
|
59
|
+
if (eqIdx === -1) continue;
|
|
60
|
+
|
|
61
|
+
const key = line.slice(0, eqIdx).trim();
|
|
62
|
+
let value = line.slice(eqIdx + 1).trim();
|
|
63
|
+
|
|
64
|
+
// Validate key name — only word chars + dots
|
|
65
|
+
if (!/^[\w.]+$/.test(key)) continue;
|
|
66
|
+
|
|
67
|
+
// Quoted values
|
|
68
|
+
const q = value[0];
|
|
69
|
+
if ((q === '"' || q === "'" || q === '`') && value.endsWith(q) && value.length >= 2)
|
|
70
|
+
{
|
|
71
|
+
value = value.slice(1, -1);
|
|
72
|
+
}
|
|
73
|
+
else if (q === '"' || q === "'" || q === '`')
|
|
74
|
+
{
|
|
75
|
+
// Multiline — read until closing quote
|
|
76
|
+
let multiline = value.slice(1);
|
|
77
|
+
while (i < lines.length)
|
|
78
|
+
{
|
|
79
|
+
const nextLine = lines[i];
|
|
80
|
+
i++;
|
|
81
|
+
if (nextLine.trimEnd().endsWith(q))
|
|
82
|
+
{
|
|
83
|
+
multiline += '\n' + nextLine.trimEnd().slice(0, -1);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
multiline += '\n' + nextLine;
|
|
87
|
+
}
|
|
88
|
+
value = multiline;
|
|
89
|
+
}
|
|
90
|
+
else
|
|
91
|
+
{
|
|
92
|
+
// Unquoted — strip inline comment
|
|
93
|
+
const hashIdx = value.indexOf(' #');
|
|
94
|
+
if (hashIdx !== -1) value = value.slice(0, hashIdx).trim();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Variable interpolation: ${VAR} → process.env.VAR or already-parsed value
|
|
98
|
+
value = value.replace(/\$\{([^}]+)\}/g, (_, name) =>
|
|
99
|
+
{
|
|
100
|
+
return result[name] || process.env[name] || '';
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
result[key] = value;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ═══════════════════════════════════════════════════════════
|
|
110
|
+
// Type coercion
|
|
111
|
+
// ═══════════════════════════════════════════════════════════
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Coerce a string value to the specified type.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} raw - Raw string value.
|
|
117
|
+
* @param {object} fieldDef - Schema field definition.
|
|
118
|
+
* @param {string} key - Variable name (for error messages).
|
|
119
|
+
* @returns {*} Coerced value.
|
|
120
|
+
* @throws {Error} When the value cannot be coerced or fails validation.
|
|
121
|
+
*/
|
|
122
|
+
function coerce(raw, fieldDef, key)
|
|
123
|
+
{
|
|
124
|
+
const type = fieldDef.type || 'string';
|
|
125
|
+
|
|
126
|
+
switch (type)
|
|
127
|
+
{
|
|
128
|
+
case 'string':
|
|
129
|
+
{
|
|
130
|
+
const val = String(raw);
|
|
131
|
+
if (fieldDef.min !== undefined && val.length < fieldDef.min)
|
|
132
|
+
throw new Error(`env "${key}" must be at least ${fieldDef.min} characters`);
|
|
133
|
+
if (fieldDef.max !== undefined && val.length > fieldDef.max)
|
|
134
|
+
throw new Error(`env "${key}" must be at most ${fieldDef.max} characters`);
|
|
135
|
+
if (fieldDef.match && !fieldDef.match.test(val))
|
|
136
|
+
throw new Error(`env "${key}" does not match pattern ${fieldDef.match}`);
|
|
137
|
+
return val;
|
|
138
|
+
}
|
|
139
|
+
case 'number':
|
|
140
|
+
{
|
|
141
|
+
const val = Number(raw);
|
|
142
|
+
if (isNaN(val)) throw new Error(`env "${key}" must be a number, got "${raw}"`);
|
|
143
|
+
if (fieldDef.min !== undefined && val < fieldDef.min)
|
|
144
|
+
throw new Error(`env "${key}" must be >= ${fieldDef.min}`);
|
|
145
|
+
if (fieldDef.max !== undefined && val > fieldDef.max)
|
|
146
|
+
throw new Error(`env "${key}" must be <= ${fieldDef.max}`);
|
|
147
|
+
return val;
|
|
148
|
+
}
|
|
149
|
+
case 'integer':
|
|
150
|
+
{
|
|
151
|
+
const val = parseInt(raw, 10);
|
|
152
|
+
if (isNaN(val)) throw new Error(`env "${key}" must be an integer, got "${raw}"`);
|
|
153
|
+
if (fieldDef.min !== undefined && val < fieldDef.min)
|
|
154
|
+
throw new Error(`env "${key}" must be >= ${fieldDef.min}`);
|
|
155
|
+
if (fieldDef.max !== undefined && val > fieldDef.max)
|
|
156
|
+
throw new Error(`env "${key}" must be <= ${fieldDef.max}`);
|
|
157
|
+
return val;
|
|
158
|
+
}
|
|
159
|
+
case 'port':
|
|
160
|
+
{
|
|
161
|
+
const val = parseInt(raw, 10);
|
|
162
|
+
if (isNaN(val) || val < 0 || val > 65535)
|
|
163
|
+
throw new Error(`env "${key}" must be a valid port (0-65535), got "${raw}"`);
|
|
164
|
+
return val;
|
|
165
|
+
}
|
|
166
|
+
case 'boolean':
|
|
167
|
+
{
|
|
168
|
+
const lower = String(raw).toLowerCase().trim();
|
|
169
|
+
if (['true', '1', 'yes', 'on'].includes(lower)) return true;
|
|
170
|
+
if (['false', '0', 'no', 'off', ''].includes(lower)) return false;
|
|
171
|
+
throw new Error(`env "${key}" must be a boolean, got "${raw}"`);
|
|
172
|
+
}
|
|
173
|
+
case 'array':
|
|
174
|
+
{
|
|
175
|
+
const sep = fieldDef.separator || ',';
|
|
176
|
+
return String(raw).split(sep).map(s => s.trim()).filter(Boolean);
|
|
177
|
+
}
|
|
178
|
+
case 'json':
|
|
179
|
+
{
|
|
180
|
+
try { return JSON.parse(raw); }
|
|
181
|
+
catch (e) { throw new Error(`env "${key}" must be valid JSON: ${e.message}`); }
|
|
182
|
+
}
|
|
183
|
+
case 'url':
|
|
184
|
+
{
|
|
185
|
+
try { new URL(raw); return raw; }
|
|
186
|
+
catch (e) { throw new Error(`env "${key}" must be a valid URL, got "${raw}"`); }
|
|
187
|
+
}
|
|
188
|
+
case 'enum':
|
|
189
|
+
{
|
|
190
|
+
const values = fieldDef.values || [];
|
|
191
|
+
if (!values.includes(raw))
|
|
192
|
+
throw new Error(`env "${key}" must be one of [${values.join(', ')}], got "${raw}"`);
|
|
193
|
+
return raw;
|
|
194
|
+
}
|
|
195
|
+
default:
|
|
196
|
+
return raw;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ═══════════════════════════════════════════════════════════
|
|
201
|
+
// Env store
|
|
202
|
+
// ═══════════════════════════════════════════════════════════
|
|
203
|
+
|
|
204
|
+
/** @type {Object<string, *>} Typed/validated values store */
|
|
205
|
+
const _store = {};
|
|
206
|
+
|
|
207
|
+
/** @type {Object<string, object>} Schema definitions */
|
|
208
|
+
let _schema = null;
|
|
209
|
+
|
|
210
|
+
/** @type {boolean} */
|
|
211
|
+
let _loaded = false;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Load environment variables from `.env` files and validate against a typed schema.
|
|
215
|
+
*
|
|
216
|
+
* Files are loaded in precedence order (later overrides earlier):
|
|
217
|
+
* 1. `.env` — shared defaults
|
|
218
|
+
* 2. `.env.local` — local overrides (gitignored)
|
|
219
|
+
* 3. `.env.{NODE_ENV}` — environment-specific (e.g. `.env.production`)
|
|
220
|
+
* 4. `.env.{NODE_ENV}.local` — env-specific local overrides
|
|
221
|
+
*
|
|
222
|
+
* Process environment variables (`process.env`) always take precedence.
|
|
223
|
+
*
|
|
224
|
+
* @param {Object<string, object>} [schema] - Typed schema definition.
|
|
225
|
+
* @param {object} [options]
|
|
226
|
+
* @param {string} [options.path] - Custom directory to load from (default: `process.cwd()`).
|
|
227
|
+
* @param {boolean} [options.override=false] - Override `process.env` with file values.
|
|
228
|
+
* @returns {Object<string, *>} The validated env store.
|
|
229
|
+
*
|
|
230
|
+
* @throws {Error} On validation failures (missing required vars, bad types, etc.).
|
|
231
|
+
*/
|
|
232
|
+
function load(schema, options = {})
|
|
233
|
+
{
|
|
234
|
+
const dir = options.path || process.cwd();
|
|
235
|
+
const nodeEnv = process.env.NODE_ENV || 'development';
|
|
236
|
+
|
|
237
|
+
// Load files in precedence order
|
|
238
|
+
const files = [
|
|
239
|
+
'.env',
|
|
240
|
+
'.env.local',
|
|
241
|
+
`.env.${nodeEnv}`,
|
|
242
|
+
`.env.${nodeEnv}.local`,
|
|
243
|
+
];
|
|
244
|
+
|
|
245
|
+
const raw = {};
|
|
246
|
+
|
|
247
|
+
for (const file of files)
|
|
248
|
+
{
|
|
249
|
+
const filePath = path.resolve(dir, file);
|
|
250
|
+
try
|
|
251
|
+
{
|
|
252
|
+
if (fs.existsSync(filePath))
|
|
253
|
+
{
|
|
254
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
255
|
+
Object.assign(raw, parse(content));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch (e) { /* silently skip unreadable files */ }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Process.env always wins
|
|
262
|
+
const merged = {};
|
|
263
|
+
if (schema)
|
|
264
|
+
{
|
|
265
|
+
_schema = schema;
|
|
266
|
+
for (const key of Object.keys(schema))
|
|
267
|
+
{
|
|
268
|
+
if (process.env[key] !== undefined) merged[key] = process.env[key];
|
|
269
|
+
else if (raw[key] !== undefined) merged[key] = raw[key];
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
else
|
|
273
|
+
{
|
|
274
|
+
// No schema — load everything
|
|
275
|
+
Object.assign(merged, raw);
|
|
276
|
+
for (const key of Object.keys(raw))
|
|
277
|
+
{
|
|
278
|
+
if (process.env[key] !== undefined) merged[key] = process.env[key];
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// If override is true, write file values into process.env
|
|
283
|
+
if (options.override)
|
|
284
|
+
{
|
|
285
|
+
for (const [k, v] of Object.entries(raw))
|
|
286
|
+
{
|
|
287
|
+
if (process.env[k] === undefined) process.env[k] = v;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Validate and coerce
|
|
292
|
+
const errors = [];
|
|
293
|
+
|
|
294
|
+
if (schema)
|
|
295
|
+
{
|
|
296
|
+
for (const [key, def] of Object.entries(schema))
|
|
297
|
+
{
|
|
298
|
+
const rawVal = merged[key];
|
|
299
|
+
|
|
300
|
+
if (rawVal === undefined || rawVal === '')
|
|
301
|
+
{
|
|
302
|
+
if (def.required)
|
|
303
|
+
{
|
|
304
|
+
errors.push(`env "${key}" is required but not set`);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (def.default !== undefined)
|
|
308
|
+
{
|
|
309
|
+
_store[key] = typeof def.default === 'function' ? def.default() : def.default;
|
|
310
|
+
}
|
|
311
|
+
else
|
|
312
|
+
{
|
|
313
|
+
_store[key] = undefined;
|
|
314
|
+
}
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
try
|
|
319
|
+
{
|
|
320
|
+
_store[key] = coerce(rawVal, def, key);
|
|
321
|
+
}
|
|
322
|
+
catch (e)
|
|
323
|
+
{
|
|
324
|
+
errors.push(e.message);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
else
|
|
329
|
+
{
|
|
330
|
+
// No schema — store raw strings
|
|
331
|
+
Object.assign(_store, merged);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (errors.length > 0)
|
|
335
|
+
{
|
|
336
|
+
throw new Error('Environment validation failed:\n • ' + errors.join('\n • '));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
_loaded = true;
|
|
340
|
+
return _store;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Get a typed environment variable.
|
|
345
|
+
* Can also be called as `env(key)`.
|
|
346
|
+
*
|
|
347
|
+
* @param {string} key - Variable name.
|
|
348
|
+
* @returns {*} The typed value.
|
|
349
|
+
*/
|
|
350
|
+
function get(key)
|
|
351
|
+
{
|
|
352
|
+
if (_store.hasOwnProperty(key)) return _store[key];
|
|
353
|
+
return process.env[key];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Get a required environment variable. Throws if missing.
|
|
358
|
+
*
|
|
359
|
+
* @param {string} key - Variable name.
|
|
360
|
+
* @returns {*} The typed value.
|
|
361
|
+
* @throws {Error} If the variable is not set.
|
|
362
|
+
*/
|
|
363
|
+
function require_(key)
|
|
364
|
+
{
|
|
365
|
+
const val = get(key);
|
|
366
|
+
if (val === undefined || val === null || val === '')
|
|
367
|
+
{
|
|
368
|
+
throw new Error(`Required environment variable "${key}" is not set`);
|
|
369
|
+
}
|
|
370
|
+
return val;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Check if a variable is set (not undefined).
|
|
375
|
+
*
|
|
376
|
+
* @param {string} key - Variable name.
|
|
377
|
+
* @returns {boolean}
|
|
378
|
+
*/
|
|
379
|
+
function has(key)
|
|
380
|
+
{
|
|
381
|
+
return _store.hasOwnProperty(key) || process.env.hasOwnProperty(key);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Get all loaded values as a plain object.
|
|
386
|
+
*
|
|
387
|
+
* @returns {Object<string, *>}
|
|
388
|
+
*/
|
|
389
|
+
function all()
|
|
390
|
+
{
|
|
391
|
+
return { ..._store };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Reset the env store (useful for testing).
|
|
396
|
+
*/
|
|
397
|
+
function reset()
|
|
398
|
+
{
|
|
399
|
+
for (const k of Object.keys(_store)) delete _store[k];
|
|
400
|
+
_schema = null;
|
|
401
|
+
_loaded = false;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ═══════════════════════════════════════════════════════════
|
|
405
|
+
// Proxy-based accessor — env.PORT, env('PORT'), env.get('PORT')
|
|
406
|
+
// ═══════════════════════════════════════════════════════════
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* The env function — callable as `env(key)` or `env.key`.
|
|
410
|
+
*
|
|
411
|
+
* @param {string} key
|
|
412
|
+
* @returns {*}
|
|
413
|
+
*/
|
|
414
|
+
function envFn(key)
|
|
415
|
+
{
|
|
416
|
+
return get(key);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Attach methods
|
|
420
|
+
envFn.load = load;
|
|
421
|
+
envFn.get = get;
|
|
422
|
+
envFn.require = require_;
|
|
423
|
+
envFn.has = has;
|
|
424
|
+
envFn.all = all;
|
|
425
|
+
envFn.reset = reset;
|
|
426
|
+
envFn.parse = parse;
|
|
427
|
+
|
|
428
|
+
// Proxy for dotenv.PORT style access
|
|
429
|
+
const envProxy = new Proxy(envFn, {
|
|
430
|
+
get(target, prop)
|
|
431
|
+
{
|
|
432
|
+
// Return own methods first
|
|
433
|
+
if (prop in target) return target[prop];
|
|
434
|
+
// Then check the store
|
|
435
|
+
if (typeof prop === 'string') return get(prop);
|
|
436
|
+
return undefined;
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
module.exports = envProxy;
|
package/lib/errors.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module errors
|
|
3
|
+
* @description HTTP error classes with status codes, error codes, and structured details.
|
|
4
|
+
* Every error extends HttpError which carries a statusCode, code, and optional details.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// --- Status Text Map ---------------------------------------------
|
|
8
|
+
|
|
9
|
+
const STATUS_TEXT = {
|
|
10
|
+
400: 'Bad Request',
|
|
11
|
+
401: 'Unauthorized',
|
|
12
|
+
402: 'Payment Required',
|
|
13
|
+
403: 'Forbidden',
|
|
14
|
+
404: 'Not Found',
|
|
15
|
+
405: 'Method Not Allowed',
|
|
16
|
+
406: 'Not Acceptable',
|
|
17
|
+
408: 'Request Timeout',
|
|
18
|
+
409: 'Conflict',
|
|
19
|
+
410: 'Gone',
|
|
20
|
+
413: 'Payload Too Large',
|
|
21
|
+
415: 'Unsupported Media Type',
|
|
22
|
+
418: "I'm a Teapot",
|
|
23
|
+
422: 'Unprocessable Entity',
|
|
24
|
+
429: 'Too Many Requests',
|
|
25
|
+
500: 'Internal Server Error',
|
|
26
|
+
501: 'Not Implemented',
|
|
27
|
+
502: 'Bad Gateway',
|
|
28
|
+
503: 'Service Unavailable',
|
|
29
|
+
504: 'Gateway Timeout',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// --- Base HttpError ----------------------------------------------
|
|
33
|
+
|
|
34
|
+
class HttpError extends Error
|
|
35
|
+
{
|
|
36
|
+
/**
|
|
37
|
+
* @param {number} statusCode - HTTP status code.
|
|
38
|
+
* @param {string} [message] - Human-readable message.
|
|
39
|
+
* @param {object} [opts]
|
|
40
|
+
* @param {string} [opts.code] - Machine-readable error code (e.g. 'VALIDATION_FAILED').
|
|
41
|
+
* @param {*} [opts.details] - Extra data (field errors, debug info, etc.).
|
|
42
|
+
*/
|
|
43
|
+
constructor(statusCode, message, opts = {})
|
|
44
|
+
{
|
|
45
|
+
super(message || STATUS_TEXT[statusCode] || 'Error');
|
|
46
|
+
this.name = this.constructor.name;
|
|
47
|
+
this.statusCode = statusCode;
|
|
48
|
+
this.code = opts.code || this._defaultCode();
|
|
49
|
+
if (opts.details !== undefined) this.details = opts.details;
|
|
50
|
+
Error.captureStackTrace(this, this.constructor);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @private */
|
|
54
|
+
_defaultCode()
|
|
55
|
+
{
|
|
56
|
+
return (STATUS_TEXT[this.statusCode] || 'ERROR')
|
|
57
|
+
.toUpperCase()
|
|
58
|
+
.replace(/[^A-Z0-9]+/g, '_')
|
|
59
|
+
.replace(/(^_|_$)/g, '');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Serialize for JSON responses.
|
|
64
|
+
* @returns {{ error: string, code: string, statusCode: number, details?: * }}
|
|
65
|
+
*/
|
|
66
|
+
toJSON()
|
|
67
|
+
{
|
|
68
|
+
const obj = { error: this.message, code: this.code, statusCode: this.statusCode };
|
|
69
|
+
if (this.details !== undefined) obj.details = this.details;
|
|
70
|
+
return obj;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- Specific Error Classes --------------------------------------
|
|
75
|
+
|
|
76
|
+
class BadRequestError extends HttpError
|
|
77
|
+
{
|
|
78
|
+
constructor(message, opts) { super(400, message, opts); }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
class UnauthorizedError extends HttpError
|
|
82
|
+
{
|
|
83
|
+
constructor(message, opts) { super(401, message, opts); }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
class ForbiddenError extends HttpError
|
|
87
|
+
{
|
|
88
|
+
constructor(message, opts) { super(403, message, opts); }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
class NotFoundError extends HttpError
|
|
92
|
+
{
|
|
93
|
+
constructor(message, opts) { super(404, message, opts); }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
class MethodNotAllowedError extends HttpError
|
|
97
|
+
{
|
|
98
|
+
constructor(message, opts) { super(405, message, opts); }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
class ConflictError extends HttpError
|
|
102
|
+
{
|
|
103
|
+
constructor(message, opts) { super(409, message, opts); }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
class GoneError extends HttpError
|
|
107
|
+
{
|
|
108
|
+
constructor(message, opts) { super(410, message, opts); }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
class PayloadTooLargeError extends HttpError
|
|
112
|
+
{
|
|
113
|
+
constructor(message, opts) { super(413, message, opts); }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
class UnprocessableEntityError extends HttpError
|
|
117
|
+
{
|
|
118
|
+
constructor(message, opts) { super(422, message, opts); }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Validation error with field-level details.
|
|
123
|
+
*/
|
|
124
|
+
class ValidationError extends HttpError
|
|
125
|
+
{
|
|
126
|
+
/**
|
|
127
|
+
* @param {string} [message] - Summary message.
|
|
128
|
+
* @param {object|Array} [errors] - Field errors, e.g. { email: 'required', age: 'must be >= 18' }.
|
|
129
|
+
* @param {object} [opts]
|
|
130
|
+
*/
|
|
131
|
+
constructor(message, errors, opts = {})
|
|
132
|
+
{
|
|
133
|
+
super(422, message || 'Validation Failed', { code: 'VALIDATION_FAILED', ...opts, details: errors });
|
|
134
|
+
this.errors = errors || {};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
class TooManyRequestsError extends HttpError
|
|
139
|
+
{
|
|
140
|
+
constructor(message, opts) { super(429, message, opts); }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
class InternalError extends HttpError
|
|
144
|
+
{
|
|
145
|
+
constructor(message, opts) { super(500, message, opts); }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
class NotImplementedError extends HttpError
|
|
149
|
+
{
|
|
150
|
+
constructor(message, opts) { super(501, message, opts); }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
class BadGatewayError extends HttpError
|
|
154
|
+
{
|
|
155
|
+
constructor(message, opts) { super(502, message, opts); }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
class ServiceUnavailableError extends HttpError
|
|
159
|
+
{
|
|
160
|
+
constructor(message, opts) { super(503, message, opts); }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// --- Factory -----------------------------------------------------
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Create an HttpError by status code.
|
|
167
|
+
*
|
|
168
|
+
* @param {number} statusCode
|
|
169
|
+
* @param {string} [message]
|
|
170
|
+
* @param {object} [opts]
|
|
171
|
+
* @returns {HttpError}
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* throw createError(404, 'User not found');
|
|
175
|
+
* throw createError(422, 'Invalid input', { details: { email: 'required' } });
|
|
176
|
+
*/
|
|
177
|
+
function createError(statusCode, message, opts)
|
|
178
|
+
{
|
|
179
|
+
const map = {
|
|
180
|
+
400: BadRequestError,
|
|
181
|
+
401: UnauthorizedError,
|
|
182
|
+
403: ForbiddenError,
|
|
183
|
+
404: NotFoundError,
|
|
184
|
+
405: MethodNotAllowedError,
|
|
185
|
+
409: ConflictError,
|
|
186
|
+
410: GoneError,
|
|
187
|
+
413: PayloadTooLargeError,
|
|
188
|
+
422: UnprocessableEntityError,
|
|
189
|
+
429: TooManyRequestsError,
|
|
190
|
+
500: InternalError,
|
|
191
|
+
501: NotImplementedError,
|
|
192
|
+
502: BadGatewayError,
|
|
193
|
+
503: ServiceUnavailableError,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const Cls = map[statusCode];
|
|
197
|
+
if (Cls) return new Cls(message, opts);
|
|
198
|
+
return new HttpError(statusCode, message, opts);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Check if a value is an HttpError (or duck-typed equivalent).
|
|
203
|
+
* @param {*} err
|
|
204
|
+
* @returns {boolean}
|
|
205
|
+
*/
|
|
206
|
+
function isHttpError(err)
|
|
207
|
+
{
|
|
208
|
+
if (!err || !(err instanceof Error)) return false;
|
|
209
|
+
return err instanceof HttpError || typeof err.statusCode === 'number';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = {
|
|
213
|
+
HttpError,
|
|
214
|
+
BadRequestError,
|
|
215
|
+
UnauthorizedError,
|
|
216
|
+
ForbiddenError,
|
|
217
|
+
NotFoundError,
|
|
218
|
+
MethodNotAllowedError,
|
|
219
|
+
ConflictError,
|
|
220
|
+
GoneError,
|
|
221
|
+
PayloadTooLargeError,
|
|
222
|
+
UnprocessableEntityError,
|
|
223
|
+
ValidationError,
|
|
224
|
+
TooManyRequestsError,
|
|
225
|
+
InternalError,
|
|
226
|
+
NotImplementedError,
|
|
227
|
+
BadGatewayError,
|
|
228
|
+
ServiceUnavailableError,
|
|
229
|
+
createError,
|
|
230
|
+
isHttpError,
|
|
231
|
+
};
|