dbtasker 1.0.0 → 2.5.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/function.js CHANGED
@@ -1,7 +1,23 @@
1
1
  const mysql = require('mysql2/promise');
2
+ const fs = require("fs/promises"); // Importing fs.promises for async operations
3
+ const path = require("path"); // Importing Node's path module
4
+ const cstyler = require("cstyler");
2
5
 
3
6
 
4
- function getDateTime(seperator) {
7
+
8
+ function isNumber(str) {
9
+ if (str === null || str === undefined) {
10
+ return false;
11
+ }
12
+ if (typeof str === "number") {
13
+ return true;
14
+ }
15
+ if (Array.isArray(str) || typeof str === "object") {
16
+ return false;
17
+ }
18
+ return !isNaN(str) && str.trim() !== "";
19
+ }
20
+ function getDateTime(seperator = "/") {
5
21
  const today = new Date();
6
22
  const formattedDateTime =
7
23
  today.getFullYear() +
@@ -40,7 +56,120 @@ function getDateTime(seperator) {
40
56
  datetime: formattedDateTime,
41
57
  };
42
58
  }
59
+ async function getMySQLVersion(config) {
60
+ const connection = await mysql.createConnection(config);
61
+ try {
62
+ const [rows] = await connection.execute('SELECT VERSION() AS version');
63
+ const version = rows[0].version;
64
+ console.log("Mysql database version is: ", cstyler.green(version));
65
+ return version;
66
+ } finally {
67
+ await connection.end();
68
+ }
69
+ }
70
+ async function isMySQL578OrAbove(config) {
71
+ const versionStr = await getMySQLVersion(config); // e.g., '5.7.9-log' or '8.0.34'
72
+ // Extract numeric version
73
+ const match = versionStr.match(/^(\d+)\.(\d+)\.(\d+)/);
74
+ if (!match) return false;
75
+ const [major, minor, patch] = match.slice(1).map(Number);
76
+
77
+ if (major > 5) return true;
78
+ if (major < 5) return false;
79
+ if (minor > 7) return true;
80
+ if (minor < 7) return false;
81
+ // major==5, minor==7
82
+ return patch >= 8;
83
+ }
84
+ async function getCharsetAndCollations(config) {
85
+ try {
86
+ const conn = await mysql.createConnection(config);
87
+
88
+ const [charsetRows] = await conn.query("SHOW CHARACTER SET");
89
+ const characterSets = charsetRows.map(row => row.Charset);
90
+
91
+ const [collationRows] = await conn.query("SHOW COLLATION");
92
+ const collations = collationRows.map(row => row.Collation);
93
+
94
+ await conn.end();
95
+ return { characterSets, collations };
96
+ } catch (err) {
97
+ return null;
98
+ }
99
+ }
100
+ async function isCharsetCollationValid(config, charset, collation) {
101
+ let connection;
102
+
103
+ try {
104
+ connection = await mysql.createConnection(config);
105
+
106
+ const [rows] = await connection.execute(`
107
+ SELECT 1
108
+ FROM information_schema.COLLATIONS
109
+ WHERE COLLATION_NAME = ?
110
+ AND CHARACTER_SET_NAME = ?
111
+ `, [collation, charset]);
112
+
113
+ return rows.length > 0;
114
+
115
+ } catch (err) {
116
+ return null;
117
+ } finally {
118
+ if (connection) await connection.end();
119
+ }
120
+ }
121
+ async function getMySQLEngines(config) {
122
+ let connection;
123
+
124
+ try {
125
+ connection = await mysql.createConnection(config);
126
+
127
+ const [rows] = await connection.query("SHOW ENGINES");
128
+
129
+ const engines = {};
130
+
131
+ for (const row of rows) {
132
+ engines[row.Engine] = {
133
+ support: row.Support,
134
+ comment: row.Comment
135
+ };
136
+ }
137
+
138
+ return engines;
139
+
140
+ } catch (err) {
141
+ console.error(`Failed to fetch MySQL engines: ${err.message}`);
142
+ return null;
143
+ } finally {
144
+ if (connection) await connection.end();
145
+ }
146
+ }
147
+ function isValidMySQLConfig(config) {
148
+ if (typeof config !== 'object' || config === null) return false;
149
+
150
+ const requiredKeys = ['host', 'user', 'password', 'port'];
151
+
152
+ for (const key of requiredKeys) {
153
+ if (!(key in config)) return false;
154
+
155
+ const value = config[key];
156
+ const type = typeof value;
157
+
158
+ // Allow string, number, boolean
159
+ if (!['string', 'number', 'boolean'].includes(type)) return false;
160
+
161
+ // Extra string validation
162
+ if (type === 'string' && value.trim() === '') return false;
163
+ }
164
+
165
+ return true;
166
+ }
43
167
  async function isMySQLDatabase(config) {
168
+ const isvalidconfig = isValidMySQLConfig(config);
169
+ if (isvalidconfig === false) {
170
+ throw new Error("There is some information missing in config.");
171
+ }
172
+ console.log("Config is okay. We are good to go.");
44
173
  let connection;
45
174
  try {
46
175
  connection = await mysql.createConnection(config);
@@ -75,26 +204,152 @@ async function checkDatabaseExists(config, dbName) {
75
204
  return null;
76
205
  }
77
206
  }
78
- async function createDatabase(config, dbName) {
207
+ async function dropDatabase(config, databaseName) {
79
208
  let connection;
209
+ try {
210
+ // Connect to server without specifying database
211
+ connection = await mysql.createConnection({
212
+ host: config.host,
213
+ user: config.user,
214
+ password: config.password
215
+ });
80
216
 
217
+ // Check if database exists
218
+ const [rows] = await connection.query(
219
+ `SELECT SCHEMA_NAME
220
+ FROM INFORMATION_SCHEMA.SCHEMATA
221
+ WHERE SCHEMA_NAME = ?`,
222
+ [databaseName]
223
+ );
224
+
225
+ if (rows.length === 0) {
226
+ console.log(`Database '${databaseName}' does not exist.`);
227
+ return false;
228
+ }
229
+
230
+ // Drop the database
231
+ await connection.query(`DROP DATABASE \`${databaseName}\``);
232
+ console.log(`Database '${databaseName}' dropped successfully.`);
233
+ return true;
234
+ } catch (err) {
235
+ console.error("Error dropping database:", err.message);
236
+ return null;
237
+ } finally {
238
+ if (connection) await connection.end();
239
+ }
240
+ }
241
+ async function dropTable(config, databaseName, tableName) {
242
+ let connection;
81
243
  try {
82
- connection = await mysql.createConnection(config);
83
- await connection.query(`CREATE DATABASE \`${dbName}\``);
84
- console.log(`✅ Database '${dbName}' created successfully.`);
244
+ config.database = databaseName;
245
+ connection = await mysql.createConnection({ ...config, database: databaseName });
246
+
247
+ // Check if table exists
248
+ const [tables] = await connection.query(
249
+ `SELECT TABLE_NAME
250
+ FROM INFORMATION_SCHEMA.TABLES
251
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?`,
252
+ [databaseName, tableName]
253
+ );
254
+
255
+ if (tables.length === 0) {
256
+ console.log(`Table '${tableName}' does not exist in ${databaseName}`);
257
+ return false;
258
+ }
259
+
260
+ // Drop foreign keys from other tables referencing this table
261
+ const [fkRefs] = await connection.query(
262
+ `SELECT TABLE_NAME, CONSTRAINT_NAME
263
+ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
264
+ WHERE REFERENCED_TABLE_SCHEMA = ? AND REFERENCED_TABLE_NAME = ?`,
265
+ [databaseName, tableName]
266
+ );
267
+
268
+ for (const ref of fkRefs) {
269
+ console.log(`Dropping foreign key '${ref.CONSTRAINT_NAME}' from table '${ref.TABLE_NAME}'`);
270
+ await connection.query(
271
+ `ALTER TABLE \`${ref.TABLE_NAME}\` DROP FOREIGN KEY \`${ref.CONSTRAINT_NAME}\``
272
+ );
273
+ }
274
+
275
+ // Drop the table
276
+ await connection.query(`DROP TABLE \`${tableName}\``);
277
+
278
+ console.log(`Table '${tableName}' dropped successfully from ${databaseName}`);
85
279
  return true;
86
280
  } catch (err) {
87
- if (err.code === 'ER_DB_CREATE_EXISTS') {
88
- console.log(`⚠️ Database '${dbName}' already exists.`);
281
+ console.error("Error dropping table:", err.message);
282
+ return null;
283
+ } finally {
284
+ if (connection) await connection.end();
285
+ }
286
+ }
287
+ async function dropColumn(config, databaseName, tableName, columnName) {
288
+ let connection;
289
+ try {
290
+ config.database = databaseName;
291
+ connection = await mysql.createConnection(config);
292
+
293
+ // 1️⃣ Check if column exists
294
+ const [columns] = await connection.query(
295
+ `SELECT COLUMN_NAME, COLUMN_KEY
296
+ FROM INFORMATION_SCHEMA.COLUMNS
297
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?`,
298
+ [databaseName, tableName, columnName]
299
+ );
300
+
301
+ // ❌ Column does not exist → return false
302
+ if (columns.length === 0) {
303
+ console.log(
304
+ `Column '${columnName}' does not exist in ${databaseName}.${tableName}`
305
+ );
89
306
  return false;
90
- } else {
91
- console.error(`❌ Error creating database:`, err.message);
92
- return null;
93
307
  }
308
+
309
+ const columnKey = columns[0].COLUMN_KEY;
310
+
311
+ // 2️⃣ Drop foreign key constraints
312
+ const [fkConstraints] = await connection.query(
313
+ `SELECT CONSTRAINT_NAME
314
+ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
315
+ WHERE TABLE_SCHEMA = ?
316
+ AND TABLE_NAME = ?
317
+ AND COLUMN_NAME = ?
318
+ AND REFERENCED_TABLE_NAME IS NOT NULL`,
319
+ [databaseName, tableName, columnName]
320
+ );
321
+
322
+ for (const fk of fkConstraints) {
323
+ await connection.query(
324
+ `ALTER TABLE \`${tableName}\` DROP FOREIGN KEY \`${fk.CONSTRAINT_NAME}\``
325
+ );
326
+ }
327
+
328
+ // 3️⃣ Drop primary key if needed
329
+ if (columnKey === "PRI") {
330
+ await connection.query(
331
+ `ALTER TABLE \`${tableName}\` DROP PRIMARY KEY`
332
+ );
333
+ }
334
+
335
+ // 4️⃣ Drop column
336
+ await connection.query(
337
+ `ALTER TABLE \`${tableName}\` DROP COLUMN \`${columnName}\``
338
+ );
339
+
340
+ console.log(
341
+ `Column '${columnName}' dropped successfully from ${databaseName}.${tableName}`
342
+ );
343
+
344
+ return true;
345
+ } catch (err) {
346
+ console.error("Error dropping column:", err.message);
347
+ return null;
94
348
  } finally {
95
349
  if (connection) await connection.end();
96
350
  }
97
351
  }
352
+
98
353
  async function getAllDatabaseNames(config) {
99
354
  let connection;
100
355
 
@@ -112,6 +367,10 @@ async function getAllDatabaseNames(config) {
112
367
  if (connection) await connection.end();
113
368
  }
114
369
  }
370
+ function isValidMySQLIdentifier(name) {
371
+ if (typeof name !== "string") return false;
372
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
373
+ }
115
374
  function isValidDatabaseName(name) {
116
375
  if (typeof name !== 'string') return false;
117
376
 
@@ -177,7 +436,7 @@ function isValidTableName(name) {
177
436
  }
178
437
  function isValidColumnName(name) {
179
438
  if (typeof name !== 'string') return false;
180
-
439
+ if (name.length === 0) return false;
181
440
  const maxLength = 64;
182
441
 
183
442
  const reservedKeywords = new Set([
@@ -201,51 +460,136 @@ function isValidColumnName(name) {
201
460
 
202
461
  return true;
203
462
  }
204
- function parseColumnWithOptionalLoopStrict(text) {
205
- if (typeof text !== 'string') return false;
206
-
463
+ function createloopname(text, seperator = "_") {
464
+ if (!isJsonObject(text)) {
465
+ return null;
466
+ }
467
+ seperator = seperator.toString();
468
+ if (text.loop === null) {
469
+ return text.name;
470
+ } else if (['year', 'years'].includes(text.loop)) {
471
+ return text.name + seperator + getDateTime().year + seperator;
472
+ } else if (['month', 'months'].includes(text.loop)) {
473
+ return text.name + seperator + getDateTime().year + seperator + getDateTime().month + seperator;
474
+ } else if (['day', 'days'].includes(text.loop)) {
475
+ return text.name + seperator + getDateTime().year + seperator + getDateTime().month + seperator + getDateTime().day + seperator;
476
+ } else {
477
+ return false;
478
+ }
479
+ }
480
+ function getloop(text) {
481
+ if (typeof text !== "string") {
482
+ return null;
483
+ }
484
+ if (text.startsWith("(year)") || text.endsWith("(year)")) {
485
+ return { name: text.replace("(year)", ""), loop: "year" };
486
+ } else if (text.startsWith("(years)") || text.endsWith("(years)")) {
487
+ return { name: text.replace("(years)", ""), loop: "year" };
488
+ } else if (text.startsWith("(month)") || text.endsWith("(month)")) {
489
+ return { name: text.replace("(month)", ""), loop: "month" };
490
+ } else if (text.startsWith("(months)") || text.endsWith("(months)")) {
491
+ return { name: text.replace("(months)", ""), loop: "month" };
492
+ } else if (text.startsWith("(day)") || text.endsWith("(day)")) {
493
+ return { name: text.replace("(day)", ""), loop: "day" };
494
+ } else if (text.startsWith("(days)") || text.endsWith("(days)")) {
495
+ return { name: text.replace("(days)", ""), loop: "day" };
496
+ } else {
497
+ return { name: text, loop: null }
498
+ }
499
+ }
500
+ function perseTableNameWithLoop(text, seperator = "_") {
501
+ if (typeof text !== 'string') return null;
207
502
  text = text.trim();
208
-
209
- // Case 1: product(year)
210
- let match = text.match(/^([a-zA-Z_][\w]*)\(([^()]+)\)$/);
211
- if (match) {
212
- const name = match[1];
213
- const loop = match[2];
214
- if (isValidColumnName(name) && isValidColumnName(loop)) {
215
- return { name, loop };
503
+ let gtlp = getloop(text);
504
+ if (gtlp.loop === null) {
505
+ if (isValidTableName(gtlp.name)) {
506
+ gtlp.loopname = gtlp.name;
507
+ return gtlp;
508
+ } else {
509
+ return false;
216
510
  }
511
+ } else if (gtlp === null) {
217
512
  return false;
513
+ } else {
514
+ const loopname = createloopname(gtlp, seperator);
515
+ if (isValidTableName(loopname)) {
516
+ return { name: gtlp.name, loop: gtlp.loop, loopname: loopname }
517
+ } else {
518
+ return false;
519
+ }
218
520
  }
219
-
220
- // Case 2: (year)product
221
- match = text.match(/^\(([^()]+)\)([a-zA-Z_][\w]*)$/);
222
- if (match) {
223
- const loop = match[1];
224
- const name = match[2];
225
- if (isValidColumnName(name) && isValidColumnName(loop)) {
226
- return { name, loop };
521
+ }
522
+ function perseDatabaseNameWithLoop(text, seperator = "_") {
523
+ if (typeof text !== 'string') return false;
524
+ text = text.trim();
525
+ let gtlp = getloop(text);
526
+ if (gtlp.loop === null) {
527
+ if (isValidDatabaseName(gtlp.name)) {
528
+ gtlp.loopname = gtlp.name;
529
+ return gtlp;
530
+ } else {
531
+ return false;
227
532
  }
533
+ } else if (gtlp === null) {
228
534
  return false;
535
+ } else {
536
+ const loopname = createloopname(gtlp, seperator);
537
+ if (isValidDatabaseName(loopname)) {
538
+ return { name: gtlp.name, loop: gtlp.loop, loopname: loopname }
539
+ } else {
540
+ return false;
541
+ }
229
542
  }
230
-
231
- // Case 3: just a column name without loop
232
- if (isValidColumnName(text)) {
233
- return { name: text, loop: null };
543
+ }
544
+ function reverseLoopName(text) {
545
+ if (typeof text !== "string") return text;
546
+ let a = text.split(text[text.length - 1]);
547
+ while (a.includes("")) {
548
+ a = fncs.removefromarray(a);
234
549
  }
235
-
236
- // Any invalid format or invalid name
237
- return false;
550
+ if (fncs.isNumber(a[a.length - 1])) {
551
+ if (a[a.length - 1].length === 2 && Number(a[a.length - 1]) <= 31) {
552
+ if (a[a.length - 2].length === 2 && Number(a[a.length - 2]) <= 12) {
553
+ if (a[a.length - 3].length === 4) {
554
+ const year = new Date().getFullYear();
555
+ if (Number(a[a.length - 3]) <= year && Number(a[a.length - 2]) <= 12 && Number(a[a.length - 1]) <= 31) {
556
+ let y = "";
557
+ for (let i = 0; i < text.length - 12; i++) {
558
+ y += text[i];
559
+ }
560
+ return [y + "(day)", y + "(days)"];
561
+ }
562
+ return text;
563
+ }
564
+ } else if (a[a.length - 2].length === 4) {
565
+ const year = new Date().getFullYear();
566
+ if (Number(a[a.length - 2]) <= year && Number(a[a.length - 1]) <= 12) {
567
+ let y = "";
568
+ for (let i = 0; i < text.length - 9; i++) {
569
+ y += text[i];
570
+ }
571
+ return [y + "(month)", y + "(months)"];
572
+ }
573
+ return text;
574
+ }
575
+ } else if (a[a.length - 1].length === 4) {
576
+ const year = new Date().getFullYear();
577
+ if (Number(a[a.length - 1]) <= year) {
578
+ let y = "";
579
+ for (let i = 0; i < text.length - 6; i++) {
580
+ y += text[i];
581
+ }
582
+ return [y + "(year)", y + "(years)"];
583
+ }
584
+ return text;
585
+ }
586
+ return text;
587
+ }
588
+ return text;
238
589
  }
239
590
 
240
591
 
241
592
 
242
-
243
-
244
-
245
-
246
-
247
-
248
-
249
593
  async function getLastSavedFile(directory) {
250
594
  try {
251
595
  // Read the directory
@@ -367,7 +711,6 @@ async function writeJsonFile(filePath, data) {
367
711
  return null;
368
712
  }
369
713
  }
370
- //Write js file
371
714
  const writeJsFile = async (filePath, content) => {
372
715
  try {
373
716
  await fs.access(filePath).catch(() => fs.mkdir(path.dirname(filePath), { recursive: true }));
@@ -376,11 +719,61 @@ const writeJsFile = async (filePath, content) => {
376
719
  return true;
377
720
  } catch (error) {
378
721
  console.error(`Error writing file at ${filePath}:`, error);
722
+ return false;
379
723
  }
380
724
  };
381
- function removefromarray(arr, text) {
725
+
726
+
727
+ function stringifyAny(data) {
728
+ // If the data is undefined or a symbol, handle it explicitly.
729
+ if (typeof data === 'undefined') {
730
+ return 'undefined';
731
+ } else if (data === null) {
732
+ return 'null';
733
+ } else if (data === true) {
734
+ return "true";
735
+ } else if (data === false) {
736
+ return "false";
737
+ }
738
+ if (typeof data === 'symbol') {
739
+ return data.toString();
740
+ }
741
+ // For non-objects (primitives) that are not undefined, simply convert them.
742
+ if (typeof data !== 'object' && typeof data !== 'function') {
743
+ return String(data);
744
+ }
745
+ if (typeof data === "string") {
746
+ return data;
747
+ }
748
+ // Handle objects and functions using JSON.stringify with a custom replacer.
749
+ const seen = new WeakSet();
750
+ const replacer = (key, value) => {
751
+ if (typeof value === 'function') {
752
+ // Convert functions to their string representation.
753
+ return value.toString();
754
+ }
755
+ if (typeof value === 'undefined') {
756
+ return 'undefined';
757
+ }
758
+ if (typeof value === 'object' && value !== null) {
759
+ // Check for circular references
760
+ if (seen.has(value)) {
761
+ return '[Circular]';
762
+ }
763
+ seen.add(value);
764
+ }
765
+ return value;
766
+ };
767
+ try {
768
+ return JSON.stringify(data, replacer, 2);
769
+ } catch (error) {
770
+ // Fallback to a simple string conversion if JSON.stringify fails
771
+ return String(data);
772
+ }
773
+ }
774
+ function removefromarray(arr, text = "") {
382
775
  if (!Array.isArray(arr)) {
383
- throw new Error("data must be an array.");
776
+ return false;
384
777
  }
385
778
  let index = arr.indexOf(text);
386
779
  if (index !== -1) {
@@ -424,6 +817,27 @@ function isJsonSame(a, b) {
424
817
 
425
818
  return true;
426
819
  }
820
+ function JoinJsonObjects(target = {}, source = {}) {
821
+ const result = { ...target };
822
+
823
+ for (const key of Object.keys(source)) {
824
+ if (
825
+ typeof source[key] === "object" &&
826
+ source[key] !== null &&
827
+ !Array.isArray(source[key]) &&
828
+ typeof target[key] === "object" &&
829
+ target[key] !== null &&
830
+ !Array.isArray(target[key])
831
+ ) {
832
+ result[key] = deepJoinObjects(target[key], source[key]);
833
+ } else {
834
+ result[key] = source[key];
835
+ }
836
+ }
837
+
838
+ return result;
839
+ }
840
+
427
841
  function bypassQuotes(data) {
428
842
  let stringData = "";
429
843
 
@@ -439,18 +853,6 @@ function bypassQuotes(data) {
439
853
  // Now escape all quotes and backslashes
440
854
  return stringData.replace(/(["'\\])/g, "\\$1");
441
855
  }
442
- function isNumber(str) {
443
- if (str === null || str === undefined) {
444
- return false;
445
- }
446
- if (typeof str === "number") {
447
- return true;
448
- }
449
- if (Array.isArray(str) || typeof str === "object") {
450
- return false;
451
- }
452
- return !isNaN(str) && str.trim() !== "";
453
- }
454
856
  async function getTableNames(config, databaseName) {
455
857
  const dbName = databaseName || config.database;
456
858
 
@@ -462,7 +864,7 @@ async function getTableNames(config, databaseName) {
462
864
  WHERE table_schema = ?
463
865
  `;
464
866
 
465
- const pool = await mysql.createPool(config);
867
+ const pool = mysql.createPool(config);
466
868
  try {
467
869
  const [results] = await pool.query(query, [dbName]);
468
870
  return results.map(row => row.TABLE_NAME || row.table_name);
@@ -497,53 +899,219 @@ async function getColumnNames(config, databaseName, tableName) {
497
899
  if (connection) await connection.end(); // Ensure connection is closed
498
900
  }
499
901
  }
500
- async function getColumnDetails(config, databaseName, tableName, columnName = null) {
902
+ async function getDatabaseCharsetAndCollation(config, databaseName) {
501
903
  let connection;
904
+ try {
905
+ // Connect to the server (not to a specific database)
906
+ connection = await mysql.createConnection(config);
502
907
 
503
- const query = `
504
- SELECT
505
- COLUMN_NAME AS column_name,
506
- DATA_TYPE AS data_type,
507
- COLUMN_TYPE AS column_type,
508
- CHARACTER_MAXIMUM_LENGTH AS character_maximum_length,
509
- IS_NULLABLE AS is_nullable,
510
- COLUMN_DEFAULT AS default_value,
511
- COLUMN_COMMENT AS column_comment,
512
- COLLATION_NAME AS collation_name
513
- FROM INFORMATION_SCHEMA.COLUMNS
514
- WHERE
515
- TABLE_SCHEMA = ?
516
- AND TABLE_NAME = ?
517
- ${columnName ? "AND COLUMN_NAME = ?" : ""}
518
- `;
908
+ // Query the information_schema for the given database
909
+ const [rows] = await connection.execute(
910
+ `SELECT DEFAULT_CHARACTER_SET_NAME AS characterSet, DEFAULT_COLLATION_NAME AS collation
911
+ FROM information_schema.SCHEMATA
912
+ WHERE SCHEMA_NAME = ?`,
913
+ [databaseName]
914
+ );
915
+
916
+ if (rows.length === 0) {
917
+ console.error(`Database "${databaseName}" not found.`);
918
+ return null;
919
+ }
920
+
921
+ return {
922
+ characterSet: rows[0].characterSet,
923
+ collation: rows[0].collation,
924
+ };
925
+ } catch (err) {
926
+ console.error("Error fetching charset/collation:", err.message);
927
+ return null;
928
+ } finally {
929
+ if (connection) await connection.end();
930
+ }
931
+ }
932
+ async function getColumnDetails(config, dbName, tableName, columnName) {
933
+ let connection;
934
+ try {
935
+ connection = await mysql.createConnection({ ...config, database: dbName });
936
+
937
+ // 1. Column metadata
938
+ const [cols] = await connection.execute(
939
+ `
940
+ SELECT
941
+ COLUMN_TYPE,
942
+ DATA_TYPE,
943
+ CHARACTER_MAXIMUM_LENGTH,
944
+ NUMERIC_PRECISION,
945
+ NUMERIC_SCALE,
946
+ IS_NULLABLE,
947
+ COLUMN_DEFAULT,
948
+ EXTRA,
949
+ COLUMN_KEY,
950
+ CHARACTER_SET_NAME,
951
+ COLLATION_NAME,
952
+ COLUMN_COMMENT
953
+ FROM INFORMATION_SCHEMA.COLUMNS
954
+ WHERE TABLE_SCHEMA = ?
955
+ AND TABLE_NAME = ?
956
+ AND COLUMN_NAME = ?
957
+ `,
958
+ [dbName, tableName, columnName]
959
+ );
960
+
961
+ if (!cols.length) return false;
962
+ const c = cols[0];
963
+
964
+ // 2. Parse ENUM / SET
965
+ let length_value = null;
966
+ // DECIMAL / FLOAT / DOUBLE ONLY
967
+ if (["decimal", "float", "double"].includes(c.DATA_TYPE)) {
968
+ length_value =
969
+ c.NUMERIC_SCALE !== null
970
+ ? [c.NUMERIC_PRECISION, c.NUMERIC_SCALE]
971
+ : c.NUMERIC_PRECISION;
972
+ }
519
973
 
520
- const params = columnName
521
- ? [databaseName, tableName, columnName]
522
- : [databaseName, tableName];
974
+ // INTEGER TYPES → no length_value
975
+ else if (
976
+ ["tinyint", "smallint", "mediumint", "int", "bigint"].includes(c.DATA_TYPE)
977
+ ) {
978
+ length_value = null;
979
+ }
980
+ else if (c.DATA_TYPE === "enum" || c.DATA_TYPE === "set") {
981
+ length_value = c.COLUMN_TYPE
982
+ .slice(c.DATA_TYPE.length + 1, -1)
983
+ .split(",")
984
+ .map(v => v.trim().replace(/^'(.*)'$/, "$1"));
985
+ }
986
+ // CHAR / VARCHAR
987
+ else if (c.CHARACTER_MAXIMUM_LENGTH !== null) {
988
+ length_value = c.CHARACTER_MAXIMUM_LENGTH;
989
+ }
523
990
 
991
+ return {
992
+ columntype: c.DATA_TYPE.toUpperCase(),
993
+ length_value,
994
+ unsigned: /unsigned/i.test(c.COLUMN_TYPE),
995
+ zerofill: /zerofill/i.test(c.COLUMN_TYPE),
996
+ nulls: c.IS_NULLABLE === "YES",
997
+ defaults: c.COLUMN_DEFAULT,
998
+ autoincrement: c.EXTRA.includes("auto_increment"),
999
+ index:
1000
+ c.COLUMN_KEY === "PRI" ? "PRIMARY KEY" :
1001
+ c.COLUMN_KEY === "UNI" ? "UNIQUE" :
1002
+ c.COLUMN_KEY === "MUL" ? "KEY" : "",
1003
+ _charset_: c.CHARACTER_SET_NAME,
1004
+ _collate_: c.COLLATION_NAME,
1005
+ comment: c.COLUMN_COMMENT
1006
+ };
1007
+
1008
+ } catch (err) {
1009
+ console.error(err.message);
1010
+ return null;
1011
+ } finally {
1012
+ if (connection) await connection.end();
1013
+ }
1014
+ }
1015
+ async function columnHasKey(config, databaseName, tableName, columnName) {
1016
+ let connection;
524
1017
  try {
525
1018
  connection = await mysql.createConnection({ ...config, database: databaseName });
526
- const [rows] = await connection.execute(query, params);
527
-
528
- // Process ENUM and SET types
529
- rows.forEach((row) => {
530
- if (row.data_type === "enum" || row.data_type === "set") {
531
- row.enum_set_values = row.column_type
532
- .replace(/(enum|set)\((.*)\)/i, "$2")
533
- .split(",")
534
- .map((val) => val.replace(/'/g, ""));
535
- }
1019
+
1020
+ // Query for PRIMARY and UNIQUE keys
1021
+ const [indexRows] = await connection.execute(`
1022
+ SELECT INDEX_NAME
1023
+ FROM information_schema.STATISTICS
1024
+ WHERE TABLE_SCHEMA = ?
1025
+ AND TABLE_NAME = ?
1026
+ AND COLUMN_NAME = ?
1027
+ `, [databaseName, tableName, columnName]);
1028
+
1029
+ // Query for FOREIGN KEY constraints
1030
+ const [fkRows] = await connection.execute(`
1031
+ SELECT CONSTRAINT_NAME
1032
+ FROM information_schema.KEY_COLUMN_USAGE
1033
+ WHERE TABLE_SCHEMA = ?
1034
+ AND TABLE_NAME = ?
1035
+ AND COLUMN_NAME = ?
1036
+ AND REFERENCED_TABLE_NAME IS NOT NULL
1037
+ `, [databaseName, tableName, columnName]);
1038
+
1039
+ const keys = [
1040
+ ...indexRows.map(row => row.INDEX_NAME),
1041
+ ...fkRows.map(row => row.CONSTRAINT_NAME)
1042
+ ];
1043
+
1044
+ return { hasKey: keys.length > 0, keys };
1045
+ } catch (err) {
1046
+ throw err;
1047
+ } finally {
1048
+ if (connection) await connection.end();
1049
+ }
1050
+ }
1051
+ async function getForeignKeyDetails(config, databaseName, tableName, columnName = null) {
1052
+ let connection;
1053
+ try {
1054
+ connection = await mysql.createConnection({
1055
+ ...config,
1056
+ database: databaseName
536
1057
  });
537
1058
 
538
- return rows;
539
- } catch (error) {
540
- console.error("Error fetching column details:", error.message);
1059
+ const sql = `
1060
+ SELECT
1061
+ kcu.CONSTRAINT_NAME,
1062
+ kcu.REFERENCED_TABLE_NAME,
1063
+ kcu.REFERENCED_COLUMN_NAME,
1064
+ rc.DELETE_RULE,
1065
+ rc.UPDATE_RULE,
1066
+ kcu.COLUMN_NAME
1067
+ FROM information_schema.KEY_COLUMN_USAGE kcu
1068
+ JOIN information_schema.REFERENTIAL_CONSTRAINTS rc
1069
+ ON rc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA
1070
+ AND rc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
1071
+ WHERE kcu.TABLE_SCHEMA = ?
1072
+ AND kcu.TABLE_NAME = ?
1073
+ AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
1074
+ ${columnName ? "AND kcu.COLUMN_NAME = ?" : ""}
1075
+ `;
1076
+
1077
+ const params = columnName
1078
+ ? [databaseName, tableName, columnName]
1079
+ : [databaseName, tableName];
1080
+
1081
+ const [rows] = await connection.query(sql, params);
1082
+
1083
+ if (!rows.length) return false;
1084
+
1085
+ // single-column FK
1086
+ if (columnName) {
1087
+ const r = rows[0];
1088
+ return {
1089
+ table: r.REFERENCED_TABLE_NAME,
1090
+ column: r.REFERENCED_COLUMN_NAME,
1091
+ deleteOption: r.DELETE_RULE,
1092
+ updateOption: r.UPDATE_RULE,
1093
+ constraintName: r.CONSTRAINT_NAME
1094
+ };
1095
+ }
1096
+
1097
+ // multiple FKs
1098
+ return rows.map(r => ({
1099
+ column: r.COLUMN_NAME,
1100
+ table: r.REFERENCED_TABLE_NAME,
1101
+ referencedColumn: r.REFERENCED_COLUMN_NAME,
1102
+ deleteOption: r.DELETE_RULE,
1103
+ updateOption: r.UPDATE_RULE,
1104
+ constraintName: r.CONSTRAINT_NAME
1105
+ }));
1106
+
1107
+ } catch (err) {
1108
+ console.error("FK lookup error:", err.message);
541
1109
  return null;
542
1110
  } finally {
543
1111
  if (connection) await connection.end();
544
1112
  }
545
1113
  }
546
- async function getForeignKeyDetails(config, databaseName, tableName) {
1114
+ async function getAllForeignKeyDetails(config, databaseName, tableName) {
547
1115
  let connection;
548
1116
 
549
1117
  const query = `
@@ -574,100 +1142,302 @@ async function getForeignKeyDetails(config, databaseName, tableName) {
574
1142
  if (connection) await connection.end();
575
1143
  }
576
1144
  }
577
- async function dropTables(databaseName, tableNames) {
578
- if (typeof databaseName !== "string" || !databaseName.trim()) {
579
- throw new Error("databaseName must be a non-empty string.");
580
- }
581
- if (!Array.isArray(tableNames)) {
582
- throw new Error("tableNames must be an array of strings.");
583
- }
584
- if (tableNames.length === 0) return;
1145
+ async function addForeignKeyWithIndex(config, dbname, tableName, columnName, refTable, refColumn, options = {}) {
1146
+ const {
1147
+ onDelete = "RESTRICT",
1148
+ onUpdate = "RESTRICT"
1149
+ } = options;
585
1150
 
586
- try {
587
- console.log(`Attempting to drop tables in database ${databaseName}: ${tableNames.join(", ")}`);
1151
+ const indexName = `idx_${tableName}_${columnName}`;
1152
+ const fkName = `fk_${tableName}_${refTable}_${columnName}`;
588
1153
 
589
- // Escape table names and prefix with database name, safely quoted
590
- const escapedTables = tableNames
591
- .map(t => `\`${databaseName.replace(/`/g, "``")}\`.\`${t.replace(/`/g, "``")}\``)
592
- .join(", ");
593
-
594
- const query = `DROP TABLE IF EXISTS ${escapedTables}`;
595
- await pool.query(query);
1154
+ let connection;
1155
+ try {
1156
+ connection = await mysql.createConnection({ ...config, database: dbname });
1157
+
1158
+ // 1. Add index if it does not exist
1159
+ await connection.query(`
1160
+ ALTER TABLE \`${tableName}\`
1161
+ ADD INDEX \`${indexName}\` (\`${columnName}\`)
1162
+ `).catch(() => { }); // ignore if index already exists
1163
+
1164
+ // 2. Add foreign key
1165
+ await connection.query(`
1166
+ ALTER TABLE \`${tableName}\`
1167
+ ADD CONSTRAINT \`${fkName}\`
1168
+ FOREIGN KEY (\`${columnName}\`)
1169
+ REFERENCES \`${refTable}\` (\`${refColumn}\`)
1170
+ ON DELETE ${onDelete}
1171
+ ON UPDATE ${onUpdate}
1172
+ `);
596
1173
 
597
- console.log(`Tables dropped successfully in database ${databaseName}: ${tableNames.join(", ")}`);
598
- } catch (error) {
599
- console.error(`Error dropping tables in database ${databaseName}:`, error.message);
1174
+ return true;
1175
+ } catch (err) {
1176
+ const errmess = err.message;
1177
+ console.error("FK add error:", errmess);
1178
+ if (errmess.toLowerCase().includes("duplicate")) {
1179
+ return false;
1180
+ }
1181
+ return null;
1182
+ } finally {
1183
+ if (connection) await connection.end();
600
1184
  }
601
1185
  }
602
- async function createOrModifyTable(queryText, databaseName) {
1186
+ async function removeForeignKeyFromColumn(config, databaseName, tableName, columnName) {
1187
+ let connection;
1188
+
603
1189
  try {
604
- if (databaseName && typeof databaseName === "string" && databaseName.trim()) {
605
- // Add databaseName prefix to the table name(s) inside queryText
606
-
607
- // This is a naive regex that finds first table name after CREATE TABLE or ALTER TABLE
608
- // and prefixes it with databaseName.
609
- queryText = queryText.replace(
610
- /(CREATE TABLE IF NOT EXISTS|CREATE TABLE|ALTER TABLE)\s+(`?)(\w+)(`?)/i,
611
- (match, p1, p2, p3, p4) => {
612
- const dbNameEscaped = `\`${databaseName.replace(/`/g, "``")}\``;
613
- const tableNameEscaped = `${p2}${p3}${p4}`;
614
- return `${p1} ${dbNameEscaped}.${tableNameEscaped}`;
615
- }
616
- );
1190
+ connection = await mysql.createConnection({
1191
+ ...config,
1192
+ database: databaseName
1193
+ });
1194
+
1195
+ // 1️⃣ Find FK constraint(s) for this column
1196
+ const fkRowsSql = `
1197
+ SELECT
1198
+ CONSTRAINT_NAME,
1199
+ ORDINAL_POSITION
1200
+ FROM information_schema.KEY_COLUMN_USAGE
1201
+ WHERE TABLE_SCHEMA = ?
1202
+ AND TABLE_NAME = ?
1203
+ AND COLUMN_NAME = ?
1204
+ AND REFERENCED_TABLE_NAME IS NOT NULL
1205
+ `;
1206
+
1207
+ const [fkRows] = await connection.query(fkRowsSql, [
1208
+ databaseName,
1209
+ tableName,
1210
+ columnName
1211
+ ]);
1212
+
1213
+ if (!fkRows.length) {
1214
+ return false; // no FK
617
1215
  }
618
1216
 
619
- await pool.query(queryText);
1217
+ // Multiple rows possible for composite FKs
1218
+ const constraintName = fkRows[0].CONSTRAINT_NAME;
620
1219
 
621
- function getTableName(input) {
622
- if (typeof input !== "string") {
623
- throw new Error("Query text must be a string");
624
- }
625
- const words = input.trim().split(/\s+/);
626
- if (words.length < 5) {
627
- return "";
1220
+ // 2️⃣ Get all columns in this FK constraint
1221
+ const fkColsSql = `
1222
+ SELECT COLUMN_NAME
1223
+ FROM information_schema.KEY_COLUMN_USAGE
1224
+ WHERE TABLE_SCHEMA = ?
1225
+ AND TABLE_NAME = ?
1226
+ AND CONSTRAINT_NAME = ?
1227
+ ORDER BY ORDINAL_POSITION
1228
+ `;
1229
+
1230
+ const [fkCols] = await connection.query(fkColsSql, [
1231
+ databaseName,
1232
+ tableName,
1233
+ constraintName
1234
+ ]);
1235
+
1236
+ const fkColumnNames = fkCols.map(r => r.COLUMN_NAME);
1237
+
1238
+ // 3️⃣ Drop the foreign key constraint
1239
+ await connection.query(`
1240
+ ALTER TABLE \`${tableName}\`
1241
+ DROP FOREIGN KEY \`${constraintName}\`
1242
+ `);
1243
+
1244
+ // 4️⃣ Find candidate indexes that exactly match FK columns
1245
+ const indexSql = `
1246
+ SELECT
1247
+ INDEX_NAME,
1248
+ COLUMN_NAME,
1249
+ SEQ_IN_INDEX
1250
+ FROM information_schema.STATISTICS
1251
+ WHERE TABLE_SCHEMA = ?
1252
+ AND TABLE_NAME = ?
1253
+ AND COLUMN_NAME IN (?)
1254
+ ORDER BY INDEX_NAME, SEQ_IN_INDEX
1255
+ `;
1256
+
1257
+ const [indexRows] = await connection.query(indexSql, [
1258
+ databaseName,
1259
+ tableName,
1260
+ fkColumnNames
1261
+ ]);
1262
+
1263
+ // Group index columns
1264
+ const indexMap = new Map();
1265
+
1266
+ for (const row of indexRows) {
1267
+ if (!indexMap.has(row.INDEX_NAME)) {
1268
+ indexMap.set(row.INDEX_NAME, []);
628
1269
  }
629
- if (input.startsWith("CREATE TABLE IF NOT EXISTS") && words.length > 5) {
630
- return [words[5]];
631
- } else {
632
- return [words[2], words[5]];
1270
+ indexMap.get(row.INDEX_NAME).push(row.COLUMN_NAME);
1271
+ }
1272
+
1273
+ // 5️⃣ Drop index only if it exactly matches FK columns
1274
+ for (const [indexName, cols] of indexMap.entries()) {
1275
+ if (
1276
+ cols.length === fkColumnNames.length &&
1277
+ cols.every((c, i) => c === fkColumnNames[i])
1278
+ ) {
1279
+ await connection.query(`
1280
+ ALTER TABLE \`${tableName}\`
1281
+ DROP INDEX \`${indexName}\`
1282
+ `);
1283
+ break; // only one index per FK
633
1284
  }
634
1285
  }
635
1286
 
636
- let table_name = getTableName(queryText);
637
- if (table_name.length > 1) {
638
- return {
639
- success: true,
640
- message: `${table_name[1]} column of ${table_name[0]} table created or modified successfully.`,
641
- querytext: queryText,
642
- };
643
- } else {
644
- return {
645
- success: true,
646
- message: `${table_name[0]} table created or modified successfully.`,
647
- querytext: queryText,
648
- };
1287
+ return true;
1288
+
1289
+ } catch (err) {
1290
+ console.error("Drop FK error:", err.message);
1291
+ return null;
1292
+ } finally {
1293
+ if (connection) await connection.end();
1294
+ }
1295
+ }
1296
+ async function removeForeignKeyConstraintFromColumn(config, databaseName, tableName, columnName) {
1297
+ let connection;
1298
+ let removed = false;
1299
+
1300
+ try {
1301
+ connection = await mysql.createConnection({
1302
+ ...config,
1303
+ database: databaseName
1304
+ });
1305
+
1306
+ /* 1️⃣ Remove FOREIGN KEY if exists */
1307
+ const [fkRows] = await connection.execute(`
1308
+ SELECT CONSTRAINT_NAME
1309
+ FROM information_schema.KEY_COLUMN_USAGE
1310
+ WHERE TABLE_SCHEMA = ?
1311
+ AND TABLE_NAME = ?
1312
+ AND COLUMN_NAME = ?
1313
+ AND REFERENCED_TABLE_NAME IS NOT NULL
1314
+ `, [databaseName, tableName, columnName]);
1315
+
1316
+ for (const row of fkRows) {
1317
+ await connection.execute(`
1318
+ ALTER TABLE \`${tableName}\`
1319
+ DROP FOREIGN KEY \`${row.CONSTRAINT_NAME}\`
1320
+ `);
1321
+ removed = true;
1322
+ }
1323
+
1324
+ /* 2️⃣ Remove INDEX / UNIQUE KEY (excluding PRIMARY) */
1325
+ const [indexRows] = await connection.execute(`
1326
+ SELECT DISTINCT INDEX_NAME
1327
+ FROM information_schema.STATISTICS
1328
+ WHERE TABLE_SCHEMA = ?
1329
+ AND TABLE_NAME = ?
1330
+ AND COLUMN_NAME = ?
1331
+ AND INDEX_NAME <> 'PRIMARY'
1332
+ `, [databaseName, tableName, columnName]);
1333
+
1334
+ for (const row of indexRows) {
1335
+ await connection.execute(`
1336
+ ALTER TABLE \`${tableName}\`
1337
+ DROP INDEX \`${row.INDEX_NAME}\`
1338
+ `);
1339
+ removed = true;
649
1340
  }
1341
+
1342
+ return removed;
1343
+
1344
+ } catch (err) {
1345
+ console.error(err.message);
1346
+ return null;
1347
+ } finally {
1348
+ if (connection) await connection.end();
1349
+ }
1350
+ }
1351
+ async function columnExists(config, databaseName, tableName, columnName) {
1352
+ let connection;
1353
+ try {
1354
+ connection = await mysql.createConnection(config);
1355
+
1356
+ const [rows] = await connection.execute(
1357
+ `
1358
+ SELECT 1
1359
+ FROM INFORMATION_SCHEMA.COLUMNS
1360
+ WHERE TABLE_SCHEMA = ?
1361
+ AND TABLE_NAME = ?
1362
+ AND COLUMN_NAME = ?
1363
+ LIMIT 1
1364
+ `,
1365
+ [databaseName, tableName, columnName]
1366
+ );
1367
+
1368
+ return rows.length > 0;
1369
+ } catch (err) {
1370
+ console.error("columnExists error:", err.message);
1371
+ return null;
1372
+ } finally {
1373
+ if (connection) await connection.end();
1374
+ }
1375
+ }
1376
+ async function runQuery(config, databaseName, queryText) {
1377
+ let connection;
1378
+ try {
1379
+ if (!queryText || typeof queryText !== "string") return null;
1380
+
1381
+ connection = await mysql.createConnection({
1382
+ ...config,
1383
+ database: databaseName
1384
+ });
1385
+
1386
+ await connection.execute(queryText);
1387
+ return true;
650
1388
  } catch (err) {
651
- return { success: false, message: err.message };
1389
+ const errms = err.message;
1390
+ console.error(errms);
1391
+ return null;
1392
+ } finally {
1393
+ if (connection) await connection.end();
652
1394
  }
653
1395
  }
654
1396
 
655
1397
 
1398
+
1399
+
656
1400
  module.exports = {
1401
+ isNumber,
1402
+ getDateTime,
1403
+ removefromarray,
1404
+ getMySQLVersion,
1405
+ isMySQL578OrAbove,
1406
+ isValidMySQLConfig,
657
1407
  isMySQLDatabase,
1408
+ getCharsetAndCollations,
1409
+ isCharsetCollationValid,
1410
+ getMySQLEngines,
658
1411
  checkDatabaseExists,
659
- createDatabase,
660
1412
  getAllDatabaseNames,
1413
+ isValidMySQLIdentifier,
661
1414
  isValidDatabaseName,
662
1415
  isValidTableName,
663
1416
  isValidColumnName,
664
- parseColumnWithOptionalLoopStrict,
665
- getDateTime,
1417
+ createloopname,
1418
+ perseTableNameWithLoop,
1419
+ perseDatabaseNameWithLoop,
1420
+ getloop,
1421
+ reverseLoopName,
1422
+ stringifyAny,
666
1423
  isJsonString,
667
1424
  isJsonObject,
668
1425
  isJsonSame,
1426
+ JoinJsonObjects,
669
1427
  getTableNames,
670
1428
  getColumnNames,
1429
+ getDatabaseCharsetAndCollation,
671
1430
  getColumnDetails,
1431
+ columnHasKey,
672
1432
  getForeignKeyDetails,
1433
+ getAllForeignKeyDetails,
1434
+ addForeignKeyWithIndex,
1435
+ removeForeignKeyFromColumn,
1436
+ removeForeignKeyConstraintFromColumn,
1437
+ columnExists,
1438
+ dropDatabase,
1439
+ dropTable,
1440
+ dropColumn,
1441
+ writeJsFile,
1442
+ runQuery,
673
1443
  }