zero-http 0.2.4 → 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.
Files changed (89) hide show
  1. package/README.md +1250 -283
  2. package/documentation/config/db.js +25 -0
  3. package/documentation/config/middleware.js +44 -0
  4. package/documentation/config/tls.js +12 -0
  5. package/documentation/controllers/cookies.js +34 -0
  6. package/documentation/controllers/tasks.js +108 -0
  7. package/documentation/full-server.js +28 -177
  8. package/documentation/models/Task.js +21 -0
  9. package/documentation/public/data/api.json +404 -24
  10. package/documentation/public/data/docs.json +1139 -0
  11. package/documentation/public/data/examples.json +80 -2
  12. package/documentation/public/data/options.json +23 -8
  13. package/documentation/public/index.html +138 -99
  14. package/documentation/public/scripts/app.js +1 -3
  15. package/documentation/public/scripts/custom-select.js +189 -0
  16. package/documentation/public/scripts/data-sections.js +233 -250
  17. package/documentation/public/scripts/playground.js +270 -0
  18. package/documentation/public/scripts/ui.js +4 -3
  19. package/documentation/public/styles.css +56 -5
  20. package/documentation/public/vendor/icons/compress.svg +17 -17
  21. package/documentation/public/vendor/icons/database.svg +21 -0
  22. package/documentation/public/vendor/icons/env.svg +21 -0
  23. package/documentation/public/vendor/icons/fetch.svg +11 -14
  24. package/documentation/public/vendor/icons/security.svg +15 -0
  25. package/documentation/public/vendor/icons/sse.svg +12 -13
  26. package/documentation/public/vendor/icons/static.svg +12 -26
  27. package/documentation/public/vendor/icons/stream.svg +7 -13
  28. package/documentation/public/vendor/icons/validate.svg +17 -0
  29. package/documentation/routes/api.js +41 -0
  30. package/documentation/routes/core.js +20 -0
  31. package/documentation/routes/playground.js +29 -0
  32. package/documentation/routes/realtime.js +49 -0
  33. package/documentation/routes/uploads.js +71 -0
  34. package/index.js +62 -1
  35. package/lib/app.js +200 -8
  36. package/lib/body/json.js +28 -5
  37. package/lib/body/multipart.js +29 -1
  38. package/lib/body/raw.js +1 -1
  39. package/lib/body/sendError.js +1 -0
  40. package/lib/body/text.js +1 -1
  41. package/lib/body/typeMatch.js +6 -2
  42. package/lib/body/urlencoded.js +5 -2
  43. package/lib/debug.js +345 -0
  44. package/lib/env/index.js +440 -0
  45. package/lib/errors.js +231 -0
  46. package/lib/http/request.js +219 -1
  47. package/lib/http/response.js +410 -6
  48. package/lib/middleware/compress.js +39 -6
  49. package/lib/middleware/cookieParser.js +237 -0
  50. package/lib/middleware/cors.js +13 -2
  51. package/lib/middleware/csrf.js +135 -0
  52. package/lib/middleware/errorHandler.js +90 -0
  53. package/lib/middleware/helmet.js +176 -0
  54. package/lib/middleware/index.js +7 -2
  55. package/lib/middleware/rateLimit.js +12 -1
  56. package/lib/middleware/requestId.js +54 -0
  57. package/lib/middleware/static.js +95 -11
  58. package/lib/middleware/timeout.js +72 -0
  59. package/lib/middleware/validator.js +257 -0
  60. package/lib/orm/adapters/json.js +215 -0
  61. package/lib/orm/adapters/memory.js +383 -0
  62. package/lib/orm/adapters/mongo.js +444 -0
  63. package/lib/orm/adapters/mysql.js +272 -0
  64. package/lib/orm/adapters/postgres.js +394 -0
  65. package/lib/orm/adapters/sql-base.js +142 -0
  66. package/lib/orm/adapters/sqlite.js +311 -0
  67. package/lib/orm/index.js +276 -0
  68. package/lib/orm/model.js +895 -0
  69. package/lib/orm/query.js +807 -0
  70. package/lib/orm/schema.js +172 -0
  71. package/lib/router/index.js +136 -47
  72. package/lib/sse/stream.js +15 -3
  73. package/lib/ws/connection.js +19 -3
  74. package/lib/ws/handshake.js +3 -0
  75. package/lib/ws/index.js +3 -1
  76. package/lib/ws/room.js +222 -0
  77. package/package.json +15 -5
  78. package/types/app.d.ts +120 -0
  79. package/types/env.d.ts +80 -0
  80. package/types/errors.d.ts +147 -0
  81. package/types/fetch.d.ts +43 -0
  82. package/types/index.d.ts +135 -0
  83. package/types/middleware.d.ts +292 -0
  84. package/types/orm.d.ts +610 -0
  85. package/types/request.d.ts +99 -0
  86. package/types/response.d.ts +142 -0
  87. package/types/router.d.ts +78 -0
  88. package/types/sse.d.ts +78 -0
  89. package/types/websocket.d.ts +119 -0
