mango-orm 1.0.0
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/LICENSE +15 -0
- package/README.md +1244 -0
- package/dist/src/mango.d.ts +276 -0
- package/dist/src/mango.js +713 -0
- package/package.json +49 -0
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Requires sql for connection
|
|
3
|
+
*/
|
|
4
|
+
import * as sql from "mysql";
|
|
5
|
+
/**
|
|
6
|
+
* MangoType - Schema builder for defining table column types
|
|
7
|
+
* Provides chainable methods to build SQL column definitions
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const type = new MangoType();
|
|
11
|
+
* type.varchar(255).notNull().unique();
|
|
12
|
+
*/
|
|
13
|
+
class MangoType {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.query = "";
|
|
16
|
+
}
|
|
17
|
+
/** Define an INT column */
|
|
18
|
+
int() {
|
|
19
|
+
this.query += " INT ";
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
22
|
+
/** Define a BIGINT column */
|
|
23
|
+
bigInt() {
|
|
24
|
+
this.query += " BIGINT ";
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
/** Define a FLOAT column */
|
|
28
|
+
float() {
|
|
29
|
+
this.query += " FLOAT ";
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Define a CHAR column with fixed length
|
|
34
|
+
* @param length - The fixed character length
|
|
35
|
+
*/
|
|
36
|
+
char(length) {
|
|
37
|
+
this.query += ` CHAR(${length}) `;
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
/** Define a TEXT column for large text data */
|
|
41
|
+
text() {
|
|
42
|
+
this.query += " TEXT ";
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
/** Define a DATE column (YYYY-MM-DD) */
|
|
46
|
+
date() {
|
|
47
|
+
this.query += " DATE ";
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
/** Define a DATETIME column (YYYY-MM-DD HH:MM:SS) */
|
|
51
|
+
dateTime() {
|
|
52
|
+
this.query += " DATETIME ";
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
/** Define a TIMESTAMP column (auto-updated on changes) */
|
|
56
|
+
timeStamp() {
|
|
57
|
+
this.query += " TIMESTAMP ";
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
/** Define a BOOLEAN column (stored as TINYINT(1)) */
|
|
61
|
+
boolean() {
|
|
62
|
+
this.query += " BOOLEAN ";
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Define a TINYINT column
|
|
67
|
+
* @param length - Display width (e.g., TINYINT(1))
|
|
68
|
+
*/
|
|
69
|
+
tinyInt(length) {
|
|
70
|
+
this.query += ` TINYINT(${length})`;
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
/** Make column auto-incrementing (use with INT/BIGINT) */
|
|
74
|
+
autoIncrement() {
|
|
75
|
+
this.query += " AUTO_INCREMENT ";
|
|
76
|
+
return this;
|
|
77
|
+
}
|
|
78
|
+
/** Mark column as primary key */
|
|
79
|
+
primaryKey() {
|
|
80
|
+
this.query += " PRIMARY KEY ";
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Define a VARCHAR column with variable length
|
|
85
|
+
* @param length - Maximum character length (e.g., 255)
|
|
86
|
+
*/
|
|
87
|
+
varchar(length) {
|
|
88
|
+
this.query += ` VARCHAR(${length}) `;
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
/** Make column NOT NULL (required field) */
|
|
92
|
+
notNull() {
|
|
93
|
+
this.query += ` NOT NULL `;
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
/** Add UNIQUE constraint (no duplicate values) */
|
|
97
|
+
unique() {
|
|
98
|
+
this.query += " UNIQUE ";
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
/** Get the built SQL column definition */
|
|
102
|
+
getQuery() {
|
|
103
|
+
return this.query;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* MangoQuery - Internal query executor
|
|
108
|
+
* Handles SQL query execution and prepared statements
|
|
109
|
+
* Automatically resets state after execution to prevent query contamination
|
|
110
|
+
*/
|
|
111
|
+
class MangoQuery {
|
|
112
|
+
constructor() {
|
|
113
|
+
this.query = "";
|
|
114
|
+
this.supplies = []; // Prepared statement parameters
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Configure the database connection pool
|
|
118
|
+
* @param db - MySQL connection pool instance
|
|
119
|
+
*/
|
|
120
|
+
config(db) {
|
|
121
|
+
this.db = db;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Execute the built query with prepared statements
|
|
125
|
+
* Automatically resets query and supplies after execution
|
|
126
|
+
* @returns Promise resolving to query results
|
|
127
|
+
*/
|
|
128
|
+
execute() {
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
this.db.query(this.query, this.supplies, (err, result) => {
|
|
131
|
+
// Reset state BEFORE resolving to prevent contamination
|
|
132
|
+
this.query = "";
|
|
133
|
+
this.supplies = [];
|
|
134
|
+
if (err)
|
|
135
|
+
reject(err);
|
|
136
|
+
else
|
|
137
|
+
resolve(result);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Execute a custom SQL query with parameters
|
|
143
|
+
* Use for complex queries not covered by the query builder
|
|
144
|
+
* @param query - Raw SQL query string
|
|
145
|
+
* @param supplies - Array of parameter values
|
|
146
|
+
* @returns Promise resolving to query results
|
|
147
|
+
*/
|
|
148
|
+
customQuery(query, supplies) {
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
this.db.query(query, supplies, (err, result) => {
|
|
151
|
+
if (err)
|
|
152
|
+
reject(err);
|
|
153
|
+
else
|
|
154
|
+
resolve(result);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* MangoTable<T> - Main query builder class for table operations
|
|
161
|
+
* Provides chainable methods for building SQL queries
|
|
162
|
+
* Generic type T represents the shape of table rows
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* const users = new MangoTable(db, 'users', ['id', 'name', 'email']);
|
|
166
|
+
* const result = await users.selectAll().where('id', '=', 1).execute();
|
|
167
|
+
*/
|
|
168
|
+
class MangoTable {
|
|
169
|
+
/**
|
|
170
|
+
* Create a new table instance
|
|
171
|
+
* @param db - MySQL connection pool
|
|
172
|
+
* @param name - Table name (no spaces allowed)
|
|
173
|
+
* @param fields - Array of column names in the table
|
|
174
|
+
*/
|
|
175
|
+
constructor(db, name, fields = []) {
|
|
176
|
+
this.query = new MangoQuery();
|
|
177
|
+
// Validate that fields are provided
|
|
178
|
+
if (fields.length == 0 || (fields.length == 1 && fields[0] === "")) {
|
|
179
|
+
throw new Error("no fields provided for table " + name);
|
|
180
|
+
}
|
|
181
|
+
this.db = db;
|
|
182
|
+
// Validate table name doesn't contain spaces
|
|
183
|
+
if (Array.from(name.split(" ")).length > 1) {
|
|
184
|
+
throw new Error("No spaces in table name allowed:");
|
|
185
|
+
}
|
|
186
|
+
this.tableName = name;
|
|
187
|
+
this.tableFields = [...fields]; // Clone to prevent external mutations
|
|
188
|
+
// Initialize query executor
|
|
189
|
+
// this.query = new MangoQuery();
|
|
190
|
+
this.query.config(db);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Add new columns to an existing table
|
|
194
|
+
* Updates internal field list for validation
|
|
195
|
+
* @param fields - Object mapping column names to MangoType definitions
|
|
196
|
+
* @returns this for method chaining
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* table.addColumns({
|
|
200
|
+
* age: mango.types().int().notNull(),
|
|
201
|
+
* status: mango.types().varchar(50)
|
|
202
|
+
* }).execute();
|
|
203
|
+
*/
|
|
204
|
+
addColumns(fields) {
|
|
205
|
+
// Early return if no fields provided
|
|
206
|
+
if (Object.entries(fields).length === 0)
|
|
207
|
+
return this;
|
|
208
|
+
this.query.query += "ALTER TABLE " + this.tableName + "\n";
|
|
209
|
+
const entries = Object.entries(fields);
|
|
210
|
+
this.query.query += entries
|
|
211
|
+
.map(([key, value]) => {
|
|
212
|
+
let QUERY = " ADD COLUMN ";
|
|
213
|
+
// Validate column name doesn't contain spaces
|
|
214
|
+
if (Array.from(key.split(" ")).length > 1) {
|
|
215
|
+
throw new Error("Field/Column name cannot have spaces: " +
|
|
216
|
+
key +
|
|
217
|
+
" from table : " +
|
|
218
|
+
this.tableName);
|
|
219
|
+
}
|
|
220
|
+
QUERY += key + " ";
|
|
221
|
+
QUERY += " " + value.getQuery();
|
|
222
|
+
return QUERY;
|
|
223
|
+
})
|
|
224
|
+
.join(",\n");
|
|
225
|
+
this.query.query += ";\n";
|
|
226
|
+
// Update internal field list for future validations
|
|
227
|
+
entries.forEach(([key]) => this.tableFields.push(key));
|
|
228
|
+
return this;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Remove columns from the table
|
|
232
|
+
* Validates columns exist before removal
|
|
233
|
+
* @param fields - Array of column names to remove
|
|
234
|
+
* @returns this for method chaining
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* table.removeColumns(['old_field', 'deprecated_col']).execute();
|
|
238
|
+
*/
|
|
239
|
+
removeColumns(fields) {
|
|
240
|
+
// Early return if no fields provided
|
|
241
|
+
if (fields.length === 0)
|
|
242
|
+
return this;
|
|
243
|
+
this.query.query += "ALTER TABLE " + this.tableName + "\n";
|
|
244
|
+
this.query.query += fields
|
|
245
|
+
.map((field) => {
|
|
246
|
+
let QUERY = " DROP COLUMN ";
|
|
247
|
+
// Validate field exists in table
|
|
248
|
+
if (!this.tableFields.includes(field)) {
|
|
249
|
+
throw new Error("field/column : " +
|
|
250
|
+
field +
|
|
251
|
+
" does not exist in table : " +
|
|
252
|
+
this.tableName);
|
|
253
|
+
}
|
|
254
|
+
QUERY += field;
|
|
255
|
+
return QUERY;
|
|
256
|
+
})
|
|
257
|
+
.join(",\n");
|
|
258
|
+
this.query.query += ";\n";
|
|
259
|
+
// Remove fields from internal tracking
|
|
260
|
+
this.tableFields = this.tableFields.filter((value) => !fields.includes(value));
|
|
261
|
+
return this;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Select all columns from the table
|
|
265
|
+
* @returns this for method chaining
|
|
266
|
+
*
|
|
267
|
+
* @example
|
|
268
|
+
* await table.selectAll().where('active', '=', true).execute();
|
|
269
|
+
*/
|
|
270
|
+
selectAll() {
|
|
271
|
+
this.query.query = `SELECT * from ${this.tableName}`;
|
|
272
|
+
return this;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Select specific columns from the table
|
|
276
|
+
* Validates all columns exist before building query
|
|
277
|
+
* @param columns - Array of column names to select
|
|
278
|
+
* @returns this for method chaining
|
|
279
|
+
*
|
|
280
|
+
* @example
|
|
281
|
+
* await table.selectColumns(['id', 'name', 'email']).execute();
|
|
282
|
+
*/
|
|
283
|
+
selectColumns(columns) {
|
|
284
|
+
// Early return if no columns specified
|
|
285
|
+
if (columns.length === 0)
|
|
286
|
+
return this;
|
|
287
|
+
this.query.query = `SELECT `;
|
|
288
|
+
// Build column list with proper comma separation
|
|
289
|
+
for (let i = 0; i < columns.length; i++) {
|
|
290
|
+
// Validate each column exists in table
|
|
291
|
+
if (!this.tableFields.includes(columns[i])) {
|
|
292
|
+
throw new Error("Table field: " +
|
|
293
|
+
columns[i] +
|
|
294
|
+
" does not exist in table: " +
|
|
295
|
+
this.tableName);
|
|
296
|
+
}
|
|
297
|
+
this.query.query += " " + columns[i];
|
|
298
|
+
// Add comma between columns, but not after the last one
|
|
299
|
+
if (i < columns.length - 1) {
|
|
300
|
+
this.query.query += " , ";
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
this.query.query += " ";
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
this.query.query += ` FROM ${this.tableName} `;
|
|
307
|
+
return this;
|
|
308
|
+
}
|
|
309
|
+
selectDistinctColumns(columns) {
|
|
310
|
+
if (columns.length === 0)
|
|
311
|
+
return;
|
|
312
|
+
this.query.query = `SELECT DISTINCT `;
|
|
313
|
+
for (let i = 0; i < columns.length; i++) {
|
|
314
|
+
if (!this.tableFields.includes(columns[i])) {
|
|
315
|
+
throw new Error("Table field :" + columns[i] + " does not exist in table: " + this.tableName);
|
|
316
|
+
}
|
|
317
|
+
this.query.query += " " + columns[i];
|
|
318
|
+
if (i < columns.length - 1) {
|
|
319
|
+
this.query.query += " , ";
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
this.query.query += " ";
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
this.query.query += ` FROM ${this.tableName}`;
|
|
326
|
+
return this;
|
|
327
|
+
}
|
|
328
|
+
orderBy(columnName) {
|
|
329
|
+
if (!this.tableFields.includes(columnName)) {
|
|
330
|
+
throw new Error("Field/Column : " + columnName + " does not exist in table : " + this.tableName);
|
|
331
|
+
}
|
|
332
|
+
this.query.query += ` ORDER BY ${columnName} `;
|
|
333
|
+
return this;
|
|
334
|
+
}
|
|
335
|
+
sort(sort = 1) {
|
|
336
|
+
this.query.query += " " + (sort > 0 ? "ASC" : "DESC") + " ";
|
|
337
|
+
return this;
|
|
338
|
+
}
|
|
339
|
+
limit(length) {
|
|
340
|
+
if (length <= 0)
|
|
341
|
+
return this;
|
|
342
|
+
this.query.query += ` LIMIT ${length} `;
|
|
343
|
+
return this;
|
|
344
|
+
}
|
|
345
|
+
offset(length) {
|
|
346
|
+
if (length <= 0)
|
|
347
|
+
return this;
|
|
348
|
+
this.query.query += ` OFFSET ${length} `;
|
|
349
|
+
return this;
|
|
350
|
+
}
|
|
351
|
+
insertOne(data) {
|
|
352
|
+
const column_name = Object.keys(data);
|
|
353
|
+
const column_value = Object.values(data);
|
|
354
|
+
if (column_name.length === 0 || column_value.length === 0)
|
|
355
|
+
return;
|
|
356
|
+
for (const field of column_name) {
|
|
357
|
+
if (!this.tableFields.includes(field)) {
|
|
358
|
+
throw new Error("Error: The column/fieldname : " + field + " does not exist in table: " + this.tableName);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const columns = column_name.join(", ");
|
|
362
|
+
const values = column_value;
|
|
363
|
+
this.query.query += ` INSERT INTO ${this.tableName} (${columns}) VALUES `;
|
|
364
|
+
this.query.query += ` ( `;
|
|
365
|
+
for (let i = 0; i < values.length; i++) {
|
|
366
|
+
this.query.query += ` ? `;
|
|
367
|
+
if (i < values.length - 1) {
|
|
368
|
+
this.query.query += ' , ';
|
|
369
|
+
}
|
|
370
|
+
if (i == values.length - 1) {
|
|
371
|
+
this.query.query += ` ) `;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
this.query.supplies = values;
|
|
375
|
+
return this;
|
|
376
|
+
}
|
|
377
|
+
insertMany(fields, data = [[]]) {
|
|
378
|
+
if (fields.length === 0 || data.length === 0 || data.flat().length == 0)
|
|
379
|
+
return;
|
|
380
|
+
for (const field of fields) {
|
|
381
|
+
if (!this.tableFields.includes(field)) {
|
|
382
|
+
throw new Error("field/column_name " + field + " does not exists in table " + this.tableName);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const columns = fields.join(", ");
|
|
386
|
+
const placeholders = data.map(row => `(${row.map(() => '?').join(', ')})`).join(', ');
|
|
387
|
+
this.query.query = `INSERT INTO ${this.tableName} (${columns}) VALUES ${placeholders}`;
|
|
388
|
+
this.query.supplies = data.flat();
|
|
389
|
+
return this;
|
|
390
|
+
}
|
|
391
|
+
getFields() {
|
|
392
|
+
return [...this.tableFields];
|
|
393
|
+
}
|
|
394
|
+
getName() {
|
|
395
|
+
return this.tableName;
|
|
396
|
+
}
|
|
397
|
+
truncate() {
|
|
398
|
+
this.query.query = " TRUNCATE TABLE " + this.tableName;
|
|
399
|
+
return this;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Add WHERE clause to filter query results
|
|
403
|
+
* Use with comparison operators to filter rows
|
|
404
|
+
* Supports falsy values (0, false, empty string)
|
|
405
|
+
* @param field - Column name to filter on
|
|
406
|
+
* @param operator - SQL comparison operator (=, !=, <, >, LIKE, etc.)
|
|
407
|
+
* @param value - Value to compare against (any type)
|
|
408
|
+
* @returns this for method chaining
|
|
409
|
+
*
|
|
410
|
+
* @example
|
|
411
|
+
* table.selectAll().where('age', '>=', 18).execute();
|
|
412
|
+
* table.selectAll().where('active', '=', false).execute(); // Works with false!
|
|
413
|
+
* table.selectAll().where('count', '=', 0).execute(); // Works with 0!
|
|
414
|
+
*/
|
|
415
|
+
where(field, operator, value) {
|
|
416
|
+
// Validate field exists in table
|
|
417
|
+
if (!this.tableFields.includes(field)) {
|
|
418
|
+
throw new Error(`Field/Column: ${field} does not exist in table: ${this.tableName}`);
|
|
419
|
+
}
|
|
420
|
+
// Validate operator is allowed
|
|
421
|
+
const validOperators = [
|
|
422
|
+
"=",
|
|
423
|
+
"!=",
|
|
424
|
+
"<>",
|
|
425
|
+
">",
|
|
426
|
+
"<",
|
|
427
|
+
">=",
|
|
428
|
+
"<=",
|
|
429
|
+
"LIKE",
|
|
430
|
+
"NOT LIKE",
|
|
431
|
+
"IN",
|
|
432
|
+
"NOT IN",
|
|
433
|
+
"IS",
|
|
434
|
+
"IS NOT",
|
|
435
|
+
];
|
|
436
|
+
if (!validOperators.includes(operator.toUpperCase())) {
|
|
437
|
+
throw new Error(`Invalid operator: ${operator}`);
|
|
438
|
+
}
|
|
439
|
+
// Build WHERE clause with placeholder
|
|
440
|
+
this.query.query += ` WHERE ${field} ${operator} ? `;
|
|
441
|
+
this.query.supplies.push(value);
|
|
442
|
+
return this;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Add AND condition to existing WHERE clause
|
|
446
|
+
* Chain multiple conditions together
|
|
447
|
+
* @param field - Column name
|
|
448
|
+
* @param operator - SQL comparison operator
|
|
449
|
+
* @param value - Value to compare (any type including 0, false)
|
|
450
|
+
* @returns this for method chaining
|
|
451
|
+
*
|
|
452
|
+
* @example
|
|
453
|
+
* table.selectAll()
|
|
454
|
+
* .where('age', '>=', 18)
|
|
455
|
+
* .and('active', '=', true)
|
|
456
|
+
* .execute();
|
|
457
|
+
*/
|
|
458
|
+
and(field, operator, value) {
|
|
459
|
+
if (!this.tableFields.includes(field)) {
|
|
460
|
+
throw new Error(`Field/Column: ${field} does not exist in table: ${this.tableName}`);
|
|
461
|
+
}
|
|
462
|
+
const validOperators = [
|
|
463
|
+
"=",
|
|
464
|
+
"!=",
|
|
465
|
+
"<>",
|
|
466
|
+
">",
|
|
467
|
+
"<",
|
|
468
|
+
">=",
|
|
469
|
+
"<=",
|
|
470
|
+
"LIKE",
|
|
471
|
+
"NOT LIKE",
|
|
472
|
+
"IN",
|
|
473
|
+
"NOT IN",
|
|
474
|
+
"IS",
|
|
475
|
+
"IS NOT",
|
|
476
|
+
];
|
|
477
|
+
if (!validOperators.includes(operator.toUpperCase())) {
|
|
478
|
+
throw new Error(`Invalid operator: ${operator}`);
|
|
479
|
+
}
|
|
480
|
+
this.query.query += ` AND ${field} ${operator} ? `;
|
|
481
|
+
this.query.supplies.push(value);
|
|
482
|
+
return this;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Add OR condition to existing WHERE clause
|
|
486
|
+
* Combine alternative conditions
|
|
487
|
+
* @param field - Column name
|
|
488
|
+
* @param operator - SQL comparison operator
|
|
489
|
+
* @param value - Value to compare (any type including 0, false)
|
|
490
|
+
* @returns this for method chaining
|
|
491
|
+
*
|
|
492
|
+
* @example
|
|
493
|
+
* table.selectAll()
|
|
494
|
+
* .where('role', '=', 'admin')
|
|
495
|
+
* .or('role', '=', 'moderator')
|
|
496
|
+
* .execute();
|
|
497
|
+
*/
|
|
498
|
+
or(field, operator, value) {
|
|
499
|
+
if (!this.tableFields.includes(field)) {
|
|
500
|
+
throw new Error(`Field/Column: ${field} does not exist in table: ${this.tableName}`);
|
|
501
|
+
}
|
|
502
|
+
const validOperators = [
|
|
503
|
+
"=",
|
|
504
|
+
"!=",
|
|
505
|
+
"<>",
|
|
506
|
+
">",
|
|
507
|
+
"<",
|
|
508
|
+
">=",
|
|
509
|
+
"<=",
|
|
510
|
+
"LIKE",
|
|
511
|
+
"NOT LIKE",
|
|
512
|
+
"IN",
|
|
513
|
+
"NOT IN",
|
|
514
|
+
"IS",
|
|
515
|
+
"IS NOT",
|
|
516
|
+
];
|
|
517
|
+
if (!validOperators.includes(operator.toUpperCase())) {
|
|
518
|
+
throw new Error(`Invalid operator: ${operator}`);
|
|
519
|
+
}
|
|
520
|
+
this.query.query += ` OR ${field} ${operator} ? `;
|
|
521
|
+
this.query.supplies.push(value);
|
|
522
|
+
return this;
|
|
523
|
+
}
|
|
524
|
+
update(data) {
|
|
525
|
+
const entries = Object.entries(data);
|
|
526
|
+
if (entries.length === 0)
|
|
527
|
+
return;
|
|
528
|
+
this.query.query = `UPDATE ${this.tableName} SET `;
|
|
529
|
+
entries.forEach(([key, value], index) => {
|
|
530
|
+
if (!this.tableFields.includes(key)) {
|
|
531
|
+
throw new Error(`Field/Column: ${key} does not exist in table: ${this.tableName}`);
|
|
532
|
+
}
|
|
533
|
+
this.query.query += `${key} = ?`;
|
|
534
|
+
if (index < entries.length - 1) {
|
|
535
|
+
this.query.query += ", ";
|
|
536
|
+
}
|
|
537
|
+
this.query.supplies.push(value);
|
|
538
|
+
});
|
|
539
|
+
return this;
|
|
540
|
+
}
|
|
541
|
+
join(type, table, condition) {
|
|
542
|
+
const validTypes = ['INNER', 'LEFT', 'RIGHT', 'FULL'];
|
|
543
|
+
if (!validTypes.includes(type)) {
|
|
544
|
+
throw new Error(`Invalid join type: ${type}`);
|
|
545
|
+
}
|
|
546
|
+
this.query.query += ` ${type} JOIN ${table} ON ${condition.left} ${condition.operator} ${condition.right} `;
|
|
547
|
+
return this;
|
|
548
|
+
}
|
|
549
|
+
delete() {
|
|
550
|
+
this.query.query = `DELETE FROM ${this.tableName}`;
|
|
551
|
+
return this;
|
|
552
|
+
}
|
|
553
|
+
whereIn(field, values) {
|
|
554
|
+
if (!this.tableFields.includes(field)) {
|
|
555
|
+
throw new Error(`Field/Column: ${field} does not exist in table: ${this.tableName}`);
|
|
556
|
+
}
|
|
557
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
558
|
+
throw new Error("whereIn requires a non-empty array of values");
|
|
559
|
+
}
|
|
560
|
+
const placeholders = values.map(() => '?').join(', ');
|
|
561
|
+
this.query.query += ` WHERE ${field} IN (${placeholders}) `;
|
|
562
|
+
this.query.supplies.push(...values);
|
|
563
|
+
return this;
|
|
564
|
+
}
|
|
565
|
+
whereNotIn(field, values) {
|
|
566
|
+
if (!field || values.length === 0)
|
|
567
|
+
return;
|
|
568
|
+
if (!this.tableFields.includes(field)) {
|
|
569
|
+
throw new Error(`Field/Column: ${field} does not exist in table: ${this.tableName}`);
|
|
570
|
+
}
|
|
571
|
+
const placeholders = values.map(() => '?').join(', ');
|
|
572
|
+
this.query.query += ` WHERE ${field} NOT IN (${placeholders}) `;
|
|
573
|
+
this.query.supplies.push(...values);
|
|
574
|
+
return this;
|
|
575
|
+
}
|
|
576
|
+
getQuery() {
|
|
577
|
+
return this.query;
|
|
578
|
+
}
|
|
579
|
+
execute() {
|
|
580
|
+
return this.query.execute();
|
|
581
|
+
}
|
|
582
|
+
customQuery(query, supplies) {
|
|
583
|
+
return this.query.customQuery(query, supplies);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Mango - Main ORM class for MySQL database operations
|
|
588
|
+
* Manages connection pool and provides table instances
|
|
589
|
+
* Auto-discovers existing tables on connection
|
|
590
|
+
*
|
|
591
|
+
* @example
|
|
592
|
+
* const mango = new Mango();
|
|
593
|
+
* await mango.connect({
|
|
594
|
+
* host: 'localhost',
|
|
595
|
+
* user: 'root',
|
|
596
|
+
* password: 'pass',
|
|
597
|
+
* database: 'mydb'
|
|
598
|
+
* });
|
|
599
|
+
*
|
|
600
|
+
* const users = mango.selectTable('users');
|
|
601
|
+
* const results = await users.selectAll().execute();
|
|
602
|
+
*/
|
|
603
|
+
class Mango {
|
|
604
|
+
constructor() {
|
|
605
|
+
this.tables = []; // Cached table instances
|
|
606
|
+
this.query = new MangoQuery();
|
|
607
|
+
}
|
|
608
|
+
async connect({ host, user, password, database, connectionLimit = 10, waitForConnection = true, queueLimit = 0, connectTimeout = 10000, charset = "utf8mb4", }) {
|
|
609
|
+
this.db = sql.createPool({
|
|
610
|
+
host: host,
|
|
611
|
+
user: user,
|
|
612
|
+
password: password,
|
|
613
|
+
database: database,
|
|
614
|
+
connectionLimit: connectionLimit,
|
|
615
|
+
waitForConnections: waitForConnection,
|
|
616
|
+
queueLimit: queueLimit,
|
|
617
|
+
connectTimeout: connectTimeout,
|
|
618
|
+
charset: charset,
|
|
619
|
+
});
|
|
620
|
+
this.query.config(this.db);
|
|
621
|
+
const tables = await this.query.customQuery("SHOW TABLES", []);
|
|
622
|
+
const tableNames = tables.map((row) => Object.values(row)[0]);
|
|
623
|
+
for (const name of tableNames) {
|
|
624
|
+
const columns = await this.query.customQuery("SELECT column_name FROM information_schema.columns WHERE table_schema=? AND table_name=?", [database, name]);
|
|
625
|
+
const column_names = columns.map((row) => row.column_name);
|
|
626
|
+
this.tables.push(new MangoTable(this.db, name, column_names));
|
|
627
|
+
}
|
|
628
|
+
return this;
|
|
629
|
+
}
|
|
630
|
+
async disconnect() {
|
|
631
|
+
return new Promise((resolve, reject) => {
|
|
632
|
+
this.db.end((err) => {
|
|
633
|
+
if (err)
|
|
634
|
+
reject(err);
|
|
635
|
+
else {
|
|
636
|
+
this.tables = [];
|
|
637
|
+
resolve();
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Get a new MangoType instance for schema building
|
|
644
|
+
* Use when creating or modifying table columns
|
|
645
|
+
* @returns New MangoType instance for chaining
|
|
646
|
+
*
|
|
647
|
+
* @example
|
|
648
|
+
* const idField = mango.types().int().autoIncrement().primaryKey();
|
|
649
|
+
*/
|
|
650
|
+
types() {
|
|
651
|
+
return new MangoType();
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Get a table instance by name
|
|
655
|
+
* Returns strongly-typed table if generic provided
|
|
656
|
+
* @param name - Name of the table
|
|
657
|
+
* @returns MangoTable instance for building queries
|
|
658
|
+
*
|
|
659
|
+
* @example
|
|
660
|
+
* interface User { id: number; name: string; email: string }
|
|
661
|
+
* const users = mango.selectTable<User>('users');
|
|
662
|
+
*/
|
|
663
|
+
selectTable(name) {
|
|
664
|
+
for (const table of this.tables) {
|
|
665
|
+
if (table.getName() == name.toLowerCase()) {
|
|
666
|
+
return table;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
throw new Error("Table not found: " + name);
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Get all discovered table instances
|
|
673
|
+
* @returns Array of all MangoTable instances
|
|
674
|
+
*/
|
|
675
|
+
getTables() {
|
|
676
|
+
return [...this.tables];
|
|
677
|
+
}
|
|
678
|
+
async createTable(name, fields) {
|
|
679
|
+
this.query.query = "CREATE TABLE " + name + "( \n";
|
|
680
|
+
const fieldEnteries = Object.entries(fields);
|
|
681
|
+
let table = new MangoTable(this.db, name.toLowerCase(), [
|
|
682
|
+
...fieldEnteries.map(([key, value], index) => {
|
|
683
|
+
return key;
|
|
684
|
+
}),
|
|
685
|
+
]);
|
|
686
|
+
fieldEnteries.forEach(([key, value], index) => {
|
|
687
|
+
this.query.query += key + " " + value.getQuery();
|
|
688
|
+
if (index < fieldEnteries.length - 1) {
|
|
689
|
+
this.query.query += ", \n";
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
this.query.query += "\n)";
|
|
693
|
+
await this.query.execute();
|
|
694
|
+
this.query.query = "";
|
|
695
|
+
this.tables.push(table);
|
|
696
|
+
return table;
|
|
697
|
+
}
|
|
698
|
+
async dropTable(name) {
|
|
699
|
+
for (let i = 0; i < this.tables.length; i++) {
|
|
700
|
+
if (this.tables[i].getName() === name) {
|
|
701
|
+
this.query.query = "DROP TABLE " + name;
|
|
702
|
+
await this.query.execute();
|
|
703
|
+
this.query.query = "";
|
|
704
|
+
this.tables.splice(i, 1);
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
customQuery(query, supplies) {
|
|
710
|
+
return this.query.customQuery(query, supplies);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
export { Mango, MangoType, MangoTable };
|