diff --git a/.gitignore b/.gitignore index f65d7d8..e21fbd2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .vscode /build /coverage +jest_0 \ No newline at end of file diff --git a/src/client/Form.jsx b/src/client/Form.jsx index 89363a5..75db499 100644 --- a/src/client/Form.jsx +++ b/src/client/Form.jsx @@ -16,7 +16,7 @@ export default class Form extends Component { user: "", password: "", database: "", - type: "PostgreSQL", + type: "", formError: { twoConnect: false, incomplete: false, emptySubmit: false } }; @@ -42,7 +42,7 @@ export default class Form extends Component { user: "", password: "", database: "", - type: "PostgreSQL", + type: "", formError: { twoConnect: false, incomplete: false, emptySubmit: false } }); } diff --git a/src/server/DBMetadata/metadataProcessor.js b/src/server/DBMetadata/metadataProcessor.js index 6df8a40..2d2fa64 100644 --- a/src/server/DBMetadata/metadataProcessor.js +++ b/src/server/DBMetadata/metadataProcessor.js @@ -1,70 +1,63 @@ const ProcessedField = require("./classes/processedField"); const ProcessedTable = require("./classes/processedTable"); -function processMetadata(translateColumnType) { - return columnData => { - if (!columnData || columnData.length === 0) - throw new Error("Metadata is null or empty"); +function processMetadata(columnData) { + if (!columnData || columnData.length === 0) + throw new Error("Metadata is null or empty"); - if (!Array.isArray(columnData)) - throw new Error("Invalid data format. Column Data must be an array"); + if (!Array.isArray(columnData)) + throw new Error("Invalid data format. Column Data must be an array"); - let tblIdx = 0; - let fieldIdx = 0; - let prevTable = columnData[0].table_name; - let props = []; + let tblIdx = 0; + let fieldIdx = 0; + let prevTable = columnData[0].table_name; + let props = []; - let lookupFields = {}; + let lookupFields = {}; - const lookup = {}; - const toRef = {}; + const lookup = {}; + const toRef = {}; - const data = { - tables: {} - }; + const data = { + tables: {} + }; - columnData.forEach((tblCol, index) => { - // Previous table evaluation complete, format and assign to it the accumulated field data. - if (prevTable !== tblCol.table_name) { - data.tables[tblIdx] = new ProcessedTable(prevTable, props); - lookupFields["INDEX"] = tblIdx; - lookup[prevTable] = lookupFields; + columnData.forEach((tblCol, index) => { + // Previous table evaluation complete, format and assign to it the accumulated field data. + if (prevTable !== tblCol.table_name) { + data.tables[tblIdx] = new ProcessedTable(prevTable, props); + lookupFields["INDEX"] = tblIdx; + lookup[prevTable] = lookupFields; - tblIdx++; - fieldIdx = 0; + tblIdx++; + fieldIdx = 0; - props = []; - lookupFields = {}; - } + props = []; + lookupFields = {}; + } - if (index === columnData.length - 1) { - data.tables[tblIdx] = new ProcessedTable(prevTable, props); - } + if (index === columnData.length - 1) { + data.tables[tblIdx] = new ProcessedTable(prevTable, props); + } - const processed = new ProcessedField( - tblCol, - tblIdx, - fieldIdx, - translateColumnType - ); + const processed = new ProcessedField(tblCol, tblIdx, fieldIdx); - if (tableInToRef(toRef, tblCol)) { - processed.addRetroRelationship(toRef, tblCol, data); - } + if (tableInToRef(toRef, tblCol)) { + processed.addRetroRelationship(toRef, tblCol, data); + } - if (tblCol.constraint_type === "FOREIGN KEY") { - processed.addForeignKeyRef(lookup, tblCol, toRef, data); - } + if (tblCol.constraint_type === "FOREIGN KEY") { + processed.addForeignKeyRef(lookup, tblCol, toRef, data); + } - props.push(processed); - lookupFields[tblCol.column_name] = fieldIdx; + props.push(processed); + lookupFields[tblCol.column_name] = fieldIdx; - prevTable = tblCol.table_name; - fieldIdx++; - }); + prevTable = tblCol.table_name; + fieldIdx++; + }); - return data; - }; + return data; } function tableInToRef(toRef, tblCol) { diff --git a/src/server/DBMetadata/mysql/mysqlMetadataRetriever.js b/src/server/DBMetadata/mysql/mysqlMetadataRetriever.js new file mode 100644 index 0000000..073a4ef --- /dev/null +++ b/src/server/DBMetadata/mysql/mysqlMetadataRetriever.js @@ -0,0 +1,75 @@ +const mysql = require("mysql"); +const { URL } = require("url"); + +const metadataQuery = `SELECT distinct +t.table_name, +c.column_name, +c.is_nullable, +c.data_type, +c.character_maximum_length, +tc.constraint_type, +ccu.referenced_table_name AS foreign_table_name, +ccu.referenced_column_name AS foreign_column_name +FROM +information_schema.tables AS t JOIN information_schema.columns as c + ON t.table_name = c.table_name +LEFT JOIN information_schema.key_column_usage as kcu + ON t.table_name = kcu.table_name AND c.column_name = kcu.column_name +LEFT JOIN information_schema.table_constraints as tc + ON kcu.constraint_name = tc.constraint_name +LEFT JOIN information_schema.key_column_usage AS ccu + ON tc.constraint_name = ccu.constraint_name +WHERE table_type = 'BASE TABLE' +AND (constraint_type = 'FOREIGN KEY' OR (constraint_type is null OR constraint_type <> 'FOREIGN KEY')) +AND t.table_schema not in ('information_schema', 'mysql', 'performance_schema', 'phpmyadmin','sys', 'test') +ORDER BY table_name;`; + +function getSchemaInfo(connString) { + const connection = mysql.createConnection(buildMysqlParams(connString)); + + return new Promise((resolve, reject) => { + try { + connection.query(metadataQuery, (error, results) => { + if (error) reject(error); + resolve(results); + }); + } finally { + connection.end(); + } + }); +} + +function parseUri(uri) { + const { + protocol = "", + username: user, + password, + port, + hostname: host, + pathname = "" + } = new URL(uri); + return { + scheme: protocol.replace(":", ""), + user, + password, + host, + port, + database: pathname.replace("/", "") + }; +} + +function buildMysqlParams(uri) { + const { user, password, host, port, database } = parseUri(uri); + return { + host: host, + user: user, + password: password, + database: database + // port: Number(port) + }; +} + +module.exports = { + getSchemaInfo, + buildMysqlParams +}; diff --git a/src/server/DBMetadata/mysql/mysqlMetadataRetriever.test.js b/src/server/DBMetadata/mysql/mysqlMetadataRetriever.test.js new file mode 100644 index 0000000..1f5193c --- /dev/null +++ b/src/server/DBMetadata/mysql/mysqlMetadataRetriever.test.js @@ -0,0 +1,15 @@ +const pgMetadataRetriever = require("./mysqlMetadataRetriever"); + +test("takes in connection string returns properly formatted connection object", () => { + expect( + pgMetadataRetriever.buildMysqlParams( + "mysql://root:pass@cloud.com:3306/test-db" + ) + ).toStrictEqual({ + user: "root", + password: "pass", + host: "cloud.com", + database: "test-db", + port: 3306 + }); +}); diff --git a/src/server/DBMetadata/postgres/pgMetadataRetriever.js b/src/server/DBMetadata/postgres/pgMetadataRetriever.js new file mode 100644 index 0000000..f31b8d0 --- /dev/null +++ b/src/server/DBMetadata/postgres/pgMetadataRetriever.js @@ -0,0 +1,78 @@ +const pgp = require("pg-promise")(); +const crypto = require("crypto"); +const utilty = require("../../util"); + +const poolCache = {}; + +const metadataQuery = `SELECT +t.table_name, +c.column_name, +c.is_nullable, +c.data_type, +c.character_maximum_length, +tc.constraint_type, +ccu.table_name AS foreign_table_name, +ccu.column_name AS foreign_column_name +FROM +information_schema.tables AS t JOIN information_schema.columns as c + ON t.table_name = c.table_name +LEFT JOIN information_schema.key_column_usage as kcu + ON t.table_name = kcu.table_name AND c.column_name = kcu.column_name +LEFT JOIN information_schema.table_constraints as tc + ON kcu.constraint_name = tc.constraint_name +LEFT JOIN information_schema.constraint_column_usage AS ccu + ON tc.constraint_name = ccu.constraint_name +WHERE table_type = 'BASE TABLE' +AND t.table_schema = 'public' +AND (constraint_type = 'FOREIGN KEY' or (constraint_type is null OR constraint_type <> 'FOREIGN KEY')) +ORDER BY t.table_name`; + +async function getSchemaInfo(connString) { + const db = getDbPool(connString); + try { + return (metadataInfo = await utilty.promiseTimeout( + 10000, + db.any(metadataQuery) + )); + } catch (err) { + removeFromCache(connString); + throw err; + } +} + +function getDbPool(connString) { + const hash = crypto.createHash("sha256"); + hash.update(connString); + + const digest = hash.digest("base64"); + + if (poolCache[digest]) { + return poolCache[digest]; + } + + let db = pgp(connString); + poolCache[digest] = db; + + return db; +} + +function removeFromCache(connString) { + const hash = crypto.createHash("sha256"); + hash.update(connString); + + delete poolCache[hash.digest("base64")]; +} + +function buildConnectionString(info) { + let connectionString = ""; + info.port = info.port || 5432; + connectionString += `postgres://${info.user}:${info.password}@${info.host}:${ + info.port + }/${info.database}`; + return connectionString; +} + +module.exports = { + getSchemaInfo, + buildConnectionString +}; diff --git a/src/server/DBMetadata/postgres/pgMetadataRetriever.test.js b/src/server/DBMetadata/postgres/pgMetadataRetriever.test.js new file mode 100644 index 0000000..4619632 --- /dev/null +++ b/src/server/DBMetadata/postgres/pgMetadataRetriever.test.js @@ -0,0 +1,15 @@ +const pgMetadataRetriever = require("./pgMetadataRetriever"); + +test("takes in object with data returns properly formatted string", () => { + expect( + pgMetadataRetriever.buildConnectionString({ + user: "user", + password: "securePassword", + port: 5432, + host: "stampy.db.elephantsql.com", + database: "database" + }) + ).toBe( + "postgres://user:securePassword@stampy.db.elephantsql.com:5432/database" + ); +}); diff --git a/src/server/Generators/classes/mysqlProvider.js b/src/server/Generators/classes/mysqlProvider.js new file mode 100644 index 0000000..d7bd66e --- /dev/null +++ b/src/server/Generators/classes/mysqlProvider.js @@ -0,0 +1,87 @@ +const tab = ` `; + +class MySqlProvider { + constructor(connString) { + this.connString = connString; + } + + connection() { + let conn = `const mysql = require("mysql");\n`; + conn += `// WARNING - Properly secure the connection string\n`; + conn += `const connection = mysql.createConnection(buildMysqlParams(${ + this.connString + }));\n`; + + return conn; + } + + selectWithWhere(table, col, val, returnsMany) { + let query = `'SELECT * FROM "${table}" WHERE "${col}" = $1';\n`; + + returnsMany + ? (query += `${tab.repeat(4)}return connection.query(sql, ${val})\n`) + : (query += `${tab.repeat(4)}return connection.query(sql, ${val})\n`); + + query += addPromiseResolution(); + + return query; + } + + select(table) { + let query = `'SELECT * FROM "${table}"';\n`; + query += `${tab.repeat(4)}return connection.query(sql)\n`; + + query += addPromiseResolution(); + + return query; + } + + insert(table, cols, args) { + const normalized = args + .split(",") + .map(a => a.replace(/[' | { | } | \$]/g, "")); + + const params = normalized.map((val, idx) => `$${idx + 1}`).join(", "); + + let query = `'INSERT INTO "${table}" (${cols}) VALUES (${params}) RETURNING *';\n`; + query += `${tab.repeat(4)}return connection.query(sql, [${normalized.join( + ", " + )}])\n`; + + query += addPromiseResolution(); + + return query; + } + + update(table, idColumnName) { + let query = `\`UPDATE "${table}" SET \${updateValues} WHERE "${idColumnName}" = $1 RETURNING *\`;\n`; + query += `${tab.repeat( + 4 + )}return connection.query(sql, [id, ...Object.values(rest)])\n`; + + query += addPromiseResolution(); + return query; + } + + delete(table, column) { + let query = `'DELETE FROM "${table}" WHERE "${column}" = $1';\n`; + query += `${tab.repeat(4)}return connection.query(sql, args.${column})\n`; + + query += addPromiseResolution(); + + return query; + } +} + +const addPromiseResolution = () => { + let str = `${tab.repeat(5)}.then(data => {\n`; + str += `${tab.repeat(6)}return data;\n`; + str += `${tab.repeat(5)}})\n`; + str += `${tab.repeat(5)}.catch(err => {\n`; + str += `${tab.repeat(6)}return ('The error is', err);\n`; + str += `${tab.repeat(5)}})`; + + return str; +}; + +module.exports = MySqlProvider;