@@ -0,0 +1,215 @@
1
+ /**
2
+ * @module orm/adapters/json
3
+ * @description JSON file-backed database adapter.
4
+ * Persists data to JSON files on disk — one file per table.
5
+ * Zero-dependency, suitable for prototyping, small apps, and
6
+ * embedded scenarios. Uses atomic writes for safety.
7
+ */
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const MemoryAdapter = require('./memory');
11
+
12
+ class JsonAdapter extends MemoryAdapter
13
+ {
14
+ /**
15
+ * @param {object} options
16
+ * @param {string} options.dir - Directory to store JSON files. Created if needed.
17
+ * @param {boolean} [options.pretty=true] - Pretty-print JSON files.
18
+ * @param {number} [options.flushInterval=50] - Debounce interval in ms for writes.
19
+ * @param {boolean} [options.autoFlush=true] - Automatically flush writes (set false for manual flush()).
20
+ */
21
+ constructor(options = {})
22
+ {
23
+ super();
24
+ if (!options.dir) throw new Error('JsonAdapter requires a "dir" option');
25
+
26
+ /** @private */ this._dir = path.resolve(options.dir);
27
+ /** @private */ this._pretty = options.pretty !== false;
28
+ /** @private */ this._dirty = new Set();
29
+ /** @private */ this._flushTimer = null;
30
+ /** @private */ this._flushInterval = options.flushInterval || 50;
31
+ /** @private */ this._autoFlush = options.autoFlush !== false;
32
+
33
+ // Ensure directory exists
34
+ if (!fs.existsSync(this._dir))
35
+ {
36
+ fs.mkdirSync(this._dir, { recursive: true });
37
+ }
38
+
39
+ // Load any existing tables
40
+ try
41
+ {
42
+ const files = fs.readdirSync(this._dir).filter(f => f.endsWith('.json'));
43
+ for (const file of files)
44
+ {
45
+ const table = path.basename(file, '.json');
46
+ const content = fs.readFileSync(path.join(this._dir, file), 'utf8');
47
+ const parsed = JSON.parse(content);
48
+ this._tables.set(table, parsed.rows || []);
49
+ this._autoIncrements.set(table, parsed.autoIncrement || 1);
50
+ }
51
+ }
52
+ catch (e) { /* fresh start */ }
53
+ }
54
+
55
+ /** @override */
56
+ async createTable(table, schema)
57
+ {
58
+ await super.createTable(table, schema);
59
+ this._scheduleSave(table);
60
+ }
61
+
62
+ /** @override */
63
+ async dropTable(table)
64
+ {
65
+ await super.dropTable(table);
66
+ const filePath = path.join(this._dir, `${table}.json`);
67
+ try { fs.unlinkSync(filePath); } catch (e) { /* ignore */ }
68
+ }
69
+
70
+ /** @override */
71
+ async insert(table, data)
72
+ {
73
+ const result = await super.insert(table, data);
74
+ this._scheduleSave(table);
75
+ return result;
76
+ }
77
+
78
+ /** @override */
79
+ async update(table, pk, pkVal, data)
80
+ {
81
+ await super.update(table, pk, pkVal, data);
82
+ this._scheduleSave(table);
83
+ }
84
+
85
+ /** @override */
86
+ async updateWhere(table, conditions, data)
87
+ {
88
+ const count = await super.updateWhere(table, conditions, data);
89
+ if (count > 0) this._scheduleSave(table);
90
+ return count;
91
+ }
92
+
93
+ /** @override */
94
+ async remove(table, pk, pkVal)
95
+ {
96
+ await super.remove(table, pk, pkVal);
97
+ this._scheduleSave(table);
98
+ }
99
+
100
+ /** @override */
101
+ async deleteWhere(table, conditions)
102
+ {
103
+ const count = await super.deleteWhere(table, conditions);
104
+ if (count > 0) this._scheduleSave(table);
105
+ return count;
106
+ }
107
+
108
+ /** @override */
109
+ async clear()
110
+ {
111
+ await super.clear();
112
+ for (const table of this._tables.keys()) this._scheduleSave(table);
113
+ }
114
+
115
+ /**
116
+ * Immediately flush all pending writes.
117
+ */
118
+ async flush()
119
+ {
120
+ for (const table of this._dirty) this._saveTable(table);
121
+ this._dirty.clear();
122
+ if (this._flushTimer) { clearTimeout(this._flushTimer); this._flushTimer = null; }
123
+ }
124
+
125
+ /** @private Schedule a debounced save for the given table. */
126
+ _scheduleSave(table)
127
+ {
128
+ this._dirty.add(table);
129
+ if (this._autoFlush && !this._flushTimer)
130
+ {
131
+ this._flushTimer = setTimeout(() =>
132
+ {
133
+ this.flush();
134
+ }, this._flushInterval);
135
+ }
136
+ }
137
+
138
+ /** @private Write table data to JSON file. */
139
+ _saveTable(table)
140
+ {
141
+ const filePath = path.join(this._dir, `${table}.json`);
142
+ const data = {
143
+ autoIncrement: this._autoIncrements.get(table) || 1,
144
+ rows: this._tables.get(table) || [],
145
+ };
146
+ const json = this._pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data);
147
+
148
+ // Atomic write: write to temp file then rename
149
+ const tmpPath = filePath + '.tmp';
150
+ fs.writeFileSync(tmpPath, json, 'utf8');
151
+ fs.renameSync(tmpPath, filePath);
152
+ }
153
+
154
+ // -- JSON Adapter Utilities --------------------------
155
+
156
+ /**
157
+ * Get the directory where JSON files are stored.
158
+ * @returns {string}
159
+ */
160
+ get directory() { return this._dir; }
161
+
162
+ /**
163
+ * Get the total size of all JSON files in bytes.
164
+ * @returns {number}
165
+ */
166
+ fileSize()
167
+ {
168
+ let total = 0;
169
+ try
170
+ {
171
+ const files = fs.readdirSync(this._dir).filter(f => f.endsWith('.json'));
172
+ for (const file of files)
173
+ {
174
+ total += fs.statSync(path.join(this._dir, file)).size;
175
+ }
176
+ }
177
+ catch { /* empty dir, no files */ }
178
+ return total;
179
+ }
180
+
181
+ /**
182
+ * Check if there are pending writes that haven't been flushed.
183
+ * @returns {boolean}
184
+ */
185
+ get hasPendingWrites() { return this._dirty.size > 0; }
186
+
187
+ /**
188
+ * Compact a specific table's JSON file (re-serialize, removes whitespace bloat).
189
+ * @param {string} table
190
+ */
191
+ compact(table)
192
+ {
193
+ if (this._tables.has(table)) this._saveTable(table);
194
+ }
195
+
196
+ /**
197
+ * Back up the entire data directory to a target path.
198
+ * @param {string} destDir - Destination directory (will be created).
199
+ */
200
+ backup(destDir)
201
+ {
202
+ const dest = path.resolve(destDir);
203
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
204
+ const files = fs.readdirSync(this._dir).filter(f => f.endsWith('.json'));
205
+ for (const file of files)
206
+ {
207
+ fs.copyFileSync(
208
+ path.join(this._dir, file),
209
+ path.join(dest, file)
210
+ );
211
+ }
212
+ }
213
+ }
214
+
215
+ module.exports = JsonAdapter;
@@ -0,0 +1,383 @@
1
+ /**
2
+ * @module orm/adapters/memory
3
+ * @description In-memory database adapter.
4
+ * Zero-dependency, perfect for testing, prototyping, and
5
+ * applications that don't need persistence beyond the process lifecycle.
6
+ *
7
+ * All data is stored in plain JavaScript Maps and arrays.
8
+ * Supports full CRUD, filtering, ordering, pagination, and counting.
9
+ */
10
+
11
+ class MemoryAdapter
12
+ {
13
+ constructor()
14
+ {
15
+ /** @private */ this._tables = new Map();
16
+ /** @private */ this._autoIncrements = new Map();
17
+ }
18
+
19
+ /**
20
+ * Create a table (register schema).
21
+ * @param {string} table - Table name.
22
+ * @param {object} schema - Column definitions.
23
+ */
24
+ async createTable(table, schema)
25
+ {
26
+ if (!this._tables.has(table))
27
+ {
28
+ this._tables.set(table, []);
29
+ this._autoIncrements.set(table, 1);
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Drop a table.
35
+ * @param {string} table
36
+ */
37
+ async dropTable(table)
38
+ {
39
+ this._tables.delete(table);
40
+ this._autoIncrements.delete(table);
41
+ }
42
+
43
+ /**
44
+ * Insert a row.
45
+ * @param {string} table - Table name.
46
+ * @param {object} data - Row data.
47
+ * @returns {Promise<object>} Inserted row (with auto-increment ID if applicable).
48
+ */
49
+ async insert(table, data)
50
+ {
51
+ const rows = this._getTable(table);
52
+ const row = { ...data };
53
+
54
+ // Auto-increment: find any key not provided
55
+ if (row.id === undefined || row.id === null)
56
+ {
57
+ row.id = this._autoIncrements.get(table) || 1;
58
+ this._autoIncrements.set(table, row.id + 1);
59
+ }
60
+
61
+ // Serialize Date objects
62
+ for (const [k, v] of Object.entries(row))
63
+ {
64
+ if (v instanceof Date) row[k] = v.toISOString();
65
+ }
66
+
67
+ rows.push(row);
68
+ return row;
69
+ }
70
+
71
+ /**
72
+ * Update a row by primary key.
73
+ * @param {string} table - Table name.
74
+ * @param {string} pk - Primary key column name.
75
+ * @param {*} pkVal - Primary key value.
76
+ * @param {object} data - Fields to update.
77
+ */
78
+ async update(table, pk, pkVal, data)
79
+ {
80
+ const rows = this._getTable(table);
81
+ const row = rows.find(r => r[pk] === pkVal);
82
+ if (row)
83
+ {
84
+ for (const [k, v] of Object.entries(data))
85
+ {
86
+ row[k] = v instanceof Date ? v.toISOString() : v;
87
+ }
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Update all rows matching conditions.
93
+ * @param {string} table - Table name.
94
+ * @param {object} conditions - WHERE conditions.
95
+ * @param {object} data - Fields to update.
96
+ * @returns {Promise<number>} Number of updated rows.
97
+ */
98
+ async updateWhere(table, conditions, data)
99
+ {
100
+ const rows = this._getTable(table);
101
+ let count = 0;
102
+ for (const row of rows)
103
+ {
104
+ if (this._matchConditions(row, conditions))
105
+ {
106
+ for (const [k, v] of Object.entries(data))
107
+ {
108
+ row[k] = v instanceof Date ? v.toISOString() : v;
109
+ }
110
+ count++;
111
+ }
112
+ }
113
+ return count;
114
+ }
115
+
116
+ /**
117
+ * Remove a row by primary key.
118
+ * @param {string} table
119
+ * @param {string} pk
120
+ * @param {*} pkVal
121
+ */
122
+ async remove(table, pk, pkVal)
123
+ {
124
+ const rows = this._getTable(table);
125
+ const idx = rows.findIndex(r => r[pk] === pkVal);
126
+ if (idx !== -1) rows.splice(idx, 1);
127
+ }
128
+
129
+ /**
130
+ * Delete all rows matching conditions.
131
+ * @param {string} table
132
+ * @param {object} conditions
133
+ * @returns {Promise<number>}
134
+ */
135
+ async deleteWhere(table, conditions)
136
+ {
137
+ const rows = this._getTable(table);
138
+ let count = 0;
139
+ for (let i = rows.length - 1; i >= 0; i--)
140
+ {
141
+ if (this._matchConditions(rows[i], conditions))
142
+ {
143
+ rows.splice(i, 1);
144
+ count++;
145
+ }
146
+ }
147
+ return count;
148
+ }
149
+
150
+ /**
151
+ * Execute a query descriptor (from the Query builder).
152
+ * @param {object} descriptor - Abstract query descriptor.
153
+ * @returns {Promise<Array|number>}
154
+ */
155
+ async execute(descriptor)
156
+ {
157
+ const { action, table, fields, where, orderBy, limit, offset, distinct, includeDeleted } = descriptor;
158
+ let rows = [...this._getTable(table)];
159
+
160
+ // Apply WHERE filters
161
+ if (where && where.length > 0)
162
+ {
163
+ rows = rows.filter(row => this._applyWhereChain(row, where));
164
+ }
165
+
166
+ // Count action
167
+ if (action === 'count') return rows.length;
168
+
169
+ // ORDER BY
170
+ if (orderBy && orderBy.length > 0)
171
+ {
172
+ rows.sort((a, b) =>
173
+ {
174
+ for (const { field, dir } of orderBy)
175
+ {
176
+ const av = a[field], bv = b[field];
177
+ if (av < bv) return dir === 'ASC' ? -1 : 1;
178
+ if (av > bv) return dir === 'ASC' ? 1 : -1;
179
+ }
180
+ return 0;
181
+ });
182
+ }
183
+
184
+ // OFFSET
185
+ if (offset) rows = rows.slice(offset);
186
+
187
+ // LIMIT
188
+ if (limit) rows = rows.slice(0, limit);
189
+
190
+ // SELECT specific fields
191
+ if (fields && fields.length > 0)
192
+ {
193
+ rows = rows.map(row =>
194
+ {
195
+ const filtered = {};
196
+ for (const f of fields) filtered[f] = row[f];
197
+ return filtered;
198
+ });
199
+ }
200
+
201
+ // DISTINCT
202
+ if (distinct)
203
+ {
204
+ const seen = new Set();
205
+ rows = rows.filter(row =>
206
+ {
207
+ const key = JSON.stringify(row);
208
+ if (seen.has(key)) return false;
209
+ seen.add(key);
210
+ return true;
211
+ });
212
+ }
213
+
214
+ return rows;
215
+ }
216
+
217
+ /**
218
+ * Clear all data (for testing).
219
+ */
220
+ async clear()
221
+ {
222
+ for (const key of this._tables.keys())
223
+ {
224
+ this._tables.set(key, []);
225
+ this._autoIncrements.set(key, 1);
226
+ }
227
+ }
228
+
229
+ // -- Internal Helpers -------------------------------
230
+
231
+ /** @private Get or create table array. */
232
+ _getTable(table)
233
+ {
234
+ if (!this._tables.has(table)) this._tables.set(table, []);
235
+ return this._tables.get(table);
236
+ }
237
+
238
+ /** @private Match simple object conditions { key: value }. */
239
+ _matchConditions(row, conditions)
240
+ {
241
+ if (!conditions || typeof conditions !== 'object') return true;
242
+ for (const [k, v] of Object.entries(conditions))
243
+ {
244
+ if (row[k] !== v) return false;
245
+ }
246
+ return true;
247
+ }
248
+
249
+ /** @private Apply the where chain from query builder. */
250
+ _applyWhereChain(row, where)
251
+ {
252
+ let result = true;
253
+ for (let i = 0; i < where.length; i++)
254
+ {
255
+ const clause = where[i];
256
+ // Skip raw SQL clauses — not supported in memory adapter
257
+ if (clause.raw) continue;
258
+ const matches = this._matchClause(row, clause);
259
+
260
+ if (i === 0 || clause.logic === 'AND')
261
+ {
262
+ result = i === 0 ? matches : (result && matches);
263
+ }
264
+ else if (clause.logic === 'OR')
265
+ {
266
+ result = result || matches;
267
+ }
268
+ }
269
+ return result;
270
+ }
271
+
272
+ /** @private Match a single WHERE clause. */
273
+ _matchClause(row, clause)
274
+ {
275
+ const val = row[clause.field];
276
+ const { op, value } = clause;
277
+
278
+ switch (op)
279
+ {
280
+ case '=': return val === value;
281
+ case '!=':
282
+ case '<>': return val !== value;
283
+ case '>': return val > value;
284
+ case '<': return val < value;
285
+ case '>=': return val >= value;
286
+ case '<=': return val <= value;
287
+ case 'LIKE':
288
+ {
289
+ // Simple LIKE: % = any, _ = single char
290
+ const pattern = String(value).replace(/%/g, '.*').replace(/_/g, '.');
291
+ return new RegExp('^' + pattern + '$', 'i').test(String(val));
292
+ }
293
+ case 'IN': return Array.isArray(value) && value.includes(val);
294
+ case 'NOT IN': return Array.isArray(value) && !value.includes(val);
295
+ case 'BETWEEN': return Array.isArray(value) && val >= value[0] && val <= value[1];
296
+ case 'NOT BETWEEN': return Array.isArray(value) && (val < value[0] || val > value[1]);
297
+ case 'IS NULL': return val === null || val === undefined;
298
+ case 'IS NOT NULL': return val !== null && val !== undefined;
299
+ default: return val === value;
300
+ }
301
+ }
302
+
303
+ // -- Memory Adapter Utilities ------------------------
304
+
305
+ /**
306
+ * List all registered table names.
307
+ * @returns {string[]}
308
+ */
309
+ tables()
310
+ {
311
+ return [...this._tables.keys()];
312
+ }
313
+
314
+ /**
315
+ * Get the total number of rows across all tables.
316
+ * @returns {number}
317
+ */
318
+ totalRows()
319
+ {
320
+ let total = 0;
321
+ for (const rows of this._tables.values()) total += rows.length;
322
+ return total;
323
+ }
324
+
325
+ /**
326
+ * Get memory usage stats.
327
+ * @returns {{ tables: number, totalRows: number, estimatedBytes: number }}
328
+ */
329
+ stats()
330
+ {
331
+ const tables = this._tables.size;
332
+ let totalRows = 0;
333
+ let estimatedBytes = 0;
334
+ for (const rows of this._tables.values())
335
+ {
336
+ totalRows += rows.length;
337
+ estimatedBytes += JSON.stringify(rows).length * 2; // rough UTF-16 estimate
338
+ }
339
+ return { tables, totalRows, estimatedBytes };
340
+ }
341
+
342
+ /**
343
+ * Export all data as a plain object.
344
+ * @returns {object} { tableName: rows[], ... }
345
+ */
346
+ toJSON()
347
+ {
348
+ const out = {};
349
+ for (const [table, rows] of this._tables) out[table] = [...rows];
350
+ return out;
351
+ }
352
+
353
+ /**
354
+ * Import data from a plain object, merging with existing data.
355
+ * @param {object} data - { tableName: rows[], ... }
356
+ */
357
+ fromJSON(data)
358
+ {
359
+ for (const [table, rows] of Object.entries(data))
360
+ {
361
+ if (!this._tables.has(table)) this._tables.set(table, []);
362
+ const existing = this._tables.get(table);
363
+ for (const row of rows) existing.push({ ...row });
364
+ // Update auto-increment
365
+ const maxId = rows.reduce((max, r) => Math.max(max, r.id || 0), 0);
366
+ const currentAi = this._autoIncrements.get(table) || 1;
367
+ if (maxId >= currentAi) this._autoIncrements.set(table, maxId + 1);
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Clone the entire database state (deep copy).
373
+ * @returns {MemoryAdapter}
374
+ */
375
+ clone()
376
+ {
377
+ const copy = new MemoryAdapter();
378
+ copy.fromJSON(this.toJSON());
379
+ return copy;
380
+ }
381
+ }
382
+
383
+ module.exports = MemoryAdapter;