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,444 @@
1
+ /**
2
+ * @module orm/adapters/mongo
3
+ * @description MongoDB adapter using the optional `mongodb` driver.
4
+ * Requires: `npm install mongodb`
5
+ *
6
+ * @example
7
+ * const db = Database.connect('mongo', {
8
+ * url: 'mongodb://localhost:27017',
9
+ * database: 'myapp',
10
+ * });
11
+ */
12
+
13
+ class MongoAdapter
14
+ {
15
+ /**
16
+ * @param {object} options
17
+ * @param {string} [options.url='mongodb://127.0.0.1:27017'] - Connection string.
18
+ * @param {string} options.database - Database name.
19
+ * @param {number} [options.maxPoolSize=10] - Max connection pool size.
20
+ * @param {number} [options.minPoolSize=0] - Min connection pool size.
21
+ * @param {number} [options.connectTimeoutMS=10000] - Connection timeout.
22
+ * @param {number} [options.socketTimeoutMS=0] - Socket timeout (0 = no limit).
23
+ * @param {number} [options.serverSelectionTimeoutMS=30000] - Server selection timeout.
24
+ * @param {boolean} [options.retryWrites=true] - Retry writes on network errors.
25
+ * @param {boolean} [options.retryReads=true] - Retry reads on network errors.
26
+ * @param {string} [options.authSource] - Auth database name.
27
+ * @param {string} [options.replicaSet] - Replica set name.
28
+ * @param {object} [options.clientOptions] - Extra MongoClient options (passed directly).
29
+ */
30
+ constructor(options = {})
31
+ {
32
+ let mongodb;
33
+ try { mongodb = require('mongodb'); }
34
+ catch (e)
35
+ {
36
+ throw new Error(
37
+ 'MongoDB adapter requires "mongodb" package.\n' +
38
+ 'Install it with: npm install mongodb'
39
+ );
40
+ }
41
+
42
+ const url = options.url || 'mongodb://127.0.0.1:27017';
43
+ this._client = new mongodb.MongoClient(url, {
44
+ maxPoolSize: options.maxPoolSize || 10,
45
+ ...options.clientOptions,
46
+ });
47
+ this._dbName = options.database;
48
+ this._db = null;
49
+ this._connected = false;
50
+ }
51
+
52
+ /**
53
+ * Ensure client is connected and return the database handle.
54
+ * @returns {Promise<import('mongodb').Db>}
55
+ * @private
56
+ */
57
+ async _getDb()
58
+ {
59
+ if (!this._connected)
60
+ {
61
+ await this._client.connect();
62
+ this._connected = true;
63
+ this._db = this._client.db(this._dbName);
64
+ }
65
+ return this._db;
66
+ }
67
+
68
+ /** @private */
69
+ _col(table) { return this._getDb().then(db => db.collection(table)); }
70
+
71
+ // -- DDL ---------------------------------------------
72
+
73
+ async createTable(table /*, schema */)
74
+ {
75
+ const db = await this._getDb();
76
+ const existing = await db.listCollections({ name: table }).toArray();
77
+ if (existing.length === 0) await db.createCollection(table);
78
+ }
79
+
80
+ async dropTable(table)
81
+ {
82
+ const db = await this._getDb();
83
+ try { await db.collection(table).drop(); } catch (e) { /* ignore if not exists */ }
84
+ }
85
+
86
+ // -- CRUD --------------------------------------------
87
+
88
+ async insert(table, data)
89
+ {
90
+ const col = await this._col(table);
91
+ // Handle auto-increment for numeric id columns
92
+ if (data.id === undefined || data.id === null)
93
+ {
94
+ const last = await col.find().sort({ id: -1 }).limit(1).toArray();
95
+ data.id = last.length > 0 ? (last[0].id || 0) + 1 : 1;
96
+ }
97
+ const doc = { ...data };
98
+ await col.insertOne(doc);
99
+ // Remove internal _id, return clean object
100
+ delete doc._id;
101
+ return doc;
102
+ }
103
+
104
+ async update(table, pk, pkVal, data)
105
+ {
106
+ const col = await this._col(table);
107
+ await col.updateOne({ [pk]: pkVal }, { $set: data });
108
+ }
109
+
110
+ async updateWhere(table, conditions, data)
111
+ {
112
+ const col = await this._col(table);
113
+ const filter = this._buildFilter(conditions);
114
+ const result = await col.updateMany(filter, { $set: data });
115
+ return result.modifiedCount;
116
+ }
117
+
118
+ async remove(table, pk, pkVal)
119
+ {
120
+ const col = await this._col(table);
121
+ await col.deleteOne({ [pk]: pkVal });
122
+ }
123
+
124
+ async deleteWhere(table, conditions)
125
+ {
126
+ const col = await this._col(table);
127
+ const filter = this._buildFilter(conditions);
128
+ const result = await col.deleteMany(filter);
129
+ return result.deletedCount;
130
+ }
131
+
132
+ // -- Query execution ---------------------------------
133
+
134
+ async execute(descriptor)
135
+ {
136
+ const { action, table, fields, where, orderBy, limit, offset, distinct } = descriptor;
137
+ const col = await this._col(table);
138
+
139
+ const filter = this._buildFilterFromChain(where);
140
+
141
+ if (action === 'count')
142
+ {
143
+ return col.countDocuments(filter);
144
+ }
145
+
146
+ // Projection
147
+ const projection = { _id: 0 };
148
+ if (fields && fields.length > 0)
149
+ {
150
+ for (const f of fields) projection[f] = 1;
151
+ }
152
+
153
+ let cursor = col.find(filter, { projection });
154
+
155
+ // Sort
156
+ if (orderBy && orderBy.length > 0)
157
+ {
158
+ const sort = {};
159
+ for (const o of orderBy) sort[o.field] = o.dir === 'desc' ? -1 : 1;
160
+ cursor = cursor.sort(sort);
161
+ }
162
+
163
+ if (offset) cursor = cursor.skip(offset);
164
+ if (limit) cursor = cursor.limit(limit);
165
+
166
+ let results = await cursor.toArray();
167
+
168
+ // Distinct — in-memory since MongoDB distinct() only returns values for a single field
169
+ if (distinct && fields && fields.length > 0)
170
+ {
171
+ const seen = new Set();
172
+ results = results.filter(row =>
173
+ {
174
+ const key = JSON.stringify(fields.map(f => row[f]));
175
+ if (seen.has(key)) return false;
176
+ seen.add(key);
177
+ return true;
178
+ });
179
+ }
180
+
181
+ return results;
182
+ }
183
+
184
+ // -- Filter builders ---------------------------------
185
+
186
+ /**
187
+ * Build a MongoDB filter from simple { key: value } conditions.
188
+ * @param {object} conditions
189
+ * @returns {object}
190
+ * @private
191
+ */
192
+ _buildFilter(conditions)
193
+ {
194
+ if (!conditions || Object.keys(conditions).length === 0) return {};
195
+ const filter = {};
196
+ for (const [k, v] of Object.entries(conditions))
197
+ {
198
+ filter[k] = v === null ? null : v;
199
+ }
200
+ return filter;
201
+ }
202
+
203
+ /**
204
+ * Build a MongoDB filter from Query builder where chain.
205
+ * @param {Array} where
206
+ * @returns {object}
207
+ * @private
208
+ */
209
+ _buildFilterFromChain(where)
210
+ {
211
+ if (!where || where.length === 0) return {};
212
+
213
+ const andParts = [];
214
+ let currentOr = [];
215
+
216
+ for (let i = 0; i < where.length; i++)
217
+ {
218
+ const w = where[i];
219
+ // Skip raw SQL clauses — not applicable to MongoDB
220
+ if (w.raw) continue;
221
+ const { field, op, value, logic } = w;
222
+ const clause = this._opToMongo(field, op, value);
223
+
224
+ if (i === 0 || logic === 'AND')
225
+ {
226
+ if (currentOr.length > 1)
227
+ {
228
+ andParts.push({ $or: currentOr });
229
+ currentOr = [];
230
+ }
231
+ else if (currentOr.length === 1)
232
+ {
233
+ andParts.push(currentOr[0]);
234
+ currentOr = [];
235
+ }
236
+ currentOr.push(clause);
237
+ }
238
+ else // OR
239
+ {
240
+ currentOr.push(clause);
241
+ }
242
+ }
243
+
244
+ // Flush remaining or group
245
+ if (currentOr.length > 1) andParts.push({ $or: currentOr });
246
+ else if (currentOr.length === 1) andParts.push(currentOr[0]);
247
+
248
+ if (andParts.length === 0) return {};
249
+ if (andParts.length === 1) return andParts[0];
250
+ return { $and: andParts };
251
+ }
252
+
253
+ /**
254
+ * Convert a single operator clause to a MongoDB filter expression.
255
+ * @private
256
+ */
257
+ _opToMongo(field, op, value)
258
+ {
259
+ switch (op)
260
+ {
261
+ case '=': return { [field]: value };
262
+ case '!=':
263
+ case '<>': return { [field]: { $ne: value } };
264
+ case '>': return { [field]: { $gt: value } };
265
+ case '<': return { [field]: { $lt: value } };
266
+ case '>=': return { [field]: { $gte: value } };
267
+ case '<=': return { [field]: { $lte: value } };
268
+ case 'IN': return { [field]: { $in: value } };
269
+ case 'NOT IN': return { [field]: { $nin: value } };
270
+ case 'BETWEEN': return { [field]: { $gte: value[0], $lte: value[1] } };
271
+ case 'IS NULL': return { [field]: null };
272
+ case 'IS NOT NULL': return { [field]: { $ne: null } };
273
+ case 'LIKE':
274
+ {
275
+ // Convert SQL LIKE to regex: % → .*, _ → .
276
+ const pattern = value
277
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
278
+ .replace(/%/g, '.*')
279
+ .replace(/_/g, '.');
280
+ return { [field]: { $regex: new RegExp(`^${pattern}$`, 'i') } };
281
+ }
282
+ default: return { [field]: value };
283
+ }
284
+ }
285
+
286
+ // -- Utility -----------------------------------------
287
+
288
+ async close() { await this._client.close(); this._connected = false; }
289
+
290
+ /**
291
+ * Run a raw MongoDB command.
292
+ * @param {object} command - MongoDB command document.
293
+ * @returns {Promise<*>}
294
+ */
295
+ async raw(command)
296
+ {
297
+ const db = await this._getDb();
298
+ return db.command(command);
299
+ }
300
+
301
+ /**
302
+ * Run multiple operations in a transaction (requires replica set).
303
+ * @param {Function} fn - Receives a session object.
304
+ * @returns {Promise<*>}
305
+ */
306
+ async transaction(fn)
307
+ {
308
+ const session = this._client.startSession();
309
+ try
310
+ {
311
+ session.startTransaction();
312
+ const result = await fn(session);
313
+ await session.commitTransaction();
314
+ return result;
315
+ }
316
+ catch (e)
317
+ {
318
+ await session.abortTransaction();
319
+ throw e;
320
+ }
321
+ finally
322
+ {
323
+ await session.endSession();
324
+ }
325
+ }
326
+
327
+ // -- MongoDB Utilities -------------------------------
328
+
329
+ /**
330
+ * List all collections in the database.
331
+ * @returns {Promise<string[]>}
332
+ */
333
+ async collections()
334
+ {
335
+ const db = await this._getDb();
336
+ const list = await db.listCollections().toArray();
337
+ return list.map(c => c.name);
338
+ }
339
+
340
+ /**
341
+ * Get database stats (document count, storage size, indexes, etc.).
342
+ * @returns {Promise<{ collections: number, objects: number, dataSize: number, storageSize: number, indexes: number, indexSize: number }>}
343
+ */
344
+ async stats()
345
+ {
346
+ const db = await this._getDb();
347
+ const s = await db.command({ dbStats: 1 });
348
+ return {
349
+ collections: s.collections || 0,
350
+ objects: s.objects || 0,
351
+ dataSize: s.dataSize || 0,
352
+ storageSize: s.storageSize || 0,
353
+ indexes: s.indexes || 0,
354
+ indexSize: s.indexSize || 0,
355
+ };
356
+ }
357
+
358
+ /**
359
+ * Get collection stats.
360
+ * @param {string} name - Collection name.
361
+ * @returns {Promise<{ count: number, size: number, avgObjSize: number, storageSize: number, nindexes: number }>}
362
+ */
363
+ async collectionStats(name)
364
+ {
365
+ const db = await this._getDb();
366
+ const s = await db.command({ collStats: name });
367
+ return {
368
+ count: s.count || 0,
369
+ size: s.size || 0,
370
+ avgObjSize: s.avgObjSize || 0,
371
+ storageSize: s.storageSize || 0,
372
+ nindexes: s.nindexes || 0,
373
+ };
374
+ }
375
+
376
+ /**
377
+ * Create an index on a collection.
378
+ * @param {string} collection - Collection name.
379
+ * @param {object} keys - Index specification, e.g. { email: 1 } or { name: 1, age: -1 }.
380
+ * @param {object} [options] - Index options (unique, sparse, expireAfterSeconds, etc.).
381
+ * @returns {Promise<string>} Index name.
382
+ */
383
+ async createIndex(collection, keys, options = {})
384
+ {
385
+ const col = await this._col(collection);
386
+ return col.createIndex(keys, options);
387
+ }
388
+
389
+ /**
390
+ * List indexes on a collection.
391
+ * @param {string} collection
392
+ * @returns {Promise<Array>}
393
+ */
394
+ async indexes(collection)
395
+ {
396
+ const col = await this._col(collection);
397
+ return col.indexes();
398
+ }
399
+
400
+ /**
401
+ * Drop an index from a collection.
402
+ * @param {string} collection
403
+ * @param {string} indexName
404
+ */
405
+ async dropIndex(collection, indexName)
406
+ {
407
+ const col = await this._col(collection);
408
+ return col.dropIndex(indexName);
409
+ }
410
+
411
+ /**
412
+ * Ping the MongoDB server.
413
+ * @returns {Promise<boolean>}
414
+ */
415
+ async ping()
416
+ {
417
+ try
418
+ {
419
+ const db = await this._getDb();
420
+ const result = await db.command({ ping: 1 });
421
+ return result.ok === 1;
422
+ }
423
+ catch { return false; }
424
+ }
425
+
426
+ /**
427
+ * Get MongoDB server version and build info.
428
+ * @returns {Promise<string>}
429
+ */
430
+ async version()
431
+ {
432
+ const db = await this._getDb();
433
+ const info = await db.command({ buildInfo: 1 });
434
+ return info.version;
435
+ }
436
+
437
+ /**
438
+ * Check if connected.
439
+ * @returns {boolean}
440
+ */
441
+ get isConnected() { return this._connected; }
442
+ }
443
+
444
+ module.exports = MongoAdapter;
@@ -0,0 +1,272 @@
1
+ /**
2
+ * @module orm/adapters/mysql
3
+ * @description MySQL / MariaDB adapter using the optional `mysql2` driver.
4
+ * Requires: `npm install mysql2`
5
+ *
6
+ * @example
7
+ * const db = Database.connect('mysql', {
8
+ * host: '127.0.0.1', user: 'root', password: '', database: 'myapp',
9
+ * });
10
+ */
11
+ const BaseSqlAdapter = require('./sql-base');
12
+
13
+ class MysqlAdapter extends BaseSqlAdapter
14
+ {
15
+ /**
16
+ * @param {object} options
17
+ * @param {string} [options.host='localhost'] - Server hostname.
18
+ * @param {number} [options.port=3306] - Server port.
19
+ * @param {string} [options.user='root'] - Database user.
20
+ * @param {string} [options.password=''] - Database password.
21
+ * @param {string} options.database - Database name.
22
+ * @param {number} [options.connectionLimit=10] - Max pool connections.
23
+ * @param {boolean} [options.waitForConnections=true] - Queue when pool is full.
24
+ * @param {number} [options.queueLimit=0] - Max queued requests (0 = unlimited).
25
+ * @param {number} [options.connectTimeout=10000] - Connection timeout in ms.
26
+ * @param {string} [options.charset='utf8mb4'] - Default character set.
27
+ * @param {string} [options.timezone='Z'] - Session timezone.
28
+ * @param {boolean} [options.multipleStatements=false] - Allow multi-statement queries.
29
+ * @param {boolean} [options.decimalNumbers=false] - Return DECIMAL as numbers instead of strings.
30
+ * @param {string} [options.ssl] - SSL profile or options object.
31
+ */
32
+ constructor(options = {})
33
+ {
34
+ super();
35
+ let mysql;
36
+ try { mysql = require('mysql2/promise'); }
37
+ catch (e)
38
+ {
39
+ throw new Error(
40
+ 'MySQL adapter requires "mysql2" package.\n' +
41
+ 'Install it with: npm install mysql2'
42
+ );
43
+ }
44
+ this._pool = mysql.createPool({
45
+ connectionLimit: 10,
46
+ waitForConnections: true,
47
+ ...options,
48
+ });
49
+ this._options = options;
50
+ }
51
+
52
+ _typeMap(colDef)
53
+ {
54
+ const map = {
55
+ string: `VARCHAR(${colDef.maxLength || 255})`, text: 'TEXT',
56
+ integer: 'INT', float: 'DOUBLE', boolean: 'TINYINT(1)',
57
+ date: 'DATE', datetime: 'DATETIME', json: 'JSON', blob: 'BLOB', uuid: 'CHAR(36)',
58
+ };
59
+ return map[colDef.type] || 'TEXT';
60
+ }
61
+
62
+ _q(name) { return '`' + name.replace(/`/g, '``') + '`'; }
63
+
64
+ async createTable(table, schema)
65
+ {
66
+ const cols = [];
67
+ for (const [name, def] of Object.entries(schema))
68
+ {
69
+ let line = `${this._q(name)} ${this._typeMap(def)}`;
70
+ if (def.primaryKey) line += ' PRIMARY KEY';
71
+ if (def.autoIncrement) line += ' AUTO_INCREMENT';
72
+ if (def.required && !def.primaryKey) line += ' NOT NULL';
73
+ if (def.unique) line += ' UNIQUE';
74
+ if (def.default !== undefined && typeof def.default !== 'function')
75
+ line += ` DEFAULT ${this._sqlDefault(def.default)}`;
76
+ cols.push(line);
77
+ }
78
+ await this._pool.execute(`CREATE TABLE IF NOT EXISTS ${this._q(table)} (${cols.join(', ')})`);
79
+ }
80
+
81
+ async dropTable(table)
82
+ {
83
+ await this._pool.execute(`DROP TABLE IF EXISTS ${this._q(table)}`);
84
+ }
85
+
86
+ async insert(table, data)
87
+ {
88
+ const keys = Object.keys(data);
89
+ const placeholders = keys.map(() => '?').join(', ');
90
+ const values = keys.map(k => this._toSqlValue(data[k]));
91
+ const [result] = await this._pool.execute(
92
+ `INSERT INTO ${this._q(table)} (${keys.map(k => this._q(k)).join(', ')}) VALUES (${placeholders})`,
93
+ values
94
+ );
95
+ return { ...data, id: result.insertId || data.id };
96
+ }
97
+
98
+ async update(table, pk, pkVal, data)
99
+ {
100
+ const sets = Object.keys(data).map(k => `${this._q(k)} = ?`).join(', ');
101
+ const values = [...Object.values(data).map(v => this._toSqlValue(v)), pkVal];
102
+ await this._pool.execute(`UPDATE ${this._q(table)} SET ${sets} WHERE ${this._q(pk)} = ?`, values);
103
+ }
104
+
105
+ async updateWhere(table, conditions, data)
106
+ {
107
+ const { clause, values: whereVals } = this._buildWhere(conditions);
108
+ const sets = Object.keys(data).map(k => `${this._q(k)} = ?`).join(', ');
109
+ const values = [...Object.values(data).map(v => this._toSqlValue(v)), ...whereVals];
110
+ const sql = `UPDATE ${this._q(table)} SET ${sets}${clause.replace(/"/g, '`')}`;
111
+ const [result] = await this._pool.execute(sql, values);
112
+ return result.affectedRows;
113
+ }
114
+
115
+ async remove(table, pk, pkVal)
116
+ {
117
+ await this._pool.execute(`DELETE FROM ${this._q(table)} WHERE ${this._q(pk)} = ?`, [pkVal]);
118
+ }
119
+
120
+ async deleteWhere(table, conditions)
121
+ {
122
+ const { clause, values } = this._buildWhere(conditions);
123
+ const sql = `DELETE FROM ${this._q(table)}${clause.replace(/"/g, '`')}`;
124
+ const [result] = await this._pool.execute(sql, values);
125
+ return result.affectedRows;
126
+ }
127
+
128
+ async execute(descriptor)
129
+ {
130
+ const { action, table, fields, where, orderBy, limit, offset, distinct } = descriptor;
131
+
132
+ if (action === 'count')
133
+ {
134
+ const { clause, values } = this._buildWhereFromChain(where);
135
+ const sql = `SELECT COUNT(*) as count FROM ${this._q(table)}${clause.replace(/"/g, '`')}`;
136
+ const [rows] = await this._pool.execute(sql, values);
137
+ return rows[0].count;
138
+ }
139
+
140
+ const selectFields = fields && fields.length ? fields.map(f => this._q(f)).join(', ') : '*';
141
+ const distinctStr = distinct ? 'DISTINCT ' : '';
142
+ let sql = `SELECT ${distinctStr}${selectFields} FROM ${this._q(table)}`;
143
+ const values = [];
144
+
145
+ if (where && where.length)
146
+ {
147
+ const { clause, values: wv } = this._buildWhereFromChain(where);
148
+ sql += clause.replace(/"/g, '`');
149
+ values.push(...wv);
150
+ }
151
+
152
+ if (orderBy && orderBy.length)
153
+ sql += ' ORDER BY ' + orderBy.map(o => `${this._q(o.field)} ${o.dir}`).join(', ');
154
+ if (limit !== null && limit !== undefined) { sql += ' LIMIT ?'; values.push(limit); }
155
+ if (offset !== null && offset !== undefined) { sql += ' OFFSET ?'; values.push(offset); }
156
+
157
+ const [rows] = await this._pool.execute(sql, values);
158
+ return rows;
159
+ }
160
+
161
+ async close() { await this._pool.end(); }
162
+
163
+ async raw(sql, ...params) { const [rows] = await this._pool.execute(sql, params); return rows; }
164
+
165
+ async transaction(fn)
166
+ {
167
+ const conn = await this._pool.getConnection();
168
+ try
169
+ {
170
+ await conn.beginTransaction();
171
+ const result = await fn(conn);
172
+ await conn.commit();
173
+ return result;
174
+ }
175
+ catch (e) { await conn.rollback(); throw e; }
176
+ finally { conn.release(); }
177
+ }
178
+
179
+ // -- MySQL Utilities ---------------------------------
180
+
181
+ /**
182
+ * List all user-created tables in the current database.
183
+ * @returns {Promise<string[]>}
184
+ */
185
+ async tables()
186
+ {
187
+ const [rows] = await this._pool.execute('SHOW TABLES');
188
+ return rows.map(r => Object.values(r)[0]);
189
+ }
190
+
191
+ /**
192
+ * Get the columns of a table.
193
+ * @param {string} table - Table name.
194
+ * @returns {Promise<Array<{ Field: string, Type: string, Null: string, Key: string, Default: *, Extra: string }>>}
195
+ */
196
+ async columns(table)
197
+ {
198
+ const [rows] = await this._pool.execute(`SHOW COLUMNS FROM ${this._q(table)}`);
199
+ return rows;
200
+ }
201
+
202
+ /**
203
+ * Get the current database size in bytes.
204
+ * @returns {Promise<number>}
205
+ */
206
+ async databaseSize()
207
+ {
208
+ const db = this._options.database;
209
+ if (!db) return 0;
210
+ const [rows] = await this._pool.execute(
211
+ `SELECT SUM(data_length + index_length) AS size
212
+ FROM information_schema.tables WHERE table_schema = ?`, [db]
213
+ );
214
+ return Number(rows[0].size) || 0;
215
+ }
216
+
217
+ /**
218
+ * Get connection pool status.
219
+ * @returns {{ total: number, idle: number, used: number, queued: number }}
220
+ */
221
+ poolStatus()
222
+ {
223
+ const pool = this._pool.pool;
224
+ if (!pool) return { total: 0, idle: 0, used: 0, queued: 0 };
225
+ return {
226
+ total: pool._allConnections?.length || 0,
227
+ idle: pool._freeConnections?.length || 0,
228
+ used: (pool._allConnections?.length || 0) - (pool._freeConnections?.length || 0),
229
+ queued: pool._connectionQueue?.length || 0,
230
+ };
231
+ }
232
+
233
+ /**
234
+ * Get the MySQL/MariaDB server version string.
235
+ * @returns {Promise<string>}
236
+ */
237
+ async version()
238
+ {
239
+ const [rows] = await this._pool.execute('SELECT VERSION() AS ver');
240
+ return rows[0].ver;
241
+ }
242
+
243
+ /**
244
+ * Ping the database to check connectivity.
245
+ * @returns {Promise<boolean>}
246
+ */
247
+ async ping()
248
+ {
249
+ try
250
+ {
251
+ const conn = await this._pool.getConnection();
252
+ await conn.ping();
253
+ conn.release();
254
+ return true;
255
+ }
256
+ catch { return false; }
257
+ }
258
+
259
+ /**
260
+ * Execute a raw statement that doesn't return rows (INSERT, UPDATE, DDL).
261
+ * @param {string} sql
262
+ * @param {...*} params
263
+ * @returns {Promise<{ affectedRows: number, insertId: number }>}
264
+ */
265
+ async exec(sql, ...params)
266
+ {
267
+ const [result] = await this._pool.execute(sql, params);
268
+ return { affectedRows: result.affectedRows || 0, insertId: result.insertId || 0 };
269
+ }
270
+ }
271
+
272
+ module.exports = MysqlAdapter;