dbtasker 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.
@@ -0,0 +1,614 @@
1
+ const path = require("path");
2
+ const fs = require("fs/promises"); // Importing fs.promises for async operations
3
+ const { pool, DBInfo } = require("./server_setup.js"); // Import the promise-based pool
4
+ const fncs = require("./functions.js");
5
+ const { table, error } = require("console");
6
+
7
+
8
+ const jsonClientfilePath = path.join(__dirname, "../tables.json");
9
+ let matchDatabase = path.join(__dirname, "./readonly");
10
+ const dropTable = false;
11
+ const dropColumn = true;
12
+ const createTable = true;
13
+ const alterTable = true;
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+ //generate table query
23
+ function generateCreateTableQuery(databaseName, tableName, schemaObject, dbType = "mysql") {
24
+ const errors = [];
25
+
26
+ // Validate inputs
27
+ if (!databaseName || typeof databaseName !== "string") {
28
+ errors.push("Invalid database name.");
29
+ }
30
+
31
+ if (!tableName || typeof tableName !== "string") {
32
+ errors.push("Invalid table name.");
33
+ }
34
+
35
+ if (
36
+ !schemaObject ||
37
+ typeof schemaObject !== "object" ||
38
+ Array.isArray(schemaObject)
39
+ ) {
40
+ errors.push("Invalid schema object.");
41
+ }
42
+
43
+ if (errors.length > 0) {
44
+ throw new Error(errors.join("\n"));
45
+ }
46
+
47
+ const columns = [];
48
+ const foreignKeys = [];
49
+
50
+ for (let columnName in schemaObject) {
51
+ const columnInfo = schemaObject[columnName];
52
+
53
+ if (
54
+ typeof columnInfo === "object" &&
55
+ columnInfo !== null &&
56
+ !Array.isArray(columnInfo)
57
+ ) {
58
+ const type = columnInfo.type;
59
+
60
+ if (!type || typeof type.name !== "string") {
61
+ errors.push(`Invalid type for column: ${columnName}`);
62
+ continue;
63
+ }
64
+
65
+ let columnDef = `"${columnName}" ${type.name}`;
66
+
67
+ // Length/Precision
68
+ if (type.LengthValues !== undefined) {
69
+ columnDef += `(${type.LengthValues})`;
70
+ }
71
+
72
+ // NULL/NOT NULL
73
+ columnDef += columnInfo.NULL === false ? " NOT NULL" : " NULL";
74
+
75
+ // DEFAULT
76
+ if (columnInfo.DEFAULT !== undefined) {
77
+ let defaultValue = columnInfo.DEFAULT;
78
+
79
+ if (
80
+ typeof defaultValue === "string" &&
81
+ defaultValue.match(/CURRENT_TIMESTAMP/i)
82
+ ) {
83
+ columnDef += ` DEFAULT ${defaultValue}`;
84
+ } else {
85
+ defaultValue = `'${defaultValue}'`;
86
+ columnDef += ` DEFAULT ${defaultValue}`;
87
+ }
88
+ }
89
+
90
+ // ON UPDATE (MySQL only)
91
+ if (dbType === "mysql" && columnInfo.on_update) {
92
+ if (typeof columnInfo.on_update === "string") {
93
+ columnDef += ` ON UPDATE ${columnInfo.on_update}`;
94
+ }
95
+ }
96
+
97
+ // COMMENT (MySQL only)
98
+ if (dbType === "mysql" && columnInfo.comment) {
99
+ const cmnt = columnInfo.comment.replace(/'/g, "");
100
+ columnDef += ` COMMENT '${cmnt}'`;
101
+ }
102
+
103
+ // AUTO_INCREMENT (MySQL) or SERIAL (PostgreSQL)
104
+ if (columnInfo.AUTO_INCREMENT) {
105
+ if (dbType === "mysql") {
106
+ columnDef += " AUTO_INCREMENT";
107
+ } else if (dbType === "postgres") {
108
+ columnDef = `"${columnName}" SERIAL`;
109
+ }
110
+ }
111
+
112
+ // INDEX (MySQL only)
113
+ if (dbType === "mysql" && columnInfo.index) {
114
+ columnDef += ` ${columnInfo.index}`;
115
+ }
116
+
117
+ columns.push(columnDef);
118
+
119
+ // Foreign Keys
120
+ if (columnInfo.foreign_key) {
121
+ const fk = columnInfo.foreign_key;
122
+ if (
123
+ fk &&
124
+ typeof fk === "object" &&
125
+ fk.REFERENCES &&
126
+ fk.REFERENCES.table &&
127
+ fk.REFERENCES.column
128
+ ) {
129
+ let foreignKeyDef = `FOREIGN KEY ("${columnName}") REFERENCES "${fk.REFERENCES.table}"("${fk.REFERENCES.column}")`;
130
+
131
+ if (fk.delete === true) {
132
+ foreignKeyDef += " ON DELETE CASCADE";
133
+ } else if (fk.delete === null) {
134
+ foreignKeyDef += " ON DELETE SET NULL";
135
+ }
136
+
137
+ if (fk.update === "CASCADE") {
138
+ foreignKeyDef += " ON UPDATE CASCADE";
139
+ }
140
+
141
+ foreignKeys.push(foreignKeyDef);
142
+ } else {
143
+ errors.push(`Invalid foreign key for column "${columnName}": ${JSON.stringify(fk)}`);
144
+ }
145
+ }
146
+ } else {
147
+ errors.push(`Invalid column definition for: ${columnName}`);
148
+ }
149
+ }
150
+
151
+ if (errors.length > 0) {
152
+ throw new Error(errors.join("\n"));
153
+ }
154
+
155
+ const tableDefinition = [...columns, ...foreignKeys].join(", ");
156
+ let createTableQuery = "";
157
+
158
+ if (dbType === "mysql") {
159
+ createTableQuery = `CREATE TABLE IF NOT EXISTS \`${tableName}\` (${tableDefinition}) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;`;
160
+ } else if (dbType === "postgres") {
161
+ createTableQuery = `CREATE TABLE IF NOT EXISTS "${tableName}" (${tableDefinition});`;
162
+ } else {
163
+ throw new Error(`Unsupported database type: ${dbType}`);
164
+ }
165
+
166
+ return createTableQuery.replace(/''/g, "'");
167
+ }
168
+
169
+
170
+ //Generate modifying and droping table query
171
+ async function generateAlterTableQuery(databaseName, tableName, schema, dropColumn) {
172
+ try {
173
+ if (!schema) {
174
+ throw new Error(`Schema for table ${tableName} is undefined or null`);
175
+ }
176
+
177
+ const fullTableName = `\`${databaseName}\`.\`${tableName}\``;
178
+
179
+ const existingColumns = await getColumnDetails(databaseName, tableName);
180
+ console.log(`Fetched column details for table: ${tableName}`);
181
+ const existingForeignKey = await getForeignKeyDetails(databaseName, tableName);
182
+ console.log(`Fetched foreign key details for table: ${tableName}`);
183
+
184
+ if (!existingColumns) {
185
+ throw new Error(`Failed to fetch column details for table ${tableName}`);
186
+ }
187
+
188
+ let alterStatements = [];
189
+ let isModifyIncludefk = [];
190
+ let isIncludefk = [];
191
+ let dropforeignkeyconstraint = [];
192
+ let unmatchedforeignkey = [];
193
+
194
+ for (const [columnName, columnDetails] of Object.entries(schema)) {
195
+ if (!columnDetails) continue;
196
+
197
+ const existingColumn = existingColumns.find(col => col.column_name === columnName);
198
+
199
+ if (existingColumn) {
200
+ let isSameType = existingColumn.data_type.toLowerCase() === columnDetails.type?.name.toLowerCase();
201
+
202
+ let isSameLength = false;
203
+ if (
204
+ (existingColumn.character_maximum_length > 50000 &&
205
+ columnDetails.type?.LengthValues === undefined) ||
206
+ (existingColumn.character_maximum_length === null &&
207
+ columnDetails.type?.LengthValues === undefined) ||
208
+ existingColumn.character_maximum_length === columnDetails.type?.LengthValues
209
+ ) {
210
+ isSameLength = true;
211
+ } else if (columnDetails.type?.name === "ENUM") {
212
+ let length_val = columnDetails.type?.LengthValues?.replace(/,\s+/g, ",");
213
+ if (`enum(${length_val})` === existingColumn.column_type) {
214
+ isSameLength = true;
215
+ }
216
+ }
217
+
218
+ let isSameNull =
219
+ (existingColumn.is_nullable === "YES" && columnDetails.NULL !== false) ||
220
+ (existingColumn.is_nullable === "NO" && columnDetails.NULL === false);
221
+
222
+ let isDefaultMatch = false;
223
+ if (isNumber(columnDetails.DEFAULT)) {
224
+ columnDetails.DEFAULT = columnDetails.DEFAULT.toString();
225
+ }
226
+
227
+ if (
228
+ existingColumn.default_value === `'${columnDetails.DEFAULT}'` ||
229
+ existingColumn.default_value === columnDetails.DEFAULT ||
230
+ (["NULL", null].includes(existingColumn.default_value) && columnDetails.DEFAULT === undefined) ||
231
+ (existingColumn.default_value === "current_timestamp()" && columnDetails.DEFAULT === "CURRENT_TIMESTAMP")
232
+ ) {
233
+ isDefaultMatch = true;
234
+ } else if (columnDetails.DEFAULT || columnDetails.DEFAULT === 0) {
235
+ isDefaultMatch = existingColumn.default_value === columnDetails.DEFAULT.toString();
236
+ }
237
+
238
+ let isSameComment =
239
+ existingColumn.column_comment === columnDetails.comment?.replace(/'/g, "") ||
240
+ (existingColumn.column_comment === "" && columnDetails.comment === undefined);
241
+
242
+ if (columnDetails.foreign_key) {
243
+ isIncludefk.push(columnName);
244
+ for (const keys of existingForeignKey) {
245
+ if (keys.COLUMN_NAME == columnName) {
246
+ if (
247
+ keys.REFERENCED_TABLE_NAME !== columnDetails.foreign_key.REFERENCES.table ||
248
+ keys.REFERENCED_COLUMN_NAME !== columnDetails.foreign_key.REFERENCES.column ||
249
+ (keys.DELETE_RULE === "CASCADE" && columnDetails.foreign_key.delete !== true) ||
250
+ (keys.DELETE_RULE !== "CASCADE" && columnDetails.foreign_key.delete === true)
251
+ ) {
252
+ dropforeignkeyconstraint.push(keys.CONSTRAINT_NAME);
253
+ unmatchedforeignkey.push(keys.COLUMN_NAME);
254
+ }
255
+ }
256
+ }
257
+ }
258
+
259
+ if (
260
+ isSameType &&
261
+ isSameLength &&
262
+ isSameNull &&
263
+ isDefaultMatch &&
264
+ isSameComment
265
+ ) continue;
266
+
267
+ if (columnDetails.foreign_key) {
268
+ isModifyIncludefk.push(columnName);
269
+ }
270
+
271
+ let modifyStatement = `ALTER TABLE ${fullTableName} MODIFY COLUMN \`${columnName}\` ${columnDetails.type?.name}`;
272
+
273
+ if (
274
+ ["enum", "varchar"].includes(columnDetails.type?.name) &&
275
+ !columnDetails.type?.LengthValues
276
+ ) {
277
+ throw new Error(`ENUM or VARCHAR column "${columnName}" needs a LengthValues definition.`);
278
+ }
279
+
280
+ if (columnDetails.type?.LengthValues !== undefined) {
281
+ modifyStatement += `(${columnDetails.type.LengthValues})`;
282
+ }
283
+
284
+ modifyStatement += columnDetails.NULL === false ? " NOT NULL" : " NULL";
285
+
286
+ if (columnDetails.DEFAULT || columnDetails.DEFAULT === 0) {
287
+ if (columnDetails.DEFAULT === "CURRENT_TIMESTAMP") {
288
+ modifyStatement += ` DEFAULT CURRENT_TIMESTAMP`;
289
+ } else if (isJsonObject(columnDetails.DEFAULT)) {
290
+ throw new Error("Default value is restricted for BLOB, TEXT, JSON, etc.");
291
+ } else {
292
+ modifyStatement += ` DEFAULT '${columnDetails.DEFAULT}'`;
293
+ }
294
+ }
295
+
296
+ if (columnDetails.on_update) {
297
+ modifyStatement += ` ON UPDATE ${columnDetails.on_update}`;
298
+ }
299
+
300
+ if (columnDetails.comment) {
301
+ modifyStatement += ` COMMENT '${bypassQuotes(columnDetails.comment)}'`;
302
+ }
303
+
304
+ if (columnDetails.AUTO_INCREMENT) {
305
+ modifyStatement += " AUTO_INCREMENT";
306
+ }
307
+
308
+ alterStatements.push(modifyStatement);
309
+ } else {
310
+ // New column
311
+ let addStatement = `ALTER TABLE ${fullTableName} ADD COLUMN \`${columnName}\` ${columnDetails.type?.name}`;
312
+
313
+ if (
314
+ ["enum", "varchar"].includes(columnDetails.type?.name) &&
315
+ !columnDetails.type?.LengthValues
316
+ ) {
317
+ throw new Error(`ENUM or VARCHAR column "${columnName}" needs a LengthValues definition.`);
318
+ }
319
+
320
+ if (columnDetails.type?.LengthValues !== undefined) {
321
+ addStatement += `(${columnDetails.type.LengthValues})`;
322
+ }
323
+
324
+ addStatement += columnDetails.NULL === false ? " NOT NULL" : " NULL";
325
+
326
+ if (columnDetails.DEFAULT || columnDetails.DEFAULT === 0) {
327
+ if (columnDetails.DEFAULT === "CURRENT_TIMESTAMP") {
328
+ addStatement += ` DEFAULT CURRENT_TIMESTAMP`;
329
+ } else if (isJsonObject(columnDetails.DEFAULT)) {
330
+ throw new Error("Default value is restricted for BLOB, TEXT, JSON, etc.");
331
+ } else {
332
+ addStatement += ` DEFAULT '${columnDetails.DEFAULT}'`;
333
+ }
334
+ }
335
+
336
+ if (columnDetails.on_update) {
337
+ addStatement += ` ON UPDATE ${columnDetails.on_update}`;
338
+ }
339
+
340
+ if (columnDetails.comment) {
341
+ addStatement += ` COMMENT '${bypassQuotes(columnDetails.comment)}'`;
342
+ }
343
+
344
+ if (columnDetails.AUTO_INCREMENT) {
345
+ addStatement += " AUTO_INCREMENT";
346
+ }
347
+
348
+ alterStatements.push(addStatement);
349
+
350
+ if (columnDetails.foreign_key) {
351
+ isIncludefk.push(columnName);
352
+ }
353
+ }
354
+ }
355
+
356
+ // Handle foreign keys
357
+ const addForeignkeyquery = [];
358
+ const dropForeignkeyquery = [];
359
+ let findnewfk = [...isIncludefk];
360
+
361
+ if (existingForeignKey.length > 0) {
362
+ for (const fk of existingForeignKey) {
363
+ if (
364
+ isModifyIncludefk.includes(fk.COLUMN_NAME) ||
365
+ !isIncludefk.includes(fk.COLUMN_NAME)
366
+ ) {
367
+ if (!dropforeignkeyconstraint.includes(fk.CONSTRAINT_NAME)) {
368
+ dropforeignkeyconstraint.push(fk.CONSTRAINT_NAME);
369
+ }
370
+ }
371
+ findnewfk = removefromarray(findnewfk, fk.COLUMN_NAME);
372
+ }
373
+ }
374
+
375
+ for (const dropKey of dropforeignkeyconstraint) {
376
+ dropForeignkeyquery.push(`ALTER TABLE ${fullTableName} DROP FOREIGN KEY \`${dropKey}\``);
377
+ }
378
+
379
+ let ibfk = 1;
380
+ for (const fkCol of [...findnewfk, ...isModifyIncludefk, ...unmatchedforeignkey]) {
381
+ const fk = schema[fkCol].foreign_key;
382
+ let fkStmt = `ALTER TABLE ${fullTableName} ADD CONSTRAINT \`${tableName}_ibfk${ibfk++}\` FOREIGN KEY (\`${fkCol}\`) REFERENCES \`${fk.REFERENCES.table}\`(\`${fk.REFERENCES.column}\`)`;
383
+ if (fk.delete === true) {
384
+ fkStmt += " ON DELETE CASCADE";
385
+ } else if (fk.delete === null) {
386
+ fkStmt += " ON DELETE SET NULL";
387
+ }
388
+ if (fk.on_update === null) {
389
+ fkStmt += " ON UPDATE SET NULL";
390
+ }
391
+ addForeignkeyquery.push(fkStmt);
392
+ }
393
+
394
+ if (dropColumn) {
395
+ for (const col of existingColumns) {
396
+ if (!schema[col.column_name]) {
397
+ alterStatements.push(`ALTER TABLE ${fullTableName} DROP COLUMN \`${col.column_name}\``);
398
+ }
399
+ }
400
+ }
401
+
402
+ const result = [
403
+ ...dropForeignkeyquery,
404
+ ...alterStatements,
405
+ ...addForeignkeyquery,
406
+ ];
407
+
408
+ if (result.length === 0) {
409
+ console.log(`No changes needed for table "${tableName}".`);
410
+ return [];
411
+ }
412
+
413
+ return result;
414
+ } catch (err) {
415
+ console.error(`Error generating alter table queries for ${tableName}:`, err.message);
416
+ throw err;
417
+ }
418
+ }
419
+
420
+ // Do anything with the table
421
+ async function createOrModifyTable(databaseName, queryText) {
422
+ try {
423
+ // Select the target database first
424
+ await pool.query(`USE \`${databaseName}\`;`);
425
+
426
+ await pool.query(queryText);
427
+
428
+ function getTableName(input) {
429
+ if (typeof input !== "string") {
430
+ throw new Error("Query text must be a string");
431
+ }
432
+ const words = input.trim().split(/\s+/);
433
+ if (words.length < 5) {
434
+ return [];
435
+ }
436
+ if (input.startsWith("CREATE TABLE IF NOT EXISTS") && words.length > 5) {
437
+ return [words[5]];
438
+ } else {
439
+ return [words[2], words[5]];
440
+ }
441
+ }
442
+
443
+ const table_name = getTableName(queryText);
444
+
445
+ if (table_name.length > 1) {
446
+ return {
447
+ success: true,
448
+ message: `${table_name[1]} column of ${table_name[0]} table created or modified successfully.`,
449
+ querytext: queryText,
450
+ };
451
+ } else if (table_name.length === 1) {
452
+ return {
453
+ success: true,
454
+ message: `${table_name[0]} table created or modified successfully.`,
455
+ querytext: queryText,
456
+ };
457
+ } else {
458
+ return {
459
+ success: true,
460
+ message: `Table operation successful.`,
461
+ querytext: queryText,
462
+ };
463
+ }
464
+ } catch (err) {
465
+ return {
466
+ success: false,
467
+ message: err.message,
468
+ querytext: queryText,
469
+ };
470
+ }
471
+ }
472
+
473
+
474
+ async function createTables(databaseName, tableQueries) {
475
+ console.log(`Creating or Modifying Tables in Database: ${databaseName}`);
476
+
477
+ for (const query of tableQueries) {
478
+ console.log("Operating query is:");
479
+ console.log([query]);
480
+ console.log("...");
481
+
482
+ const result = await createOrModifyTable(databaseName, query);
483
+
484
+ if (!result.success) {
485
+ console.error(
486
+ "Error creating table:",
487
+ result.message,
488
+ "queryText:",
489
+ result.querytext
490
+ );
491
+ throw new Error("Error creating table");
492
+ } else {
493
+ console.log(result.message);
494
+ }
495
+ }
496
+ }
497
+
498
+
499
+ async function tableOperation({ createTable, alterTable, dropTable, dropColumn, databaseName }) {
500
+ try {
501
+ let isTableModified = false;
502
+ let newJsonStoreFilePath = "";
503
+
504
+ const lastSavedFile = await getLastSavedFile(databaseName);
505
+ newJsonStoreFilePath = lastSavedFile
506
+ ? path.join(databaseName, lastSavedFile)
507
+ : databaseName + path.join(__dirname, "/file.json");
508
+
509
+ const isSame = await compareJsonFiles(jsonClientfilePath, newJsonStoreFilePath);
510
+ if (isSame) {
511
+ console.log("\x1b[1m\x1b[32mThe JSON files are the same. No update needed. We are good to go...\x1b[0m");
512
+ return;
513
+ }
514
+
515
+ console.log("The JSON files are different. Updating the database and the store file...");
516
+ const jsonData = await readJsonFile(jsonClientfilePath);
517
+ const objectTableNames = Object.keys(jsonData.tables);
518
+ const dbTableNames = await getTableNames(databaseName);
519
+
520
+ let queryList = [];
521
+
522
+ for (const tableName of objectTableNames) {
523
+ try {
524
+ const tableDef = jsonData.tables[tableName];
525
+
526
+ // Support LengthValue as array for enum/decimal
527
+ if (Array.isArray(tableDef.columns)) {
528
+ tableDef.columns = tableDef.columns.map((col) => {
529
+ if (Array.isArray(col.LengthValue)) {
530
+ col.LengthValue = col.LengthValue.join(",");
531
+ }
532
+ return col;
533
+ });
534
+ }
535
+
536
+ let query;
537
+ if (dbTableNames.includes(tableName.toLowerCase())) {
538
+ if (alterTable) {
539
+ query = await generateAlterTableQuery(
540
+ tableName.toLowerCase(),
541
+ tableDef,
542
+ dropColumn,
543
+ databaseName
544
+ );
545
+ console.log(`Alter query generated for table: ${tableName}`);
546
+ }
547
+ } else if (createTable) {
548
+ query = await generateCreateTableQuery(
549
+ tableName.toLowerCase(),
550
+ tableDef,
551
+ databaseName
552
+ );
553
+ console.log(`Create query generated for new table: ${tableName}`);
554
+ }
555
+
556
+ if (query) queryList.push(query);
557
+ } catch (err) {
558
+ console.error(`Error generating query for table ${tableName}:`, err.message);
559
+ return;
560
+ }
561
+ }
562
+
563
+ // Execute the queries
564
+ if (queryList.length) {
565
+ try {
566
+ await createTables(queryList.flat(), databaseName);
567
+ console.log("All tables created or modified successfully.");
568
+ isTableModified = true;
569
+ } catch (err) {
570
+ console.error("Error in creating/modifying tables:", err.message);
571
+ return;
572
+ }
573
+ } else {
574
+ console.log("No table changes needed.");
575
+ }
576
+
577
+ // Drop unused tables
578
+ if (dropTable) {
579
+ try {
580
+ const tablesToDrop = dbTableNames.filter(
581
+ (dbTable) => !objectTableNames.includes(dbTable)
582
+ );
583
+
584
+ if (tablesToDrop.length > 0) {
585
+ await dropTables(tablesToDrop, databaseName);
586
+ console.log("Dropped tables:", tablesToDrop);
587
+ } else {
588
+ console.log("No tables to drop.");
589
+ }
590
+ } catch (err) {
591
+ console.error("Error during dropTable operation:", err.message);
592
+ }
593
+ }
594
+
595
+ // Update store file
596
+ if (isTableModified) {
597
+ try {
598
+ const dateTime = getDateTime("_");
599
+ const filename = `${databaseName}/${dateTime.date}__${dateTime.time}_storeTableJSON.json`;
600
+ const jsonData = await readJsonFile(jsonClientfilePath);
601
+ await writeJsonFile(filename, jsonData);
602
+ console.log("Updated store file at:", filename);
603
+ } catch (err) {
604
+ console.error("Error saving updated JSON store file:", err.message);
605
+ }
606
+ }
607
+ } catch (error) {
608
+ console.error("Error in tableOperation:", error.message);
609
+ }
610
+ }
611
+
612
+ //tableOperation(true, true, true, true);
613
+ module.exports = { tableOperation };
614
+ //tableOperation(createTable, alterTable, dropTable, dropColumn);
package/tables.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = {}