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.
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 +25 -184
  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,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;
@@ -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 };