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
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module orm/adapters/sqlite
|
|
3
|
+
* @description SQLite adapter using the optional `better-sqlite3` driver.
|
|
4
|
+
* Requires: `npm install better-sqlite3`
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const db = Database.connect('sqlite', { filename: './data.db' });
|
|
8
|
+
* // or ':memory:' for in-memory database
|
|
9
|
+
*/
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const BaseSqlAdapter = require('./sql-base');
|
|
13
|
+
|
|
14
|
+
class SqliteAdapter extends BaseSqlAdapter
|
|
15
|
+
{
|
|
16
|
+
/**
|
|
17
|
+
* @param {object} options
|
|
18
|
+
* @param {string} [options.filename=':memory:'] - Path to SQLite file, or ':memory:'.
|
|
19
|
+
* @param {boolean} [options.readonly=false] - Open database in read-only mode.
|
|
20
|
+
* @param {boolean} [options.fileMustExist=false] - Throw if the database file does not exist.
|
|
21
|
+
* @param {boolean} [options.verbose] - Log every SQL statement (debug).
|
|
22
|
+
* @param {boolean} [options.createDir=true] - Automatically create parent directories for the file.
|
|
23
|
+
* @param {object} [options.pragmas] - PRAGMA settings to apply on open.
|
|
24
|
+
* @param {string} [options.pragmas.journal_mode='WAL'] - Journal mode (WAL, DELETE, TRUNCATE, MEMORY, OFF).
|
|
25
|
+
* @param {string} [options.pragmas.foreign_keys='ON'] - Enforce foreign-key constraints.
|
|
26
|
+
* @param {string} [options.pragmas.busy_timeout='5000'] - Milliseconds to wait on a locked database.
|
|
27
|
+
* @param {string} [options.pragmas.synchronous='NORMAL'] - Sync mode (OFF, NORMAL, FULL, EXTRA).
|
|
28
|
+
* @param {string} [options.pragmas.cache_size='-64000'] - Page cache size (negative = KiB, e.g. -64000 = 64 MB).
|
|
29
|
+
* @param {string} [options.pragmas.temp_store='MEMORY'] - Temp tables in memory for speed.
|
|
30
|
+
* @param {string} [options.pragmas.mmap_size='268435456'] - Memory-mapped I/O size (256 MB).
|
|
31
|
+
* @param {string} [options.pragmas.page_size] - Page size in bytes (must be set before WAL).
|
|
32
|
+
* @param {string} [options.pragmas.auto_vacuum] - Auto-vacuum mode (NONE, FULL, INCREMENTAL).
|
|
33
|
+
* @param {string} [options.pragmas.secure_delete] - Overwrite deleted content with zeros.
|
|
34
|
+
* @param {string} [options.pragmas.wal_autocheckpoint] - Pages before auto-checkpoint (default 1000).
|
|
35
|
+
* @param {string} [options.pragmas.locking_mode] - NORMAL or EXCLUSIVE.
|
|
36
|
+
*/
|
|
37
|
+
constructor(options = {})
|
|
38
|
+
{
|
|
39
|
+
super();
|
|
40
|
+
let Database;
|
|
41
|
+
try { Database = require('better-sqlite3'); }
|
|
42
|
+
catch (e)
|
|
43
|
+
{
|
|
44
|
+
throw new Error(
|
|
45
|
+
'SQLite adapter requires "better-sqlite3" package.\n' +
|
|
46
|
+
'Install it with: npm install better-sqlite3'
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const filename = options.filename || ':memory:';
|
|
51
|
+
|
|
52
|
+
// Auto-create parent directories for file-based databases
|
|
53
|
+
if (filename !== ':memory:' && options.createDir !== false)
|
|
54
|
+
{
|
|
55
|
+
const dir = path.dirname(path.resolve(filename));
|
|
56
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Build better-sqlite3 constructor options
|
|
60
|
+
const dbOpts = {};
|
|
61
|
+
if (options.readonly) dbOpts.readonly = true;
|
|
62
|
+
if (options.fileMustExist) dbOpts.fileMustExist = true;
|
|
63
|
+
if (options.verbose) dbOpts.verbose = console.log;
|
|
64
|
+
|
|
65
|
+
this._db = new Database(filename, dbOpts);
|
|
66
|
+
this._filename = filename;
|
|
67
|
+
|
|
68
|
+
// Apply pragmas (with production-ready defaults)
|
|
69
|
+
const pragmas = {
|
|
70
|
+
journal_mode: 'WAL',
|
|
71
|
+
foreign_keys: 'ON',
|
|
72
|
+
busy_timeout: '5000',
|
|
73
|
+
synchronous: 'NORMAL',
|
|
74
|
+
cache_size: '-64000',
|
|
75
|
+
temp_store: 'MEMORY',
|
|
76
|
+
mmap_size: '268435456',
|
|
77
|
+
...options.pragmas,
|
|
78
|
+
};
|
|
79
|
+
for (const [key, val] of Object.entries(pragmas))
|
|
80
|
+
this._db.pragma(`${key} = ${val}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** @override */
|
|
84
|
+
_typeMap(colDef)
|
|
85
|
+
{
|
|
86
|
+
const map = {
|
|
87
|
+
string: 'TEXT', text: 'TEXT', integer: 'INTEGER', float: 'REAL',
|
|
88
|
+
boolean: 'INTEGER', date: 'TEXT', datetime: 'TEXT',
|
|
89
|
+
json: 'TEXT', blob: 'BLOB', uuid: 'TEXT',
|
|
90
|
+
};
|
|
91
|
+
return map[colDef.type] || 'TEXT';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** @override */
|
|
95
|
+
async createTable(table, schema)
|
|
96
|
+
{
|
|
97
|
+
const cols = [];
|
|
98
|
+
for (const [name, def] of Object.entries(schema))
|
|
99
|
+
{
|
|
100
|
+
let line = `"${name}" ${this._typeMap(def)}`;
|
|
101
|
+
if (def.primaryKey) line += ' PRIMARY KEY';
|
|
102
|
+
if (def.autoIncrement) line += ' AUTOINCREMENT';
|
|
103
|
+
if (def.required && !def.primaryKey) line += ' NOT NULL';
|
|
104
|
+
if (def.unique) line += ' UNIQUE';
|
|
105
|
+
if (def.default !== undefined && typeof def.default !== 'function')
|
|
106
|
+
{
|
|
107
|
+
line += ` DEFAULT ${this._sqlDefault(def.default)}`;
|
|
108
|
+
}
|
|
109
|
+
cols.push(line);
|
|
110
|
+
}
|
|
111
|
+
this._db.exec(`CREATE TABLE IF NOT EXISTS "${table}" (${cols.join(', ')})`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** @override */
|
|
115
|
+
async dropTable(table)
|
|
116
|
+
{
|
|
117
|
+
this._db.exec(`DROP TABLE IF EXISTS "${table}"`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** @override */
|
|
121
|
+
async insert(table, data)
|
|
122
|
+
{
|
|
123
|
+
const keys = Object.keys(data);
|
|
124
|
+
const placeholders = keys.map(() => '?').join(', ');
|
|
125
|
+
const values = keys.map(k => this._toSqlValue(data[k]));
|
|
126
|
+
const stmt = this._db.prepare(`INSERT INTO "${table}" (${keys.map(k => `"${k}"`).join(', ')}) VALUES (${placeholders})`);
|
|
127
|
+
const result = stmt.run(...values);
|
|
128
|
+
return { ...data, id: result.lastInsertRowid };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** @override */
|
|
132
|
+
async update(table, pk, pkVal, data)
|
|
133
|
+
{
|
|
134
|
+
const sets = Object.keys(data).map(k => `"${k}" = ?`).join(', ');
|
|
135
|
+
const values = [...Object.values(data).map(v => this._toSqlValue(v)), pkVal];
|
|
136
|
+
this._db.prepare(`UPDATE "${table}" SET ${sets} WHERE "${pk}" = ?`).run(...values);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** @override */
|
|
140
|
+
async updateWhere(table, conditions, data)
|
|
141
|
+
{
|
|
142
|
+
const { clause, values: whereVals } = this._buildWhere(conditions);
|
|
143
|
+
const sets = Object.keys(data).map(k => `"${k}" = ?`).join(', ');
|
|
144
|
+
const values = [...Object.values(data).map(v => this._toSqlValue(v)), ...whereVals];
|
|
145
|
+
const result = this._db.prepare(`UPDATE "${table}" SET ${sets}${clause}`).run(...values);
|
|
146
|
+
return result.changes;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** @override */
|
|
150
|
+
async remove(table, pk, pkVal)
|
|
151
|
+
{
|
|
152
|
+
this._db.prepare(`DELETE FROM "${table}" WHERE "${pk}" = ?`).run(pkVal);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** @override */
|
|
156
|
+
async deleteWhere(table, conditions)
|
|
157
|
+
{
|
|
158
|
+
const { clause, values } = this._buildWhere(conditions);
|
|
159
|
+
const result = this._db.prepare(`DELETE FROM "${table}"${clause}`).run(...values);
|
|
160
|
+
return result.changes;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** @override */
|
|
164
|
+
async execute(descriptor)
|
|
165
|
+
{
|
|
166
|
+
const { action, table, fields, where, orderBy, limit, offset, distinct } = descriptor;
|
|
167
|
+
|
|
168
|
+
if (action === 'count')
|
|
169
|
+
{
|
|
170
|
+
const { clause, values } = this._buildWhereFromChain(where);
|
|
171
|
+
const row = this._db.prepare(`SELECT COUNT(*) as count FROM "${table}"${clause}`).get(...values);
|
|
172
|
+
return row.count;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const selectFields = fields && fields.length
|
|
176
|
+
? fields.map(f => `"${f}"`).join(', ')
|
|
177
|
+
: '*';
|
|
178
|
+
const distinctStr = distinct ? 'DISTINCT ' : '';
|
|
179
|
+
let sql = `SELECT ${distinctStr}${selectFields} FROM "${table}"`;
|
|
180
|
+
|
|
181
|
+
const values = [];
|
|
182
|
+
if (where && where.length > 0)
|
|
183
|
+
{
|
|
184
|
+
const { clause, values: wv } = this._buildWhereFromChain(where);
|
|
185
|
+
sql += clause;
|
|
186
|
+
values.push(...wv);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (orderBy && orderBy.length > 0)
|
|
190
|
+
{
|
|
191
|
+
sql += ' ORDER BY ' + orderBy.map(o => `"${o.field}" ${o.dir}`).join(', ');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (limit !== null && limit !== undefined)
|
|
195
|
+
{
|
|
196
|
+
sql += ' LIMIT ?';
|
|
197
|
+
values.push(limit);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (offset !== null && offset !== undefined)
|
|
201
|
+
{
|
|
202
|
+
sql += ' OFFSET ?';
|
|
203
|
+
values.push(offset);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return this._db.prepare(sql).all(...values);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// -- SQLite Utilities -----------------------------------------------
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Read a single PRAGMA value.
|
|
213
|
+
* @param {string} key - PRAGMA name (e.g. 'journal_mode').
|
|
214
|
+
* @returns {*} Current value.
|
|
215
|
+
*/
|
|
216
|
+
pragma(key)
|
|
217
|
+
{
|
|
218
|
+
const rows = this._db.pragma(key);
|
|
219
|
+
if (Array.isArray(rows) && rows.length === 1) return Object.values(rows[0])[0];
|
|
220
|
+
return rows;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Force a WAL checkpoint (only useful in WAL mode).
|
|
225
|
+
* @param {'PASSIVE'|'FULL'|'RESTART'|'TRUNCATE'} [mode='PASSIVE']
|
|
226
|
+
* @returns {{ busy: number, log: number, checkpointed: number }}
|
|
227
|
+
*/
|
|
228
|
+
checkpoint(mode = 'PASSIVE')
|
|
229
|
+
{
|
|
230
|
+
const allowed = ['PASSIVE', 'FULL', 'RESTART', 'TRUNCATE'];
|
|
231
|
+
const m = String(mode).toUpperCase();
|
|
232
|
+
if (!allowed.includes(m)) throw new Error(`Invalid checkpoint mode: ${mode}`);
|
|
233
|
+
const row = this._db.pragma(`wal_checkpoint(${m})`);
|
|
234
|
+
return Array.isArray(row) ? row[0] : row;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Run `PRAGMA integrity_check`.
|
|
239
|
+
* @returns {string} 'ok' if healthy, or a description of the problem.
|
|
240
|
+
*/
|
|
241
|
+
integrity()
|
|
242
|
+
{
|
|
243
|
+
const rows = this._db.pragma('integrity_check');
|
|
244
|
+
const val = Array.isArray(rows) ? rows[0] : rows;
|
|
245
|
+
return (val && typeof val === 'object') ? Object.values(val)[0] : val;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Rebuild the database file, reclaiming free pages.
|
|
250
|
+
*/
|
|
251
|
+
vacuum()
|
|
252
|
+
{
|
|
253
|
+
this._db.exec('VACUUM');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get the size of the database file in bytes.
|
|
258
|
+
* Returns 0 for in-memory databases.
|
|
259
|
+
* @returns {number}
|
|
260
|
+
*/
|
|
261
|
+
fileSize()
|
|
262
|
+
{
|
|
263
|
+
if (this._filename === ':memory:') return 0;
|
|
264
|
+
try { return fs.statSync(path.resolve(this._filename)).size; }
|
|
265
|
+
catch { return 0; }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* List all user-created tables.
|
|
270
|
+
* @returns {string[]}
|
|
271
|
+
*/
|
|
272
|
+
tables()
|
|
273
|
+
{
|
|
274
|
+
const rows = this._db.prepare(
|
|
275
|
+
"SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'"
|
|
276
|
+
).all();
|
|
277
|
+
return rows.map(r => r.name);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Close the database connection.
|
|
282
|
+
*/
|
|
283
|
+
close()
|
|
284
|
+
{
|
|
285
|
+
this._db.close();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Run a raw SQL query.
|
|
290
|
+
* @param {string} sql
|
|
291
|
+
* @param {...*} params
|
|
292
|
+
* @returns {*}
|
|
293
|
+
*/
|
|
294
|
+
raw(sql, ...params)
|
|
295
|
+
{
|
|
296
|
+
const stmt = this._db.prepare(sql);
|
|
297
|
+
return stmt.all(...params);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Begin a transaction.
|
|
302
|
+
* @param {Function} fn - Function to run inside the transaction.
|
|
303
|
+
* @returns {*} Return value of fn.
|
|
304
|
+
*/
|
|
305
|
+
transaction(fn)
|
|
306
|
+
{
|
|
307
|
+
return this._db.transaction(fn)();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
module.exports = SqliteAdapter;
|
package/lib/orm/index.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module orm
|
|
3
|
+
* @description ORM entry point. Provides the `Database` factory that creates
|
|
4
|
+
* a connection to a backing store, the base `Model` class, the
|
|
5
|
+
* `TYPES` enum, and schema helpers.
|
|
6
|
+
*
|
|
7
|
+
* Supported adapters (all optional "bring your own driver"):
|
|
8
|
+
* - `memory` — in-process (no driver needed)
|
|
9
|
+
* - `json` — JSON file persistence (no driver needed)
|
|
10
|
+
* - `sqlite` — requires `better-sqlite3`
|
|
11
|
+
* - `mysql` — requires `mysql2`
|
|
12
|
+
* - `postgres` — requires `pg`
|
|
13
|
+
* - `mongo` — requires `mongodb`
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const { Database, Model, TYPES } = require('zero-http');
|
|
17
|
+
*
|
|
18
|
+
* const db = Database.connect('memory');
|
|
19
|
+
*
|
|
20
|
+
* class User extends Model {
|
|
21
|
+
* static table = 'users';
|
|
22
|
+
* static schema = {
|
|
23
|
+
* id: { type: TYPES.INTEGER, primaryKey: true, autoIncrement: true },
|
|
24
|
+
* name: { type: TYPES.STRING, required: true },
|
|
25
|
+
* email: { type: TYPES.STRING, required: true, unique: true },
|
|
26
|
+
* };
|
|
27
|
+
* static timestamps = true;
|
|
28
|
+
* }
|
|
29
|
+
*
|
|
30
|
+
* db.register(User);
|
|
31
|
+
* await db.sync();
|
|
32
|
+
*
|
|
33
|
+
* const user = await User.create({ name: 'Alice', email: 'a@b.com' });
|
|
34
|
+
*/
|
|
35
|
+
const Model = require('./model');
|
|
36
|
+
const { TYPES, validate, validateValue } = require('./schema');
|
|
37
|
+
const Query = require('./query');
|
|
38
|
+
|
|
39
|
+
// -- Adapter loaders (lazy) ------------------------------
|
|
40
|
+
|
|
41
|
+
const ADAPTERS = {
|
|
42
|
+
memory: () => require('./adapters/memory'),
|
|
43
|
+
json: () => require('./adapters/json'),
|
|
44
|
+
sqlite: () => require('./adapters/sqlite'),
|
|
45
|
+
mysql: () => require('./adapters/mysql'),
|
|
46
|
+
postgres: () => require('./adapters/postgres'),
|
|
47
|
+
mongo: () => require('./adapters/mongo'),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// -- Database class --------------------------------------
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Validate adapter connection options and sanitize credentials.
|
|
54
|
+
* @param {string} type
|
|
55
|
+
* @param {object} options
|
|
56
|
+
* @returns {object} sanitised options
|
|
57
|
+
*/
|
|
58
|
+
function _validateOptions(type, options)
|
|
59
|
+
{
|
|
60
|
+
const opts = { ...options };
|
|
61
|
+
|
|
62
|
+
// Adapters that take network credentials
|
|
63
|
+
if (type === 'mysql' || type === 'postgres')
|
|
64
|
+
{
|
|
65
|
+
if (opts.host !== undefined)
|
|
66
|
+
{
|
|
67
|
+
if (typeof opts.host !== 'string' || !opts.host.trim())
|
|
68
|
+
throw new Error(`${type}: "host" must be a non-empty string`);
|
|
69
|
+
opts.host = opts.host.trim();
|
|
70
|
+
}
|
|
71
|
+
if (opts.port !== undefined)
|
|
72
|
+
{
|
|
73
|
+
opts.port = Number(opts.port);
|
|
74
|
+
if (!Number.isInteger(opts.port) || opts.port < 1 || opts.port > 65535)
|
|
75
|
+
throw new Error(`${type}: "port" must be an integer 1-65535`);
|
|
76
|
+
}
|
|
77
|
+
if (opts.user !== undefined)
|
|
78
|
+
{
|
|
79
|
+
if (typeof opts.user !== 'string')
|
|
80
|
+
throw new Error(`${type}: "user" must be a string`);
|
|
81
|
+
}
|
|
82
|
+
if (opts.password !== undefined)
|
|
83
|
+
{
|
|
84
|
+
if (typeof opts.password !== 'string')
|
|
85
|
+
throw new Error(`${type}: "password" must be a string`);
|
|
86
|
+
}
|
|
87
|
+
if (opts.database !== undefined)
|
|
88
|
+
{
|
|
89
|
+
if (typeof opts.database !== 'string' || !opts.database.trim())
|
|
90
|
+
throw new Error(`${type}: "database" must be a non-empty string`);
|
|
91
|
+
opts.database = opts.database.trim();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Mongo connection string
|
|
96
|
+
if (type === 'mongo')
|
|
97
|
+
{
|
|
98
|
+
if (opts.url !== undefined)
|
|
99
|
+
{
|
|
100
|
+
if (typeof opts.url !== 'string' || !opts.url.trim())
|
|
101
|
+
throw new Error('mongo: "url" must be a non-empty string');
|
|
102
|
+
opts.url = opts.url.trim();
|
|
103
|
+
}
|
|
104
|
+
if (opts.database !== undefined)
|
|
105
|
+
{
|
|
106
|
+
if (typeof opts.database !== 'string' || !opts.database.trim())
|
|
107
|
+
throw new Error('mongo: "database" must be a non-empty string');
|
|
108
|
+
opts.database = opts.database.trim();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// SQLite validation
|
|
113
|
+
if (type === 'sqlite')
|
|
114
|
+
{
|
|
115
|
+
if (opts.filename !== undefined && typeof opts.filename !== 'string')
|
|
116
|
+
throw new Error('sqlite: "filename" must be a string');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return opts;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
class Database
|
|
123
|
+
{
|
|
124
|
+
/**
|
|
125
|
+
* @param {object} adapter - Instantiated adapter.
|
|
126
|
+
*/
|
|
127
|
+
constructor(adapter)
|
|
128
|
+
{
|
|
129
|
+
/** @type {object} The underlying adapter instance. */
|
|
130
|
+
this.adapter = adapter;
|
|
131
|
+
|
|
132
|
+
/** @type {Map<string, typeof Model>} Registered model classes. */
|
|
133
|
+
this._models = new Map();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Create a Database connection.
|
|
138
|
+
*
|
|
139
|
+
* @param {string} type - Adapter type: memory, json, sqlite, mysql, postgres, mongo.
|
|
140
|
+
* @param {object} [options] - Adapter-specific options.
|
|
141
|
+
* @returns {Database}
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* const db = Database.connect('sqlite', { filename: './app.db' });
|
|
145
|
+
* const db = Database.connect('mysql', { host: '127.0.0.1', user: 'root', database: 'app' });
|
|
146
|
+
* const db = Database.connect('postgres', { host: '127.0.0.1', user: 'postgres', database: 'app' });
|
|
147
|
+
* const db = Database.connect('mongo', { url: 'mongodb://localhost:27017', database: 'app' });
|
|
148
|
+
* const db = Database.connect('json', { directory: './data' });
|
|
149
|
+
* const db = Database.connect('memory');
|
|
150
|
+
*/
|
|
151
|
+
static connect(type, options = {})
|
|
152
|
+
{
|
|
153
|
+
const loader = ADAPTERS[type];
|
|
154
|
+
if (!loader) throw new Error(`Unknown adapter "${type}". Supported: ${Object.keys(ADAPTERS).join(', ')}`);
|
|
155
|
+
|
|
156
|
+
const opts = _validateOptions(type, options);
|
|
157
|
+
const AdapterClass = loader();
|
|
158
|
+
const adapter = new AdapterClass(opts);
|
|
159
|
+
return new Database(adapter);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Register a Model class with this database.
|
|
164
|
+
* Binds the adapter to the model so all CRUD operations go through it.
|
|
165
|
+
*
|
|
166
|
+
* @param {typeof Model} ModelClass
|
|
167
|
+
* @returns {Database} this (for chaining)
|
|
168
|
+
*/
|
|
169
|
+
register(ModelClass)
|
|
170
|
+
{
|
|
171
|
+
ModelClass._adapter = this.adapter;
|
|
172
|
+
this._models.set(ModelClass.table || ModelClass.name, ModelClass);
|
|
173
|
+
return this;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Register multiple Model classes at once.
|
|
178
|
+
*
|
|
179
|
+
* @param {...typeof Model} models
|
|
180
|
+
* @returns {Database}
|
|
181
|
+
*/
|
|
182
|
+
registerAll(...models)
|
|
183
|
+
{
|
|
184
|
+
for (const m of models) this.register(m);
|
|
185
|
+
return this;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Synchronise all registered models — create tables if they don't exist.
|
|
190
|
+
* @returns {Promise<void>}
|
|
191
|
+
*/
|
|
192
|
+
async sync()
|
|
193
|
+
{
|
|
194
|
+
for (const ModelClass of this._models.values())
|
|
195
|
+
{
|
|
196
|
+
await ModelClass.sync();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Drop all registered model tables (in reverse order to respect FK deps).
|
|
202
|
+
* @returns {Promise<void>}
|
|
203
|
+
*/
|
|
204
|
+
async drop()
|
|
205
|
+
{
|
|
206
|
+
const models = [...this._models.values()].reverse();
|
|
207
|
+
for (const ModelClass of models)
|
|
208
|
+
{
|
|
209
|
+
await ModelClass.drop();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Close the underlying connection / pool.
|
|
215
|
+
* @returns {Promise<void>|void}
|
|
216
|
+
*/
|
|
217
|
+
async close()
|
|
218
|
+
{
|
|
219
|
+
if (typeof this.adapter.close === 'function')
|
|
220
|
+
{
|
|
221
|
+
await this.adapter.close();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Get a registered model by table name.
|
|
227
|
+
* @param {string} name
|
|
228
|
+
* @returns {typeof Model|undefined}
|
|
229
|
+
*/
|
|
230
|
+
model(name)
|
|
231
|
+
{
|
|
232
|
+
return this._models.get(name);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Execute a callback within a database transaction.
|
|
237
|
+
* If the callback throws, the transaction is rolled back.
|
|
238
|
+
* If the callback returns normally, the transaction is committed.
|
|
239
|
+
*
|
|
240
|
+
* Note: Transaction support depends on the adapter.
|
|
241
|
+
* Memory and JSON adapters run the callback directly (no real transaction).
|
|
242
|
+
*
|
|
243
|
+
* @param {Function} fn - Async callback to execute within the transaction.
|
|
244
|
+
* @returns {Promise<*>} The return value of the callback.
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* await db.transaction(async () => {
|
|
248
|
+
* await User.create({ name: 'Alice', email: 'a@b.com' });
|
|
249
|
+
* await Account.create({ userId: 1, balance: 100 });
|
|
250
|
+
* });
|
|
251
|
+
*/
|
|
252
|
+
async transaction(fn)
|
|
253
|
+
{
|
|
254
|
+
if (typeof this.adapter.beginTransaction === 'function')
|
|
255
|
+
{
|
|
256
|
+
await this.adapter.beginTransaction();
|
|
257
|
+
try
|
|
258
|
+
{
|
|
259
|
+
const result = await fn();
|
|
260
|
+
await this.adapter.commit();
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
catch (err)
|
|
264
|
+
{
|
|
265
|
+
await this.adapter.rollback();
|
|
266
|
+
throw err;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// Adapters without transaction support run directly
|
|
270
|
+
return fn();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// -- Exports ---------------------------------------------
|
|
275
|
+
|
|
276
|
+
module.exports = { Database, Model, TYPES, Query, validate, validateValue };
|