/$TRAVIS_COMMIT/" Dockerrun.aws.json
16 | # Zip up our codebase, along with modified Dockerrun and our .ebextensions directory
17 | zip -r mm-prod-deploy.zip Dockerrun.aws.json .ebextensions
18 | # Upload zip file to s3 bucket
19 | aws s3 cp mm-prod-deploy.zip s3://$EB_BUCKET/mm-prod-deploy.zip
20 | # Create a new application version with new Dockerrun
21 | aws elasticbeanstalk create-application-version --application-name lucidQL --version-label $TRAVIS_COMMIT --source-bundle S3Bucket=$EB_BUCKET,S3Key=mm-prod-deploy.zip
22 | # Update environment to use new version number
23 | aws elasticbeanstalk update-environment --environment-name lucidql-env --version-label $TRAVIS_COMMIT
--------------------------------------------------------------------------------
/client/forceGraph/generators/helperFunctions.js:
--------------------------------------------------------------------------------
1 | function toPascalCase(str) {
2 | return capitalize(toCamelCase(str));
3 | }
4 |
5 | function capitalize(str) {
6 | return `${str[0].toUpperCase()}${str.slice(1)}`;
7 | }
8 |
9 | function toCamelCase(str) {
10 | let varName = str.toLowerCase();
11 | for (let i = 0, len = varName.length; i < len; i++) {
12 | const isSeparator = varName[i] === '-' || varName[i] === '_';
13 | if (varName[i + 1] && isSeparator) {
14 | const char = i === 0 ? varName[i + 1] : varName[i + 1].toUpperCase();
15 | varName = varName.slice(0, i) + char + varName.slice(i + 2);
16 | } else if (isSeparator) {
17 | varName = varName.slice(0, i);
18 | }
19 | }
20 | return varName;
21 | }
22 |
23 | function typeSet(str) {
24 | switch (str) {
25 | case 'character varying':
26 | return 'String';
27 | break;
28 | case 'character':
29 | return 'String';
30 | break;
31 | case 'integer':
32 | return 'Int';
33 | break;
34 | case 'text':
35 | return 'String';
36 | break;
37 | case 'date':
38 | return 'String';
39 | break;
40 | case 'boolean':
41 | return 'Boolean';
42 | break;
43 | default:
44 | return 'Int';
45 | }
46 | }
47 |
48 | function getPrimaryKeyType(primaryKey, columns) {
49 | return typeSet(columns[primaryKey].dataType);
50 | }
51 |
52 | export {
53 | capitalize,
54 | toPascalCase,
55 | toCamelCase,
56 | typeSet,
57 | getPrimaryKeyType,
58 | };
59 |
--------------------------------------------------------------------------------
/server/SDL-definedSchemas/helpers/helperFunctions.js:
--------------------------------------------------------------------------------
1 | function toPascalCase(str) {
2 | return capitalize(toCamelCase(str));
3 | }
4 |
5 | function capitalize(str) {
6 | return `${str[0].toUpperCase()}${str.slice(1)}`;
7 | }
8 |
9 | function toCamelCase(str) {
10 | let varName = str.toLowerCase();
11 | for (let i = 0, len = varName.length; i < len; i++) {
12 | const isSeparator = varName[i] === '-' || varName[i] === '_';
13 | if (varName[i + 1] && isSeparator) {
14 | const char = i === 0 ? varName[i + 1] : varName[i + 1].toUpperCase();
15 | varName = varName.slice(0, i) + char + varName.slice(i + 2);
16 | } else if (isSeparator) {
17 | varName = varName.slice(0, i);
18 | }
19 | }
20 | return varName;
21 | }
22 |
23 | function typeSet(str) {
24 | switch (str) {
25 | case 'character varying':
26 | return 'String';
27 | break;
28 | case 'character':
29 | return 'String';
30 | break;
31 | case 'integer':
32 | return 'Int';
33 | break;
34 | case 'text':
35 | return 'String';
36 | break;
37 | case 'date':
38 | return 'String';
39 | break;
40 | case 'boolean':
41 | return 'Boolean';
42 | break;
43 | default:
44 | return 'Int';
45 | }
46 | }
47 |
48 | function getPrimaryKeyType(primaryKey, columns) {
49 | return typeSet(columns[primaryKey].dataType);
50 | }
51 |
52 | module.exports = {
53 | capitalize,
54 | toPascalCase,
55 | toCamelCase,
56 | typeSet,
57 | getPrimaryKeyType,
58 | };
59 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const path = require('path');
3 | const https = require('https');
4 | const fs = require('fs');
5 | const expressGraphQL = require('express-graphql');
6 | const expressPlayground = require('graphql-playground-middleware-express').default;
7 | const pgRouter = require('./routes/pgRoute');
8 | const mySQLRouter = require('./routes/mySQLRoute');
9 | const schema = require('./dummy_server/schema');
10 |
11 | const app = express();
12 | const PORT = process.env.PORT || 8081;
13 |
14 | /* Express logic/handler */
15 | app.use(express.json());
16 | app.use(express.urlencoded({ extended: true }));
17 |
18 | app.use(express.static(path.join(__dirname, '../build')));
19 |
20 | app.use('/db/pg', pgRouter);
21 | app.use('/db/mySQL', mySQLRouter);
22 |
23 | app.get('/', (req, res) => {
24 | res.sendFile(path.join(__dirname, '../client/index.html'));
25 | });
26 |
27 | app.use(
28 | '/graphql',
29 | expressGraphQL({
30 | schema,
31 | })
32 | );
33 |
34 | app.get('/playground', expressPlayground({ endpoint: '/graphql' }));
35 |
36 | app.use((err, req, res, next) => {
37 | const defaultErr = {
38 | log: 'An error occured in unknown middleware',
39 | status: 500,
40 | message: { err: 'An error occurred' },
41 | };
42 | const errObj = { ...defaultErr, ...err };
43 | console.log(errObj.log);
44 | res.status(errObj.status).json(errObj.message);
45 | });
46 |
47 | app.listen(PORT, () => console.log(`lucidQL is ready at: http://localhost:${PORT}`));
48 |
49 | module.exports = app;
50 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 |
4 | module.exports = {
5 | // webpack will take the files from ./client/index
6 | entry: './client/index.tsx',
7 | // and output it into /dist as bundle.js
8 | output: {
9 | path: path.join(__dirname, '/build'),
10 | filename: 'bundle.js',
11 | },
12 | // adding .ts and .tsx to resolve.extensions will help babel look for .ts and .tsx files to transpile
13 | resolve: {
14 | extensions: ['.ts', '.tsx', '.js', '.jsx'],
15 | },
16 | module: {
17 | rules: [
18 | // we use babel-loader to load our jsx and tsx files
19 | {
20 | test: /\.(ts|js)x?$/,
21 | exclude: /node_modules/,
22 | use: {
23 | loader: 'babel-loader',
24 | },
25 | },
26 | // css-loader to bundle all the css files into one file and style-loader to add all the styles inside the style tag of the document
27 | {
28 | test: /\.css$/,
29 | use: ['style-loader', 'css-loader'],
30 | },
31 | {
32 | test: /.(png|jpg|woff|woff2|eot|ttf|svg|gif)$/,
33 | loader: 'url-loader?limit=1024000',
34 | },
35 | ],
36 | },
37 | devServer: {
38 | publicPath: '/build',
39 | proxy: {
40 | '/': 'http://localhost:8081',
41 | },
42 | hot: true,
43 | },
44 | plugins: [
45 | new HtmlWebpackPlugin({
46 | template: './client/index.html',
47 | filename: 'index.html',
48 | inject: 'body',
49 | }),
50 | ],
51 | };
52 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // Target latest version of ECMAScript.
4 | "target": "esnext",
5 | // path to output directory
6 | "outDir": "./build/",
7 | // enable strict null checks as a best practice
8 | "strictNullChecks": true,
9 | // Search under node_modules for non-relative imports.
10 | "moduleResolution": "node",
11 | // Process & infer types from .js files.
12 | "allowJs": true,
13 | // Don't emit; allow Babel to transform files.
14 | "noEmit": true,
15 | // Enable strictest settings like strictNullChecks & noImplicitAny.
16 | "strict": true,
17 | // Import non-ES modules as default imports.
18 | "esModuleInterop": true,
19 | // use typescript to transpile jsx to js
20 | "jsx": "react",
21 | "baseUrl": "./client",
22 | "lib": ["es2015", "dom.iterable", "es2016.array.include", "es2017.object", "dom"],
23 | "module": "es6",
24 | "removeComments": true,
25 | "alwaysStrict": true,
26 | "allowUnreachableCode": false,
27 | "noImplicitAny": true,
28 | "noImplicitThis": true,
29 | "noUnusedLocals": true,
30 | "noUnusedParameters": true,
31 | "noImplicitReturns": true,
32 | "noFallthroughCasesInSwitch": true,
33 | "forceConsistentCasingInFileNames": true,
34 | "importHelpers": true
35 | }
36 | }
37 | // {
38 | // "compilerOptions": {
39 | // "target": "es5",
40 | // "jsx": "react",
41 | // "module": "es2015"
42 | // },
43 | // "exclude": [
44 | // "build"
45 | // ]
46 | // }
47 |
--------------------------------------------------------------------------------
/client/components/link-popup/Form.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const Form = ({ onSubmit, onSubmitSample, mySQLButton }) => {
4 | return (
5 |
42 | );
43 | };
44 | export default Form;
45 |
--------------------------------------------------------------------------------
/client/UI.tsx:
--------------------------------------------------------------------------------
1 | function createGraph(data) {
2 | const colors = ['red', 'blue', 'green', 'orange', 'purple', 'gray'];
3 | let nodes = [];
4 | let edges = [];
5 | // access all table names on object
6 | let keys = Object.keys(data);
7 |
8 | for (let i = 0; i < colors.length; i++) {
9 | // access properties on each table (columns, pointsTo, referencedBy)
10 | if (!data[keys[i]]) {
11 | break;
12 | } else {
13 | let columns = data[keys[i]].columns;
14 | let pointsTo = data[keys[i]].pointsTo;
15 | let referencedBy = data[keys[i]].referecedBy;
16 |
17 | nodes.push({ id: keys[i], label: keys[i], color: colors[i], size: 20, shape: 'circle' });
18 | for (let j = 0; j < columns.length; j++) {
19 | nodes.push({ id: columns[j] + j, label: columns[j], color: colors[i], shape: 'circle' });
20 | edges.push({ from: keys[i], to: columns[j] + j });
21 | }
22 |
23 | if (!pointsTo[0]) {
24 | continue;
25 | } else {
26 | pointsTo.forEach((point) => {
27 | edges.push({
28 | from: keys[i],
29 | to: point,
30 | hightlight: 'red',
31 | physics: { springLength: 200 },
32 | });
33 | });
34 | }
35 |
36 | if (!referencedBy[0]) {
37 | continue;
38 | } else {
39 | referencedBy.forEach((ref) => {
40 | edges.push({ from: ref, to: keys[i], highlight: 'cyan', physics: { springLength: 200 } });
41 | });
42 | }
43 | }
44 | }
45 | return { nodes, edges };
46 | }
47 |
48 | export default createGraph;
49 |
--------------------------------------------------------------------------------
/client/components/nav-bars/TopNav.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | Navbar,
4 | Nav,
5 | NavDropdown,
6 | Form,
7 | FormControl,
8 | Button
9 | } from "react-bootstrap";
10 | import logo from "../../../public/lucidQL-logo.png";
11 | import logoBrand from "../../../public/lucidQL.png";
12 |
13 | const TopNav: React.FC = ({ showModal }) => {
14 | function openNav() {
15 | document.querySelector("#mySidebar").style.width = "250px";
16 | document.querySelector("#main").style.marginLeft = "250px";
17 | }
18 |
19 | return (
20 |
27 |
28 | ☰
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | export default TopNav;
55 |
56 | /* */
57 |
--------------------------------------------------------------------------------
/client/forceGraph/generators/schemaGenerator.js:
--------------------------------------------------------------------------------
1 | import TypeGenerator from './typeGenerator';
2 | import ResolverGenerator from './resolverGenerator';
3 |
4 | // assembles all programmatic schema and resolvers together in one string
5 | export const assembleSchema = (tables) => {
6 | let queryType = '';
7 | let mutationType = '';
8 | let customTypes = '';
9 | let queryResolvers = '';
10 | let mutationResolvers = '';
11 | let relationshipResolvers = '';
12 | for (const tableName in tables) {
13 | const tableData = tables[tableName];
14 | const { foreignKeys, columns } = tableData;
15 | queryType += TypeGenerator.queries(tableName, tableData);
16 | mutationType += TypeGenerator.mutations(tableName, tableData);
17 | customTypes += TypeGenerator.customTypes(tableName, tables);
18 | if (!foreignKeys || Object.keys(columns).length !== Object.keys(foreignKeys).length + 1) {
19 | queryResolvers += ResolverGenerator.queries(tableName, tableData);
20 | mutationResolvers += ResolverGenerator.mutations(tableName, tableData);
21 | relationshipResolvers += ResolverGenerator.getRelationships(tableName, tables);
22 | }
23 | }
24 | return (
25 | `${'const typeDefs = `\n' + ' type Query {\n'}${queryType} }\n\n` +
26 | ` type Mutation {${mutationType} }\n\n` +
27 | `${customTypes}\`;\n\n` +
28 | 'const resolvers = {\n' +
29 | ' Query: {' +
30 | ` ${queryResolvers}\n` +
31 | ' },\n\n' +
32 | ` Mutation: {\n${mutationResolvers} },\n${relationshipResolvers}}\n\n` +
33 | 'const schema = makeExecutableSchema({\n' +
34 | ' typeDefs,\n' +
35 | ' resolvers,\n' +
36 | '});\n\n' +
37 | 'module.exports = schema;'
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
13 |
14 |
15 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | Welcome to lucidQL
28 |
29 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/server/SDL-definedSchemas/generators/schemaGenerator.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-syntax */
2 | /* eslint-disable guard-for-in */
3 | const TypeGenerator = require('./typeGenerator');
4 | const ResolverGenerator = require('./resolverGenerator');
5 |
6 | const SchemaGenerator = {};
7 | // assembles all programmatic schema and resolvers together in one string
8 | SchemaGenerator.assembleSchema = function assembleSchema(tables) {
9 | let queryType = '';
10 | let mutationType = '';
11 | let customTypes = '';
12 | let queryResolvers = '';
13 | let mutationResolvers = '';
14 | let relationshipResolvers = '';
15 | for (const tableName in tables) {
16 | const tableData = tables[tableName];
17 | const { foreignKeys, columns } = tableData;
18 | queryType += TypeGenerator.queries(tableName, tableData);
19 | mutationType += TypeGenerator.mutations(tableName, tableData);
20 | customTypes += TypeGenerator.customTypes(tableName, tables);
21 | if (!foreignKeys || Object.keys(columns).length !== Object.keys(foreignKeys).length + 1) {
22 | queryResolvers += ResolverGenerator.queries(tableName, tableData);
23 | mutationResolvers += ResolverGenerator.mutations(tableName, tableData);
24 | relationshipResolvers += ResolverGenerator.getRelationships(tableName, tables);
25 | }
26 | }
27 | return (
28 | `${'const typeDefs = `\n' + ' type Query {\n'}${queryType} }\n\n` +
29 | ` type Mutation {${mutationType} }\n\n` +
30 | `${customTypes}\`;\n\n` +
31 | 'const resolvers = {\n' +
32 | ' Query: {' +
33 | ` ${queryResolvers}\n` +
34 | ' },\n\n' +
35 | ` Mutation: {\n${mutationResolvers} },\n${relationshipResolvers}}\n\n` +
36 | 'const schema = makeExecutableSchema({\n' +
37 | ' typeDefs,\n' +
38 | ' resolvers,\n' +
39 | '});\n\n' +
40 | 'module.exports = schema;'
41 | );
42 | };
43 |
44 | module.exports = SchemaGenerator;
45 |
--------------------------------------------------------------------------------
/server/SDL-definedSchemas/controllers/mySQLController.js:
--------------------------------------------------------------------------------
1 | const mysql = require('mysql2');
2 |
3 | const mySQLController = {};
4 |
5 | // This method is suitable for older SQL databases
6 | mySQLController.getTables = (req, res, next) => {
7 | const config = {
8 | host: req.body.host,
9 | user: req.body.user,
10 | password: req.body.password,
11 | database: req.body.database,
12 | };
13 | const connection = mysql.createConnection(config);
14 |
15 | connection.query('SHOW tables', async (err, results, fields) => {
16 | if (err) {
17 | return res.json('error');
18 | }
19 | const allTables = {};
20 | const tables = [];
21 | for (let i = 0; i < results.length; i++) {
22 | const table = Object.values(results[i])[0];
23 | allTables[table] = {};
24 | tables.push(table);
25 | }
26 | const columns = [];
27 | const keys = [];
28 | for (let i = 0; i < tables.length; i++) {
29 | const data = await connection.promise().query(`SHOW KEYS FROM ${tables[i]};`);
30 | const key = data[0];
31 | allTables[tables[i]].primaryKey = {};
32 | allTables[tables[i]].foreignKeys = {};
33 | for (let j = 0; j < key.length; j++) {
34 | const { Key_name, Column_name } = key[j];
35 | if (Key_name === 'PRIMARY') {
36 | allTables[tables[i]].primaryKey = Column_name;
37 | } else {
38 | allTables[tables[i]].foreignKeys[Key_name] = {};
39 | allTables[tables[i]].foreignKeys[Key_name].referenceKey = allTables[tables[i]].primaryKey;
40 | }
41 | }
42 | keys.push(key);
43 | }
44 | for (let i = 0; i < tables.length; i++) {
45 | const data = await connection.promise().query(`SHOW FIELDS FROM ${tables[i]};`);
46 | const column = data[0];
47 | allTables[tables[i]].columns = {};
48 | for (let j = 0; j < column.length; j++) {
49 | allTables[tables[i]].columns[column[j].Field] = {};
50 | const { Field, Null } = column[j];
51 | allTables[tables[i]].columns[column[j].Field].dataType = Field;
52 | allTables[tables[i]].columns[column[j].Field].isNullable = Null;
53 | }
54 | columns.push(column);
55 | }
56 |
57 | res.locals.tables = allTables;
58 |
59 | return next();
60 | });
61 | };
62 |
63 | module.exports = mySQLController;
64 |
--------------------------------------------------------------------------------
/client/components/link-popup/Modal.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Form } from './Form';
4 | import { FormMySQL } from './FormMySQL';
5 | import FocusTrap from 'focus-trap-react';
6 |
7 | export const Modal: React.FC = ({ onSubmit, onSubmitMySQL, closeModal, onSubmitSample }) => {
8 | const [modalState, setModalState] = useState('postgres');
9 |
10 | const mySQLButton = () => {
11 | return modalState === 'mysql' ? setModalState('postgres') : setModalState('mysql');
12 | };
13 |
14 | if (modalState === 'postgres') {
15 | return ReactDOM.createPortal(
16 |
17 |
18 |
19 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | ,
36 | document.body
37 | );
38 | } else {
39 | return ReactDOM.createPortal(
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | ,
54 | document.body
55 | );
56 | }
57 | };
58 |
59 | export default Modal;
60 |
--------------------------------------------------------------------------------
/__tests__/enzyme.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { configure, shallow, mount } from 'enzyme';
3 | import Adapter from 'enzyme-adapter-react-16';
4 | import { RecoilRoot } from 'recoil';
5 |
6 | // import toJson from 'enzyme-to-json';
7 | import TopNav from '../client/components/nav-bars/TopNav';
8 | import App from '../client/App';
9 | import Modal from '../client/components/link-popup/Modal.tsx';
10 | import Footer from '../client/components/nav-bars/Footer.tsx';
11 | import Form from '../client/components/link-popup/Form.tsx';
12 |
13 | configure({ adapter: new Adapter() });
14 |
15 | describe('React Unit Test', () => {
16 | describe('Modal Sample Button Test', () => {
17 | let wrapper;
18 | const props = {
19 | onSubmitSample: jest.fn(),
20 | };
21 | beforeAll(() => {
22 | wrapper = shallow();
23 | });
24 | it('Button will invoke a function once Sample Button is clicked', () => {
25 | wrapper.find('#testenzyme').simulate('click');
26 | expect(props.onSubmitSample).toHaveBeenCalled();
27 | });
28 | });
29 | describe('mySQL Test', () => {
30 | let wrapper;
31 | const props = {
32 | mySQLButton: jest.fn(),
33 | };
34 | beforeAll(() => {
35 | wrapper = shallow();
36 | });
37 | it('Button will bring you to mySQL modal once it is clicked', () => {
38 | wrapper.find('#testenzyme2').simulate('click');
39 | expect(props.mySQLButton).toHaveBeenCalled();
40 | });
41 | });
42 |
43 | describe('Close Button Test', () => {
44 | let wrapper;
45 | //wrapper
46 | const props = {
47 | closeModal: jest.fn(),
48 | };
49 | beforeAll(() => {
50 | wrapper = shallow( );
51 | });
52 | it('Modal will close once this button is clicked', () => {
53 | wrapper.find('#closeModal1').simulate('click');
54 | expect(props.closeModal).toHaveBeenCalled();
55 | });
56 | });
57 |
58 | describe('Enter Postgres URI', () => {
59 | let wrapper;
60 | const props = {
61 | showModal: jest.fn(),
62 | };
63 | beforeAll(() => {
64 | wrapper = shallow( );
65 | });
66 | it('Modal to insert postgres URI opens', () => {
67 | wrapper.find('#postgresURIbutton').simulate('click');
68 | expect(props.showModal).toHaveBeenCalled();
69 | });
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/__tests__/server-test.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line import/no-unresolved, import/no-extraneous-dependencies
2 | const request = require('supertest');
3 |
4 | const server = 'http://localhost:8081';
5 |
6 | describe('Route integration', () => {
7 | describe('Get request to "/" endpoint', () => {
8 | describe('GET', () => {
9 | it('responds with 200 status and text/html content type', () =>
10 | request(server)
11 | .get('/')
12 | .expect('Content-Type', /text\/html/)
13 | .expect(200));
14 | });
15 | });
16 |
17 | describe('Get request to "/graphql" endpoint', () => {
18 | describe('GET', () => {
19 | it('responds with 400 status and application/json content type', () =>
20 | request(server)
21 | .get('/graphql')
22 | .expect('Content-Type', /application\/json/)
23 | .expect(400));
24 |
25 | it('responds with 400 status and application/json content type', () =>
26 | request(server)
27 | .get(
28 | '/graphql?query=%7B%0A%20%20people%20%7B%0A%20%20%20%20name%0A%20%20%20%20hair_color%0A%20%20%20%20eye_color%0A%20%20%7D%0A%7D%0A'
29 | )
30 | .expect('Content-Type', /application\/json/)
31 | .expect(200));
32 | });
33 | });
34 |
35 | describe('Get request to "/playground" endpoint', () => {
36 | describe('GET', () => {
37 | it('responds with 200 status and text/html content type', () =>
38 | request(server)
39 | .get('/playground')
40 | .expect('Content-Type', /text\/html/)
41 | .expect(200));
42 | });
43 | });
44 |
45 | describe('Get request to "/db/pg/sdl" endpoint', () => {
46 | describe('POST', () => {
47 | it('responds with 500 status and application/json content type when not sending a request body', () =>
48 | request(server)
49 | .post('/db/pg/sdl')
50 | .expect('Content-Type', /application\/json/)
51 | .expect(500));
52 |
53 | it('responds with 200 status and application/json content type when sending URI', () =>
54 | request(server)
55 | .post('/db/pg/sdl')
56 | .send({ uri: 'postgres://mxnahgtg:V5_1wi1TPrDLRvmsl0pKczgf9SMQy1j6@lallah.db.elephantsql.com:5432/mxnahgtg' })
57 | .set('Accept', 'application/json')
58 | .expect('Content-Type', /application\/json/)
59 | .expect(200));
60 | });
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/client/components/VisGraph.tsx:
--------------------------------------------------------------------------------
1 | import Graph from 'react-graph-vis';
2 | import React, { Component, useEffect, useState } from 'react';
3 | import { atom, selector, useRecoilValue, useRecoilState } from 'recoil';
4 | import createGraph from '../UI';
5 | import { state } from '../App';
6 |
7 | const options = {
8 | edges: {
9 | arrows: {
10 | to: { enabled: true, scaleFactor: 1 },
11 | middle: { enabled: false, scaleFactor: 1 },
12 | from: { enabled: false, scaleFactor: 1 },
13 | },
14 | color: {
15 | // color: 'black',
16 | },
17 | smooth: false,
18 | },
19 | nodes: {
20 | borderWidth: 1,
21 | borderWidthSelected: 2,
22 | shape: 'circle',
23 | color: {
24 | border: 'black',
25 | },
26 | font: {
27 | color: 'white',
28 | size: 10,
29 | face: 'Tahoma',
30 | background: 'none',
31 | strokeWidth: 0,
32 | strokeColor: '#ffffff',
33 | },
34 | shadow: true,
35 | },
36 | physics: {
37 | maxVelocity: 146,
38 | solver: 'forceAtlas2Based',
39 | timestep: 0.35,
40 | stabilization: {
41 | enabled: true,
42 | iterations: 100,
43 | updateInterval: 10,
44 | },
45 | },
46 | layout: {
47 | randomSeed: undefined,
48 | improvedLayout: true,
49 | },
50 | };
51 |
52 | const style = {
53 | display: 'flex',
54 | width: '100rem',
55 | height: '70rem',
56 | };
57 |
58 | const VisGraph: React.FC = () => {
59 | const [fetched, setFetched] = useState(false);
60 | const [graph, setGraph] = useState({});
61 | const [data, setData] = useRecoilState(state);
62 |
63 | const getData = () => {
64 | fetch('/db/pg/sdl', {
65 | method: 'POST',
66 | headers: {
67 | 'Content-Type': 'application/json',
68 | },
69 |
70 | body: JSON.stringify({ uri: data.link }),
71 | })
72 | .then((response) => response.json())
73 | .then((response) => {
74 | setGraph(createGraph(response.d3Data));
75 | setFetched(true);
76 | });
77 | };
78 |
79 | useEffect(() => {
80 | getData();
81 | }, [data]);
82 |
83 | if (fetched) {
84 | return (
85 |
86 |
87 |
88 | );
89 | } else if (!fetched) {
90 | return (
91 |
92 |
Please enter URI to display GraphQL endpoints...
93 |
94 | );
95 | }
96 | };
97 |
98 | export default VisGraph;
99 |
--------------------------------------------------------------------------------
/client/forceGraph/ForceGraph.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react';
2 | import * as d3 from 'd3';
3 | import { useRecoilState } from 'recoil';
4 | import { state } from '../App';
5 | import { runForceGraph } from './generators/ForceGraphGenerator';
6 | import { deleteTables, deleteColumns } from './generators/deleteFunctions';
7 |
8 | // The ForceGraph component will be the container for the generated force graph and ForceGraphGenerator will generate the graph using D3
9 | function ForceGraph() {
10 | // useRef hook to reference the container element (common practice for React + d3)
11 | const containerRef = useRef(null);
12 | const [data, setData] = useRecoilState(state);
13 |
14 | const nodeHoverTooltip = React.useCallback((node) => {
15 | if (node.primary)
16 | return `Table: ${node.name} (SQL info)Primary Key : ${node.primaryKey}Columns Count : ${
17 | node.columnCount
18 | }Foreign Keys :${node.foreignKeys.length > 0 ? node.foreignKeys : 'N/A'}Referenced by :${
19 | node.referencedBy.length > 0 ? node.referencedBy : 'N/A'
20 | }`;
21 | return `
Column: ${node.name} (SQL info)dataType : ${node.dataType}isNullable : ${node.isNullable}charMaxLength : ${node.charMaxLength}columnDefault : ${node.columnDefault}
`;
22 | }, []);
23 |
24 | const handleDeleteTables = (currentState, tableToExclude) => {
25 | const { newTables, newSchema, history } = deleteTables(currentState, tableToExclude);
26 | setData({ ...data, tables: newTables, schema: newSchema, tableModified: true, history });
27 | };
28 |
29 | const handleDeleteColumns = (currentState, columnToExclude, parentName, foreignKeyToDelete) => {
30 | const { newTables, newSchema, history } = deleteColumns(currentState, columnToExclude, parentName, foreignKeyToDelete);
31 | setData({ ...data, tables: newTables, schema: newSchema, tableModified: true, history });
32 | };
33 |
34 | // useEffect hook to detect successful ref mount, as well as data change
35 | useEffect(() => {
36 | if (containerRef.current) {
37 | if (data.tableModified) {
38 | // if this is an update, clear up the svg before re-run graph
39 | d3.select(containerRef.current).html('');
40 | }
41 | runForceGraph(containerRef.current, data, nodeHoverTooltip, handleDeleteTables, handleDeleteColumns);
42 | }
43 | }, [data.tables]);
44 |
45 | // create a reference to the div which will wrap the generated graph and nothing more.
46 | return ;
47 | }
48 |
49 | export default ForceGraph;
50 |
--------------------------------------------------------------------------------
/client/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { RecoilRoot, atom, useRecoilState } from 'recoil';
3 | import LinkContainer from './components/link-popup/LinkContainer';
4 | import TopNav from './components/nav-bars/TopNav';
5 | import CodeBox from './components/codebox';
6 | import SplitPane from 'react-split-pane';
7 | import ForceGraph from './forceGraph/ForceGraph';
8 | import Footer from './components/nav-bars/Footer';
9 | import Sidebar from './components/Sidebar';
10 | import './styles.css';
11 |
12 | export const state = atom({
13 | key: 'state',
14 | default: {
15 | link: '',
16 | modal: true,
17 | schema: '',
18 | tables: {},
19 | tableModified: false,
20 | history: [],
21 | },
22 | });
23 |
24 | const App: React.FC = () => {
25 | const [data, setData] = useRecoilState(state);
26 |
27 | const showModal = () => {
28 | setData({ ...data, modal: true });
29 | };
30 |
31 | const handleUndo = () => {
32 | if (data.history.length > 0) {
33 | const newHistory = [...data.history]
34 | const prevData = newHistory.pop(); // get latest tableObj
35 | const prevTable = prevData.table;
36 | const prevSchema = prevData.schema;
37 | setData({ ...data, schema: prevSchema, tables: prevTable, history: newHistory });
38 | }
39 | }
40 |
41 | const Annotation = () => {
42 | return (
43 |
44 |
⟶ Foreign Key To (Postgres)
45 | ⎯⎯⎯⎯⎯ Able To Query Each Other (GraphQL)
46 | handleUndo()} >⎌ UNDO
47 |
48 | )
49 | }
50 |
51 | return (
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | {!data.modal ?
: null}
60 | {/* {!data.modal ?
: null} */}
61 | {!data.modal ? : null}
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | );
71 | };
72 |
73 | function Root() {
74 | return (
75 |
76 |
77 |
78 | );
79 | }
80 |
81 | export default Root;
82 |
--------------------------------------------------------------------------------
/client/components/nav-bars/Footer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { packagejsonFile } from '../../downloadHelperFunctions/packagejsonFile';
3 | import { connectToDB } from '../../downloadHelperFunctions/connectToDB';
4 | import { schemaFile } from '../../downloadHelperFunctions/schemaFile';
5 | import { serverFile } from '../../downloadHelperFunctions/serverFile';
6 | import JSZip from 'jszip';
7 | //below lets you download files within your browser
8 | import FileSaver from 'file-saver';
9 | import { useRecoilValue } from 'recoil';
10 | import { state } from '../../App';
11 | import { Navbar, Nav, Form, Button } from 'react-bootstrap';
12 |
13 | export const writeToDummyServer = (schema: string, link: string) => {
14 | const connectToDBFile = connectToDB(link);
15 | const toSchemaFile = schemaFile(schema);
16 |
17 | const postOptions = {
18 | method: 'POST',
19 | headers: { 'Content-Type': 'application/json' },
20 | body: JSON.stringify({ db: connectToDBFile, schema: toSchemaFile }),
21 | };
22 |
23 | fetch('/db/pg/writefile', postOptions)
24 | .then((response) => response.json())
25 | .then((data) => console.log(data));
26 | };
27 |
28 | const Footer: React.FC = () => {
29 | const data = useRecoilValue(state);
30 |
31 | const handleDownloadFiles = (event: React.MouseEvent) => {
32 | event.preventDefault();
33 | const zip = new JSZip();
34 |
35 | zip.folder('lucidQL').file('package.json', packagejsonFile());
36 | zip.folder('lucidQL').file('server.js', serverFile());
37 | zip.folder('lucidQL').file('schema.js', schemaFile(data.schema));
38 | zip.folder('lucidQL').file('connectToDB.js', connectToDB(data.link));
39 | zip.generateAsync({ type: 'blob' }).then(function (content: any) {
40 | FileSaver.saveAs(content, 'lucidQL.zip');
41 | });
42 | };
43 |
44 | return (
45 |
46 |
47 |
48 |
49 |
50 |
63 |
64 |
65 |
66 | );
67 | };
68 |
69 | export default Footer;
70 |
--------------------------------------------------------------------------------
/public/icon/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
14 |
15 |
19 |
20 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | lucidQL
37 |
38 |
39 | You need to enable JavaScript to run this app.
40 |
41 |
51 |
56 |
61 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/client/components/link-popup/LinkContainer.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { state } from '../../App';
3 | import { useRecoilState } from 'recoil';
4 | import { Modal } from './Modal';
5 | import 'regenerator-runtime/runtime';
6 |
7 | const LinkContainer: React.FC = () => {
8 | const [data, setData] = useRecoilState(state);
9 |
10 | useEffect(() => {
11 | toggleScrollLock();
12 | }, [data]);
13 |
14 | const fetchSchemaPG = (link) => {
15 | fetch('/db/pg/sdl', {
16 | method: 'POST',
17 | headers: {
18 | 'Content-Type': 'application/json',
19 | },
20 | body: JSON.stringify({ uri: link }),
21 | })
22 | .then((response) => response.json())
23 | .then((response) => {
24 | setData({ ...data, link: link, modal: false, schema: response.schema, tables: response.tables });
25 | });
26 | };
27 |
28 | const onSubmit = async (event) => {
29 | event.preventDefault(event);
30 | if (event.target.link.value.trim() !== '') {
31 | fetchSchemaPG(event.target.link.value);
32 | }
33 | };
34 |
35 | const fetchSchemaMySQL = (host, user, password, database) => {
36 | fetch('/db/mySQL/sdl', {
37 | method: 'POST',
38 | headers: {
39 | 'Content-Type': 'application/json',
40 | },
41 | body: JSON.stringify({ host, user, password, database }),
42 | })
43 | .then((response) => response.json())
44 | .then((response) => {
45 | setData({ link: '', modal: false, schema: response.schema, d3Data: response.d3Data });
46 | });
47 | };
48 |
49 | const onSubmitMySQL = async (event) => {
50 | event.preventDefault(event);
51 | const { host, user, password, database } = event.target;
52 | // if (event.target.link.value.trim() !== '') {
53 | // fetchSchemaPG(event.target.link.value);
54 | // }
55 | fetchSchemaMySQL(host.value, user.value, password.value, database.value);
56 | };
57 |
58 | const onSubmitSample = async (event) => {
59 | event.preventDefault(event);
60 | fetch('/db/pg/sdl', {
61 | method: 'POST',
62 | headers: {
63 | 'Content-Type': 'application/json',
64 | },
65 | body: JSON.stringify({
66 | uri: 'postgres://ordddiou:g5OjOyAIFxf-tsLk1uwu4ZOfbJfiCFbh@ruby.db.elephantsql.com:5432/ordddiou',
67 | }),
68 | })
69 | .then((response) => response.json())
70 | .then((response) => {
71 | setData({
72 | ...data,
73 | link: 'postgres://ordddiou:g5OjOyAIFxf-tsLk1uwu4ZOfbJfiCFbh@ruby.db.elephantsql.com:5432/ordddiou',
74 | modal: false,
75 | schema: response.schema,
76 | tables: response.tables,
77 | });
78 | });
79 | };
80 |
81 | const closeModal = () => {
82 | setData({ ...data, modal: false });
83 | };
84 |
85 | const toggleScrollLock = () => {
86 | document.querySelector('html').classList.toggle('scroll-lock');
87 | };
88 |
89 | return (
90 |
91 | {data.modal ? (
92 |
93 | ) : null}
94 |
95 | );
96 | };
97 |
98 | export default LinkContainer;
99 |
--------------------------------------------------------------------------------
/client/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
15 |
16 |
125 |
--------------------------------------------------------------------------------
/server/queries/tableData.sql:
--------------------------------------------------------------------------------
1 | SELECT json_object_agg(
2 | pk.table_name, json_build_object(
3 | 'primaryKey', pk.primary_key,
4 | 'foreignKeys', fk.foreign_keys,
5 | 'referencedBy', rd.referenced_by,
6 | 'columns', td.columns
7 | )
8 | ) AS tables
9 |
10 | FROM ( -- Primary key data (pk)
11 | ---------------------------------------------------------------------------
12 | SELECT conrelid::regclass AS table_name, -- regclass will turn conrelid to actual tale name
13 | substring(pg_get_constraintdef(oid), '\((.*?)\)') AS primary_key --(.*?) matches any character (except for line terminators))
14 | FROM pg_constraint
15 | WHERE contype = 'p' AND connamespace = 'public'::regnamespace -- regnamespace will turn connamespace(number) to actual name space
16 | ---------------------------------------------------------------------------
17 | ) AS pk
18 |
19 | LEFT OUTER JOIN ( -- Foreign key data (fk)
20 | ---------------------------------------------------------------------------------------
21 | SELECT conrelid::regclass AS table_name,
22 | json_object_agg(
23 | substring(pg_get_constraintdef(oid), '\((.*?)\)'), json_build_object(
24 | 'referenceTable', substring(pg_get_constraintdef(oid), 'REFERENCES (.*?)\('),
25 | 'referenceKey', substring(pg_get_constraintdef(oid), 'REFERENCES.*?\((.*?)\)')
26 | )
27 | ) AS foreign_keys
28 | FROM pg_constraint
29 | WHERE contype = 'f' AND connamespace = 'public'::regnamespace
30 | GROUP BY table_name
31 | ---------------------------------------------------------------------------------------
32 | ) AS fk
33 | ON pk.table_name = fk.table_name
34 |
35 | LEFT OUTER JOIN ( -- Reference data (rd)
36 | ---------------------------------------------------------------------------------------------------
37 | SELECT substring(pg_get_constraintdef(oid), 'REFERENCES (.*?)\(') AS table_name, json_object_agg(
38 | conrelid::regclass, substring(pg_get_constraintdef(oid), '\((.*?)\)')
39 | ) AS referenced_by
40 | FROM pg_constraint
41 | WHERE contype = 'f' AND connamespace = 'public'::regnamespace
42 | GROUP BY table_name
43 | ---------------------------------------------------------------------------------------------------
44 | ) AS rd
45 | ON pk.table_name::regclass = rd.table_name::regclass
46 |
47 | LEFT OUTER JOIN ( -- Table data (td)
48 | -----------------------------------------------------------------
49 | SELECT tab.table_name, json_object_agg(
50 | col.column_name, json_build_object(
51 | 'dataType', col.data_type,
52 | 'columnDefault', col.column_default,
53 | 'charMaxLength', col.character_maximum_length,
54 | 'isNullable', col.is_nullable
55 | )
56 | ) AS columns
57 |
58 | -- Table names
59 | FROM (
60 | SELECT table_name FROM information_schema.tables
61 | WHERE table_type='BASE TABLE' AND table_schema='public'
62 | ) AS tab
63 |
64 | -- Table columns
65 | INNER JOIN information_schema.columns AS col
66 | ON tab.table_name = col.table_name
67 | GROUP BY tab.table_name
68 | -----------------------------------------------------------------
69 | ) AS td
70 | ON td.table_name::regclass = pk.table_name
71 |
72 | -- postgres://ordddiou:g5OjOyAIFxf-tsLk1uwu4ZOfbJfiCFbh@ruby.db.elephantsql.com:5432/ordddiou
73 |
--------------------------------------------------------------------------------
/server/SDL-definedSchemas/controllers/pgController.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-syntax */
2 | /* eslint-disable guard-for-in */
3 | const fs = require('fs');
4 | const { Pool } = require('pg');
5 | const SchemaGenerator = require('../generators/schemaGenerator');
6 |
7 | const pgQuery = fs.readFileSync('server/queries/tableData.sql', 'utf8');
8 |
9 | const pgController = {};
10 |
11 | // Middleware function for recovering info from pg tables
12 | pgController.getPGTables = (req, res, next) => {
13 | const db = new Pool({ connectionString: req.body.uri.trim() });
14 |
15 | db.query(pgQuery)
16 | .then((data) => {
17 | res.locals.tables = data.rows[0].tables;
18 | return next();
19 | })
20 | .catch((err) => res.json('error'));
21 |
22 | console.log(req.body.uri);
23 | };
24 |
25 | // Middleware function for assembling SDL schema
26 | pgController.assembleSDLSchema = (req, res, next) => {
27 | try {
28 | res.locals.SDLSchema = SchemaGenerator.assembleSchema(res.locals.tables);
29 | return next();
30 | } catch (err) {
31 | return next({
32 | log: err,
33 | status: 500,
34 | message: { err: 'There was a problem assembling SDL schema' },
35 | });
36 | }
37 | };
38 |
39 | pgController.compileData = (req, res, next) => {
40 | try {
41 | const OriginalTables = res.locals.tables;
42 | const newTables = {};
43 |
44 | for (const table in OriginalTables) {
45 | const currentTable = OriginalTables[table];
46 | if (
47 | !currentTable.foreignKeys ||
48 | Object.keys(currentTable.columns).length !== Object.keys(currentTable.foreignKeys).length + 1
49 | ) {
50 | const pointsTo = [];
51 | for (const objName in currentTable.foreignKeys) {
52 | pointsTo.push(currentTable.foreignKeys[objName].referenceTable);
53 | }
54 | const referecedBy = [];
55 | for (const refTableName in currentTable.referencedBy) {
56 | if (
57 | !OriginalTables[refTableName].foreignKeys ||
58 | Object.keys(OriginalTables[refTableName].columns).length !==
59 | Object.keys(OriginalTables[refTableName].foreignKeys).length + 1
60 | ) {
61 | referecedBy.push(refTableName);
62 | } else {
63 | // else it's a join table
64 | for (const foreignKey in OriginalTables[refTableName].foreignKeys) {
65 | const joinedTable = OriginalTables[refTableName].foreignKeys[foreignKey].referenceTable;
66 | if (joinedTable !== table) {
67 | referecedBy.push(joinedTable);
68 | }
69 | }
70 | }
71 | }
72 | const columns = [];
73 | for (const columnName in currentTable.columns) {
74 | if (columnName !== currentTable.primaryKey) {
75 | columns.push(columnName);
76 | }
77 | }
78 |
79 | newTables[table] = {
80 | pointsTo,
81 | referecedBy,
82 | columns,
83 | };
84 | }
85 | }
86 | res.locals.d3Data = newTables;
87 | return next();
88 | } catch (err) {
89 | return next({
90 | log: err,
91 | status: 500,
92 | message: {
93 | err: 'There was a problem compiling tables in pgController.compileData',
94 | },
95 | });
96 | }
97 | };
98 |
99 | module.exports = pgController;
100 |
--------------------------------------------------------------------------------
/__tests__/puppeteer.js:
--------------------------------------------------------------------------------
1 | const puppeteer = require('puppeteer');
2 |
3 | const APP = `http://localhost:${process.env.PORT || 8081}/`;
4 |
5 | describe('Front-end Integration/Features', () => {
6 | let browser;
7 | let page;
8 |
9 | beforeAll(async () => {
10 | browser = await puppeteer.launch({
11 | args: ['--no-sandbox', '--disable-setuid-sandbox'],
12 | headless: true,
13 | });
14 | page = await browser.newPage();
15 | });
16 |
17 | afterAll(() => {
18 | APP.destroy();
19 | });
20 |
21 | describe('Initial display', () => {
22 | it('loads successfully', async () => {
23 | // We navigate to the page at the beginning of each case so we have a fresh start
24 | await page.goto(APP);
25 | await page.waitForSelector('.modal-body');
26 | const title = await page.$eval('h3', (el) => el.innerHTML);
27 | expect(title).toBe('LucidQL');
28 | });
29 |
30 | it('displays a usable input field for a Postgres URI', async () => {
31 | await page.goto(APP);
32 | await page.waitForSelector('.form-group');
33 | await page.focus('#link');
34 | await page.keyboard.type('www.sometestlink.com');
35 | const inputValue = await page.$eval('#link', (el) => el.value);
36 | expect(inputValue).toBe('www.sometestlink.com');
37 | });
38 |
39 | it('clicking on submit while input is empty does not close modal', async () => {
40 | await page.goto(APP);
41 | await page.waitForSelector('.modal-body');
42 | const title = await page.$eval('h3', (el) => el.innerHTML);
43 | await page.click('.submit');
44 | expect(title).toBe('LucidQL');
45 | });
46 |
47 | it('MySQL button exists on modal', async () => {
48 | await page.goto(APP);
49 | await page.waitForSelector('.modal-body');
50 | const mySQLButton = await page.$eval('.mySQL', (el) => el.innerHTML);
51 | expect(mySQLButton).toBe('Use MySQL Database');
52 | });
53 |
54 | it('Clicking MySQL button brings you to the MySQL page', async () => {
55 | await page.goto(APP);
56 | await page.waitForSelector('.modal-body');
57 | await page.click('.mySQL');
58 | const mySQLModal = await page.$eval('p', (el) => el.innerHTML);
59 |
60 | expect(mySQLModal).toBe('Please enter MySQL information below.');
61 | });
62 |
63 | it('clicking on close button closes the modal', async () => {
64 | await page.goto(APP);
65 | await page.waitForSelector('.form-group');
66 | await page.click('._modal-close-icon');
67 | const title = (await page.$('h3', (el) => el.innerHTML)) || null;
68 | expect(title).toBe(null);
69 | });
70 | });
71 | describe('Main Page', () => {
72 | it('successfully loads top navbar', async () => {
73 | // We navigate to the page at the beginning of each case so we have a fresh start
74 | await page.goto(APP);
75 | const topNav = (await page.$('.sticky-nav')) !== null;
76 | expect(topNav).toBe(true);
77 | });
78 | it('successfully loads footer navbar', async () => {
79 | const footerNav = (await page.$('.footer')) !== null;
80 | expect(footerNav).toBe(true);
81 | });
82 | it('successfully loads d3 graph container', async () => {
83 | const graphArea = (await page.$('.footer')) !== null;
84 | expect(graphArea).toBe(true);
85 | });
86 | it('successfully loads code mirror', async () => {
87 | const codeMirror = (await page.$('.CodeMirror')) !== null;
88 | expect(codeMirror).toBe(true);
89 | });
90 | it('successfully loads split bar in middle of page', async () => {
91 | const splitBar = (await page.$('.sc-htpNat.fXAXjb.sc-bdVaJa.cjjWdp')) !== null;
92 | expect(splitBar).toBe(true);
93 | });
94 | });
95 | });
96 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | #
4 |
5 | [](https://github.com/oslabs-beta/LucidQL/blob/master/LICENSE)  [](https://github.com/oslabs-beta/LucidQL/issues)
6 |
7 | ### What is lucidQL?
8 |
9 | lucidQL is an open-source tool to assist developers in the migration from a legacy RESTful API to GraphQL. It also serves as a visualizer tool for your GraphQL API. lucidQL allows the user to view the GraphQL schema layout and relationships between data.
10 |
11 | Check out the tool - www.lucidQL.com
12 |
13 | ### How does lucidQL work?
14 |
15 | First choose your desired Postgres database that you want to use to create a graphQL API, and enter the link where prompted, and click submit. lucidQL will immediately start by reading your database, extracting all the tables, and their relationships, and generate a GraphQL schema.
16 |
17 | 
18 |
19 | A visual representation will display on the left side of the tool, users can analyze their current database and their table relationships.
20 |
21 | The lucidQL tool allows the user to simplify their schema by being able to delete any relationships that they no longer require.
22 |
23 | - If any tables are undesired in the final product, simply drag a table to the garbage icon, lucidQL will handle the rest.
24 | - The GraphQL schemas will be regenerated accordingly to your changes.
25 | - if you make a mistake, simply click the "undo" button.
26 |
27 | 
28 |
29 | ### How to Test Your Schema, Resolvers and Mutations
30 |
31 | #### Option A: Enter your Postgres URI and start testing your GraphQL API right away!
32 |
33 | The lucidQL tool comes pre-packaged with a backend, which enables the user to access GraphQL playground and start querying your new API, immediately. After entering a Postgres URI. The user will just have to click on "GraphQL PLayground" which can be accessed through the side menu bar.
34 |
35 |
36 |
37 | #### Option B: Download your package by clicking on the "Download" button
38 |
39 | 1. Download and Save
40 | 2. Unzip package
41 | 3. Open directory
42 | 4. Install dependencies: `npm install`
43 | 5. Run the application: `npm start`
44 | 6. Once the application is running, enter localhost:3000/playground in your browser to start querying your database
45 | 7. Test and Query!
46 |
47 | The downloaded package includes the following -
48 |
49 | - A connection file connects your Postgres API to your server.
50 | - The server file sets up your server and includes playground so that you can query the information from your API as needed.
51 | - Lastly, your schema file will provide you with queries, mutations, and resolvers based on your pre-existing relational database.
52 |
53 | ### Contributing
54 |
55 | We would love for you to test this application out and submit any issues you encounter. Also, feel free to fork to your own repository and submit PRs.
56 |
57 | Here are some of the ways you can start contributing!
58 |
59 | - Bug Fixes
60 | - Adding Features (Check Future Features above)
61 | - Submitting or resolving any GitHub Issues
62 | - Help market our platform
63 | - Any way that can spread word or improve lucidQL
64 |
65 | ### Authors
66 |
67 | - Martin Chiang
68 | - Stanley Huang
69 | - Darwin Sinchi
70 |
71 | ### License
72 |
73 | MIT
74 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "LucidQL",
3 | "version": "0.1.0",
4 | "description": "A developer tool that helps developers migrate to a GraphQL API from an established relational database and allows the user to view and simplify these relations.",
5 | "authors": "Darwin Sinchi, Martin Chiang, Stanley Huang",
6 | "license": "MIT",
7 | "bugs": {
8 | "url": "https://github.com/oslabs-beta/lucidQL/issues"
9 | },
10 | "homepage": "",
11 | "main": "index.js",
12 | "scripts": {
13 | "test": "NODE_ENV=test jest --verbose",
14 | "start": "nodemon server/server.js",
15 | "build": "webpack --mode production",
16 | "dev": "nodemon server/server.js & webpack-dev-server --mode development --open --hot",
17 | "dev:hot": "NODE_ENV=development nodemon ./server/server.js & NODE_ENV=development webpack-dev-server --open --hot --inline --progress --colors --watch --content-base ./"
18 | },
19 | "jest": {
20 | "globalSetup": "./jest-setup.js",
21 | "globalTeardown": "./jest-teardown.js",
22 | "moduleNameMapper": {
23 | "\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js",
24 | "\\.(css|less)$": "/__mocks__/fileMock.js"
25 | }
26 | },
27 | "dependencies": {
28 | "@babel/plugin-transform-async-to-generator": "^7.10.4",
29 | "@babel/plugin-transform-runtime": "^7.11.5",
30 | "@babel/runtime": "^7.11.2",
31 | "@types/d3": "^5.7.2",
32 | "@types/d3-selection-multi": "^1.0.8",
33 | "@types/file-saver": "^2.0.1",
34 | "@types/react": "^16.9.49",
35 | "@types/react-dom": "^16.9.8",
36 | "@types/recoil": "0.0.1",
37 | "codemirror": "^5.57.0",
38 | "d3": "^6.1.1",
39 | "d3-selection": "^2.0.0",
40 | "d3-selection-multi": "^1.0.1",
41 | "enzyme": "^3.11.0",
42 | "express": "^4.17.1",
43 | "express-graphql": "^0.9.0",
44 | "file-saver": "^2.0.2",
45 | "focus-trap-react": "^8.0.0",
46 | "fs": "0.0.1-security",
47 | "graphql": "^15.3.0",
48 | "graphql-playground-middleware-express": "^1.7.20",
49 | "graphql-playground-react": "^1.7.26",
50 | "graphql-tools": "^6.2.3",
51 | "jszip": "^3.5.0",
52 | "mysql2": "^2.2.3",
53 | "pg": "^8.3.3",
54 | "pg-hstore": "^2.3.3",
55 | "pluralize": "^8.0.0",
56 | "react": "^16.13.1",
57 | "react-bootstrap": "^1.3.0",
58 | "react-codemirror2": "^7.2.1",
59 | "react-dom": "^16.13.1",
60 | "react-graph-vis": "^1.0.5",
61 | "react-router": "^5.2.0",
62 | "react-split-pane": "^2.0.3",
63 | "recoil": "0.0.10",
64 | "regenerator-runtime": "^0.13.7",
65 | "vis-react": "^0.5.1"
66 | },
67 | "devDependencies": {
68 | "@babel/core": "^7.11.5",
69 | "@babel/plugin-proposal-class-properties": "^7.10.4",
70 | "@babel/plugin-proposal-object-rest-spread": "^7.11.0",
71 | "@babel/preset-env": "^7.11.5",
72 | "@babel/preset-react": "^7.10.4",
73 | "@babel/preset-typescript": "^7.10.4",
74 | "babel-core": "^6.26.3",
75 | "babel-jest": "^26.3.0",
76 | "babel-loader": "^8.1.0",
77 | "css-loader": "^4.2.2",
78 | "enzyme-adapter-react-16": "^1.15.4",
79 | "eslint": "^7.2.0",
80 | "eslint-config-airbnb": "^18.2.0",
81 | "eslint-config-prettier": "^6.11.0",
82 | "eslint-plugin-import": "^2.22.0",
83 | "eslint-plugin-jsx-a11y": "^6.3.1",
84 | "eslint-plugin-prettier": "^3.1.4",
85 | "eslint-plugin-react": "^7.20.6",
86 | "eslint-plugin-react-hooks": "^4.0.0",
87 | "html-webpack-plugin": "^4.4.1",
88 | "jest": "^26.4.2",
89 | "jest-puppeteer": "^3.4.0",
90 | "nodemon": "^2.0.4",
91 | "prettier": "^2.1.1",
92 | "puppeteer": "^5.3.1",
93 | "style-loader": "^1.2.1",
94 | "supertest": "^4.0.2",
95 | "ts-loader": "^8.0.3",
96 | "tslint": "^6.1.3",
97 | "tslint-immutable": "^6.0.1",
98 | "typescript": "^4.0.2",
99 | "url-loader": "^4.1.0",
100 | "webpack": "^4.44.1",
101 | "webpack-cli": "^3.3.12",
102 | "webpack-dev-server": "^3.11.0"
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/client/forceGraph/generators/d3DataBuilder.js:
--------------------------------------------------------------------------------
1 | export const generateNodeAndLink = (data) => {
2 | const d3Arrays = { tableNodes:[], columnNodes:[], referencedBy:[], pointsTo: [], linksToColumns:[] };
3 | for (let tableName in data) { // handle table node
4 | const parentNode = {
5 | id: `Parent-${tableName}`,
6 | name: tableName,
7 | primary: true,
8 | primaryKey: data[tableName].primaryKey,
9 | foreignKeys: data[tableName].foreignKeys,
10 | columnCount: data[tableName].columns.length,
11 | referencedBy: data[tableName].referencedBy
12 | }
13 | d3Arrays.tableNodes.push(parentNode);
14 |
15 | // handle links between tables
16 | data[tableName].pointsTo.forEach((targetTable) => {
17 | const parentLink = {
18 | source: `Parent-${tableName}`,
19 | target: `Parent-${targetTable}`,
20 | type: 'pointsTo',
21 | }
22 | d3Arrays.pointsTo.push(parentLink);
23 | })
24 | data[tableName].referencedBy.forEach(refTable => {
25 | const parentLink = {
26 | source: `Parent-${refTable}`,
27 | target: `Parent-${tableName}`,
28 | type: 'referencedBy',
29 | }
30 | d3Arrays.referencedBy.push(parentLink);
31 | })
32 |
33 | // handle links between column and its parent
34 | data[tableName].columns.forEach((columnObj) => {
35 | const childNode = {
36 | id: `${tableName}-${columnObj.columnName}`,
37 | name: columnObj.columnName,
38 | parent: tableName,
39 | ...columnObj,
40 | };
41 | const childLink = {
42 | source: `Parent-${tableName}`,
43 | target: `${tableName}-${columnObj.columnName}`
44 | }
45 | if (data[tableName].foreignKeys.includes(columnObj.columnName)) { // if this column is a foreign key
46 | childNode.foreignKey = true;
47 | }
48 | d3Arrays.columnNodes.push(childNode);
49 | d3Arrays.linksToColumns.push(childLink)
50 | })
51 | }
52 | return d3Arrays;
53 | };
54 |
55 | export const simplifyTable = (OriginalTables) => {
56 | const newTables = {};
57 |
58 | for (const table in OriginalTables) {
59 | const currentTable = OriginalTables[table];
60 | if ( // if this is not a join table
61 | !currentTable.foreignKeys ||
62 | Object.keys(currentTable.columns).length !== Object.keys(currentTable.foreignKeys).length + 1
63 | ) {
64 | const pointsTo = [];
65 | const foreignKeys = [];
66 | for (const objName in currentTable.foreignKeys) {
67 | pointsTo.push(currentTable.foreignKeys[objName].referenceTable);
68 | foreignKeys.push(objName);
69 | }
70 | const referencedBy = [];
71 | for (const refTableName in currentTable.referencedBy) {
72 | if (
73 | !OriginalTables[refTableName].foreignKeys ||
74 | Object.keys(OriginalTables[refTableName].columns).length !==
75 | Object.keys(OriginalTables[refTableName].foreignKeys).length + 1
76 | ) {
77 | referencedBy.push(refTableName);
78 | } else { // else it's a join table
79 | for (const foreignKey in OriginalTables[refTableName].foreignKeys) {
80 | const joinedTable = OriginalTables[refTableName].foreignKeys[foreignKey].referenceTable;
81 | if (joinedTable !== table) {
82 | referencedBy.push(joinedTable);
83 | }
84 | }
85 | }
86 | }
87 | const columns = [];
88 | for (const columnName in currentTable.columns) {
89 | if (columnName !== currentTable.primaryKey) {
90 | const columnInfo = {
91 | columnName,
92 | ...currentTable.columns[columnName]
93 | }
94 | columns.push(columnInfo);
95 | }
96 | }
97 |
98 | newTables[table] = {
99 | pointsTo,
100 | referencedBy,
101 | columns,
102 | foreignKeys,
103 | primaryKey: currentTable.primaryKey
104 | };
105 | }
106 | }
107 |
108 | return newTables;
109 | };
110 |
--------------------------------------------------------------------------------
/client/forceGraph/generators/deleteFunctions.js:
--------------------------------------------------------------------------------
1 | import { assembleSchema } from './schemaGenerator';
2 |
3 | export function deleteTables(currentState, tableToExclude) {
4 | const { tables: tablesObj, schema: currentSchema, history: currentHistory } = currentState;
5 | const newTables = {}
6 | const helperFuncOnJoinTable = (table, tableToExclude) => { // if this is a join table and it has included tableToExclude
7 | if (!table.foreignKeys) return false; // no foreign keys, not a join table so just return false
8 | if (Object.keys(table.columns).length === Object.keys(table.foreignKeys).length + 1) { // if this is a join table
9 | for(let key in table.foreignKeys) {
10 | if (table.foreignKeys[key].referenceTable === tableToExclude) return true;
11 | }
12 | }
13 | return false;
14 | }
15 | for (let tableName in tablesObj) {
16 | if (tableName !== tableToExclude && !helperFuncOnJoinTable(tablesObj[tableName], tableToExclude)) {
17 | newTables[tableName] = {};
18 | newTables[tableName].primaryKey = tablesObj[tableName].primaryKey;
19 | newTables[tableName].columns = tablesObj[tableName].columns;
20 | let newFK = {}
21 | for (let key in tablesObj[tableName].foreignKeys) {
22 | if (tablesObj[tableName].foreignKeys[key].referenceTable !== tableToExclude) {
23 | newFK[key] = tablesObj[tableName].foreignKeys[key]
24 | }
25 | }
26 | newTables[tableName].foreignKeys = newFK
27 | let newRefby = {}
28 | for (let refByTableName in tablesObj[tableName].referencedBy) {
29 | if (refByTableName !== tableToExclude && !helperFuncOnJoinTable(tablesObj[refByTableName], tableToExclude)) {
30 | newRefby[refByTableName] = tablesObj[tableName].referencedBy[refByTableName]
31 | }
32 | }
33 | newTables[tableName].referencedBy = newRefby;
34 | }
35 | }
36 | const newSchema = assembleSchema(newTables);
37 | const history = [...currentHistory, {table: tablesObj, schema: currentSchema}];
38 | return { newTables, newSchema, history }
39 | }
40 |
41 | export function deleteColumns(currentState, columnToExclude, parentName, foreignKeyToDelete) {
42 | const { tables: tablesObj, schema: currentSchema, history: currentHistory } = currentState;
43 | const newTables = {};
44 |
45 | let FKTargetTable = null;
46 | if (foreignKeyToDelete) {
47 | FKTargetTable = tablesObj[parentName].foreignKeys[columnToExclude].referenceTable
48 | }
49 |
50 | for (let tableName in tablesObj) {
51 | if (tableName === parentName) {
52 | const newColumns = {};
53 | for (let objName in tablesObj[parentName].columns) {
54 | if (objName !== columnToExclude) {
55 | newColumns[objName] = tablesObj[parentName].columns[objName]
56 | }
57 | }
58 |
59 | if (foreignKeyToDelete) { // if foreignKeyToDelete is true, modify newForeignKeys
60 | const newForeignKeys = {};
61 | for (let objName in tablesObj[parentName].foreignKeys) {
62 | if (objName !== columnToExclude) {
63 | newForeignKeys[objName] = tablesObj[parentName].foreignKeys[objName];
64 | }
65 | }
66 | newTables[tableName] = {...tablesObj[tableName], columns: newColumns, foreignKeys: newForeignKeys}
67 | } else { // if foreignKeyToDelete is false...
68 | newTables[tableName] = {...tablesObj[tableName], columns: newColumns}
69 | }
70 | } else if (foreignKeyToDelete && tableName === FKTargetTable) {
71 | // if we need to deal with a table that is referenced by this FK
72 | const newRefby = {}
73 | for (let refObjName in tablesObj[FKTargetTable].referencedBy) {
74 | if (refObjName !== parentName) {
75 | newRefby[refObjName] = tablesObj[FKTargetTable].referencedBy[refObjName];
76 | }
77 | }
78 | newTables[FKTargetTable] = {...tablesObj[FKTargetTable], referencedBy: newRefby}
79 | } else { // else for other tables we can just make a copy
80 | newTables[tableName] = tablesObj[tableName]
81 | }
82 | }
83 |
84 | const newSchema = assembleSchema(newTables);
85 | const history = [...currentHistory, {table: tablesObj, schema: currentSchema}];
86 | return { newTables, newSchema, history }
87 | }
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "tslint-immutable"
4 | ],
5 | "rules": {
6 | "no-var-keyword": true,
7 | "no-parameter-reassignment": true,
8 | "typedef": false,
9 | "readonly-keyword": false,
10 | "readonly-array": false,
11 | "no-let": false,
12 | "no-array-mutation": true,
13 | "no-object-mutation": false,
14 | "no-this": false,
15 | "no-class": false,
16 | "no-mixed-interface": false,
17 | "no-expression-statement": false,
18 | "member-ordering": [
19 | true,
20 | "variables-before-functions"
21 | ],
22 | "no-any": false,
23 | "no-inferrable-types": [
24 | false
25 | ],
26 | "no-internal-module": true,
27 | "no-var-requires": false,
28 | "typedef-whitespace": [
29 | true,
30 | {
31 | "call-signature": "nospace",
32 | "index-signature": "nospace",
33 | "parameter": "nospace",
34 | "property-declaration": "nospace",
35 | "variable-declaration": "nospace"
36 | },
37 | {
38 | "call-signature": "space",
39 | "index-signature": "space",
40 | "parameter": "space",
41 | "property-declaration": "space",
42 | "variable-declaration": "space"
43 | }
44 | ],
45 | "ban": false,
46 | "curly": false,
47 | "forin": true,
48 | "label-position": true,
49 | "no-arg": true,
50 | "no-bitwise": false,
51 | "no-conditional-assignment": true,
52 | "no-console": [
53 | true,
54 | "info",
55 | "time",
56 | "timeEnd",
57 | "trace"
58 | ],
59 | "no-construct": true,
60 | "no-debugger": true,
61 | "no-duplicate-variable": true,
62 | "no-empty": false,
63 | "no-eval": true,
64 | "strictPropertyInitialization": false,
65 | "no-null-keyword": false,
66 | "no-shadowed-variable": false,
67 | "no-string-literal": false,
68 | "no-switch-case-fall-through": true,
69 | "no-unused-expression": [
70 | true,
71 | "allow-fast-null-checks"
72 | ],
73 | "no-use-before-declare": true,
74 | "radix": true,
75 | "switch-default": true,
76 | "triple-equals": [
77 | true,
78 | "allow-undefined-check",
79 | "allow-null-check"
80 | ],
81 | "eofline": false,
82 | "indent": [
83 | true,
84 | "tabs"
85 | ],
86 | "max-line-length": [
87 | true,
88 | 250
89 | ],
90 | "no-require-imports": false,
91 | "no-trailing-whitespace": true,
92 | "object-literal-sort-keys": false,
93 | "trailing-comma": [
94 | true,
95 | {
96 | "multiline": "never",
97 | "singleline": "never"
98 | }
99 | ],
100 | "align": [
101 | true
102 | ],
103 | "class-name": true,
104 | "comment-format": [
105 | true,
106 | "check-space"
107 | ],
108 | "interface-name": [
109 | false
110 | ],
111 | "jsdoc-format": true,
112 | "no-consecutive-blank-lines": [
113 | true
114 | ],
115 | "no-parameter-properties": false,
116 | "one-line": [
117 | true,
118 | "check-open-brace",
119 | "check-catch",
120 | "check-else",
121 | "check-finally",
122 | "check-whitespace"
123 | ],
124 | "quotemark": [
125 | true,
126 | "single",
127 | "avoid-escape"
128 | ],
129 | "semicolon": [
130 | true,
131 | "always"
132 | ],
133 | "variable-name": [
134 | false,
135 | "check-format",
136 | "allow-leading-underscore",
137 | "ban-keywords"
138 | ],
139 | "whitespace": [
140 | true,
141 | "check-branch",
142 | "check-decl",
143 | "check-operator",
144 | "check-separator",
145 | "check-type"
146 | ]
147 | }
148 | }
--------------------------------------------------------------------------------
/public/icon/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 | Created by potrace 1.11, written by Peter Selinger 2001-2013
9 |
10 |
12 |
17 |
19 |
21 |
26 |
31 |
33 |
35 |
37 |
39 |
41 |
43 |
45 |
50 |
55 |
57 |
59 |
61 |
63 |
65 |
71 |
76 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/client/styles.css:
--------------------------------------------------------------------------------
1 | .App {
2 | font-family: sans-serif;
3 | text-align: center;
4 | }
5 |
6 | body {
7 | margin: 0;
8 | padding: 0;
9 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans',
10 | 'Droid Sans', 'Helvetica Neue', sans-serif;
11 | -webkit-font-smoothing: antialiased;
12 | -moz-osx-font-smoothing: grayscale;
13 | box-sizing: border-box;
14 | overflow: hidden;
15 | }
16 |
17 | *,
18 | *:before,
19 | *:after {
20 | box-sizing: inherit;
21 | }
22 |
23 | /* ---Nav Bar CSS--- */
24 | .sticky-nav {
25 | position: sticky;
26 | top: 0;
27 | width: 100%;
28 | z-index: 10;
29 | border-bottom: 1px solid rgb(130, 130, 130);
30 | color: black;
31 | }
32 |
33 | .btn {
34 | background-color: #967bb6;
35 | color: white;
36 | }
37 |
38 | .secondary-btn {
39 | background: #f6f6f7;
40 | color: black;
41 | }
42 |
43 | .logo {
44 | width: 1.5%;
45 | }
46 |
47 | .logo-brand {
48 | width: 15%;
49 | margin: 0px auto;
50 | }
51 |
52 | .footer {
53 | position: sticky;
54 | background-color: #f5f5f5;
55 | bottom: 0;
56 | width: 100%;
57 | z-index: 10;
58 | border-top: 1px solid rgb(203, 201, 201);
59 | }
60 |
61 | /* .footer-container {
62 | display: flex;
63 | width: 100%;
64 | align-items: center;
65 | justify-content: right;
66 | } */
67 |
68 | /* ---Nav Bar CSS--- END*/
69 |
70 | /* D3/Visgraph Div */
71 | .graph-div {
72 | display: flex;
73 | align-items: center;
74 | justify-content: space-evenly;
75 | height: 100%;
76 | padding: 1rem;
77 | background-color: #f5f5f5;
78 | }
79 |
80 | .annotation {
81 | position: fixed;
82 | bottom: 6vh;
83 | left: 1vw;
84 | z-index: 2;
85 | }
86 |
87 | .SVG_container {
88 | width: 100%;
89 | height: 100%;
90 | position: relative;
91 | }
92 |
93 | .primary_node {
94 | font-size: 24px;
95 | fill: white;
96 | }
97 |
98 | .column_node {
99 | font-size: 12px;
100 | fill: white
101 | }
102 |
103 | div.tooltip {
104 | position: absolute;
105 | text-align: center;
106 | width: 200px;
107 | padding: 10px;
108 | font: 14px sans-serif;
109 | background: #1a1a2e;
110 | color: white;
111 | border: 0;
112 | border-radius: 8px;
113 | pointer-events: none;
114 | z-index: 5;
115 | line-height: 1.5rem;
116 | }
117 |
118 | /* ---Codebox CSS--- */
119 | .code-box {
120 | display: inline-flexbox;
121 | width: 100%;
122 | background-color: black;
123 | }
124 |
125 | .CodeMirror-hscrollbar {
126 | overflow: hidden;
127 | }
128 |
129 | .CodeMirror {
130 | width: 100%;
131 | height: 90vh;
132 | overflow: hidden;
133 | }
134 |
135 | /* ----POPUP LINK BOX---- */
136 | .modal-cover {
137 | position: fixed;
138 | top: 0;
139 | left: 0;
140 | width: 100%;
141 | height: 100%;
142 | z-index: 10;
143 | transform: translateZ(0);
144 | background-color: rgba(0, 0, 0, 0.5);
145 | }
146 |
147 | .modal-area {
148 | position: fixed;
149 | top: 0;
150 | left: 0;
151 | width: 100%;
152 | height: 100%;
153 | padding: 5em 3em 3em 3em;
154 | background-color: #ffffff;
155 | box-shadow: 0 0 10px 3px rgba(0, 0, 0, 0.1);
156 | overflow-y: auto;
157 | -webkit-overflow-scrolling: touch;
158 | border-radius: 3%;
159 | }
160 |
161 | @media screen and (min-width: 500px) {
162 | .modal-area {
163 | left: 50%;
164 | top: 50%;
165 | height: auto;
166 | transform: translate(-50%, -50%);
167 | max-width: 55em;
168 | max-height: calc(100% - 1em);
169 | }
170 | }
171 |
172 | ._modal-close {
173 | position: absolute;
174 | top: 0;
175 | right: 0;
176 | padding: 0.5em;
177 | line-height: 1;
178 | background: #f6f6f7;
179 | border: 0;
180 | box-shadow: 0;
181 | cursor: pointer;
182 | }
183 |
184 | ._modal-close-icon {
185 | width: 25px;
186 | height: 25px;
187 | fill: transparent;
188 | stroke: black;
189 | stroke-linecap: round;
190 | stroke-width: 2;
191 | }
192 |
193 | .modal-body {
194 | padding-top: 0.25em;
195 | }
196 |
197 | ._hide-visual {
198 | border: 0 !important;
199 | clip: rect(0 0 0 0) !important;
200 | height: 1px !important;
201 | margin: -1px !important;
202 | overflow: hidden !important;
203 | padding: 0 !important;
204 | position: absolute !important;
205 | width: 1px !important;
206 | white-space: nowrap !important;
207 | }
208 |
209 | .scroll-lock {
210 | overflow: hidden;
211 | /* margin-right: 17px; */
212 | }
213 |
214 | /* ----POPUP LINK BOX---- END*/
215 |
216 | /* ---SIDEBAR---- */
217 |
218 | .sidebar {
219 | height: 100%;
220 | width: 0;
221 | position: fixed;
222 | z-index: 1;
223 | top: 0;
224 | left: 0;
225 | background-color: #111;
226 | overflow-x: hidden;
227 | transition: 0.5s;
228 | padding-top: 60px;
229 | }
230 |
231 | .sidebar a {
232 | padding: 8px 8px 8px 32px;
233 | text-decoration: none;
234 | font-size: 25px;
235 | color: #818181;
236 | display: block;
237 | transition: 0.3s;
238 | }
239 |
240 | .sidebar a:hover {
241 | color: #f1f1f1;
242 | }
243 |
244 | .sidebar .closebtn {
245 | position: absolute;
246 | top: 0;
247 | right: 25px;
248 | font-size: 36px;
249 | margin-left: 50px;
250 | }
251 |
252 | .openbtn {
253 | font-size: 20px;
254 | cursor: pointer;
255 | color: black;
256 | padding: 10px 15px;
257 | border: none;
258 | margin: auto;
259 | /* margin-top: 20px; */
260 | }
261 |
262 | .openbtn:hover {
263 | background-color: #444;
264 | }
265 |
266 | #main {
267 | transition: margin-left 0.5s;
268 | margin: 0;
269 | }
270 |
271 | /* On smaller screens, where height is less than 450px, change the style of the sidenav (less padding and a smaller font size) */
272 | @media screen and (max-height: 450px) {
273 | .sidebar {
274 | padding-top: 15px;
275 | }
276 | .sidebar a {
277 | font-size: 18px;
278 | }
279 | }
280 |
281 | /* Resizer bar */
282 |
283 | .sc-htpNat.fXAXjb.sc-bdVaJa.cjjWdp {
284 | width: 19px;
285 | margin: 0 -5px;
286 | border-left: 5px solid rgba(255, 255, 255, 1);
287 | border-right: 5px solid rgba(255, 255, 255, 1);
288 | }
289 |
290 | .sc-htpNat.fXAXjb.sc-bdVaJa.cjjWdp:hover {
291 | width: 22px;
292 | margin: 0 -6px;
293 | cursor: col-resize;
294 | -webkit-transition: all 0.1s ease;
295 | transition: all 0.1s ease;
296 | }
297 |
298 | hr {
299 | background-color: rgb(211, 211, 211);
300 | }
301 |
--------------------------------------------------------------------------------
/client/forceGraph/generators/typeGenerator.js:
--------------------------------------------------------------------------------
1 | import { singular } from 'pluralize';
2 | import {
3 | toCamelCase,
4 | toPascalCase,
5 | typeSet,
6 | getPrimaryKeyType,
7 | } from './helperFunctions';
8 |
9 | const TypeGenerator = {};
10 |
11 | TypeGenerator.queries = function queries(tableName, tableData) {
12 | const { primaryKey, foreignKeys, columns } = tableData;
13 | const nameSingular = singular(tableName);
14 | const primaryKeyType = getPrimaryKeyType(primaryKey, columns);
15 | if (!foreignKeys || Object.keys(columns).length !== Object.keys(foreignKeys).length + 1) {
16 | // Do not output pure join tables
17 | let byID = toCamelCase(nameSingular);
18 | if (nameSingular === tableName) byID += 'ByID';
19 | return (
20 | ` ${toCamelCase(tableName)}: [${toPascalCase(nameSingular)}!]!\n` +
21 | ` ${byID}(${primaryKey}: ${primaryKeyType}!): ${toPascalCase(nameSingular)}!\n`
22 | );
23 | }
24 | return '';
25 | };
26 |
27 | TypeGenerator.mutations = function mutations(tableName, tableData) {
28 | const { primaryKey, foreignKeys, columns } = tableData;
29 | if (!foreignKeys || Object.keys(columns).length !== Object.keys(foreignKeys).length + 1) {
30 | // Do not output pure join tables
31 | return (
32 | this._create(tableName, primaryKey, foreignKeys, columns) +
33 | this._update(tableName, primaryKey, foreignKeys, columns) +
34 | this._destroy(tableName, primaryKey)
35 | );
36 | }
37 | return '';
38 | };
39 |
40 | TypeGenerator.customTypes = function customTypes(tableName, tables) {
41 | const { primaryKey, foreignKeys, columns } = tables[tableName];
42 | const primaryKeyType = getPrimaryKeyType(primaryKey, columns);
43 | if (foreignKeys === null || Object.keys(columns).length !== Object.keys(foreignKeys).length + 1) {
44 | return `${
45 | ` type ${toPascalCase(singular(tableName))} {\n` + ` ${primaryKey}: ${primaryKeyType}!`
46 | }${this._columns(primaryKey, foreignKeys, columns)}${this._getRelationships(
47 | tableName,
48 | tables
49 | )}\n }\n\n`;
50 | }
51 | return '';
52 | };
53 |
54 | TypeGenerator._columns = function columns(primaryKey, foreignKeys, columns) {
55 | let colStr = '';
56 | for (const columnName in columns) {
57 | if (!(foreignKeys && foreignKeys[columnName]) && columnName !== primaryKey) {
58 | const { dataType, isNullable, columnDefault } = columns[columnName];
59 | colStr += `\n ${columnName}: ${typeSet(dataType)}`;
60 | if (isNullable === 'NO' && columnDefault === null) colStr += '!';
61 | }
62 | }
63 | return colStr;
64 | };
65 |
66 | // Get table relationships
67 | TypeGenerator._getRelationships = function getRelationships(tableName, tables) {
68 | let relationships = '';
69 | const alreadyAddedType = []; // cache to track which relation has been added or not
70 | for (const refTableName in tables[tableName].referencedBy) {
71 | const {
72 | referencedBy: foreignRefBy,
73 | foreignKeys: foreignFKeys,
74 | columns: foreignColumns,
75 | } = tables[refTableName];
76 |
77 | // One-to-one: when we can find tableName in foreignRefBy, that means this is a direct one to one relation
78 | if (foreignRefBy && foreignRefBy[tableName]) {
79 | if (!alreadyAddedType.includes(refTableName)) {
80 | // check if this refTableType has already been added by other tableName
81 | alreadyAddedType.push(refTableName);
82 | const refTableType = toPascalCase(singular(refTableName));
83 | relationships += `\n ${toCamelCase(singular(reftableName))}: ${refTableType}`;
84 | }
85 | }
86 |
87 | // One-to-many: check if this is a join table, and if it's not, we can add relations)
88 | // example2: people table will meet this criteria
89 | // example3: species and people table will meet this criteria
90 | else if (Object.keys(foreignColumns).length !== Object.keys(foreignFKeys).length + 1) {
91 | if (!alreadyAddedType.includes(refTableName)) {
92 | // check if this refTableType has already been added by other tableName
93 | alreadyAddedType.push(refTableName);
94 | const refTableType = toPascalCase(singular(refTableName));
95 | relationships += `\n ${toCamelCase(refTableName)}: [${refTableType}]`;
96 | }
97 | }
98 |
99 | // Many-to-many relations (so now handling join tables!)
100 | for (const foreignFKey in foreignFKeys) {
101 |
102 | if (tableName !== foreignFKeys[foreignFKey].referenceTable) {
103 | // Do not include original table in output
104 | if (!alreadyAddedType.includes(refTableName)) {
105 | // check if this refTableType has already been added by other tableName
106 | alreadyAddedType.push(refTableName);
107 | const manyToManyTable = toCamelCase(foreignFKeys[foreignFKey].referenceTable);
108 |
109 | relationships += `\n ${manyToManyTable}: [${toPascalCase(singular(manyToManyTable))}]`;
110 | }
111 | }
112 | }
113 | }
114 | for (const FKTableName in tables[tableName].foreignKeys) {
115 | const object = tables[tableName].foreignKeys[FKTableName];
116 | const refTableName = object.referenceTable;
117 | const refTableType = toPascalCase(singular(refTableName));
118 | relationships += `\n ${toCamelCase(refTableName)}: [${refTableType}]`;
119 | }
120 |
121 | return relationships;
122 | };
123 |
124 | TypeGenerator._create = function create(tableName, primaryKey, foreignKeys, columns) {
125 | return `\n ${toCamelCase(`create_${singular(tableName)}`)}(\n${this._typeParams(
126 | primaryKey,
127 | foreignKeys,
128 | columns,
129 | false
130 | )}): ${toPascalCase(singular(tableName))}!\n`;
131 | };
132 |
133 | TypeGenerator._update = function update(tableName, primaryKey, foreignKeys, columns) {
134 | return `\n ${toCamelCase(`update_${singular(tableName)}`)}(\n${this._typeParams(
135 | primaryKey,
136 | foreignKeys,
137 | columns,
138 | true
139 | )}): ${toPascalCase(singular(tableName))}!\n`;
140 | };
141 |
142 | TypeGenerator._destroy = function destroy(tableName, primaryKey) {
143 | return `\n ${toCamelCase(`delete_${singular(tableName)}`)}(${primaryKey}: ID!): ${toPascalCase(
144 | singular(tableName)
145 | )}!\n`;
146 | };
147 |
148 | TypeGenerator._typeParams = function addParams(primaryKey, foreignKeys, columns, needId) {
149 | let typeDef = '';
150 | for (const columnName in columns) {
151 | const { dataType, isNullable } = columns[columnName];
152 | if (!needId && columnName === primaryKey) {
153 | // handle mutation on creating
154 | continue; // we don't need Id during creating, so skip this loop when columnName === primaryKey
155 | }
156 |
157 | if (needId && columnName === primaryKey) {
158 | // handle mutation on updating (will need Id)
159 | typeDef += ` ${columnName}: ${typeSet(dataType)}!,\n`; // automatically add '!,\n' (not null)
160 | } else {
161 | typeDef += ` ${columnName}: ${typeSet(dataType)}`;
162 | if (isNullable !== 'YES') typeDef += '!';
163 | typeDef += ',\n';
164 | }
165 | // }
166 | }
167 | if (typeDef !== '') typeDef += ' ';
168 | return typeDef;
169 | };
170 |
171 | export default TypeGenerator;
172 |
173 |
--------------------------------------------------------------------------------
/server/SDL-definedSchemas/generators/typeGenerator.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-syntax */
2 | /* eslint-disable guard-for-in */
3 | const { singular } = require('pluralize');
4 | const { toCamelCase, toPascalCase, typeSet, getPrimaryKeyType } = require('../helpers/helperFunctions');
5 |
6 | const TypeGenerator = {};
7 |
8 | TypeGenerator.queries = function queries(tableName, tableData) {
9 | const { primaryKey, foreignKeys, columns } = tableData;
10 | const nameSingular = singular(tableName);
11 | const primaryKeyType = getPrimaryKeyType(primaryKey, columns);
12 | if (!foreignKeys || Object.keys(columns).length !== Object.keys(foreignKeys).length + 1) {
13 | // Do not output pure join tables
14 | let byID = toCamelCase(nameSingular);
15 | if (nameSingular === tableName) byID += 'ByID';
16 | return (
17 | ` ${toCamelCase(tableName)}: [${toPascalCase(nameSingular)}!]!\n` +
18 | ` ${byID}(${primaryKey}: ${primaryKeyType}!): ${toPascalCase(nameSingular)}!\n`
19 | );
20 | }
21 | return '';
22 | };
23 |
24 | TypeGenerator.mutations = function mutations(tableName, tableData) {
25 | const { primaryKey, foreignKeys, columns } = tableData;
26 | if (!foreignKeys || Object.keys(columns).length !== Object.keys(foreignKeys).length + 1) {
27 | // Do not output pure join tables
28 | return (
29 | this._create(tableName, primaryKey, foreignKeys, columns) +
30 | this._update(tableName, primaryKey, foreignKeys, columns) +
31 | this._destroy(tableName, primaryKey)
32 | );
33 | }
34 | return '';
35 | };
36 |
37 | TypeGenerator.customTypes = function customTypes(tableName, tables) {
38 | const { primaryKey, foreignKeys, columns } = tables[tableName];
39 | const primaryKeyType = getPrimaryKeyType(primaryKey, columns);
40 | if (foreignKeys === null || Object.keys(columns).length !== Object.keys(foreignKeys).length + 1) {
41 | return `${` type ${toPascalCase(singular(tableName))} {\n` + ` ${primaryKey}: ${primaryKeyType}!`}${this._columns(
42 | primaryKey,
43 | foreignKeys,
44 | columns
45 | )}${this._getRelationships(tableName, tables)}\n }\n\n`;
46 | }
47 | return '';
48 | };
49 |
50 | TypeGenerator._columns = function columns(primaryKey, foreignKeys, columns) {
51 | let colStr = '';
52 | for (const columnName in columns) {
53 | if (!(foreignKeys && foreignKeys[columnName]) && columnName !== primaryKey) {
54 | const { dataType, isNullable, columnDefault } = columns[columnName];
55 | colStr += `\n ${columnName}: ${typeSet(dataType)}`;
56 | if (isNullable === 'NO' && columnDefault === null) colStr += '!';
57 | }
58 | }
59 | return colStr;
60 | };
61 |
62 | // Get table relationships
63 | TypeGenerator._getRelationships = function getRelationships(tableName, tables) {
64 | let relationships = '';
65 | const alreadyAddedType = []; // cache to track which relation has been added or not
66 | for (const refTableName in tables[tableName].referencedBy) {
67 | // example1 (table name: film): refTableName: planets_in_films, vessels_in_films, people_in_films, species_in_films
68 | // example2 (when table name is : species): refTableName: people, species_in_films
69 | // example3 (when table name is : planets:): refTableName: planets_in_films, species, people
70 | const { referencedBy: foreignRefBy, foreignKeys: foreignFKeys, columns: foreignColumns } = tables[refTableName];
71 |
72 | // One-to-one: when we can find tableName in foreignRefBy, that means this is a direct one to one relation
73 | if (foreignRefBy && foreignRefBy[tableName]) {
74 | if (!alreadyAddedType.includes(refTableName)) {
75 | // check if this refTableType has already been added by other tableName
76 | alreadyAddedType.push(refTableName);
77 | const refTableType = toPascalCase(singular(refTableName));
78 | relationships += `\n ${toCamelCase(singular(reftableName))}: ${refTableType}`;
79 | }
80 | }
81 |
82 | // One-to-many: check if this is a join table, and if it's not, we can add relations)
83 | // example2: people table will meet this criteria
84 | // example3: species and people table will meet this criteria
85 | else if (Object.keys(foreignColumns).length !== Object.keys(foreignFKeys).length + 1) {
86 | if (!alreadyAddedType.includes(refTableName)) {
87 | // check if this refTableType has already been added by other tableName
88 | alreadyAddedType.push(refTableName);
89 | const refTableType = toPascalCase(singular(refTableName));
90 |
91 | relationships += `\n ${toCamelCase(refTableName)}: [${refTableType}]`;
92 | // add 'people: [Person]' and to relation
93 | }
94 | }
95 |
96 | // Many-to-many relations (so now handling join tables!)
97 | for (const foreignFKey in foreignFKeys) {
98 |
99 | if (tableName !== foreignFKeys[foreignFKey].referenceTable) {
100 | // Do not include original table in output
101 | if (!alreadyAddedType.includes(refTableName)) {
102 | // check if this refTableType has already been added by other tableName
103 | alreadyAddedType.push(refTableName);
104 | const manyToManyTable = toCamelCase(foreignFKeys[foreignFKey].referenceTable);
105 | relationships += `\n ${manyToManyTable}: [${toPascalCase(singular(manyToManyTable))}]`;
106 | }
107 | }
108 | }
109 | }
110 | for (const FKTableName in tables[tableName].foreignKeys) {
111 | const object = tables[tableName].foreignKeys[FKTableName];
112 | const refTableName = object.referenceTable;
113 | if (refTableName) {
114 | const refTableType = toPascalCase(singular(refTableName));
115 | relationships += `\n ${toCamelCase(refTableName)}: [${refTableType}]`;
116 | }
117 | }
118 |
119 | return relationships;
120 | };
121 |
122 | TypeGenerator._create = function create(tableName, primaryKey, foreignKeys, columns) {
123 | return `\n ${toCamelCase(`create_${singular(tableName)}`)}(\n${this._typeParams(
124 | primaryKey,
125 | foreignKeys,
126 | columns,
127 | false
128 | )}): ${toPascalCase(singular(tableName))}!\n`;
129 | };
130 |
131 | TypeGenerator._update = function update(tableName, primaryKey, foreignKeys, columns) {
132 | return `\n ${toCamelCase(`update_${singular(tableName)}`)}(\n${this._typeParams(
133 | primaryKey,
134 | foreignKeys,
135 | columns,
136 | true
137 | )}): ${toPascalCase(singular(tableName))}!\n`;
138 | };
139 |
140 | TypeGenerator._destroy = function destroy(tableName, primaryKey) {
141 | return `\n ${toCamelCase(`delete_${singular(tableName)}`)}(${primaryKey}: ID!): ${toPascalCase(
142 | singular(tableName)
143 | )}!\n`;
144 | };
145 |
146 | TypeGenerator._typeParams = function addParams(primaryKey, foreignKeys, columns, needId) {
147 | let typeDef = '';
148 | for (const columnName in columns) {
149 | const { dataType, isNullable } = columns[columnName];
150 | if (!needId && columnName === primaryKey) {
151 | // handle mutation on creating
152 | continue; // we don't need Id during creating, so skip this loop when columnName === primaryKey
153 | }
154 |
155 | if (needId && columnName === primaryKey) {
156 | // handle mutation on updating (will need Id)
157 | typeDef += ` ${columnName}: ${typeSet(dataType)}!,\n`; // automatically add '!,\n' (not null)
158 | } else {
159 | typeDef += ` ${columnName}: ${typeSet(dataType)}`;
160 | if (isNullable !== 'YES') typeDef += '!';
161 | typeDef += ',\n';
162 | }
163 | // }
164 | }
165 | if (typeDef !== '') typeDef += ' ';
166 | return typeDef;
167 | };
168 |
169 | module.exports = TypeGenerator;
170 |
171 |
--------------------------------------------------------------------------------
/client/forceGraph/generators/ForceGraphGenerator.js:
--------------------------------------------------------------------------------
1 | import * as d3 from 'd3';
2 | import { generateNodeAndLink, simplifyTable } from './d3DataBuilder';
3 | import deleteIconSrc from '../deleteIcon.svg';
4 |
5 | export function runForceGraph(container, data, nodeHoverTooltip, handleDeleteTables, handleDeleteColumns) {
6 | const simplifiedData = simplifyTable(data.tables);
7 | const d3Arrays = generateNodeAndLink(simplifiedData)
8 | const { tableNodes, columnNodes, referencedBy, pointsTo, linksToColumns } = d3Arrays;
9 |
10 | // get the container’s width and height from bounding rectangle:
11 | const containerRect = container.getBoundingClientRect();
12 | const height = containerRect.height;
13 | const width = containerRect.width;
14 |
15 | const topPadding = 250;
16 | const deleteIconY = 160;
17 | const deleteIconRadius = 24;
18 |
19 | // add the option to drag the force graph nodes as part of it’s simulation.
20 | const drag = (simulation) => {
21 | const dragStart = (event, d) => {
22 | // if condition: dragstarted would be called once for each of the two fingers.
23 | // The first time it get's called d3.event.active would be 0, and the simulation would be restarted.
24 | // The second time d3.event.active would be 1, so the simulation wouldn't restart again, because it's already going.
25 | if (!event.active) simulation.alphaTarget(0.3).restart();
26 | d.fx = d.x;
27 | d.fy = d.y;
28 | };
29 |
30 | const dragging = (event, d) => {
31 | d.fx = event.x;
32 | d.fy = event.y;
33 | };
34 |
35 | const dragEnd = (event, d) => {
36 | if (!event.active) simulation.alphaTarget(0); // if no event.active, stop the simulation
37 | d.fx = event.x; // will stay in place after drag ends
38 | d.fy = event.y; // if set to null node will will go back to original position
39 |
40 | // check if nodes are being dragged to the trash can
41 | if (2 * width / 5 - deleteIconRadius < event.x && event.x < 2 * width / 5 + deleteIconRadius &&
42 | 2 * deleteIconY - deleteIconRadius < event.y && event.y < 2 * deleteIconY + deleteIconRadius) {
43 |
44 | if (event.subject.primary) {
45 | handleDeleteTables(data, event.subject.name);
46 | } else {
47 | handleDeleteColumns(data, event.subject.name, event.subject.parent, event.subject.foreignKey)
48 | }
49 | }
50 | };
51 |
52 | return d3.drag().on('start', dragStart).on('drag', dragging).on('end', dragEnd);
53 | };
54 |
55 | // Handle the node tooltip generation: add the tooltip element to the graph
56 | const tooltip = document.querySelector('#graph-tooltip');
57 | if (!tooltip) {
58 | const tooltipDiv = document.createElement('div');
59 | tooltipDiv.classList.add('tooltip');
60 | tooltipDiv.style.opacity = '0';
61 | tooltipDiv.id = 'graph-tooltip';
62 | document.body.appendChild(tooltipDiv);
63 | }
64 | const div = d3.select('#graph-tooltip');
65 |
66 | const addTooltip = (hoverTooltip, d, x, y) => {
67 | div.transition().duration(200).style('opacity', 0.9);
68 | div
69 | .html(hoverTooltip(d))
70 | .style('left', `${x}px`)
71 | .style('top', `${y + 10}px`);
72 | };
73 |
74 | const removeTooltip = () => {
75 | div.transition().duration(200).style('opacity', 0);
76 | };
77 |
78 | const nodesArr = tableNodes.concat(columnNodes);
79 |
80 | const linksArr = pointsTo;
81 | const linesArr = linksToColumns.concat(referencedBy)
82 |
83 | const simulation = d3
84 | .forceSimulation(nodesArr)
85 | .force('link',d3.forceLink(linksArr).id((d) => d.id).distance(400).strength(1))
86 | .force('line',d3
87 | .forceLink(linesArr)
88 | .id((d) => d.id).distance((d) => d.type? 400: 60))
89 | .force('charge', d3.forceManyBody().strength(-500)) // Negative numbers indicate a repulsive force and positive numbers an attractive force. Strength defaults to -30 upon installation.
90 | .force('collide',d3.forceCollide(25))
91 | .force('x', d3.forceX()) // These two siblings push nodes towards the desired x and y position.
92 | .force('y', d3.forceY()); // default to the middle
93 |
94 |
95 | const svg = d3
96 | .select(container)
97 | .append('svg')
98 | .attr('viewBox', [-width / 2, -height / 2, width, height])
99 | const deleteIcon = svg.append('image')
100 |
101 | .attr('x', 2 * width / 5 - deleteIconRadius)
102 | .attr('y', 2 * deleteIconY - deleteIconRadius)
103 | .attr('width', 2.5 * deleteIconRadius)
104 | .attr('height', 2.5 * deleteIconRadius)
105 | .attr('xlink:href', deleteIconSrc)
106 |
107 | // Initialize the links between tables and its columns
108 | const line = svg
109 | .append('g')
110 | .attr('stroke', '#000')
111 | .attr('stroke-opacity', 0.2)
112 | .selectAll('line')
113 | // .data(linesArr)
114 | .data(linksToColumns)
115 | .join('line')
116 |
117 | const refLine = svg
118 | .append('g')
119 | .attr('stroke', '#767c77')
120 | .attr('stroke-opacity', 1)
121 | .style('stroke-width', '1.5px')
122 | .selectAll('line')
123 | .data(referencedBy)
124 | // .data(linksToColumns)
125 | .join('line')
126 |
127 | // appending little triangles, path object, as arrowhead
128 | // The element is used to store graphical objects that will be used at a later time
129 | // The element defines the graphic that is to be used for drawing arrowheads or polymarkers
130 | // on a given , , or element.
131 | svg
132 | .append('svg:defs')
133 | .selectAll('marker')
134 | .data(['end'])
135 | .enter()
136 | .append('svg:marker')
137 | .attr('id', String)
138 | .attr('viewBox', '0 -5 10 10') //the bound of the SVG viewport for the current SVG fragment. defines a coordinate system 10 wide and 10 high starting on (0,-5)
139 | .attr('refX', 28) // x coordinate for the reference point of the marker. If circle is bigger, this need to be bigger.
140 | .attr('refY', 0)
141 | .attr('orient', 'auto')
142 | .attr('markerWidth', 15)
143 | .attr('markerHeight', 15)
144 | .attr('xoverflow', 'visible')
145 | .append('svg:path')
146 | .attr('d', 'M 0,-5 L 10,0 L 0,5')
147 | .attr('fill', 'orange')
148 | .style('stroke', 'none');
149 |
150 | // add the curved line
151 | const path = svg
152 | .append('svg:g')
153 | .selectAll('path')
154 | .data(linksArr)
155 | .join('svg:path')
156 | .attr('fill', 'none')
157 | .style('stroke', (d) => (d.type === 'pointsTo' ? 'orange' : '#767c77'))
158 | .style('stroke-width', '1.5px')
159 | .attr('marker-end', 'url(#end)');
160 | //The marker-end attribute defines the arrowhead or polymarker that will be drawn at the final vertex of the given shape.
161 |
162 | const node = svg
163 | .append('g')
164 | .attr('stroke', '#fff')
165 | .attr('stroke-width', 1)
166 | .selectAll('circle')
167 | .data(nodesArr)
168 | .join('circle')
169 | .style('cursor', 'move')
170 | .attr('r', (d) => (d.primary ? 40 : 25))
171 | .attr('fill', (d) => (d.primary ? '#967bb6' : ( d.foreignKey? 'orange' :'#aacfcf')))
172 | .call(drag(simulation));
173 |
174 | const label = svg
175 | .append('g')
176 | .attr('class', 'labels')
177 | .selectAll('text')
178 | .data(nodesArr)
179 | .join('text')
180 | .attr('text-anchor', 'middle')
181 | .attr('dominant-baseline', 'central')
182 | .attr('class', (d) => (d.primary ? 'primary_node' : 'column_node'))
183 | .text((d) => d.primary ? (d.name.length > 7 ? d.name.slice(0, 6)+'..' : d.name) : (d.name.length > 5 ? d.name.slice(0, 5)+'..' : d.name))
184 | .call(drag(simulation));
185 |
186 | label
187 | .on('mouseover', (event, d) => addTooltip(nodeHoverTooltip, d, event.pageX, event.pageY))
188 | .on('mouseout', () => removeTooltip());
189 |
190 | simulation.on('tick', () => {
191 | // update link positions
192 | line
193 | .attr('x1', (d) => d.source.x)
194 | .attr('y1', (d) => d.source.y)
195 | .attr('x2', (d) => d.target.x)
196 | .attr('y2', (d) => d.target.y);
197 |
198 | refLine
199 | .attr('x1', (d) => d.source.x)
200 | .attr('y1', (d) => d.source.y)
201 | .attr('x2', (d) => d.target.x)
202 | .attr('y2', (d) => d.target.y);
203 |
204 | // update node positions
205 | node.attr('cx', (d) => d.x).attr('cy', (d) => d.y);
206 |
207 | label.attr('x', (d) => d.x).attr('y', (d) => d.y);
208 |
209 | // update the curvy lines
210 | path.attr('d', (d) => {
211 | const dx = d.target.x - d.source.x;
212 | const dy = d.target.y - d.source.y;
213 | const dr = Math.sqrt(dx * dx + dy * dy);
214 | return `M${d.source.x},${d.source.y}A${dr},${dr} 0 0,1 ${d.target.x},${d.target.y}`;
215 | });
216 | });
217 |
218 | return {
219 | destroy: () => {
220 | simulation.stop();
221 | },
222 | nodes: () => {
223 | return svg.node();
224 | },
225 | };
226 | }
227 |
--------------------------------------------------------------------------------
/server/SDL-definedSchemas/generators/resolverGenerator.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-underscore-dangle */
2 | /* eslint-disable guard-for-in */
3 | /* eslint-disable no-restricted-syntax */
4 | // eslint-disable-next-line prettier/prettier
5 | // prettier-ignore
6 | const { singular } = require('pluralize');
7 | const { toCamelCase, toPascalCase } = require('../helpers/helperFunctions');
8 |
9 | const ResolverGenerator = {
10 | _values: {},
11 | };
12 |
13 | ResolverGenerator.reset = function () {
14 | this._values = {};
15 | };
16 |
17 | ResolverGenerator.queries = function queries(tableName, { primaryKey }) {
18 | this.reset();
19 | return `\n${this._columnQuery(tableName, primaryKey)}` + `\n${this._allColumnQuery(tableName)}`;
20 | };
21 |
22 | ResolverGenerator.mutations = function mutations(tableName, tableData) {
23 | const { primaryKey, foreignKeys, columns } = tableData;
24 | this._createValues(primaryKey, foreignKeys, columns);
25 | return (
26 | `${this._createMutation(tableName, primaryKey, foreignKeys, columns)}\n` +
27 | `${this._updateMutation(tableName, primaryKey, foreignKeys, columns)}\n` +
28 | `${this._deleteMutations(tableName, primaryKey)}\n\n`
29 | );
30 | };
31 |
32 | ResolverGenerator.getRelationships = function getRelationships(tableName, tables) {
33 |
34 | const { primaryKey, referencedBy } = tables[tableName];
35 | if (!referencedBy) return '';
36 | let relationships = `\n ${toPascalCase(singular(tableName))}: {\n`;
37 | for (const refTableName in referencedBy) {
38 | const { referencedBy: foreignRefBy, foreignKeys: foreignFKeys, columns: foreignColumns } = tables[refTableName];
39 | const refTableType = toPascalCase(singular(refTableName));
40 | // One-to-one
41 | if (foreignRefBy && foreignRefBy[tableName]) relationships += this._oneToOne(tableName, primaryKey, refTableName, referencedBy[refTableName]);
42 | // One-to-many
43 | else if (Object.keys(foreignColumns).length !== Object.keys(foreignFKeys).length + 1) relationships += this._oneToMany(tableName, primaryKey, refTableName, referencedBy[refTableName]);
44 | // Many-to-many
45 | for (const foreignFKey in foreignFKeys) {
46 | if (tableName !== foreignFKeys[foreignFKey].referenceTable) {
47 | // Do not include original table in output
48 | const manyToManyTable = foreignFKeys[foreignFKey].referenceTable;
49 | const refKey = tables[tableName].referencedBy[refTableName];
50 | const manyRefKey = tables[manyToManyTable].referencedBy[refTableName];
51 | const { primaryKey: manyPrimaryKey } = tables[manyToManyTable];
52 |
53 | relationships += this._manyToMany(tableName, primaryKey, refTableName, refKey, manyRefKey, manyToManyTable, manyPrimaryKey);
54 | }
55 | }
56 |
57 | for (const FKTableName in tables[tableName].foreignKeys) {
58 | const object = tables[tableName].foreignKeys[FKTableName];
59 | const refTableName = object.referenceTable;
60 | const refKey = object.referenceKey;
61 | const newQuery = this._FKTable(tableName, primaryKey, tableName, refKey, FKTableName, refTableName, primaryKey)
62 | if (!relationships.includes(newQuery)) relationships += newQuery
63 | }
64 | }
65 | relationships += ' },\n';
66 | return relationships;
67 | };
68 |
69 | ResolverGenerator._oneToOne = function oneToOne(tableName, primaryKey, refTableName, refKey) {
70 | return (
71 | ` ${toCamelCase(refTableName)}: async (${toCamelCase(tableName)}) => {\n` +
72 | ' try {\n' +
73 | ` const query = \'SELECT * FROM ${refTableName} WHERE ${refKey} = $1\';\n` +
74 | ` const values = [${primaryKey}]\n` +
75 | ' return await db.query(query, values).then((res) => res.rows[0]);\n' +
76 | ' } catch (err) {\n' +
77 | ' //throw new Error(err)\n' +
78 | ' }\n' +
79 | ' },\n'
80 | );
81 | };
82 |
83 | ResolverGenerator._oneToMany = function oneToMany(tableName, primaryKey, refTableName, refKey) {
84 | return (
85 | ` ${toCamelCase(refTableName)}: async (${toCamelCase(tableName)}) => {\n` +
86 | ' try {\n' +
87 | ` const query = \'SELECT * FROM ${refTableName} WHERE ${refKey} = $1\';\n` +
88 | ` const values = [${tableName}.${primaryKey}]\n` +
89 | ' return await db.query(query, values).then((res) => res.rows);\n' +
90 | ' } catch (err) {\n' +
91 | ' //throw new Error(err)\n' +
92 | ' }\n' +
93 | ' },\n'
94 | );
95 | };
96 |
97 | ResolverGenerator._manyToMany = function manyToMany(tableName, primaryKey, joinTableName, refKey, manyRefKey, manyTableName, manyPrimaryKey) {
98 | const camTableName = toCamelCase(tableName);
99 | return (
100 | ` ${toCamelCase(manyTableName)}: async (${camTableName}) => {\n` +
101 | ' try {\n' +
102 | ` const query = \'SELECT * FROM ${manyTableName} LEFT OUTER JOIN ${joinTableName} ON ${manyTableName}.${manyPrimaryKey} = ${joinTableName}.${manyRefKey} WHERE ${joinTableName}.${refKey} = $1\';\n` +
103 | ` const values = [${camTableName}.${primaryKey}]\n` +
104 | ' return await db.query(query, values).then((res) => res.rows);\n' +
105 | ' } catch (err) {\n' +
106 | ' //throw new Error(err)\n' +
107 | ' }\n' +
108 | ' },\n'
109 | );
110 | };
111 |
112 | ResolverGenerator._FKTable = function FKTable(tableName, primaryKey, joinTableName, refKey, manyRefKey, manyTableName, manyPrimaryKey) {
113 | const camTableName = toCamelCase(tableName);
114 | return (
115 | ` ${toCamelCase(manyTableName)}: async (${camTableName}) => {\n` +
116 | ' try {\n' +
117 | ` const query = \'SELECT ${manyTableName}.* FROM ${manyTableName} LEFT OUTER JOIN ${joinTableName} ON ${manyTableName}.${manyPrimaryKey} = ${joinTableName}.${manyRefKey} WHERE ${joinTableName}.${refKey} = $1\';\n` +
118 | ` const values = [${camTableName}.${primaryKey}]\n` +
119 | ' return await db.query(query, values).then((res) => res.rows);\n' +
120 | ' } catch (err) {\n' +
121 | ' //throw new Error(err)\n' +
122 | ' }\n' +
123 | ' },\n'
124 | );
125 | };
126 |
127 | ResolverGenerator._createValues = function values(primaryKey, foreignKeys, columns) {
128 | let index = 1;
129 | for (let columnName in columns) {
130 | // if (!(foreignKeys && foreignKeys[columnName]) && columnName !== primaryKey) { // why
131 | if (columnName !== primaryKey) {
132 | this._values[index++] = columnName;
133 | }
134 | }
135 | return this._values;
136 | };
137 |
138 | ResolverGenerator._columnQuery = function column(tableName, primaryKey) {
139 | let byID = toCamelCase(singular(tableName));
140 | if (byID === toCamelCase(tableName)) byID += 'ByID';
141 | return (
142 | ` ${byID}: (parent, args) => {\n` +
143 | ' try{\n' +
144 | ` const query = 'SELECT * FROM ${tableName} WHERE ${primaryKey} = $1';\n` +
145 | ` const values = [args.${primaryKey}];\n` +
146 | ' return db.query(query, values).then((res) => res.rows[0]);\n' +
147 | ' } catch (err) {\n' +
148 | ' throw new Error(err);\n' +
149 | ' }\n' +
150 | ' },'
151 | );
152 | };
153 |
154 | ResolverGenerator._allColumnQuery = function allColumn(tableName) {
155 | return (
156 | ` ${toCamelCase(tableName)}: () => {\n` +
157 | ' try {\n' +
158 | ` const query = 'SELECT * FROM ${tableName}';\n` +
159 | ' return db.query(query).then((res) => res.rows);\n' +
160 | ' } catch (err) {\n' +
161 | ' throw new Error(err);\n' +
162 | ' }\n' +
163 | ' },'
164 | );
165 | };
166 |
167 | ResolverGenerator._createMutation = function createColumn(tableName, primaryKey, foreignKeys, columns) {
168 | return (
169 | ` ${toCamelCase(`create_${singular(tableName)}`)}: (parent, args) => {\n` +
170 | ` const query = 'INSERT INTO ${tableName}(${Object.values(this._values).join(', ')}) VALUES(${Object.keys(this._values)
171 | .map((x) => `$${x}`)
172 | .join(', ')}) RETURNING *';\n` +
173 | ` const values = [${Object.values(this._values)
174 | .map((x) => `args.${x}`)
175 | .join(', ')}];\n` +
176 | ' try {\n' +
177 | ' return db.query(query, values).then(res => res.rows[0]);\n' +
178 | ' } catch (err) {\n' +
179 | ' throw new Error(err);\n' +
180 | ' }\n' +
181 | ' },'
182 | );
183 | };
184 |
185 | ResolverGenerator._updateMutation = function updateColumn(tableName, primaryKey, foreignKeys, columns) {
186 | let displaySet = '';
187 | for (const key in this._values) displaySet += `${this._values[key]}=$${key}, `;
188 | return (
189 | ` ${toCamelCase(`update_${singular(tableName)}`)}: (parent, args) => {\n` +
190 | ' try {\n' +
191 | ` const query = 'UPDATE ${tableName} SET ${displaySet.slice(0, displaySet.length - 2)} WHERE ${primaryKey} = $${Object.entries(this._values).length + 1} RETURNING *';\n` +
192 | ` const values = [${Object.values(this._values)
193 | .map((x) => `args.${x}`)
194 | .join(', ')}, args.${primaryKey}];\n` +
195 | ' return db.query(query, values).then((res) => res.rows[0]);\n' +
196 | ' } catch (err) {\n' +
197 | ' throw new Error(err);\n' +
198 | ' }\n' +
199 | ' },'
200 | );
201 | };
202 |
203 | ResolverGenerator._deleteMutations = function deleteColumn(tableName, primaryKey) {
204 | return (
205 | ` ${toCamelCase(`delete_${singular(tableName)}`)}: (parent, args) => {\n` +
206 | ' try {\n' +
207 | ` const query = 'DELETE FROM ${tableName} WHERE ${primaryKey} = $1 RETURNING *';\n` +
208 | ` const values = [args.${primaryKey}];\n` +
209 | ' return db.query(query, values).then((res) => res.rows[0]);\n' +
210 | ' } catch (err) {\n' +
211 | ' throw new Error(err);\n' +
212 | ' }\n' +
213 | ' },'
214 | );
215 | };
216 |
217 | module.exports = ResolverGenerator;
218 |
--------------------------------------------------------------------------------
/client/forceGraph/generators/resolverGenerator.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-underscore-dangle */
2 | /* eslint-disable guard-for-in */
3 | /* eslint-disable no-restricted-syntax */
4 | // eslint-disable-next-line prettier/prettier
5 | import { singular } from 'pluralize';
6 | import {
7 | toCamelCase,
8 | toPascalCase,
9 | } from './helperFunctions';
10 |
11 | const ResolverGenerator = {
12 | _values: {},
13 | };
14 |
15 | ResolverGenerator.reset = function () {
16 | this._values = {};
17 | };
18 |
19 | ResolverGenerator.queries = function queries(tableName, { primaryKey }) {
20 | return `\n${this._columnQuery(tableName, primaryKey)}` + `\n${this._allColumnQuery(tableName)}`;
21 | };
22 |
23 | ResolverGenerator.mutations = function mutations(tableName, tableData) {
24 | const { primaryKey, foreignKeys, columns } = tableData;
25 | this._createValues(primaryKey, foreignKeys, columns);
26 | return (
27 | `${this._createMutation(tableName, primaryKey, foreignKeys, columns)}\n` +
28 | `${this._updateMutation(tableName, primaryKey, foreignKeys, columns)}\n` +
29 | `${this._deleteMutations(tableName, primaryKey)}\n\n`
30 | );
31 | };
32 |
33 | ResolverGenerator.getRelationships = function getRelationships(tableName, tables) {
34 | const { primaryKey, referencedBy } = tables[tableName];
35 | if (!referencedBy) return '';
36 | let relationships = `\n ${toPascalCase(singular(tableName))}: {\n`;
37 | for (const refTableName in referencedBy) {
38 | const {
39 | referencedBy: foreignRefBy,
40 | foreignKeys: foreignFKeys,
41 | columns: foreignColumns,
42 | } = tables[refTableName];
43 | const refTableType = toPascalCase(singular(refTableName));
44 | // One-to-one
45 | if (foreignRefBy && foreignRefBy[tableName])
46 | relationships += this._oneToOne(
47 | tableName,
48 | primaryKey,
49 | refTableName,
50 | referencedBy[refTableName]
51 | );
52 | // One-to-many
53 | else if (Object.keys(foreignColumns).length !== Object.keys(foreignFKeys).length + 1)
54 | relationships += this._oneToMany(
55 | tableName,
56 | primaryKey,
57 | refTableName,
58 | referencedBy[refTableName]
59 | );
60 | // Many-to-many
61 | for (const foreignFKey in foreignFKeys) {
62 | if (tableName !== foreignFKeys[foreignFKey].referenceTable) {
63 | // Do not include original table in output
64 | const manyToManyTable = foreignFKeys[foreignFKey].referenceTable;
65 | const refKey = tables[tableName].referencedBy[refTableName];
66 | const manyRefKey = tables[manyToManyTable].referencedBy[refTableName];
67 | const { primaryKey: manyPrimaryKey } = tables[manyToManyTable];
68 |
69 | relationships += this._manyToMany(
70 | tableName,
71 | primaryKey,
72 | refTableName,
73 | refKey,
74 | manyRefKey,
75 | manyToManyTable,
76 | manyPrimaryKey
77 | );
78 | }
79 | }
80 |
81 | for (const FKTableName in tables[tableName].foreignKeys) {
82 | const object = tables[tableName].foreignKeys[FKTableName];
83 | const refTableName = object.referenceTable;
84 | const refKey = object.referenceKey;
85 |
86 | const newQuery = this._FKTable(tableName, primaryKey, tableName, refKey, FKTableName, refTableName, primaryKey)
87 | if (!relationships.includes(newQuery)) relationships += newQuery
88 | }
89 | }
90 | relationships += ' },\n';
91 | return relationships;
92 | };
93 |
94 | ResolverGenerator._oneToOne = function oneToOne(tableName, primaryKey, refTableName, refKey) {
95 | return (
96 | ` ${toCamelCase(refTableName)}: async (${toCamelCase(tableName)}) => {\n` +
97 | ' try {\n' +
98 | ` const query = \'SELECT * FROM ${refTableName} WHERE ${refKey} = $1\';\n` +
99 | ` const values = [${primaryKey}]\n` +
100 | ' return await db.query(query, values).then((res) => res.rows[0]);\n' +
101 | ' } catch (err) {\n' +
102 | ' //throw new Error(err)\n' +
103 | ' }\n' +
104 | ' },\n'
105 | );
106 | };
107 |
108 | ResolverGenerator._oneToMany = function oneToMany(tableName, primaryKey, refTableName, refKey) {
109 | return (
110 | ` ${toCamelCase(refTableName)}: async (${toCamelCase(tableName)}) => {\n` +
111 | ' try {\n' +
112 | ` const query = \'SELECT * FROM ${refTableName} WHERE ${refKey} = $1\';\n` +
113 | ` const values = [${primaryKey}]\n` +
114 | ' return await db.query(query, values).then((res) => res.rows);\n' +
115 | ' } catch (err) {\n' +
116 | ' //throw new Error(err)\n' +
117 | ' }\n' +
118 | ' },\n'
119 | );
120 | };
121 |
122 | ResolverGenerator._manyToMany = function manyToMany(
123 | tableName,
124 | primaryKey,
125 | joinTableName,
126 | refKey,
127 | manyRefKey,
128 | manyTableName,
129 | manyPrimaryKey
130 | ) {
131 | const camTableName = toCamelCase(tableName);
132 | return (
133 | ` ${toCamelCase(manyTableName)}: async (${camTableName}) => {\n` +
134 | ' try {\n' +
135 | ` const query = \'SELECT * FROM ${manyTableName} LEFT OUTER JOIN ${joinTableName} ON ${manyTableName}.${manyPrimaryKey} = ${joinTableName}.${manyRefKey} WHERE ${joinTableName}.${refKey} = $1\';\n` +
136 | ` const values = [${camTableName}.${primaryKey}]\n` +
137 | ' return await db.query(query, values).then((res) => res.rows);\n' +
138 | ' } catch (err) {\n' +
139 | ' //throw new Error(err)\n' +
140 | ' }\n' +
141 | ' },\n'
142 | );
143 | };
144 |
145 | ResolverGenerator._FKTable = function FKTable(tableName, primaryKey, joinTableName, refKey, manyRefKey, manyTableName, manyPrimaryKey) {
146 | const camTableName = toCamelCase(tableName);
147 | return (
148 | ` ${toCamelCase(manyTableName)}: async (${camTableName}) => {\n` +
149 | ' try {\n' +
150 | ` const query = \'SELECT ${manyTableName}.* FROM ${manyTableName} LEFT OUTER JOIN ${joinTableName} ON ${manyTableName}.${manyPrimaryKey} = ${joinTableName}.${manyRefKey} WHERE ${joinTableName}.${refKey} = $1\';\n` +
151 | ` const values = [${camTableName}.${primaryKey}]\n` +
152 | ' return await db.query(query, values).then((res) => res.rows);\n' +
153 | ' } catch (err) {\n' +
154 | ' //throw new Error(err)\n' +
155 | ' }\n' +
156 | ' },\n'
157 | );
158 | };
159 |
160 | ResolverGenerator._createValues = function values(primaryKey, foreignKeys, columns) {
161 | let index = 1;
162 | for (let columnName in columns) {
163 | // if (!(foreignKeys && foreignKeys[columnName]) && columnName !== primaryKey) { // why?
164 | if (columnName !== primaryKey) {
165 | this._values[index++] = columnName;
166 | }
167 | }
168 | return this._values;
169 | };
170 |
171 | ResolverGenerator._columnQuery = function column(tableName, primaryKey) {
172 | let byID = toCamelCase(singular(tableName));
173 | if (byID === toCamelCase(tableName)) byID += 'ByID';
174 | return (
175 | ` ${byID}: (parent, args) => {\n` +
176 | ' try{\n' +
177 | ` const query = 'SELECT * FROM ${tableName} WHERE ${primaryKey} = $1';\n` +
178 | ` const values = [args.${primaryKey}];\n` +
179 | ' return db.query(query, values).then((res) => res.rows[0]);\n' +
180 | ' } catch (err) {\n' +
181 | ' throw new Error(err);\n' +
182 | ' }\n' +
183 | ' },'
184 | );
185 | };
186 |
187 | ResolverGenerator._allColumnQuery = function allColumn(tableName) {
188 | return (
189 | ` ${toCamelCase(tableName)}: () => {\n` +
190 | ' try {\n' +
191 | ` const query = 'SELECT * FROM ${tableName}';\n` +
192 | ' return db.query(query).then((res) => res.rows);\n' +
193 | ' } catch (err) {\n' +
194 | ' throw new Error(err);\n' +
195 | ' }\n' +
196 | ' },'
197 | );
198 | };
199 |
200 | ResolverGenerator._createMutation = function createColumn(
201 | tableName,
202 | primaryKey,
203 | foreignKeys,
204 | columns
205 | ) {
206 | return (
207 | ` ${toCamelCase(`create_${singular(tableName)}`)}: (parent, args) => {\n` +
208 | ` const query = 'INSERT INTO ${tableName}(${Object.values(this._values).join(
209 | ', '
210 | )}) VALUES(${Object.keys(this._values)
211 | .map((x) => `$${x}`)
212 | .join(', ')})';\n` +
213 | ` const values = [${Object.values(this._values)
214 | .map((x) => `args.${x}`)
215 | .join(', ')}];\n` +
216 | ' try {\n' +
217 | ' return db.query(query, values);\n' +
218 | ' } catch (err) {\n' +
219 | ' throw new Error(err);\n' +
220 | ' }\n' +
221 | ' },'
222 | );
223 | };
224 |
225 | ResolverGenerator._updateMutation = function updateColumn(
226 | tableName,
227 | primaryKey,
228 | foreignKeys,
229 | columns
230 | ) {
231 | let displaySet = '';
232 | for (const key in this._values) displaySet += `${this._values[key]}=$${key}, `;
233 | return (
234 | ` ${toCamelCase(`update_${singular(tableName)}`)}: (parent, args) => {\n` +
235 | ' try {\n' +
236 | ` const query = 'UPDATE ${tableName} SET ${displaySet.slice(0, displaySet.length - 2)} WHERE ${primaryKey} = $${
237 | Object.entries(this._values).length + 1
238 | }';\n` +
239 | ` const values = [${Object.values(this._values)
240 | .map((x) => `args.${x}`)
241 | .join(', ')}, args.${primaryKey}];\n` +
242 | ' return db.query(query, values).then((res) => res.rows);\n' +
243 | ' } catch (err) {\n' +
244 | ' throw new Error(err);\n' +
245 | ' }\n' +
246 | ' },'
247 | );
248 | };
249 |
250 | ResolverGenerator._deleteMutations = function deleteColumn(tableName, primaryKey) {
251 | return (
252 | ` ${toCamelCase(`delete_${singular(tableName)}`)}: (parent, args) => {\n` +
253 | ' try {\n' +
254 | ` const query = 'DELETE FROM ${tableName} WHERE ${primaryKey} = $1';\n` +
255 | ` const values = [args.${primaryKey}];\n` +
256 | ' return db.query(query, values).then((res) => res.rows);\n' +
257 | ' } catch (err) {\n' +
258 | ' throw new Error(err);\n' +
259 | ' }\n' +
260 | ' },'
261 | );
262 | };
263 |
264 | export default ResolverGenerator;
265 |
--------------------------------------------------------------------------------
/server/dummy_server/schema.js:
--------------------------------------------------------------------------------
1 | const { makeExecutableSchema } = require('graphql-tools');
2 | const db = require('./connectToDB');
3 |
4 | const typeDefs = `
5 | type Query {
6 | people: [Person!]!
7 | person(_id: Int!): Person!
8 | films: [Film!]!
9 | film(_id: Int!): Film!
10 | planets: [Planet!]!
11 | planet(_id: Int!): Planet!
12 | species: [Species!]!
13 | speciesByID(_id: Int!): Species!
14 | vessels: [Vessel!]!
15 | vessel(_id: Int!): Vessel!
16 | starshipSpecs: [StarshipSpec!]!
17 | starshipSpec(_id: Int!): StarshipSpec!
18 | }
19 |
20 | type Mutation {
21 | createPerson(
22 | gender: String,
23 | species_id: Int,
24 | homeworld_id: Int,
25 | height: Int,
26 | mass: String,
27 | hair_color: String,
28 | skin_color: String,
29 | eye_color: String,
30 | name: String!,
31 | birth_year: String,
32 | ): Person!
33 |
34 | updatePerson(
35 | gender: String,
36 | species_id: Int,
37 | homeworld_id: Int,
38 | height: Int,
39 | _id: Int!,
40 | mass: String,
41 | hair_color: String,
42 | skin_color: String,
43 | eye_color: String,
44 | name: String!,
45 | birth_year: String,
46 | ): Person!
47 |
48 | deletePerson(_id: ID!): Person!
49 |
50 | createFilm(
51 | director: String!,
52 | opening_crawl: String!,
53 | episode_id: Int!,
54 | title: String!,
55 | release_date: String!,
56 | producer: String!,
57 | ): Film!
58 |
59 | updateFilm(
60 | director: String!,
61 | opening_crawl: String!,
62 | episode_id: Int!,
63 | _id: Int!,
64 | title: String!,
65 | release_date: String!,
66 | producer: String!,
67 | ): Film!
68 |
69 | deleteFilm(_id: ID!): Film!
70 |
71 | createPlanet(
72 | orbital_period: Int,
73 | climate: String,
74 | gravity: String,
75 | terrain: String,
76 | surface_water: String,
77 | population: Int,
78 | name: String,
79 | rotation_period: Int,
80 | diameter: Int,
81 | ): Planet!
82 |
83 | updatePlanet(
84 | orbital_period: Int,
85 | climate: String,
86 | gravity: String,
87 | terrain: String,
88 | surface_water: String,
89 | population: Int,
90 | _id: Int!,
91 | name: String,
92 | rotation_period: Int,
93 | diameter: Int,
94 | ): Planet!
95 |
96 | deletePlanet(_id: ID!): Planet!
97 |
98 | createSpecies(
99 | hair_colors: String,
100 | name: String!,
101 | classification: String,
102 | average_height: String,
103 | average_lifespan: String,
104 | skin_colors: String,
105 | eye_colors: String,
106 | language: String,
107 | homeworld_id: Int,
108 | ): Species!
109 |
110 | updateSpecies(
111 | hair_colors: String,
112 | name: String!,
113 | classification: String,
114 | average_height: String,
115 | average_lifespan: String,
116 | skin_colors: String,
117 | eye_colors: String,
118 | language: String,
119 | homeworld_id: Int,
120 | _id: Int!,
121 | ): Species!
122 |
123 | deleteSpecies(_id: ID!): Species!
124 |
125 | createVessel(
126 | cost_in_credits: Int,
127 | length: String,
128 | vessel_type: String!,
129 | model: String,
130 | manufacturer: String,
131 | name: String!,
132 | vessel_class: String!,
133 | max_atmosphering_speed: String,
134 | crew: Int,
135 | passengers: Int,
136 | cargo_capacity: String,
137 | consumables: String,
138 | ): Vessel!
139 |
140 | updateVessel(
141 | cost_in_credits: Int,
142 | length: String,
143 | vessel_type: String!,
144 | model: String,
145 | manufacturer: String,
146 | name: String!,
147 | vessel_class: String!,
148 | max_atmosphering_speed: String,
149 | crew: Int,
150 | passengers: Int,
151 | cargo_capacity: String,
152 | consumables: String,
153 | _id: Int!,
154 | ): Vessel!
155 |
156 | deleteVessel(_id: ID!): Vessel!
157 |
158 | createStarshipSpec(
159 | vessel_id: Int!,
160 | MGLT: String,
161 | hyperdrive_rating: String,
162 | ): StarshipSpec!
163 |
164 | updateStarshipSpec(
165 | _id: Int!,
166 | vessel_id: Int!,
167 | MGLT: String,
168 | hyperdrive_rating: String,
169 | ): StarshipSpec!
170 |
171 | deleteStarshipSpec(_id: ID!): StarshipSpec!
172 | }
173 |
174 | type Person {
175 | _id: Int!
176 | gender: String
177 | height: Int
178 | mass: String
179 | hair_color: String
180 | skin_color: String
181 | eye_color: String
182 | name: String!
183 | birth_year: String
184 | films: [Film]
185 | vessels: [Vessel]
186 | species: [Species]
187 | planets: [Planet]
188 | }
189 |
190 | type Film {
191 | _id: Int!
192 | director: String!
193 | opening_crawl: String!
194 | episode_id: Int!
195 | title: String!
196 | release_date: String!
197 | producer: String!
198 | planets: [Planet]
199 | people: [Person]
200 | vessels: [Vessel]
201 | species: [Species]
202 | }
203 |
204 | type Planet {
205 | _id: Int!
206 | orbital_period: Int
207 | climate: String
208 | gravity: String
209 | terrain: String
210 | surface_water: String
211 | population: Int
212 | name: String
213 | rotation_period: Int
214 | diameter: Int
215 | films: [Film]
216 | species: [Species]
217 | people: [Person]
218 | }
219 |
220 | type Species {
221 | _id: Int!
222 | hair_colors: String
223 | name: String!
224 | classification: String
225 | average_height: String
226 | average_lifespan: String
227 | skin_colors: String
228 | eye_colors: String
229 | language: String
230 | people: [Person]
231 | films: [Film]
232 | planets: [Planet]
233 | }
234 |
235 | type Vessel {
236 | _id: Int!
237 | cost_in_credits: Int
238 | length: String
239 | vessel_type: String!
240 | model: String
241 | manufacturer: String
242 | name: String!
243 | vessel_class: String!
244 | max_atmosphering_speed: String
245 | crew: Int
246 | passengers: Int
247 | cargo_capacity: String
248 | consumables: String
249 | films: [Film]
250 | people: [Person]
251 | starshipSpecs: [StarshipSpec]
252 | }
253 |
254 | type StarshipSpec {
255 | _id: Int!
256 | MGLT: String
257 | hyperdrive_rating: String
258 | vessels: [Vessel]
259 | }
260 |
261 | `;
262 |
263 | const resolvers = {
264 | Query: {
265 | person: (parent, args) => {
266 | try{
267 | const query = 'SELECT * FROM people WHERE _id = $1';
268 | const values = [args._id];
269 | return db.query(query, values).then((res) => res.rows[0]);
270 | } catch (err) {
271 | throw new Error(err);
272 | }
273 | },
274 | people: () => {
275 | try {
276 | const query = 'SELECT * FROM people';
277 | return db.query(query).then((res) => res.rows);
278 | } catch (err) {
279 | throw new Error(err);
280 | }
281 | },
282 | film: (parent, args) => {
283 | try{
284 | const query = 'SELECT * FROM films WHERE _id = $1';
285 | const values = [args._id];
286 | return db.query(query, values).then((res) => res.rows[0]);
287 | } catch (err) {
288 | throw new Error(err);
289 | }
290 | },
291 | films: () => {
292 | try {
293 | const query = 'SELECT * FROM films';
294 | return db.query(query).then((res) => res.rows);
295 | } catch (err) {
296 | throw new Error(err);
297 | }
298 | },
299 | planet: (parent, args) => {
300 | try{
301 | const query = 'SELECT * FROM planets WHERE _id = $1';
302 | const values = [args._id];
303 | return db.query(query, values).then((res) => res.rows[0]);
304 | } catch (err) {
305 | throw new Error(err);
306 | }
307 | },
308 | planets: () => {
309 | try {
310 | const query = 'SELECT * FROM planets';
311 | return db.query(query).then((res) => res.rows);
312 | } catch (err) {
313 | throw new Error(err);
314 | }
315 | },
316 | speciesByID: (parent, args) => {
317 | try{
318 | const query = 'SELECT * FROM species WHERE _id = $1';
319 | const values = [args._id];
320 | return db.query(query, values).then((res) => res.rows[0]);
321 | } catch (err) {
322 | throw new Error(err);
323 | }
324 | },
325 | species: () => {
326 | try {
327 | const query = 'SELECT * FROM species';
328 | return db.query(query).then((res) => res.rows);
329 | } catch (err) {
330 | throw new Error(err);
331 | }
332 | },
333 | vessel: (parent, args) => {
334 | try{
335 | const query = 'SELECT * FROM vessels WHERE _id = $1';
336 | const values = [args._id];
337 | return db.query(query, values).then((res) => res.rows[0]);
338 | } catch (err) {
339 | throw new Error(err);
340 | }
341 | },
342 | vessels: () => {
343 | try {
344 | const query = 'SELECT * FROM vessels';
345 | return db.query(query).then((res) => res.rows);
346 | } catch (err) {
347 | throw new Error(err);
348 | }
349 | },
350 | starshipSpec: (parent, args) => {
351 | try{
352 | const query = 'SELECT * FROM starship_specs WHERE _id = $1';
353 | const values = [args._id];
354 | return db.query(query, values).then((res) => res.rows[0]);
355 | } catch (err) {
356 | throw new Error(err);
357 | }
358 | },
359 | starshipSpecs: () => {
360 | try {
361 | const query = 'SELECT * FROM starship_specs';
362 | return db.query(query).then((res) => res.rows);
363 | } catch (err) {
364 | throw new Error(err);
365 | }
366 | },
367 | },
368 |
369 | Mutation: {
370 | createPerson: (parent, args) => {
371 | const query = 'INSERT INTO people(gender, species_id, homeworld_id, height, mass, hair_color, skin_color, eye_color, name, birth_year) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *';
372 | const values = [args.gender, args.species_id, args.homeworld_id, args.height, args.mass, args.hair_color, args.skin_color, args.eye_color, args.name, args.birth_year];
373 | try {
374 | return db.query(query, values).then(res => res.rows[0]);
375 | } catch (err) {
376 | throw new Error(err);
377 | }
378 | },
379 | updatePerson: (parent, args) => {
380 | try {
381 | const query = 'UPDATE people SET gender=$1, species_id=$2, homeworld_id=$3, height=$4, mass=$5, hair_color=$6, skin_color=$7, eye_color=$8, name=$9, birth_year=$10 WHERE _id = $11 RETURNING *';
382 | const values = [args.gender, args.species_id, args.homeworld_id, args.height, args.mass, args.hair_color, args.skin_color, args.eye_color, args.name, args.birth_year, args._id];
383 | return db.query(query, values).then((res) => res.rows[0]);
384 | } catch (err) {
385 | throw new Error(err);
386 | }
387 | },
388 | deletePerson: (parent, args) => {
389 | try {
390 | const query = 'DELETE FROM people WHERE _id = $1 RETURNING *';
391 | const values = [args._id];
392 | return db.query(query, values).then((res) => res.rows[0]);
393 | } catch (err) {
394 | throw new Error(err);
395 | }
396 | },
397 |
398 | createFilm: (parent, args) => {
399 | const query = 'INSERT INTO films(director, opening_crawl, episode_id, title, release_date, producer) VALUES($1, $2, $3, $4, $5, $6) RETURNING *';
400 | const values = [args.director, args.opening_crawl, args.episode_id, args.title, args.release_date, args.producer];
401 | try {
402 | return db.query(query, values).then(res => res.rows[0]);
403 | } catch (err) {
404 | throw new Error(err);
405 | }
406 | },
407 | updateFilm: (parent, args) => {
408 | try {
409 | const query = 'UPDATE films SET director=$1, opening_crawl=$2, episode_id=$3, title=$4, release_date=$5, producer=$6 WHERE _id = $7 RETURNING *';
410 | const values = [args.director, args.opening_crawl, args.episode_id, args.title, args.release_date, args.producer, args._id];
411 | return db.query(query, values).then((res) => res.rows[0]);
412 | } catch (err) {
413 | throw new Error(err);
414 | }
415 | },
416 | deleteFilm: (parent, args) => {
417 | try {
418 | const query = 'DELETE FROM films WHERE _id = $1 RETURNING *';
419 | const values = [args._id];
420 | return db.query(query, values).then((res) => res.rows[0]);
421 | } catch (err) {
422 | throw new Error(err);
423 | }
424 | },
425 |
426 | createPlanet: (parent, args) => {
427 | const query = 'INSERT INTO planets(orbital_period, climate, gravity, terrain, surface_water, population, name, rotation_period, diameter) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *';
428 | const values = [args.orbital_period, args.climate, args.gravity, args.terrain, args.surface_water, args.population, args.name, args.rotation_period, args.diameter];
429 | try {
430 | return db.query(query, values).then(res => res.rows[0]);
431 | } catch (err) {
432 | throw new Error(err);
433 | }
434 | },
435 | updatePlanet: (parent, args) => {
436 | try {
437 | const query = 'UPDATE planets SET orbital_period=$1, climate=$2, gravity=$3, terrain=$4, surface_water=$5, population=$6, name=$7, rotation_period=$8, diameter=$9 WHERE _id = $10 RETURNING *';
438 | const values = [args.orbital_period, args.climate, args.gravity, args.terrain, args.surface_water, args.population, args.name, args.rotation_period, args.diameter, args._id];
439 | return db.query(query, values).then((res) => res.rows[0]);
440 | } catch (err) {
441 | throw new Error(err);
442 | }
443 | },
444 | deletePlanet: (parent, args) => {
445 | try {
446 | const query = 'DELETE FROM planets WHERE _id = $1 RETURNING *';
447 | const values = [args._id];
448 | return db.query(query, values).then((res) => res.rows[0]);
449 | } catch (err) {
450 | throw new Error(err);
451 | }
452 | },
453 |
454 | createSpecies: (parent, args) => {
455 | const query = 'INSERT INTO species(hair_colors, name, classification, average_height, average_lifespan, skin_colors, eye_colors, language, homeworld_id) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *';
456 | const values = [args.hair_colors, args.name, args.classification, args.average_height, args.average_lifespan, args.skin_colors, args.eye_colors, args.language, args.homeworld_id];
457 | try {
458 | return db.query(query, values).then(res => res.rows[0]);
459 | } catch (err) {
460 | throw new Error(err);
461 | }
462 | },
463 | updateSpecies: (parent, args) => {
464 | try {
465 | const query = 'UPDATE species SET hair_colors=$1, name=$2, classification=$3, average_height=$4, average_lifespan=$5, skin_colors=$6, eye_colors=$7, language=$8, homeworld_id=$9 WHERE _id = $10 RETURNING *';
466 | const values = [args.hair_colors, args.name, args.classification, args.average_height, args.average_lifespan, args.skin_colors, args.eye_colors, args.language, args.homeworld_id, args._id];
467 | return db.query(query, values).then((res) => res.rows[0]);
468 | } catch (err) {
469 | throw new Error(err);
470 | }
471 | },
472 | deleteSpecies: (parent, args) => {
473 | try {
474 | const query = 'DELETE FROM species WHERE _id = $1 RETURNING *';
475 | const values = [args._id];
476 | return db.query(query, values).then((res) => res.rows[0]);
477 | } catch (err) {
478 | throw new Error(err);
479 | }
480 | },
481 |
482 | createVessel: (parent, args) => {
483 | const query = 'INSERT INTO vessels(cost_in_credits, length, vessel_type, model, manufacturer, name, vessel_class, max_atmosphering_speed, crew, passengers, cargo_capacity, consumables) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *';
484 | const values = [args.cost_in_credits, args.length, args.vessel_type, args.model, args.manufacturer, args.name, args.vessel_class, args.max_atmosphering_speed, args.crew, args.passengers, args.cargo_capacity, args.consumables];
485 | try {
486 | return db.query(query, values).then(res => res.rows[0]);
487 | } catch (err) {
488 | throw new Error(err);
489 | }
490 | },
491 | updateVessel: (parent, args) => {
492 | try {
493 | const query = 'UPDATE vessels SET cost_in_credits=$1, length=$2, vessel_type=$3, model=$4, manufacturer=$5, name=$6, vessel_class=$7, max_atmosphering_speed=$8, crew=$9, passengers=$10, cargo_capacity=$11, consumables=$12 WHERE _id = $13 RETURNING *';
494 | const values = [args.cost_in_credits, args.length, args.vessel_type, args.model, args.manufacturer, args.name, args.vessel_class, args.max_atmosphering_speed, args.crew, args.passengers, args.cargo_capacity, args.consumables, args._id];
495 | return db.query(query, values).then((res) => res.rows[0]);
496 | } catch (err) {
497 | throw new Error(err);
498 | }
499 | },
500 | deleteVessel: (parent, args) => {
501 | try {
502 | const query = 'DELETE FROM vessels WHERE _id = $1 RETURNING *';
503 | const values = [args._id];
504 | return db.query(query, values).then((res) => res.rows[0]);
505 | } catch (err) {
506 | throw new Error(err);
507 | }
508 | },
509 |
510 | createStarshipSpec: (parent, args) => {
511 | const query = 'INSERT INTO starship_specs(vessel_id, MGLT, hyperdrive_rating) VALUES($1, $2, $3) RETURNING *';
512 | const values = [args.vessel_id, args.MGLT, args.hyperdrive_rating];
513 | try {
514 | return db.query(query, values).then(res => res.rows[0]);
515 | } catch (err) {
516 | throw new Error(err);
517 | }
518 | },
519 | updateStarshipSpec: (parent, args) => {
520 | try {
521 | const query = 'UPDATE starship_specs SET vessel_id=$1, MGLT=$2, hyperdrive_rating=$3 WHERE _id = $4 RETURNING *';
522 | const values = [args.vessel_id, args.MGLT, args.hyperdrive_rating, args._id];
523 | return db.query(query, values).then((res) => res.rows[0]);
524 | } catch (err) {
525 | throw new Error(err);
526 | }
527 | },
528 | deleteStarshipSpec: (parent, args) => {
529 | try {
530 | const query = 'DELETE FROM starship_specs WHERE _id = $1 RETURNING *';
531 | const values = [args._id];
532 | return db.query(query, values).then((res) => res.rows[0]);
533 | } catch (err) {
534 | throw new Error(err);
535 | }
536 | },
537 |
538 | },
539 |
540 | Person: {
541 | films: async (people) => {
542 | try {
543 | const query = 'SELECT * FROM films LEFT OUTER JOIN people_in_films ON films._id = people_in_films.film_id WHERE people_in_films.person_id = $1';
544 | const values = [people._id]
545 | return await db.query(query, values).then((res) => res.rows);
546 | } catch (err) {
547 | //throw new Error(err)
548 | }
549 | },
550 | species: async (people) => {
551 | try {
552 | const query = 'SELECT species.* FROM species LEFT OUTER JOIN people ON species._id = people.species_id WHERE people._id = $1';
553 | const values = [people._id]
554 | return await db.query(query, values).then((res) => res.rows);
555 | } catch (err) {
556 | //throw new Error(err)
557 | }
558 | },
559 | planets: async (people) => {
560 | try {
561 | const query = 'SELECT planets.* FROM planets LEFT OUTER JOIN people ON planets._id = people.homeworld_id WHERE people._id = $1';
562 | const values = [people._id]
563 | return await db.query(query, values).then((res) => res.rows);
564 | } catch (err) {
565 | //throw new Error(err)
566 | }
567 | },
568 | vessels: async (people) => {
569 | try {
570 | const query = 'SELECT * FROM vessels LEFT OUTER JOIN pilots ON vessels._id = pilots.vessel_id WHERE pilots.person_id = $1';
571 | const values = [people._id]
572 | return await db.query(query, values).then((res) => res.rows);
573 | } catch (err) {
574 | //throw new Error(err)
575 | }
576 | },
577 | },
578 |
579 | Film: {
580 | planets: async (films) => {
581 | try {
582 | const query = 'SELECT * FROM planets LEFT OUTER JOIN planets_in_films ON planets._id = planets_in_films.planet_id WHERE planets_in_films.film_id = $1';
583 | const values = [films._id]
584 | return await db.query(query, values).then((res) => res.rows);
585 | } catch (err) {
586 | //throw new Error(err)
587 | }
588 | },
589 | people: async (films) => {
590 | try {
591 | const query = 'SELECT * FROM people LEFT OUTER JOIN people_in_films ON people._id = people_in_films.person_id WHERE people_in_films.film_id = $1';
592 | const values = [films._id]
593 | return await db.query(query, values).then((res) => res.rows);
594 | } catch (err) {
595 | //throw new Error(err)
596 | }
597 | },
598 | vessels: async (films) => {
599 | try {
600 | const query = 'SELECT * FROM vessels LEFT OUTER JOIN vessels_in_films ON vessels._id = vessels_in_films.vessel_id WHERE vessels_in_films.film_id = $1';
601 | const values = [films._id]
602 | return await db.query(query, values).then((res) => res.rows);
603 | } catch (err) {
604 | //throw new Error(err)
605 | }
606 | },
607 | species: async (films) => {
608 | try {
609 | const query = 'SELECT * FROM species LEFT OUTER JOIN species_in_films ON species._id = species_in_films.species_id WHERE species_in_films.film_id = $1';
610 | const values = [films._id]
611 | return await db.query(query, values).then((res) => res.rows);
612 | } catch (err) {
613 | //throw new Error(err)
614 | }
615 | },
616 | },
617 |
618 | Planet: {
619 | films: async (planets) => {
620 | try {
621 | const query = 'SELECT * FROM films LEFT OUTER JOIN planets_in_films ON films._id = planets_in_films.film_id WHERE planets_in_films.planet_id = $1';
622 | const values = [planets._id]
623 | return await db.query(query, values).then((res) => res.rows);
624 | } catch (err) {
625 | //throw new Error(err)
626 | }
627 | },
628 | species: async (planets) => {
629 | try {
630 | const query = 'SELECT * FROM species WHERE homeworld_id = $1';
631 | const values = [planets._id]
632 | return await db.query(query, values).then((res) => res.rows);
633 | } catch (err) {
634 | //throw new Error(err)
635 | }
636 | },
637 | people: async (planets) => {
638 | try {
639 | const query = 'SELECT * FROM people WHERE homeworld_id = $1';
640 | const values = [planets._id]
641 | return await db.query(query, values).then((res) => res.rows);
642 | } catch (err) {
643 | //throw new Error(err)
644 | }
645 | },
646 | species: async (planets) => {
647 | try {
648 | const query = 'SELECT * FROM species LEFT OUTER JOIN people ON species._id = people.species_id WHERE people.homeworld_id = $1';
649 | const values = [planets._id]
650 | return await db.query(query, values).then((res) => res.rows);
651 | } catch (err) {
652 | //throw new Error(err)
653 | }
654 | },
655 | },
656 |
657 | Species: {
658 | people: async (species) => {
659 | try {
660 | const query = 'SELECT * FROM people WHERE species_id = $1';
661 | const values = [species._id]
662 | return await db.query(query, values).then((res) => res.rows);
663 | } catch (err) {
664 | //throw new Error(err)
665 | }
666 | },
667 | planets: async (species) => {
668 | try {
669 | const query = 'SELECT * FROM planets LEFT OUTER JOIN people ON planets._id = people.homeworld_id WHERE people.species_id = $1';
670 | const values = [species._id]
671 | return await db.query(query, values).then((res) => res.rows);
672 | } catch (err) {
673 | //throw new Error(err)
674 | }
675 | },
676 | planets: async (species) => {
677 | try {
678 | const query = 'SELECT planets.* FROM planets LEFT OUTER JOIN species ON planets._id = species.homeworld_id WHERE species._id = $1';
679 | const values = [species._id]
680 | return await db.query(query, values).then((res) => res.rows);
681 | } catch (err) {
682 | //throw new Error(err)
683 | }
684 | },
685 | films: async (species) => {
686 | try {
687 | const query = 'SELECT * FROM films LEFT OUTER JOIN species_in_films ON films._id = species_in_films.film_id WHERE species_in_films.species_id = $1';
688 | const values = [species._id]
689 | return await db.query(query, values).then((res) => res.rows);
690 | } catch (err) {
691 | //throw new Error(err)
692 | }
693 | },
694 | },
695 |
696 | Vessel: {
697 | films: async (vessels) => {
698 | try {
699 | const query = 'SELECT * FROM films LEFT OUTER JOIN vessels_in_films ON films._id = vessels_in_films.film_id WHERE vessels_in_films.vessel_id = $1';
700 | const values = [vessels._id]
701 | return await db.query(query, values).then((res) => res.rows);
702 | } catch (err) {
703 | //throw new Error(err)
704 | }
705 | },
706 | people: async (vessels) => {
707 | try {
708 | const query = 'SELECT * FROM people LEFT OUTER JOIN pilots ON people._id = pilots.person_id WHERE pilots.vessel_id = $1';
709 | const values = [vessels._id]
710 | return await db.query(query, values).then((res) => res.rows);
711 | } catch (err) {
712 | //throw new Error(err)
713 | }
714 | },
715 | starshipSpecs: async (vessels) => {
716 | try {
717 | const query = 'SELECT * FROM starship_specs WHERE vessel_id = $1';
718 | const values = [vessels._id]
719 | return await db.query(query, values).then((res) => res.rows);
720 | } catch (err) {
721 | //throw new Error(err)
722 | }
723 | },
724 | },
725 | }
726 |
727 | const schema = makeExecutableSchema({
728 | typeDefs,
729 | resolvers,
730 | });
731 |
732 | module.exports = schema;
--------------------------------------------------------------------------------