From dd38cf293b9e2a241e81d0bee1e571cc6a5c7b33 Mon Sep 17 00:00:00 2001 From: Ian Laue Date: Tue, 21 May 2019 21:01:17 -0700 Subject: [PATCH 1/2] adding in mysql provider | allowing for db selection on UI | changed file structure for db providers --- npm-debug.log | 23 ++++++ src/client/Form.jsx | 5 +- .../mysql/mysqlMetadataProcessor.js | 70 ++++++++++++++++ .../mysql/mysqlMetadataProcessor.test.js | 34 ++++++++ .../mysql/mysqlMetadataRetriever.js | 73 +++++++++++++++++ .../mysqlMetadataRetriever.test.js} | 0 .../{ => postgres}/pgMetadataProcessor.js | 4 +- .../pgMetadataProcessor.test.js | 4 +- .../{ => postgres}/pgMetadataRetriever.js | 29 +++---- .../postgres/pgMetadataRetriever.test.js | 15 ++++ src/server/server.js | 79 +++++++++++++------ 11 files changed, 291 insertions(+), 45 deletions(-) create mode 100644 npm-debug.log create mode 100644 src/server/DBMetadata/mysql/mysqlMetadataProcessor.js create mode 100644 src/server/DBMetadata/mysql/mysqlMetadataProcessor.test.js create mode 100644 src/server/DBMetadata/mysql/mysqlMetadataRetriever.js rename src/server/DBMetadata/{pgMetadataRetriever.test.js => mysql/mysqlMetadataRetriever.test.js} (100%) rename src/server/DBMetadata/{ => postgres}/pgMetadataProcessor.js (93%) rename src/server/DBMetadata/{ => postgres}/pgMetadataProcessor.test.js (87%) rename src/server/DBMetadata/{ => postgres}/pgMetadataRetriever.js (76%) create mode 100644 src/server/DBMetadata/postgres/pgMetadataRetriever.test.js diff --git a/npm-debug.log b/npm-debug.log new file mode 100644 index 0000000..70504f2 --- /dev/null +++ b/npm-debug.log @@ -0,0 +1,23 @@ +0 info it worked if it ends with ok +1 verbose cli [ '/usr/bin/node', '/usr/bin/npm', 'ls', '--depth', '0', '--json' ] +2 info using npm@3.5.2 +3 info using node@v8.10.0 +4 error peer dep missing: file-loader@^1.1.6, required by ttf-loader@1.0.2 +4 error extraneous: electron-store@3.2.0 /home/ian/repos/SwitchQL/node_modules/electron-store +4 error peer dep missing: acorn@^6.0.0, required by acorn-dynamic-import@4.0.0 +5 verbose exit [ 1, true ] +6 verbose stack Error: write EPIPE +6 verbose stack at _errnoException (util.js:1022:11) +6 verbose stack at WriteWrap.afterWrite [as oncomplete] (net.js:880:14) +7 verbose cwd /home/ian/repos/SwitchQL +8 error Linux 4.18.0-20-generic +9 error argv "/usr/bin/node" "/usr/bin/npm" "ls" "--depth" "0" "--json" +10 error node v8.10.0 +11 error npm v3.5.2 +12 error code EPIPE +13 error errno EPIPE +14 error syscall write +15 error write EPIPE +16 error If you need help, you may report this error at: +16 error +17 verbose exit [ 1, true ] diff --git a/src/client/Form.jsx b/src/client/Form.jsx index 554a213..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 } }); } @@ -134,7 +134,6 @@ export default class Form extends Component { value={n} checked={this.state.type === n} onChange={this.valueChange} - disabled /> {n} diff --git a/src/server/DBMetadata/mysql/mysqlMetadataProcessor.js b/src/server/DBMetadata/mysql/mysqlMetadataProcessor.js new file mode 100644 index 0000000..563d17e --- /dev/null +++ b/src/server/DBMetadata/mysql/mysqlMetadataProcessor.js @@ -0,0 +1,70 @@ +const ProcessedField = require("../classes/processedField"); +const ProcessedTable = require("../classes/processedTable"); + +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"); + + let tblIdx = 0; + let fieldIdx = 0; + let prevTable = columnData[0].table_name; + let props = []; + + let lookupFields = {}; + + const lookup = {}; + const toRef = {}; + + 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; + + tblIdx++; + fieldIdx = 0; + + props = []; + lookupFields = {}; + } + + if (index === columnData.length - 1) { + data.tables[tblIdx] = new ProcessedTable(prevTable, props); + } + + const processed = new ProcessedField(tblCol, tblIdx, fieldIdx); + + if (tableInToRef(toRef, tblCol)) { + processed.addRetroRelationship(toRef, tblCol, data); + } + + if (tblCol.constraint_type === "FOREIGN KEY") { + processed.addForeignKeyRef(lookup, tblCol, toRef, data); + } + + props.push(processed); + lookupFields[tblCol.column_name] = fieldIdx; + + prevTable = tblCol.table_name; + fieldIdx++; + }); + + return data; +} + +function tableInToRef(toRef, tblCol) { + return ( + toRef.hasOwnProperty(tblCol.table_name) && + toRef[tblCol.table_name].hasOwnProperty(tblCol.column_name) + ); +} + +module.exports = processMetadata; diff --git a/src/server/DBMetadata/mysql/mysqlMetadataProcessor.test.js b/src/server/DBMetadata/mysql/mysqlMetadataProcessor.test.js new file mode 100644 index 0000000..5c36b94 --- /dev/null +++ b/src/server/DBMetadata/mysql/mysqlMetadataProcessor.test.js @@ -0,0 +1,34 @@ +import processed from "../sampleFiles/processedMetadata"; +import retrieved from "../sampleFiles/retrievedMetadata"; + +import processMetadata from "./pgMetadataProcessor"; + +describe("Format Metadata Tests", () => { + it("Should return correctly formatted metadata given sample input", () => { + const result = processMetadata(retrieved); + expect(result).toEqual(processed); + }); + + it("Throws an error on null or empty data", () => { + const metadataTest = () => { + processMetadata([]); + }; + + expect(metadataTest).toThrowError("Metadata is null or empty"); + }); + + it("Throws an error on non array input", () => { + const metadataTest = () => { + processMetadata({}); + }; + + expect(metadataTest).toThrowError( + "Invalid data format. Column Data must be an array" + ); + }); + + it("Should match the snapshot", () => { + const result = processMetadata(retrieved); + expect(result).toMatchSnapshot(); + }); +}); diff --git a/src/server/DBMetadata/mysql/mysqlMetadataRetriever.js b/src/server/DBMetadata/mysql/mysqlMetadataRetriever.js new file mode 100644 index 0000000..cdcc267 --- /dev/null +++ b/src/server/DBMetadata/mysql/mysqlMetadataRetriever.js @@ -0,0 +1,73 @@ +const mysql = require("mysql"); + +// var connection = mysql.createConnection({ +// host: "localhost", +// user: "me", +// password: "secret", +// database: "my_db" +// }); + +// + +// + +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;`; + +async function getSchemaInfoPG(connString) { + if (notRightFormat) { + var connection = mysql.createConnection({ + host: "switchql-mysql.c9vnkgo31mgw.us-west-1.rds.amazonaws.com", + user: "admin", + password: "password", + database: "my_db" + }); + } + + connection.connect(); + + try { + return connection.query(metadataQuery, (error, results, fields) => { + console.log("The error: " + error); + console.log("The solution is: ", results[0].solution); + console.log("The fields: " + fields); + connection.end(); + }); + } catch (err) { + throw err; + } +} + +function buildConnectionString(info) { + let connectionString = ""; + info.port = info.port || 5432; + connectionString += `mysql://${info.user}:${info.password}@${info.host}:${ + info.port + }/${info.database}`; + return connectionString; +} + +module.exports = { + getSchemaInfoPG, + buildConnectionString +}; diff --git a/src/server/DBMetadata/pgMetadataRetriever.test.js b/src/server/DBMetadata/mysql/mysqlMetadataRetriever.test.js similarity index 100% rename from src/server/DBMetadata/pgMetadataRetriever.test.js rename to src/server/DBMetadata/mysql/mysqlMetadataRetriever.test.js diff --git a/src/server/DBMetadata/pgMetadataProcessor.js b/src/server/DBMetadata/postgres/pgMetadataProcessor.js similarity index 93% rename from src/server/DBMetadata/pgMetadataProcessor.js rename to src/server/DBMetadata/postgres/pgMetadataProcessor.js index 2d2fa64..563d17e 100644 --- a/src/server/DBMetadata/pgMetadataProcessor.js +++ b/src/server/DBMetadata/postgres/pgMetadataProcessor.js @@ -1,5 +1,5 @@ -const ProcessedField = require("./classes/processedField"); -const ProcessedTable = require("./classes/processedTable"); +const ProcessedField = require("../classes/processedField"); +const ProcessedTable = require("../classes/processedTable"); function processMetadata(columnData) { if (!columnData || columnData.length === 0) diff --git a/src/server/DBMetadata/pgMetadataProcessor.test.js b/src/server/DBMetadata/postgres/pgMetadataProcessor.test.js similarity index 87% rename from src/server/DBMetadata/pgMetadataProcessor.test.js rename to src/server/DBMetadata/postgres/pgMetadataProcessor.test.js index 9f612f5..5c36b94 100644 --- a/src/server/DBMetadata/pgMetadataProcessor.test.js +++ b/src/server/DBMetadata/postgres/pgMetadataProcessor.test.js @@ -1,5 +1,5 @@ -import processed from "./sampleFiles/processedMetadata"; -import retrieved from "./sampleFiles/retrievedMetadata"; +import processed from "../sampleFiles/processedMetadata"; +import retrieved from "../sampleFiles/retrievedMetadata"; import processMetadata from "./pgMetadataProcessor"; diff --git a/src/server/DBMetadata/pgMetadataRetriever.js b/src/server/DBMetadata/postgres/pgMetadataRetriever.js similarity index 76% rename from src/server/DBMetadata/pgMetadataRetriever.js rename to src/server/DBMetadata/postgres/pgMetadataRetriever.js index c41008a..5292929 100644 --- a/src/server/DBMetadata/pgMetadataRetriever.js +++ b/src/server/DBMetadata/postgres/pgMetadataRetriever.js @@ -1,6 +1,6 @@ const pgp = require("pg-promise")(); -const crypto = require('crypto'); -const utilty = require('../util'); +const crypto = require("crypto"); +const utilty = require("../../util"); const poolCache = {}; @@ -27,23 +27,24 @@ 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 getSchemaInfoPG(connString) { const db = getDbPool(connString); try { - return metadataInfo = await utilty.promiseTimeout(10000, db.any(metadataQuery)); + return (metadataInfo = await utilty.promiseTimeout( + 10000, + db.any(metadataQuery) + )); } catch (err) { - removeFromCache(connString) + removeFromCache(connString); throw err; } } function getDbPool(connString) { - const hash = crypto.createHash('sha256'); - hash.update(connString) + const hash = crypto.createHash("sha256"); + hash.update(connString); - const digest = hash.digest('base64') + const digest = hash.digest("base64"); if (poolCache[digest]) { return poolCache[digest]; @@ -56,10 +57,10 @@ function getDbPool(connString) { } function removeFromCache(connString) { - const hash = crypto.createHash('sha256'); - hash.update(connString) + const hash = crypto.createHash("sha256"); + hash.update(connString); - delete poolCache[hash.digest('base64')] + delete poolCache[hash.digest("base64")]; } function buildConnectionString(info) { @@ -67,11 +68,11 @@ function buildConnectionString(info) { info.port = info.port || 5432; connectionString += `postgres://${info.user}:${info.password}@${info.host}:${ info.port - }/${info.database}`; + }/${info.database}`; return connectionString; } module.exports = { getSchemaInfoPG, buildConnectionString -}; \ No newline at end of file +}; 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/server.js b/src/server/server.js index c9a9c85..b1aafdb 100644 --- a/src/server/server.js +++ b/src/server/server.js @@ -1,7 +1,9 @@ const electron = require("electron"); const { ipcMain } = electron; -const dbController = require("./DBMetadata/pgMetadataRetriever"); -const processMetaData = require("./DBMetadata/pgMetadataProcessor"); +const pgDBController = require("./DBMetadata/postgres/pgMetadataRetriever"); +const pgProcessMetaData = require("./DBMetadata/postgres/pgMetadataProcessor"); +const mysqlDBController = require("./DBMetadata/mysql/mysqlMetadataRetriever"); +const mysqlProcessMetaData = require("./DBMetadata/mysql/mysqlMetadataProcessor"); const generateGraphQL = require("./Generators/graphQLGenerator"); const fs = require("fs"); const JSZip = require("jszip"); @@ -14,32 +16,61 @@ let mutationsMetaData; let queriesMetaData; ipcMain.on(events.URL, async (event, info) => { - try { - info = JSON.parse(info); - if (info.value.length === 0) { - info.value = dbController.buildConnectionString(info); + info = JSON.parse(info); + if (info.type == "MySQL") { + try { + if (info.value.length === 0) { + info.value = mysqlDBController.buildConnectionString(info); + } + + const dbMetaData = await mysqlDBController.getSchemaInfoPG(info.value); + const formattedMetaData = mysqlProcessMetaData(dbMetaData); + + ({ + types: schemaMetaData, + mutations: mutationsMetaData, + queries: queriesMetaData + } = generateGraphQL( + formattedMetaData.tables, + new PgSqlProvider(info.value) + )); + + const gqlData = { + schema: schemaMetaData, + mutations: mutationsMetaData, + queries: queriesMetaData + }; + event.sender.send(events.DATA, JSON.stringify(gqlData)); + } catch (err) { + event.sender.send(events.APP_ERROR, JSON.stringify(err)); } + } else if (info.type == "PostgreSQL") { + try { + if (info.value.length === 0) { + info.value = pgDBController.buildConnectionString(info); + } - const dbMetaData = await dbController.getSchemaInfoPG(info.value); - const formattedMetaData = processMetaData(dbMetaData); + const dbMetaData = await pgDBController.getSchemaInfoPG(info.value); + const formattedMetaData = pgProcessMetaData(dbMetaData); - ({ - types: schemaMetaData, - mutations: mutationsMetaData, - queries: queriesMetaData - } = generateGraphQL( - formattedMetaData.tables, - new PgSqlProvider(info.value) - )); + ({ + types: schemaMetaData, + mutations: mutationsMetaData, + queries: queriesMetaData + } = generateGraphQL( + formattedMetaData.tables, + new PgSqlProvider(info.value) + )); - const gqlData = { - schema: schemaMetaData, - mutations: mutationsMetaData, - queries: queriesMetaData - }; - event.sender.send(events.DATA, JSON.stringify(gqlData)); - } catch (err) { - event.sender.send(events.APP_ERROR, JSON.stringify(err)); + const gqlData = { + schema: schemaMetaData, + mutations: mutationsMetaData, + queries: queriesMetaData + }; + event.sender.send(events.DATA, JSON.stringify(gqlData)); + } catch (err) { + event.sender.send(events.APP_ERROR, JSON.stringify(err)); + } } }); From 09b1596e90efb6605a1c04e91995bda8b8c70123 Mon Sep 17 00:00:00 2001 From: Ian Laue Date: Sat, 8 Jun 2019 13:54:57 -0700 Subject: [PATCH 2/2] added mysql db meta data calls --- .gitignore | 1 + npm-debug.log | 23 - .../metadataProcessor.test.js.snap | 418 ++++++++++++++++++ ...adataProcessor.js => metadataProcessor.js} | 4 +- ...ssor.test.js => metadataProcessor.test.js} | 6 +- .../mysql/mysqlMetadataProcessor.test.js | 34 -- .../mysql/mysqlMetadataRetriever.js | 82 ++-- .../mysql/mysqlMetadataRetriever.test.js | 24 +- .../postgres/pgMetadataProcessor.js | 70 --- .../postgres/pgMetadataRetriever.js | 4 +- .../Generators/classes/mysqlProvider.js | 87 ++++ 11 files changed, 567 insertions(+), 186 deletions(-) delete mode 100644 npm-debug.log create mode 100644 src/server/DBMetadata/__snapshots__/metadataProcessor.test.js.snap rename src/server/DBMetadata/{mysql/mysqlMetadataProcessor.js => metadataProcessor.js} (93%) rename src/server/DBMetadata/{postgres/pgMetadataProcessor.test.js => metadataProcessor.test.js} (82%) delete mode 100644 src/server/DBMetadata/mysql/mysqlMetadataProcessor.test.js delete mode 100644 src/server/DBMetadata/postgres/pgMetadataProcessor.js create mode 100644 src/server/Generators/classes/mysqlProvider.js 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/npm-debug.log b/npm-debug.log deleted file mode 100644 index 70504f2..0000000 --- a/npm-debug.log +++ /dev/null @@ -1,23 +0,0 @@ -0 info it worked if it ends with ok -1 verbose cli [ '/usr/bin/node', '/usr/bin/npm', 'ls', '--depth', '0', '--json' ] -2 info using npm@3.5.2 -3 info using node@v8.10.0 -4 error peer dep missing: file-loader@^1.1.6, required by ttf-loader@1.0.2 -4 error extraneous: electron-store@3.2.0 /home/ian/repos/SwitchQL/node_modules/electron-store -4 error peer dep missing: acorn@^6.0.0, required by acorn-dynamic-import@4.0.0 -5 verbose exit [ 1, true ] -6 verbose stack Error: write EPIPE -6 verbose stack at _errnoException (util.js:1022:11) -6 verbose stack at WriteWrap.afterWrite [as oncomplete] (net.js:880:14) -7 verbose cwd /home/ian/repos/SwitchQL -8 error Linux 4.18.0-20-generic -9 error argv "/usr/bin/node" "/usr/bin/npm" "ls" "--depth" "0" "--json" -10 error node v8.10.0 -11 error npm v3.5.2 -12 error code EPIPE -13 error errno EPIPE -14 error syscall write -15 error write EPIPE -16 error If you need help, you may report this error at: -16 error -17 verbose exit [ 1, true ] diff --git a/src/server/DBMetadata/__snapshots__/metadataProcessor.test.js.snap b/src/server/DBMetadata/__snapshots__/metadataProcessor.test.js.snap new file mode 100644 index 0000000..4020753 --- /dev/null +++ b/src/server/DBMetadata/__snapshots__/metadataProcessor.test.js.snap @@ -0,0 +1,418 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Format Metadata Tests Should match the snapshot 1`] = ` +Object { + "tables": Object { + "0": ProcessedTable { + "fields": Array [ + ProcessedField { + "fieldNum": 0, + "inRelationship": true, + "name": "id", + "primaryKey": true, + "relation": Object { + "2.5": Object { + "refField": 5, + "refTable": 2, + "refType": "one to many", + }, + }, + "required": true, + "tableNum": 0, + "type": "ID", + "unique": false, + }, + ProcessedField { + "fieldNum": 1, + "inRelationship": false, + "name": "name", + "primaryKey": false, + "relation": Object {}, + "required": false, + "tableNum": 0, + "type": "String", + "unique": false, + }, + ], + "name": "author", + }, + "1": ProcessedTable { + "fields": Array [ + ProcessedField { + "fieldNum": 0, + "inRelationship": false, + "name": "id", + "primaryKey": true, + "relation": Object {}, + "required": true, + "tableNum": 1, + "type": "ID", + "unique": false, + }, + ProcessedField { + "fieldNum": 1, + "inRelationship": true, + "name": "book_id", + "primaryKey": false, + "relation": Object { + "2.1": Object { + "refField": 1, + "refTable": 2, + "refType": "many to one", + }, + }, + "required": false, + "tableNum": 1, + "type": "ID", + "unique": false, + }, + ProcessedField { + "fieldNum": 2, + "inRelationship": true, + "name": "order_id", + "primaryKey": false, + "relation": Object { + "4.0": Object { + "refField": 0, + "refTable": 4, + "refType": "many to one", + }, + }, + "required": false, + "tableNum": 1, + "type": "ID", + "unique": false, + }, + ], + "name": "book_order", + }, + "2": ProcessedTable { + "fields": Array [ + ProcessedField { + "fieldNum": 0, + "inRelationship": true, + "name": "genre_id", + "primaryKey": false, + "relation": Object { + "3.0": Object { + "refField": 0, + "refTable": 3, + "refType": "many to one", + }, + }, + "required": false, + "tableNum": 2, + "type": "ID", + "unique": false, + }, + ProcessedField { + "fieldNum": 1, + "inRelationship": true, + "name": "id", + "primaryKey": true, + "relation": Object { + "1.1": Object { + "refField": 1, + "refTable": 1, + "refType": "one to many", + }, + }, + "required": true, + "tableNum": 2, + "type": "ID", + "unique": false, + }, + ProcessedField { + "fieldNum": 2, + "inRelationship": false, + "name": "test", + "primaryKey": false, + "relation": Object {}, + "required": false, + "tableNum": 2, + "type": "Float", + "unique": false, + }, + ProcessedField { + "fieldNum": 3, + "inRelationship": false, + "name": "name", + "primaryKey": false, + "relation": Object {}, + "required": false, + "tableNum": 2, + "type": "String", + "unique": false, + }, + ProcessedField { + "fieldNum": 4, + "inRelationship": false, + "name": "publish_date", + "primaryKey": false, + "relation": Object {}, + "required": false, + "tableNum": 2, + "type": "Date", + "unique": false, + }, + ProcessedField { + "fieldNum": 5, + "inRelationship": true, + "name": "author_id", + "primaryKey": false, + "relation": Object { + "0.0": Object { + "refField": 0, + "refTable": 0, + "refType": "many to one", + }, + }, + "required": false, + "tableNum": 2, + "type": "ID", + "unique": false, + }, + ], + "name": "books", + }, + "3": ProcessedTable { + "fields": Array [ + ProcessedField { + "fieldNum": 0, + "inRelationship": true, + "name": "id", + "primaryKey": true, + "relation": Object { + "2.0": Object { + "refField": 0, + "refTable": 2, + "refType": "one to many", + }, + }, + "required": true, + "tableNum": 3, + "type": "ID", + "unique": false, + }, + ProcessedField { + "fieldNum": 1, + "inRelationship": false, + "name": "name", + "primaryKey": false, + "relation": Object {}, + "required": false, + "tableNum": 3, + "type": "String", + "unique": false, + }, + ], + "name": "genre", + }, + "4": ProcessedTable { + "fields": Array [ + ProcessedField { + "fieldNum": 0, + "inRelationship": true, + "name": "id", + "primaryKey": true, + "relation": Object { + "1.2": Object { + "refField": 2, + "refTable": 1, + "refType": "one to many", + }, + }, + "required": true, + "tableNum": 4, + "type": "ID", + "unique": false, + }, + ProcessedField { + "fieldNum": 1, + "inRelationship": false, + "name": "created_at", + "primaryKey": false, + "relation": Object {}, + "required": false, + "tableNum": 4, + "type": "Date", + "unique": false, + }, + ProcessedField { + "fieldNum": 2, + "inRelationship": true, + "name": "user_id", + "primaryKey": false, + "relation": Object { + "7.0": Object { + "refField": 0, + "refTable": 7, + "refType": "many to one", + }, + }, + "required": false, + "tableNum": 4, + "type": "ID", + "unique": false, + }, + ProcessedField { + "fieldNum": 3, + "inRelationship": true, + "name": "status_id", + "primaryKey": false, + "relation": Object { + "6.0": Object { + "refField": 0, + "refTable": 6, + "refType": "many to one", + }, + }, + "required": false, + "tableNum": 4, + "type": "ID", + "unique": false, + }, + ProcessedField { + "fieldNum": 4, + "inRelationship": true, + "name": "shipping_id", + "primaryKey": false, + "relation": Object { + "5.0": Object { + "refField": 0, + "refTable": 5, + "refType": "many to one", + }, + }, + "required": false, + "tableNum": 4, + "type": "ID", + "unique": false, + }, + ], + "name": "order", + }, + "5": ProcessedTable { + "fields": Array [ + ProcessedField { + "fieldNum": 0, + "inRelationship": true, + "name": "id", + "primaryKey": true, + "relation": Object { + "4.4": Object { + "refField": 4, + "refTable": 4, + "refType": "one to many", + }, + }, + "required": true, + "tableNum": 5, + "type": "ID", + "unique": false, + }, + ProcessedField { + "fieldNum": 1, + "inRelationship": false, + "name": "method", + "primaryKey": false, + "relation": Object {}, + "required": false, + "tableNum": 5, + "type": "String", + "unique": false, + }, + ], + "name": "shipping_method", + }, + "6": ProcessedTable { + "fields": Array [ + ProcessedField { + "fieldNum": 0, + "inRelationship": true, + "name": "id", + "primaryKey": true, + "relation": Object { + "4.3": Object { + "refField": 3, + "refTable": 4, + "refType": "one to many", + }, + }, + "required": true, + "tableNum": 6, + "type": "ID", + "unique": false, + }, + ProcessedField { + "fieldNum": 1, + "inRelationship": false, + "name": "code", + "primaryKey": false, + "relation": Object {}, + "required": false, + "tableNum": 6, + "type": "String", + "unique": false, + }, + ], + "name": "status", + }, + "7": ProcessedTable { + "fields": Array [ + ProcessedField { + "fieldNum": 0, + "inRelationship": true, + "name": "id", + "primaryKey": true, + "relation": Object { + "4.2": Object { + "refField": 2, + "refTable": 4, + "refType": "one to many", + }, + }, + "required": true, + "tableNum": 7, + "type": "ID", + "unique": false, + }, + ProcessedField { + "fieldNum": 1, + "inRelationship": false, + "name": "phone_number", + "primaryKey": false, + "relation": Object {}, + "required": false, + "tableNum": 7, + "type": "String", + "unique": false, + }, + ProcessedField { + "fieldNum": 2, + "inRelationship": false, + "name": "address", + "primaryKey": false, + "relation": Object {}, + "required": false, + "tableNum": 7, + "type": "String", + "unique": false, + }, + ProcessedField { + "fieldNum": 3, + "inRelationship": false, + "name": "name", + "primaryKey": false, + "relation": Object {}, + "required": true, + "tableNum": 7, + "type": "String", + "unique": false, + }, + ], + "name": "user", + }, + }, +} +`; diff --git a/src/server/DBMetadata/mysql/mysqlMetadataProcessor.js b/src/server/DBMetadata/metadataProcessor.js similarity index 93% rename from src/server/DBMetadata/mysql/mysqlMetadataProcessor.js rename to src/server/DBMetadata/metadataProcessor.js index 563d17e..2d2fa64 100644 --- a/src/server/DBMetadata/mysql/mysqlMetadataProcessor.js +++ b/src/server/DBMetadata/metadataProcessor.js @@ -1,5 +1,5 @@ -const ProcessedField = require("../classes/processedField"); -const ProcessedTable = require("../classes/processedTable"); +const ProcessedField = require("./classes/processedField"); +const ProcessedTable = require("./classes/processedTable"); function processMetadata(columnData) { if (!columnData || columnData.length === 0) diff --git a/src/server/DBMetadata/postgres/pgMetadataProcessor.test.js b/src/server/DBMetadata/metadataProcessor.test.js similarity index 82% rename from src/server/DBMetadata/postgres/pgMetadataProcessor.test.js rename to src/server/DBMetadata/metadataProcessor.test.js index 5c36b94..b0482db 100644 --- a/src/server/DBMetadata/postgres/pgMetadataProcessor.test.js +++ b/src/server/DBMetadata/metadataProcessor.test.js @@ -1,7 +1,7 @@ -import processed from "../sampleFiles/processedMetadata"; -import retrieved from "../sampleFiles/retrievedMetadata"; +import processed from "./sampleFiles/processedMetadata"; +import retrieved from "./sampleFiles/retrievedMetadata"; -import processMetadata from "./pgMetadataProcessor"; +import processMetadata from "./metadataProcessor"; describe("Format Metadata Tests", () => { it("Should return correctly formatted metadata given sample input", () => { diff --git a/src/server/DBMetadata/mysql/mysqlMetadataProcessor.test.js b/src/server/DBMetadata/mysql/mysqlMetadataProcessor.test.js deleted file mode 100644 index 5c36b94..0000000 --- a/src/server/DBMetadata/mysql/mysqlMetadataProcessor.test.js +++ /dev/null @@ -1,34 +0,0 @@ -import processed from "../sampleFiles/processedMetadata"; -import retrieved from "../sampleFiles/retrievedMetadata"; - -import processMetadata from "./pgMetadataProcessor"; - -describe("Format Metadata Tests", () => { - it("Should return correctly formatted metadata given sample input", () => { - const result = processMetadata(retrieved); - expect(result).toEqual(processed); - }); - - it("Throws an error on null or empty data", () => { - const metadataTest = () => { - processMetadata([]); - }; - - expect(metadataTest).toThrowError("Metadata is null or empty"); - }); - - it("Throws an error on non array input", () => { - const metadataTest = () => { - processMetadata({}); - }; - - expect(metadataTest).toThrowError( - "Invalid data format. Column Data must be an array" - ); - }); - - it("Should match the snapshot", () => { - const result = processMetadata(retrieved); - expect(result).toMatchSnapshot(); - }); -}); diff --git a/src/server/DBMetadata/mysql/mysqlMetadataRetriever.js b/src/server/DBMetadata/mysql/mysqlMetadataRetriever.js index cdcc267..073a4ef 100644 --- a/src/server/DBMetadata/mysql/mysqlMetadataRetriever.js +++ b/src/server/DBMetadata/mysql/mysqlMetadataRetriever.js @@ -1,15 +1,5 @@ const mysql = require("mysql"); - -// var connection = mysql.createConnection({ -// host: "localhost", -// user: "me", -// password: "secret", -// database: "my_db" -// }); - -// - -// +const { URL } = require("url"); const metadataQuery = `SELECT distinct t.table_name, @@ -34,40 +24,52 @@ AND (constraint_type = 'FOREIGN KEY' OR (constraint_type is null OR constraint_t AND t.table_schema not in ('information_schema', 'mysql', 'performance_schema', 'phpmyadmin','sys', 'test') ORDER BY table_name;`; -async function getSchemaInfoPG(connString) { - if (notRightFormat) { - var connection = mysql.createConnection({ - host: "switchql-mysql.c9vnkgo31mgw.us-west-1.rds.amazonaws.com", - user: "admin", - password: "password", - database: "my_db" - }); - } +function getSchemaInfo(connString) { + const connection = mysql.createConnection(buildMysqlParams(connString)); - connection.connect(); - - try { - return connection.query(metadataQuery, (error, results, fields) => { - console.log("The error: " + error); - console.log("The solution is: ", results[0].solution); - console.log("The fields: " + fields); + return new Promise((resolve, reject) => { + try { + connection.query(metadataQuery, (error, results) => { + if (error) reject(error); + resolve(results); + }); + } finally { connection.end(); - }); - } catch (err) { - throw err; - } + } + }); +} + +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 buildConnectionString(info) { - let connectionString = ""; - info.port = info.port || 5432; - connectionString += `mysql://${info.user}:${info.password}@${info.host}:${ - info.port - }/${info.database}`; - return connectionString; +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 = { - getSchemaInfoPG, - buildConnectionString + getSchemaInfo, + buildMysqlParams }; diff --git a/src/server/DBMetadata/mysql/mysqlMetadataRetriever.test.js b/src/server/DBMetadata/mysql/mysqlMetadataRetriever.test.js index 4619632..1f5193c 100644 --- a/src/server/DBMetadata/mysql/mysqlMetadataRetriever.test.js +++ b/src/server/DBMetadata/mysql/mysqlMetadataRetriever.test.js @@ -1,15 +1,15 @@ -const pgMetadataRetriever = require("./pgMetadataRetriever"); +const pgMetadataRetriever = require("./mysqlMetadataRetriever"); -test("takes in object with data returns properly formatted string", () => { +test("takes in connection string returns properly formatted connection object", () => { 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" - ); + 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/pgMetadataProcessor.js b/src/server/DBMetadata/postgres/pgMetadataProcessor.js deleted file mode 100644 index 563d17e..0000000 --- a/src/server/DBMetadata/postgres/pgMetadataProcessor.js +++ /dev/null @@ -1,70 +0,0 @@ -const ProcessedField = require("../classes/processedField"); -const ProcessedTable = require("../classes/processedTable"); - -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"); - - let tblIdx = 0; - let fieldIdx = 0; - let prevTable = columnData[0].table_name; - let props = []; - - let lookupFields = {}; - - const lookup = {}; - const toRef = {}; - - 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; - - tblIdx++; - fieldIdx = 0; - - props = []; - lookupFields = {}; - } - - if (index === columnData.length - 1) { - data.tables[tblIdx] = new ProcessedTable(prevTable, props); - } - - const processed = new ProcessedField(tblCol, tblIdx, fieldIdx); - - if (tableInToRef(toRef, tblCol)) { - processed.addRetroRelationship(toRef, tblCol, data); - } - - if (tblCol.constraint_type === "FOREIGN KEY") { - processed.addForeignKeyRef(lookup, tblCol, toRef, data); - } - - props.push(processed); - lookupFields[tblCol.column_name] = fieldIdx; - - prevTable = tblCol.table_name; - fieldIdx++; - }); - - return data; -} - -function tableInToRef(toRef, tblCol) { - return ( - toRef.hasOwnProperty(tblCol.table_name) && - toRef[tblCol.table_name].hasOwnProperty(tblCol.column_name) - ); -} - -module.exports = processMetadata; diff --git a/src/server/DBMetadata/postgres/pgMetadataRetriever.js b/src/server/DBMetadata/postgres/pgMetadataRetriever.js index 5292929..f31b8d0 100644 --- a/src/server/DBMetadata/postgres/pgMetadataRetriever.js +++ b/src/server/DBMetadata/postgres/pgMetadataRetriever.js @@ -27,7 +27,7 @@ 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 getSchemaInfoPG(connString) { +async function getSchemaInfo(connString) { const db = getDbPool(connString); try { return (metadataInfo = await utilty.promiseTimeout( @@ -73,6 +73,6 @@ function buildConnectionString(info) { } module.exports = { - getSchemaInfoPG, + getSchemaInfo, buildConnectionString }; 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;