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.
- 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 +28 -177
- 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,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;
|