├── .gitignore ├── README.md ├── backend ├── .gitignore ├── .husky │ └── pre-commit ├── .prettierignore ├── .prettierrc ├── package.json ├── src │ ├── .gitignore │ ├── controllers │ │ ├── authController.ts │ │ ├── integrationTestController.ts │ │ ├── resolverTestController.ts │ │ ├── typeTestController.ts │ │ ├── userController.ts │ │ └── userTestController.ts │ ├── index.ts │ ├── middleware │ │ └── globalErrorHandler.ts │ ├── prisma │ │ ├── migrations │ │ │ ├── 20230330004524_schema_creation │ │ │ │ └── migration.sql │ │ │ ├── 20230413022350_update_test_model │ │ │ │ └── migration.sql │ │ │ └── migration_lock.toml │ │ └── schema.prisma │ ├── routes │ │ ├── authRoutes.ts │ │ ├── resolverTestRoute.ts │ │ ├── typeTestRoutes.ts │ │ └── userRoutes.ts │ └── utils │ │ ├── constants.ts │ │ ├── getTypeName.ts │ │ ├── types.ts │ │ ├── validateRegister.ts │ │ └── validateSchema.ts └── tsconfig.json ├── client ├── .gitignore ├── .husky │ └── pre-commit ├── .prettierignore ├── .prettierrc ├── index.html ├── package-lock.json ├── package.json ├── public │ └── vite.svg ├── src │ ├── App.tsx │ ├── app │ │ └── store.ts │ ├── assets │ │ └── react.svg │ ├── components │ │ ├── Footer.tsx │ │ ├── NavBar.tsx │ │ └── TestNavBar.tsx │ ├── features │ │ ├── authSlice.ts │ │ └── testSlice.ts │ ├── images │ │ ├── background.jpg │ │ ├── generateTest.gif │ │ ├── github.png │ │ ├── jake.jpg │ │ ├── jason.jpg │ │ ├── linkedin.png │ │ ├── logo.png │ │ ├── main-logo.png │ │ ├── mason.jpg │ │ └── pierce.jpg │ ├── index.css │ ├── main.tsx │ ├── pages │ │ ├── NotFound.tsx │ │ ├── SavedTests.tsx │ │ ├── Signin.tsx │ │ ├── Test.tsx │ │ └── index.tsx │ ├── services │ │ ├── authService.ts │ │ └── testService.ts │ ├── styles │ │ ├── SliderStyle.tsx │ │ ├── homePage.scss │ │ ├── index.scss │ │ ├── navBar.scss │ │ ├── savedTest.scss │ │ ├── signin.scss │ │ └── test.scss │ ├── utils │ │ └── constants.ts │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── package-lock.json └── testServer ├── __tests__ ├── mutationintegration.test.js ├── queryintegration.test.js ├── resolver.test.ts ├── resolverMock.test.ts ├── resolvers.test.js ├── schema.test.ts └── testServer.js ├── babel.config.js ├── graphql-test1 ├── schema.ts └── server.ts ├── graphql-test2 ├── schema.ts └── server.ts ├── jest.config.ts ├── package-lock.json ├── package.json ├── schema.ts └── testServer.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | dist 4 | notes.txt 5 | package-lock.json 6 | 7 | 8 | #Prisma 9 | prisma/client 10 | prisma/migrations 11 | 12 | 13 | #Prisma 14 | prisma/client 15 | prisma/migrations 16 | 17 | #Prisma 18 | prisma/client 19 | prisma/migrations 20 | 21 | #redis 22 | dump.rdb 23 | 24 | package-lock.json 25 | .DS_Store 26 | backend/package-lock.json 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 | **Boost test-driven development speed and confidence with automated Jest type-test generation, schema validation, and smart resolver mock integration setups.** 8 | 9 | ## **Table of Contents** 10 | 11 | - [Description](https://github.com/oslabs-beta/Scribe-for-GraphQL#Description) 12 | - [Built With](https://github.com/oslabs-beta/Scribe-for-GraphQL#Built-With) 13 | - [Getting Started](https://github.com/oslabs-beta/Scribe-for-GraphQL#Getting-Started) 14 | - [Requirements](https://github.com/oslabs-beta/Scribe-for-GraphQL#Requirements) 15 | - [How to Contribute](https://github.com/oslabs-beta/Scribe-for-GraphQL#How-to-Contribute) 16 | - [Contributors](https://github.com/oslabs-beta/Scribe-for-GraphQL#Contributors) 17 | - [License](https://github.com/oslabs-beta/Scribe-for-GraphQL#License) 18 | 19 | ## **Description**: 20 | 21 | Scribe was developed with the understanding that creating Jest tests can be a tedious, time-consuming process, which may deter developers. Creating Jest-tests for GraphQL schemas and resolvers can involve significant duplication and repetition in code, especially as code-bases grow in size. Scribe thus offers general purpose, standardized Jest tests and Jest test setups for GraphQL to give developers ease of mind. 22 | 23 | Scribe’s web application offers complete type-check tests for user-inputted schemas to prevent unwanted, unnecessary changes during the development process—think of these tests as guardrails while you develop the rest of your GraphQL functionality—alongside schema parsing and validation to catch invalid types or type connections. Simply put, Scribe won’t accept invalid schemas and will let you know if that’s what you have! 24 | 25 | Scribe’s web application also generates complete test setup for mock integration tests for queries and mutations as well as resolver unit tests. When it comes to mock integration tests, we use Apollo GraphQL’s test server with built-in mocking functionality, which will mock random data according to type definitions in your schema. We give you everything you need to start your test-driven development once you’ve gotten through the preliminary creation of your schema and resolvers. 26 | 27 | ## **Built With:** 28 | 29 | - TypeScript 30 | - React 31 | - Express 32 | - Jest 33 | - Apollo GraphQL 34 | - Redis 35 | - Vite 36 | - Prisma 37 | 38 | ## **Getting Started:** 39 | 40 | To begin with, you’ll need to install a few npm packages (or from another package manager): 41 | 42 | **Apollo Packages:** 43 | 44 | npm install --save-dev @apollo/server apollo-server-testing 45 | 46 | _Choose from relevant Jest Packages_: 47 | 48 | **JS Jest Packages**: 49 | 50 | npm install --save-dev jest @jest/globals 51 | 52 | **TS Jest Packages via Babel**: 53 | 54 | npm install --save-dev jest @jest/globals @babel/preset-typescript 55 | 56 | **TS Jest Packages via TS-Jest**: 57 | 58 | npm install --save-dev jest @jest/globals ts-jest \ 59 | 60 | Afterwards, add a test script to your ‘package.json’ file: 61 | 62 | E.g. “test”: “jest --watchAll” 63 | 64 | ## **Requirements:** 65 | 66 | You’ll need this Apollo testServer file for query and mutation mock integration tests, which will mock random data for your tests according to type definitions in your schema: 67 | 68 | ``` 69 | import { ApolloServer } from "apollo-server" 70 | import { createTestClient } from 'apollo-server-testing' 71 | import { typeDefs } from '/* path to schema */' 72 | import { resolvers } from '/* path to resolvers */' 73 | 74 | export const createTestServer = (cntext) => { 75 | const server = new ApolloServer({ 76 | typeDefs, 77 | resolvers, 78 | mockEntireSchema: false, // -> since we are passing in a schema and resolvers, we need this to be false 79 | mocks: true, // -> mocks random data according to type definitions in schema 80 | context: () => cntext, 81 | }); 82 | 83 | return createTestClient(server); 84 | }; 85 | ``` 86 | 87 | A few additional requirements for Scribe, per Apollo GraphQL standards: 88 | 89 | 1. For type-tests on the basis of schemas, we are expecting a ‘typeDefs’ declaration, which consists in a valid Schema Definition Language (SDL) string. 90 | 91 | An example would be: 92 | 93 | ``` 94 | const typeDefs = ` 95 | type Book { 96 | title: String 97 | author: String 98 | } 99 | type Query { 100 | books: [Book] 101 | } 102 | `; 103 | ``` 104 | 105 | 2. For resolvers, we are expecting a ‘resolvers’ declaration, which consists in a map of functions that populate data for individual schema fields. 106 | 107 | An example would be: 108 | 109 | ``` 110 | const resolvers = { 111 | Query: { 112 | books: () => books, 113 | }, 114 | }; 115 | ``` 116 | 117 | Further documentation for schema declarations as typeDefs can be found [here](https://www.apollographql.com/docs/apollo-server/getting-started/#step-3-define-your-graphql-schema) and further documentation on resolver declarations can be found [here](https://www.apollographql.com/docs/apollo-server/data/resolvers). 118 | 119 | ## **Test Generator Guide** 120 | 121 | **Steps:** 122 | 123 | 1. Input your GraphQL queries into the input code editor 124 | 2. Choose the type of test you want to generate on the option selector on the right editor 125 | 3. Press generate 126 | 4. Once you press the "generate" button, your GraphQL queries are sent to the server where they are parsed and processed. The server then responds with a corresponding test script for the given query, which is displayed on the right-hand side of the code editor. This process involves a series of server-side operations, including query validation, execution, and response formatting, all of which are carried out in compliance with the GraphQL specification. 127 |

128 | 129 |

130 | 131 | ## **How to Contribute** 132 | 133 | The broader open source community relies on continuous contributions to empower people to learn from and create with one another. Any further contributions to this project would be greatly appreciated. Here’s how: 134 | 135 | 1. Fork the Project 136 | 2. Create your Feature Branch (git checkout -b feature/AmazingFeature) 137 | 3. Commit your Changes (git commit -m 'Added an AmazingFeature') 138 | 4. Push to the Branch (git push origin feature/AmazingFeature) 139 | 5. Open a Pull Request 140 | 141 | ## **Contributors:** 142 | 143 | Jake Gray: [GitHub](https://github.com/soxg), [LinkedIn](https://www.linkedin.com/in/jake-d-gray/) 144 | 145 | Jason Johnson: [GitHub](https://github.com/jaysenjonsin), [LinkedIn](https://www.linkedin.com/in/jasoncjohnson5/) 146 | 147 | Mason Shelton: [GitHub](https://github.com/MasonS1012), [LinkedIn](https://www.linkedin.com/in/mason-shelton-9ab25521a/) 148 | 149 | Pierce Yu: [GitHub](https://github.com/PierceYu), [LinkedIn](https://www.linkedin.com/in/pierce-yu/) 150 | 151 | ## **License:** 152 | 153 | Distributed under the MIT License. See LICENSE for more information. 154 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Keep environment variables out of version control 3 | .env 4 | -------------------------------------------------------------------------------- /backend/.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx pretty-quick --staged 5 | -------------------------------------------------------------------------------- /backend/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /backend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "es5", 8 | "bracketSpacing": true, 9 | "jsxSingleQuote": true, 10 | "arrowParens": "always", 11 | "proseWrap": "preserve", 12 | "htmlWhitespaceSensitivity": "css", 13 | "endOfLine": "lf" 14 | } 15 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.ts", 6 | "scripts": { 7 | "test": "jest", 8 | "server": "nodemon src/index.ts", 9 | "build": "tsc", 10 | "watch": "tsc -w", 11 | "serverAlt": "nodemon dist/index.js", 12 | "prettier": "npx prettier --write 'src/**/*.{ts,tsx}'", 13 | "prepare": "cd .. && husky install backend/.husky" 14 | }, 15 | "prisma": "./src/prisma/schema.prisma", 16 | "husky": { 17 | "hooks": { 18 | "pre-commit": "npx pretty-quick --staged" 19 | } 20 | }, 21 | "keywords": [], 22 | "author": "", 23 | "license": "ISC", 24 | "devDependencies": { 25 | "@graphql-tools/mock": "^8.7.18", 26 | "@graphql-tools/schema": "^9.0.16", 27 | "@types/bcrypt": "^5.0.0", 28 | "@types/cors": "^2.8.13", 29 | "@types/express": "^4.17.17", 30 | "@types/node": "^18.13.0", 31 | "dotenv": "^16.0.3", 32 | "husky": "^8.0.3", 33 | "jest": "^29.4.3", 34 | "nodemon": "^2.0.20", 35 | "prettier": "^2.8.4", 36 | "pretty-quick": "^3.1.3", 37 | "prisma": "^4.12.0", 38 | "ts-node": "^10.9.1", 39 | "typescript": "^4.9.5" 40 | }, 41 | "dependencies": { 42 | "@prisma/client": "^4.11.0", 43 | "@types/connect-redis": "^0.0.20", 44 | "@types/express-session": "^1.17.6", 45 | "bcrypt": "^5.1.0", 46 | "connect-redis": "^6.1.3", 47 | "cors": "^2.8.5", 48 | "express": "^4.18.2", 49 | "express-session": "^1.17.3", 50 | "graphql": "^16.6.0", 51 | "ioredis": "^5.3.1" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /backend/src/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Keep environment variables out of version control 3 | .env 4 | -------------------------------------------------------------------------------- /backend/src/controllers/authController.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt'; 2 | import { NextFunction, Request, Response } from 'express'; 3 | import { prisma } from '..'; 4 | import { validateRegister } from '../utils/validateRegister'; 5 | import '../utils/types'; 6 | import { COOKIE_NAME } from '../utils/constants'; 7 | 8 | export const authenticateRoute = ( 9 | req: Request, 10 | res: Response, 11 | next: NextFunction 12 | ) => { 13 | try { 14 | if (!req.session.userId) { 15 | res.status(400); 16 | throw new Error('You must log in to continue'); 17 | } else return next(); 18 | } catch (err) { 19 | return next(err); 20 | } 21 | }; 22 | 23 | export const register = async ( 24 | req: Request, 25 | res: Response, 26 | next: NextFunction 27 | ) => { 28 | //get response from req body 29 | const { name, email, password, confirmPassword } = req.body; 30 | try { 31 | const validated = validateRegister(name, email, password, confirmPassword); 32 | 33 | if (validated.errorMessage) { 34 | res.status(400); 35 | throw new Error(validated.errorMessage); 36 | } 37 | 38 | //make sure they dont exist in db 39 | const user = await prisma.user.findFirst({ 40 | where: { 41 | email, 42 | }, 43 | }); 44 | console.log('user: ', user); 45 | if (user) { 46 | res.status(400); 47 | throw new Error('user already exists'); 48 | } 49 | //if they dont make new thing 50 | const newUser = await prisma.user.create({ 51 | data: { 52 | //@ts-ignore 53 | name, 54 | email, 55 | password: await bcrypt.hash(password, 12), 56 | }, 57 | }); 58 | console.log('newUser: ', newUser.id); 59 | //save session 60 | req.session.userId = newUser.id; 61 | //send response back 62 | res.status(201).json({ 63 | name, 64 | email, 65 | }); 66 | } catch (err) { 67 | return next(err); 68 | } 69 | }; 70 | 71 | export const login = async ( 72 | req: Request, 73 | res: Response, 74 | next: NextFunction 75 | ) => { 76 | const { email, password } = req.body; 77 | try { 78 | if (!email || !password) { 79 | res.status(400); 80 | throw new Error('please enter all required fields'); 81 | } 82 | 83 | const user = await prisma.user.findFirst({ 84 | where: { 85 | email, 86 | }, 87 | }); 88 | 89 | if (user && (await bcrypt.compare(password, user.password))) { 90 | req.session.userId = user.id; 91 | res.status(200).json({ 92 | //@ts-ignore 93 | name: user.name, 94 | email: user.email, 95 | }); 96 | } else { 97 | res.status(400); 98 | throw new Error('invalid login credentials'); 99 | } 100 | } catch (err) { 101 | return next(err); 102 | } 103 | }; 104 | 105 | export const logout = async (req: Request, res: Response) => { 106 | return new Promise((resolve, reject) => { 107 | //destroy the session(server) and clear the cookie(client) 108 | req.session.destroy((err) => { 109 | res.clearCookie(COOKIE_NAME); 110 | if (err) { 111 | console.error(err); 112 | reject(err); 113 | return; 114 | } 115 | resolve(true); 116 | }); 117 | }) 118 | .then((result) => { 119 | res.json(result); 120 | }) 121 | .catch((error) => { 122 | console.error(error); 123 | res.status(500).json({ message: 'Error occurred while logging out.' }); 124 | }); 125 | }; 126 | -------------------------------------------------------------------------------- /backend/src/controllers/integrationTestController.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | 3 | export const generateIntegrationTests = async ( 4 | req: Request, 5 | res: Response, 6 | next: NextFunction 7 | ) => { 8 | let { schema, resolvers } = req.body; 9 | try { 10 | console.log(req.body); 11 | // resolvers = JSON.stringify(schema) 12 | console.log('stringify', resolvers); 13 | resolvers = resolvers.concat('\n resolvers;'); //need frontend to send resolvers instead of schema 14 | resolvers = eval(resolvers); 15 | console.log('eval', resolvers.Query.allBooks); 16 | } catch (error) { 17 | return next(error); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /backend/src/controllers/resolverTestController.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | 3 | export const generateResolverTests = async ( 4 | req: Request, 5 | res: Response, 6 | next: NextFunction 7 | ) => { 8 | let { schema, resolvers } = req.body; 9 | try { 10 | console.log(req.body); 11 | console.log('stringify', resolvers); 12 | resolvers = resolvers.concat('\n resolvers;'); 13 | resolvers = eval(resolvers); 14 | 15 | let onlyQueries: Object[] = []; 16 | 17 | let onlyMutations: Object[] = []; 18 | 19 | let onlyResolvers: Object[] = []; 20 | 21 | const sorter = () => { 22 | for (let key in resolvers) { 23 | console.log('KEY', key); 24 | if (key === 'Query') { 25 | console.log(key); 26 | //@ts-ignore 27 | for (let funcName in resolvers[key]) { 28 | console.log('funcName', funcName); 29 | //@ts-ignore 30 | onlyQueries.push({ funcName: funcName }); 31 | } 32 | } else if (key === 'Mutation') { 33 | //@ts-ignore 34 | for (let funcName in resolvers[key]) { 35 | //@ts-ignore 36 | onlyMutations.push({ funcName: funcName }); 37 | } 38 | } else { 39 | //@ts-ignore 40 | for (let funcName in resolvers[key]) { 41 | //@ts-ignore 42 | onlyResolvers.push({ funcName: [funcName, key] }); 43 | } 44 | } 45 | } 46 | }; 47 | 48 | const generateTests = () => { 49 | let tests = {}; 50 | let finalQueryIntTests: any = []; 51 | let finalMutationIntTests: any = []; 52 | sorter(); 53 | console.log(onlyQueries, onlyMutations, onlyResolvers); 54 | const QueryIntegrationTestGenerator = (onlyQueries: Object[]) => { 55 | let queryIntegrationTests: string[] = []; 56 | let queryFrontBoiler: string = `//-> ensure packages are installed 57 | import { describe, test, expect } from '@jest/globals' 58 | import { gql } from 'apollo-server' 59 | import { createTestServer } from '/*path to testServer*/' 60 | `; 61 | let queryDefinitions = Object.entries(onlyQueries) 62 | .map(([typeName, fields]) => { 63 | //@ts-ignore 64 | let name = fields.funcName; 65 | console.log('hi', name); 66 | //@ts-ignore 67 | return ` 68 | const ${name}_query = gql\` 69 | /* QUERY STRING*/ 70 | \` 71 | `; 72 | }) 73 | .join(''); 74 | console.log('queryDefs', queryDefinitions); 75 | 76 | let testFrontBoiler: string = ` 77 | describe("queries work as intended", () => {`; 78 | let integrationTests = Object.entries(onlyQueries) 79 | .map(([typeName, fields]) => { 80 | //@ts-ignore 81 | let name = fields.funcName; 82 | return ` 83 | test("${name}_query returns the correct values", async () => { 84 | const { query } = createTestServer({ 85 | /*CONTEXT OBJECT - MOCK QUERY/RESOLVER CONTEXT REQUIREMENTS */ 86 | }); 87 | const res = await query({query: ${name}_query}); 88 | expect(res).toMatchSnapshot(); // -> first run will always pass 89 | })`; 90 | }) 91 | .join(''); 92 | let testEndBoiler: string = ` 93 | })`; 94 | queryIntegrationTests.push(queryFrontBoiler); 95 | queryIntegrationTests.push(queryDefinitions); 96 | queryIntegrationTests.push(testFrontBoiler); 97 | queryIntegrationTests.push(integrationTests); 98 | queryIntegrationTests.push(testEndBoiler); 99 | console.log('TESTS', queryIntegrationTests); 100 | finalQueryIntTests.push(queryIntegrationTests.join('').toString()); 101 | //@ts-ignore 102 | tests['queryIntTests'] = finalQueryIntTests.toString(); 103 | }; 104 | 105 | const MutationTestGenerator = (onlyMutations: Object[]) => { 106 | let mutationTests: string[] = []; 107 | let mutationFrontBoiler: string = `//-> ensure packages are installed 108 | import { describe, expect, test } from '@jest/globals' 109 | import { gql } from 'apollo-server' 110 | import { createTestServer } from '*/path to testServer*/' 111 | `; 112 | let mutationDefinitions = Object.entries(onlyMutations) 113 | .map(([typeName, fields]) => { 114 | //@ts-ignore 115 | console.log(typeName, fields); 116 | //@ts-ignore 117 | let name = fields.funcName; 118 | //@ts-ignore 119 | return ` 120 | const ${name}_mutation = gql\` 121 | mutation { 122 | ${name}(/*INPUT*/) { 123 | /*DATA TO BE SENT BACK*/ 124 | } 125 | } 126 | \` 127 | `; 128 | }) 129 | .join(''); 130 | let testFrontBoiler: string = ` 131 | describe("mutations work as intended", () => { 132 | `; 133 | let integrationTests = Object.entries(onlyQueries) 134 | .map(([typeName, fields]) => { 135 | //@ts-ignore 136 | let name = fields.funcName; 137 | return ` 138 | test("${name}_mutation mutates data correctly and returns the correct values", async () => { 139 | const { mutate } = createTestServer({ 140 | /* CONTEXT OBJECT - MOCK MUTATION CONTEXT REQUIREMENTS HERE */ 141 | }); 142 | const res = await mutate({query: ${name}_mutation}); 143 | expect(res).toMatchSnapshot(); // -> first test run will always pass 144 | })`; 145 | }) 146 | .join(''); 147 | let testEndBoiler: string = ` 148 | })`; 149 | let mutationEndBoiler: string = ``; 150 | mutationTests.push(mutationFrontBoiler); 151 | mutationTests.push(mutationDefinitions); 152 | mutationTests.push(testFrontBoiler); 153 | mutationTests.push(integrationTests); 154 | mutationTests.push(testEndBoiler); 155 | mutationTests.push(mutationEndBoiler); 156 | finalMutationIntTests.push(mutationTests.join('').toString()); 157 | //@ts-ignore 158 | tests['mutationIntTests'] = finalMutationIntTests.toString(''); 159 | }; 160 | 161 | const ResolverTestGenerator = (onlyResolvers: Object[]) => { 162 | let resolverTests: string[] = []; 163 | let resolverFrontBoiler: string = `//-> ensure packages are installed 164 | import { describe, test, expect } from '@jest/globals' 165 | const resolvers = require(/*path to resolvers*/) 166 | 167 | describe ('resolvers return the correct values', ()=> {`; 168 | 169 | let resolverEndBoiler: string = ` 170 | })`; 171 | 172 | let resolverUnits = Object.entries(onlyResolvers) 173 | .map(([typeName, fields]) => { 174 | //@ts-ignore 175 | let name = fields.funcName[0]; 176 | //@ts-ignore 177 | let pre = fields.funcName[1]; 178 | return ` 179 | test('Resolver ${pre}.${name} works as intended', () => { 180 | const result = resolvers.${pre}.${name}(/*resolver mock parameters*/) 181 | 182 | expect(result).toEqual(/*expected result*/) 183 | })`; 184 | }) 185 | .join(''); 186 | 187 | resolverTests.push(resolverFrontBoiler); 188 | resolverTests.push(resolverUnits); 189 | resolverTests.push(resolverEndBoiler); 190 | console.log(resolverUnits); 191 | //@ts-ignore 192 | tests['resolverUnitTests'] = resolverTests.join('').toString(); 193 | }; 194 | QueryIntegrationTestGenerator(onlyQueries); // -> dropdown: Query Mock Integration Tests | queryIntTests 195 | MutationTestGenerator(onlyMutations); // -> dropdown: Mutation Mock Integration Tests | mutationIntTests 196 | ResolverTestGenerator(onlyResolvers); // -> dropdown: Resolver Unit Tests | resolverUnitTests 197 | //@ts-ignore 198 | return tests; //{'queryIntTests': [], 'mutationIntTests': [], 'resolverUnitTests': []} 199 | }; 200 | 201 | return res.status(200).json(generateTests()); 202 | } catch (error) { 203 | return next(error); 204 | } 205 | }; 206 | -------------------------------------------------------------------------------- /backend/src/controllers/typeTestController.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import { getTypeName } from '../utils/getTypeName'; 3 | import { validateSchema } from '../utils/validateSchema'; 4 | import { buildSchema, Source, parse } from 'graphql'; 5 | 6 | export const generateTypeTest = async ( 7 | req: Request, 8 | res: Response, 9 | next: NextFunction 10 | ) => { 11 | let { schema } = req.body; 12 | try { 13 | /*VALIDATING AND BUILDLING OUR SCHEMA */ 14 | schema = schema.concat('\n typeDefs;'); 15 | console.log('input', schema); 16 | console.log('eval', eval(schema)); 17 | const evalSchema = eval(schema); 18 | 19 | validateSchema(evalSchema); 20 | if (!validateSchema) { 21 | throw new Error('Schema is invalid GraphQL Schema'); 22 | } 23 | const typeName = getTypeName(evalSchema); 24 | if (!typeName) { 25 | throw new Error('Schema is invalid GraphQL Schema'); 26 | } 27 | const schemaBuilt = buildSchema(evalSchema); 28 | 29 | const nestedTypes = (field: any) => { 30 | let resultType = ''; 31 | 32 | let currentNode = field.loc.startToken; 33 | 34 | //@ts-ignore 35 | let start = field.loc.start; 36 | //@ts-ignore 37 | let end = field.loc.end; 38 | 39 | for (let i = field.loc.start; i <= end; i) { 40 | if (currentNode.kind === '}' || currentNode.kind === ':') 41 | return resultType; 42 | 43 | let iterate = currentNode.end - currentNode.start; 44 | if (currentNode.kind === 'Name') { 45 | resultType += currentNode.value; 46 | } else { 47 | resultType += currentNode.kind; 48 | } 49 | if (currentNode.start + 1 === end) return resultType; 50 | currentNode = currentNode.next; 51 | i += iterate; 52 | } 53 | 54 | console.log('RESULTS:', resultType); 55 | return resultType; 56 | }; 57 | 58 | // Free Bird, 59 | function generateTypeTests(schema: string) { 60 | const ast = parse(evalSchema); //converts to AST 61 | // console.log('ast', ast.definitions) // TYPES 62 | //@ts-ignore 63 | // console.log('startToken LOC, field 1', ast.definitions[0].fields[1].type) 64 | //@ts-ignore 65 | console.log('startToken LOC, field 2', ast.definitions[1].fields[2].type); 66 | 67 | //@ts-ignore 68 | // console.log('start token next next', ast.definitions[1].fields[1].type.loc.startToken) 69 | //@ts-ignore 70 | console.log('TESTING', nestedTypes(ast.definitions[1].fields[2].type)); 71 | 72 | const typeDefs = ast.definitions.reduce((acc: any, def: any) => { 73 | if (def.kind === 'ObjectTypeDefinition') { 74 | //@ts-ignore 75 | const fields = def.fields.map((field) => ({ 76 | name: field.name.value, 77 | //@ts-ignore 78 | type: field.type.type?.kind 79 | ? nestedTypes(field.type) 80 | : field.type.name.value, 81 | })); 82 | //@ts-ignore 83 | acc[def.name.value] = fields; 84 | } 85 | return acc; 86 | }, {}); 87 | 88 | console.log('typeDefs', typeDefs); 89 | 90 | const tests = Object.entries(typeDefs).map(([typeName, fields]) => { 91 | return ` 92 | test('${typeName} should have the correct types', () => { 93 | const type = schema.getType('${typeName}'); 94 | expect(type).toBeDefined();${/*@ts-ignore*/ ''} 95 | ${fields 96 | .map((field: any) => { 97 | if ( 98 | Array.isArray(field.type) || 99 | field.type.includes('!') || 100 | field.type.includes('[') 101 | ) { 102 | return ` 103 | expect(JSON.stringify(type.getFields().${field.name}.type)).toBe(JSON.stringify("${field.type}"));`; 104 | } else { 105 | return ` 106 | expect(type.getFields().${field.name}.type.name).toBe('${field.type}');`; 107 | } 108 | }) 109 | .join('')} 110 | })`; 111 | }); 112 | const boilerplate = `//> npm install graphql-tools @jest/globals jest babel-jest 113 | //> for typescript tests, npm install ts-jest @types/jest 114 | import {describe, expect, test} from '@jest/globals'; 115 | const { makeExecutableSchema, addMocksToSchema } = require('graphql-tools'); 116 | const typeDefs = require(/* schema file */) 117 | 118 | describe('Schema Types Are Correct', () => { 119 | const schema = makeExecutableSchema({ typeDefs });`; 120 | const endboiler = ` 121 | });`; 122 | // console.log(tests) 123 | tests.unshift(boilerplate); 124 | tests.push(endboiler); 125 | console.log('tests after:', tests.toString()); 126 | return tests.join('').toString(); 127 | } 128 | 129 | return res.status(200).json(generateTypeTests(schema)); 130 | } catch (err) { 131 | return next(err); 132 | } 133 | }; 134 | -------------------------------------------------------------------------------- /backend/src/controllers/userController.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Response, Request } from 'express'; 2 | import { redis, prisma } from '..'; 3 | 4 | export const getAllUsers = async ( 5 | req: Request, 6 | res: Response, 7 | next: NextFunction 8 | ) => { 9 | try { 10 | const users = await prisma.user.findMany({}); 11 | res.status(200).json(users); 12 | } catch (err) { 13 | return next(err); 14 | } 15 | }; 16 | 17 | export const getSavedTests = async ( 18 | req: Request, 19 | res: Response, 20 | next: NextFunction 21 | ) => { 22 | try { 23 | const cachedTests = await redis.get(`tests-${req.session.userId}`); 24 | if (cachedTests) return res.status(200).json(JSON.parse(cachedTests)); 25 | 26 | const tests = await prisma.test.findMany({ 27 | select: { 28 | id: true, 29 | generated_test: true, 30 | test_type: true, 31 | user: { 32 | select: { 33 | id: true, 34 | //@ts-ignore 35 | name: true, 36 | }, 37 | }, 38 | }, 39 | where: { 40 | user_id: req.session.userId, 41 | }, 42 | }); 43 | 44 | // if (!tests) { 45 | // res.status(400); 46 | // throw new Error('No tests saved'); 47 | // } 48 | 49 | redis.set(`tests-${req.session.userId}`, JSON.stringify(tests), 'EX', 3600); 50 | res.status(200).json(tests); 51 | } catch (err) { 52 | return next(err); 53 | } 54 | }; 55 | 56 | export const saveTest = async ( 57 | req: Request, 58 | res: Response, 59 | next: NextFunction 60 | ) => { 61 | const { test, testType } = req.body; 62 | 63 | try { 64 | if (!test || !testType) { 65 | res.status(400); 66 | throw new Error('Must include a test and a test type to save'); 67 | } 68 | const savedTest = await prisma.test.create({ 69 | data: { 70 | generated_test: test, 71 | //@ts-ignore 72 | test_type: testType, 73 | user: { 74 | connect: { 75 | id: req.session.userId, 76 | }, 77 | }, 78 | }, 79 | }); 80 | 81 | console.log('SAVED TEST: '); 82 | 83 | const cachedTestsString = await redis.get(`tests-${req.session.userId}`); 84 | if (cachedTestsString) { 85 | const cachedTests = JSON.parse(cachedTestsString); 86 | cachedTests.unshift(savedTest); 87 | await redis.set( 88 | `tests-${req.session.userId}`, 89 | JSON.stringify(cachedTests), 90 | 'EX', 91 | 3600 92 | ); 93 | } 94 | 95 | res.status(201).json(savedTest); 96 | } catch (err) { 97 | return next(err); 98 | } 99 | }; 100 | 101 | export const deleteTest = async ( 102 | req: Request, 103 | res: Response, 104 | next: NextFunction 105 | ) => { 106 | const { id } = req.params; 107 | try { 108 | const test = await prisma.test.delete({ 109 | where: { 110 | id: parseInt(id), 111 | }, 112 | }); 113 | res.status(200).json(test); 114 | } catch (err) { 115 | return next(err); 116 | } 117 | }; 118 | -------------------------------------------------------------------------------- /backend/src/controllers/userTestController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { prisma, redis } from '..'; 3 | 4 | export const getSavedTests = async ( 5 | req: Request, 6 | res: Response, 7 | next: NextFunction 8 | ) => { 9 | try { 10 | const cachedTests = await redis.get(`tests-${req.session.userId}`); 11 | if (cachedTests) return res.status(200).json(JSON.parse(cachedTests)); 12 | 13 | const tests = await prisma.test.findMany({ 14 | select: { 15 | id: true, 16 | generated_test: true, 17 | test_type: true, 18 | user: { 19 | select: { 20 | id: true, 21 | name: true, 22 | }, 23 | }, 24 | }, 25 | where: { 26 | user_id: req.session.userId, 27 | }, 28 | }); 29 | 30 | if (!tests) { 31 | res.status(400); 32 | throw new Error('No tests saved'); 33 | } 34 | 35 | res.status(200).json(tests); 36 | } catch (err) { 37 | return next(err); 38 | } 39 | }; 40 | 41 | export const saveTest = async ( 42 | req: Request, 43 | res: Response, 44 | next: NextFunction 45 | ) => { 46 | const { test, testType } = req.body; 47 | 48 | try { 49 | const savedTest = await prisma.test.create({ 50 | data: { 51 | generated_test: test, 52 | test_type: testType, 53 | user: { 54 | connect: { 55 | id: req.session.userId, 56 | }, 57 | }, 58 | }, 59 | }); 60 | 61 | const cachedTestsString = await redis.get(`tests-${req.session.userId}`); 62 | if (cachedTestsString) { 63 | const cachedTests = JSON.parse(cachedTestsString); 64 | cachedTests.unshift(savedTest); 65 | await redis.set( 66 | `feed-${req.session.userId}`, 67 | JSON.stringify(cachedTests), 68 | 'EX', 69 | 3600 70 | ); 71 | } 72 | 73 | res.status(201).json(savedTest); 74 | } catch (err) { 75 | return next(err); 76 | } 77 | }; 78 | export const getTests = async ( 79 | req: Request, 80 | res: Response, 81 | next: NextFunction 82 | ) => {}; 83 | -------------------------------------------------------------------------------- /backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { globalErrorHandler } from './middleware/globalErrorHandler'; 3 | import { config } from 'dotenv'; 4 | import connectRedis from 'connect-redis'; 5 | import session from 'express-session'; 6 | import Redis from 'ioredis'; 7 | import cors from 'cors'; 8 | import typeTestRouter from './routes/typeTestRoutes'; 9 | import { COOKIE_NAME, __prod__ } from './utils/constants'; 10 | import { CLIENT_URL } from './utils/constants'; 11 | import { PrismaClient } from '@prisma/client'; 12 | import authRouter from './routes/authRoutes'; 13 | import userRouter from './routes/userRoutes'; 14 | import resolverTestRouter from './routes/resolverTestRoute'; 15 | 16 | config({ path: '../.env' }); 17 | const PORT = process.env.PORT || 8080; 18 | 19 | export const prisma = new PrismaClient({ 20 | log: ['query'], //shows SQL in console log 21 | }); 22 | 23 | const RedisStore = connectRedis(session); 24 | 25 | export const redis = new Redis(); 26 | 27 | const main = async () => { 28 | const app = express(); 29 | app.use(express.json()); 30 | app.use(express.urlencoded({ extended: false })); 31 | app.use( 32 | cors({ 33 | origin: CLIENT_URL, //wherever are front end is, 34 | credentials: true, 35 | }) 36 | ); 37 | 38 | // SESSIONS ROUTE 39 | app.use( 40 | session({ 41 | name: COOKIE_NAME, 42 | store: new RedisStore({ 43 | client: redis, 44 | disableTouch: true, 45 | }), 46 | saveUninitialized: false, 47 | resave: false, 48 | secret: process.env.SESSION_SECRET ?? '', 49 | cookie: { 50 | maxAge: 1000 * 60 * 60 * 24 * 365, 51 | httpOnly: true, 52 | sameSite: 'lax', 53 | secure: __prod__, 54 | }, 55 | }) 56 | ); 57 | 58 | app.use('/typeTest', typeTestRouter); 59 | app.use('/resolverTest', resolverTestRouter); 60 | app.use('/auth', authRouter); 61 | app.use('/users', userRouter); 62 | 63 | app.use((_, res) => res.status(404).send('page not found')); 64 | app.use(globalErrorHandler); 65 | 66 | app.listen(PORT, () => { 67 | console.log(`Listening on port ${PORT} in ${process.env.NODE_ENV} mode`); 68 | }); 69 | }; 70 | 71 | main().catch((err) => console.log(err)); 72 | -------------------------------------------------------------------------------- /backend/src/middleware/globalErrorHandler.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | 3 | export const globalErrorHandler = ( 4 | err: any, 5 | _: Request, 6 | res: Response, 7 | __: NextFunction 8 | ) => { 9 | const statusCode = res.statusCode ? res.statusCode : 500; 10 | const message = err.message ? err.message : 'unknown error occured'; 11 | res.status(statusCode).json({ message }); 12 | }; 13 | -------------------------------------------------------------------------------- /backend/src/prisma/migrations/20230330004524_schema_creation/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "users" ( 3 | "id" SERIAL NOT NULL, 4 | "name" VARCHAR(25) NOT NULL, 5 | "email" VARCHAR(25) NOT NULL, 6 | "password" VARCHAR(500) NOT NULL, 7 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | "updated_at" TIMESTAMP(3) NOT NULL, 9 | 10 | CONSTRAINT "users_pkey" PRIMARY KEY ("id") 11 | ); 12 | 13 | -- CreateTable 14 | CREATE TABLE "tests" ( 15 | "id" SERIAL NOT NULL, 16 | "user_id" INTEGER NOT NULL, 17 | "generated_test" VARCHAR(50000) NOT NULL, 18 | 19 | CONSTRAINT "tests_pkey" PRIMARY KEY ("id") 20 | ); 21 | 22 | -- AddForeignKey 23 | ALTER TABLE "tests" ADD CONSTRAINT "tests_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; 24 | -------------------------------------------------------------------------------- /backend/src/prisma/migrations/20230413022350_update_test_model/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `test_type` to the `tests` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "tests" ADD COLUMN "test_type" VARCHAR(25) NOT NULL; 9 | -------------------------------------------------------------------------------- /backend/src/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /backend/src/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | //npx prisma migrate dev 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | datasource db { 11 | provider = "postgresql" 12 | url = env("DATABASE_URL") 13 | } 14 | 15 | model User { 16 | id Int @id @default(autoincrement()) 17 | name String @db.VarChar(25) 18 | email String @db.VarChar(25) 19 | password String @db.VarChar(500) 20 | tests Test[] 21 | created_at DateTime @default(now()) 22 | updated_at DateTime @updatedAt 23 | 24 | //ADD STUFF FROM REGISTER.TSX 25 | @@map("users") 26 | } 27 | 28 | model Test { 29 | id Int @id @default(autoincrement()) 30 | user_id Int 31 | generated_test String @db.VarChar(50000) 32 | test_type String @db.VarChar(25) 33 | user User @relation(fields: [user_id], references: [id], onDelete: Cascade) 34 | 35 | @@map("tests") 36 | } 37 | -------------------------------------------------------------------------------- /backend/src/routes/authRoutes.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { register, login, logout } from '../controllers/authController'; 3 | const router = express.Router(); 4 | 5 | router.route('/login').get(login).post(login); 6 | router.route('/register').post(register); 7 | router.route('/logout').post(logout); 8 | 9 | export default router; 10 | -------------------------------------------------------------------------------- /backend/src/routes/resolverTestRoute.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { generateResolverTests } from '../controllers/resolverTestController'; 3 | const router = express.Router(); 4 | 5 | router.route('/').post(generateResolverTests); 6 | 7 | // router.post('/', generateTypeTest, (_, res) => { 8 | // return res.status(200).json(res.locals.typeTests); 9 | // }); 10 | 11 | //need middleware here for when users save tests produced from a schema saved under a schema name 12 | router.get('/:schemaName', (_, res) => { 13 | return res.status(200); 14 | }); 15 | 16 | export default router; 17 | -------------------------------------------------------------------------------- /backend/src/routes/typeTestRoutes.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { generateTypeTest } from '../controllers/typeTestController'; 3 | import { generateResolverTests } from '../controllers/resolverTestController'; 4 | const router = express.Router(); 5 | 6 | router.route('/').post(generateTypeTest); 7 | 8 | // router.post('/', generateTypeTest, (_, res) => { 9 | // return res.status(200).json(res.locals.typeTests); 10 | // }); 11 | 12 | //need middleware here for when users save tests produced from a schema saved under a schema name 13 | router.get('/:schemaName', (_, res) => { 14 | return res.status(200); 15 | }); 16 | 17 | export default router; 18 | -------------------------------------------------------------------------------- /backend/src/routes/userRoutes.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { authenticateRoute } from '../controllers/authController'; 3 | import { 4 | getAllUsers, 5 | getSavedTests, 6 | saveTest, 7 | deleteTest, 8 | } from '../controllers/userController'; 9 | const router = express.Router(); 10 | 11 | router.route('/').get(getAllUsers); 12 | router.route('/tests/:id').delete(authenticateRoute, deleteTest); 13 | router 14 | .route('/tests') 15 | .get(authenticateRoute, getSavedTests) 16 | .post(authenticateRoute, saveTest); 17 | 18 | export default router; 19 | -------------------------------------------------------------------------------- /backend/src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const __prod__ = process.env.NODE_ENV === 'production'; 2 | export const CLIENT_URL = 'http://localhost:5173'; 3 | export const COOKIE_NAME = 'asa'; 4 | -------------------------------------------------------------------------------- /backend/src/utils/getTypeName.ts: -------------------------------------------------------------------------------- 1 | export const getTypeName = (typeDef: string) => { 2 | const typeName = typeDef.match(/type\s+(\w+)\s+{/i); 3 | if (typeName) { 4 | return typeName[1]; 5 | } 6 | return false; 7 | }; 8 | -------------------------------------------------------------------------------- /backend/src/utils/types.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | declare module 'express-session' { 4 | interface SessionData { 5 | userId: number; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /backend/src/utils/validateRegister.ts: -------------------------------------------------------------------------------- 1 | export const validateRegister = ( 2 | name: string, 3 | email: string, 4 | password: string, 5 | confirmPassword: string 6 | ): { errorMessage: null | string } => { 7 | let errorMessage = null; 8 | 9 | switch (true) { 10 | case !name || !email || !password || !confirmPassword: 11 | errorMessage = 'please enter all required fields'; 12 | break; 13 | 14 | //set more specific username rules here 15 | case !email.includes('@'): 16 | errorMessage = 'please enter valid an email'; 17 | break; 18 | 19 | case Number(password.length) <= 8: 20 | errorMessage = 'password must contain at least 8 characters'; 21 | break; 22 | } 23 | 24 | return { errorMessage }; 25 | }; 26 | -------------------------------------------------------------------------------- /backend/src/utils/validateSchema.ts: -------------------------------------------------------------------------------- 1 | const { buildSchema, parse } = require('graphql'); 2 | 3 | export const validateSchema = (schemaString: any) => { 4 | try { 5 | const schemaAST = parse(schemaString); 6 | const schema = buildSchema(schemaAST); 7 | console.log(schema); 8 | return true; 9 | } catch (error) { 10 | return false; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "lib": ["dom", "es6", "es2017", "esnext.asynciterable"], 6 | "skipLibCheck": true, 7 | "sourceMap": true, 8 | "outDir": "./dist", //where our typescript is built 9 | "moduleResolution": "node", 10 | "removeComments": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "strictFunctionTypes": true, 14 | "noImplicitThis": true, 15 | // "noUnusedLocals": true, 16 | // "noUnusedParameters": true, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "allowSyntheticDefaultImports": true, 20 | "esModuleInterop": true, 21 | // "emitDecoratorMetadata": true, 22 | // "experimentalDecorators": true, 23 | "resolveJsonModule": true, 24 | "baseUrl": "." 25 | }, 26 | "exclude": ["node_modules"], 27 | "include": ["./src/**/*.ts"] 28 | } 29 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | package-lock.json 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /client/.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx pretty-quick --staged 5 | -------------------------------------------------------------------------------- /client/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /client/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "es5", 8 | "bracketSpacing": true, 9 | "jsxSingleQuote": true, 10 | "arrowParens": "always", 11 | "proseWrap": "preserve", 12 | "htmlWhitespaceSensitivity": "css", 13 | "endOfLine": "lf" 14 | } 15 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | Scribe for GraphQL 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "prettier": "npx prettier --write 'src/**/*.{ts,tsx}'", 11 | "prepare": "cd .. && husky install client/.husky" 12 | }, 13 | "husky": { 14 | "hooks": { 15 | "pre-commit": "npx pretty-quick --staged" 16 | } 17 | }, 18 | "dependencies": { 19 | "@emotion/react": "^11.10.6", 20 | "@emotion/styled": "^11.10.6", 21 | "@hookform/resolvers": "^2.9.11", 22 | "@monaco-editor/react": "^4.5.0", 23 | "@mui/icons-material": "^5.11.9", 24 | "@mui/material": "^5.11.10", 25 | "@mui/styled-engine-sc": "^5.11.9", 26 | "@reduxjs/toolkit": "^1.9.3", 27 | "axios": "^1.3.4", 28 | "daisyui": "^2.50.2", 29 | "react": "^18.2.0", 30 | "react-dom": "^18.2.0", 31 | "react-hook-form": "^7.43.2", 32 | "react-redux": "^8.0.5", 33 | "react-router": "^6.8.1", 34 | "react-router-dom": "^6.8.1", 35 | "sass": "^1.60.0", 36 | "styled-components": "^5.3.6", 37 | "sweetalert2": "^11.7.3", 38 | "zod": "^3.20.6" 39 | }, 40 | "devDependencies": { 41 | "@types/react": "^18.0.27", 42 | "@types/react-dom": "^18.0.10", 43 | "@types/styled-components": "^5.1.26", 44 | "@vitejs/plugin-react": "^3.1.0", 45 | "husky": "^8.0.3", 46 | "prettier": "^2.8.4", 47 | "pretty-quick": "^3.1.3", 48 | "typescript": "^4.9.3", 49 | "vite": "^4.1.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /client/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter, Routes, Route } from 'react-router-dom'; 2 | import Homepage from './pages'; 3 | import Test from './pages/Test'; 4 | import NotFound from './pages/NotFound'; 5 | import Signin from './pages/Signin'; 6 | import SavedTests from './pages/SavedTests'; 7 | import './styles/index.scss'; 8 | 9 | function App() { 10 | return ( 11 | <> 12 | 13 | 14 | } /> 15 | } /> 16 | } /> 17 | } /> 18 | } /> 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /client/src/app/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import authReducer from '../features/authSlice'; 3 | import testReducer from '../features/testSlice'; 4 | import { useDispatch } from 'react-redux'; 5 | 6 | export const store = configureStore({ 7 | reducer: { 8 | auth: authReducer, 9 | tests: testReducer, 10 | }, 11 | }); 12 | 13 | export type RootState = ReturnType; 14 | export type AppDispatch = typeof store.dispatch; 15 | -------------------------------------------------------------------------------- /client/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import pierce from '../images/pierce.jpg'; 3 | import jake from '../images/jake.jpg'; 4 | import mason from '../images/mason.jpg'; 5 | import jason from '../images/jason.jpg'; 6 | import linkedin from '../images/linkedin.png'; 7 | import github from '../images/github.png'; 8 | 9 | function Footer() { 10 | return ( 11 | 64 | ); 65 | } 66 | 67 | export default Footer; 68 | -------------------------------------------------------------------------------- /client/src/components/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import MenuIcon from '@mui/icons-material/Menu'; 4 | import AutoGraphIcon from '@mui/icons-material/AutoGraph'; 5 | import { useDispatch, useSelector } from 'react-redux'; 6 | import { AppDispatch, RootState } from '../app/store'; 7 | import { logout } from '../features/authSlice'; 8 | import logo from '../images/logo.png'; 9 | 10 | const NavBar = () => { 11 | const { user } = useSelector((state: RootState) => state.auth); 12 | const dispatch = useDispatch(); 13 | const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); 14 | 15 | const toggleMobileMenu = () => { 16 | setIsMobileMenuOpen(!isMobileMenuOpen); 17 | }; 18 | 19 | const closeMobileMenu = () => { 20 | setIsMobileMenuOpen(false); 21 | }; 22 | 23 | const handleResize = () => { 24 | if (window.innerWidth > 800) { 25 | closeMobileMenu(); 26 | } 27 | }; 28 | 29 | const handleAuthClick = () => { 30 | if (user) { 31 | dispatch(logout()); 32 | } 33 | closeMobileMenu(); 34 | }; 35 | 36 | return ( 37 | <> 38 | 93 | 94 | ); 95 | }; 96 | export default NavBar; 97 | -------------------------------------------------------------------------------- /client/src/components/TestNavBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Link, useLocation, useNavigate } from 'react-router-dom'; 3 | import MenuIcon from '@mui/icons-material/Menu'; 4 | import AutoGraphIcon from '@mui/icons-material/AutoGraph'; 5 | import { useDispatch, useSelector } from 'react-redux'; 6 | import { AppDispatch, RootState } from '../app/store'; 7 | import { logout } from '../features/authSlice'; 8 | import logo from '../images/logo.png'; 9 | import Swal from 'sweetalert2'; 10 | 11 | const TestNavBar = () => { 12 | const { user } = useSelector((state: RootState) => state.auth); 13 | const dispatch = useDispatch(); 14 | const route = useLocation(); 15 | const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); 16 | const navigate = useNavigate(); 17 | 18 | const toggleMobileMenu = () => { 19 | setIsMobileMenuOpen(!isMobileMenuOpen); 20 | }; 21 | 22 | const closeMobileMenu = () => { 23 | setIsMobileMenuOpen(false); 24 | }; 25 | 26 | const handleResize = () => { 27 | if (window.innerWidth > 800) { 28 | closeMobileMenu(); 29 | } 30 | }; 31 | 32 | const handleAuthClick = async () => { 33 | if (user) { 34 | await dispatch(logout()); 35 | } 36 | closeMobileMenu(); 37 | }; 38 | 39 | const handleTestsClick = async (e: any) => { 40 | setIsMobileMenuOpen(false); 41 | if (!user) { 42 | e.preventDefault(); 43 | Swal.fire({ 44 | title: 'Hold Up', 45 | text: 'please sign in to save and manage your tests!', 46 | icon: 'warning', 47 | confirmButtonColor: '#6c6185', 48 | confirmButtonText: 'sign in', 49 | }).then((result) => { 50 | if (result.isConfirmed) { 51 | navigate('/signin'); 52 | } 53 | }); 54 | } 55 | }; 56 | 57 | return ( 58 | <> 59 | 105 | 106 | ); 107 | }; 108 | export default TestNavBar; 109 | -------------------------------------------------------------------------------- /client/src/features/authSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, createAsyncThunk, AnyAction } from '@reduxjs/toolkit'; 2 | import { loginFormSchemaType } from '../pages/Signin'; 3 | import { registerFormSchemaType } from '../pages/Signin'; 4 | import { loginUser, logoutUser, registerUser } from '../services/authService'; 5 | 6 | //@ts-ignore 7 | const user = JSON.parse(localStorage.getItem('user')); 8 | 9 | export interface User { 10 | email: string; 11 | name: string; 12 | } 13 | interface authState { 14 | user: User | null; 15 | isError: boolean; 16 | isSuccess: boolean; 17 | isLoading: boolean; 18 | message: string | unknown; 19 | } 20 | const initialState = { 21 | user: user ? user : null, 22 | isError: false, 23 | isSuccess: false, 24 | isLoading: false, 25 | message: '', 26 | }; 27 | 28 | export const login = createAsyncThunk( 29 | 'auth/login', 30 | async (userData: loginFormSchemaType, { rejectWithValue }) => { 31 | try { 32 | return await loginUser(userData); 33 | } catch (err: any) { 34 | const message = err.response?.data.message || err.toString(); 35 | return rejectWithValue(message); 36 | } 37 | } 38 | ); 39 | 40 | export const logout = createAsyncThunk( 41 | 'auth/logout', 42 | async (_, { rejectWithValue }) => { 43 | try { 44 | return await logoutUser(); 45 | } catch (err: any) { 46 | const message = err.response?.data.message || err.toString(); 47 | return rejectWithValue(message); 48 | } 49 | } 50 | ); 51 | 52 | export const register = createAsyncThunk( 53 | 'auth/register', 54 | async (userData: registerFormSchemaType, { rejectWithValue }) => { 55 | try { 56 | return await registerUser(userData); 57 | } catch (err: any) { 58 | const message = err.response?.data.message || err.toString(); 59 | return rejectWithValue(message); 60 | } 61 | } 62 | ); 63 | 64 | export const authSlice = createSlice({ 65 | name: 'auth', 66 | initialState, 67 | reducers: { 68 | reset: (state) => { 69 | state.isError = false; 70 | state.isSuccess = false; 71 | state.isLoading = false; 72 | state.message = ''; 73 | }, 74 | }, 75 | extraReducers: (builder) => { 76 | builder 77 | .addCase(login.pending, (state) => { 78 | state.isLoading = true; 79 | }) 80 | .addCase(login.rejected, (state, action) => { 81 | state.isLoading = false; 82 | state.isError = true; 83 | state.message = action.payload as string; 84 | }) 85 | .addCase(login.fulfilled, (state, action) => { 86 | state.isLoading = false; 87 | state.isSuccess = true; 88 | state.user = action.payload; 89 | }) 90 | .addCase(register.pending, (state) => { 91 | state.isLoading = true; 92 | }) 93 | .addCase(register.rejected, (state, action) => { 94 | state.isLoading = false; 95 | state.isError = true; 96 | state.message = action.payload as string; 97 | }) 98 | .addCase(register.fulfilled, (state, action) => { 99 | state.isLoading = false; 100 | state.isSuccess = true; 101 | state.user = action.payload; 102 | }) 103 | .addCase(logout.pending, (state) => { 104 | state.isLoading = true; 105 | }) 106 | .addCase(logout.rejected, (state, action) => { 107 | state.isLoading = false; 108 | state.isError = true; 109 | state.message = action.payload as string; 110 | }) 111 | .addCase(logout.fulfilled, (state: authState) => { 112 | state.isLoading = false; 113 | state.user = null; 114 | }); 115 | }, 116 | }); 117 | 118 | //type of action: PayloadAction 119 | 120 | export const { reset } = authSlice.actions; 121 | 122 | export default authSlice.reducer; 123 | -------------------------------------------------------------------------------- /client/src/features/testSlice.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; 2 | import { 3 | fetchTests, 4 | removeTest, 5 | saveTests as save, 6 | } from '../services/testService'; 7 | 8 | export interface Test { 9 | id: number; 10 | user_id: number; 11 | generated_test: string; 12 | test_type: string; 13 | } 14 | 15 | interface testState { 16 | tests: Test[]; 17 | isError: boolean; 18 | isSuccess: boolean; 19 | isLoading: boolean; 20 | message: string | unknown; 21 | } 22 | 23 | interface saveTestInput { 24 | test: string; 25 | testType: string; 26 | } 27 | 28 | const initialState: testState = { 29 | tests: [], 30 | isError: false, 31 | isSuccess: false, 32 | isLoading: false, 33 | message: '', 34 | }; 35 | 36 | export const getTests = createAsyncThunk( 37 | 'tests/getAll', 38 | async (_, { rejectWithValue }) => { 39 | try { 40 | return await fetchTests(); 41 | } catch (err: any) { 42 | const message = err.response?.data.message || err.toString(); 43 | return rejectWithValue(message); 44 | } 45 | } 46 | ); 47 | 48 | export const saveTests = createAsyncThunk( 49 | 'tests/save', 50 | async (testData: saveTestInput, { rejectWithValue }) => { 51 | try { 52 | console.log('incoming test data: ', testData); 53 | return await save(testData); 54 | } catch (err: any) { 55 | const message = 56 | err.response?.data?.message || err.message || err.toString(); 57 | return rejectWithValue(message); 58 | } 59 | } 60 | ); 61 | 62 | export const deleteTest = createAsyncThunk( 63 | 'tests/delete', 64 | async (id: string, { rejectWithValue }) => { 65 | try { 66 | return await removeTest(id); 67 | } catch (err: any) { 68 | const message = 69 | err.response?.data?.message || err.message || err.toString(); 70 | return rejectWithValue(message); 71 | } 72 | } 73 | ); 74 | 75 | export const testSlice = createSlice({ 76 | name: 'tests', 77 | initialState, 78 | reducers: { 79 | reset: (state) => { 80 | state.tests = []; 81 | state.isError = false; 82 | state.isSuccess = false; 83 | state.isLoading = false; 84 | state.message = ''; 85 | }, 86 | }, 87 | extraReducers: (builder) => { 88 | builder 89 | .addCase(getTests.pending, (state) => { 90 | state.isLoading = true; 91 | }) 92 | .addCase(getTests.rejected, (state, action) => { 93 | state.isLoading = false; 94 | state.isError = true; 95 | state.message = action.payload as string; 96 | }) 97 | .addCase(getTests.fulfilled, (state, action: any) => { 98 | state.isLoading = false; 99 | state.isSuccess = true; 100 | state.tests = action.payload; 101 | }) 102 | .addCase(saveTests.pending, (state) => { 103 | state.isLoading = true; 104 | }) 105 | .addCase(saveTests.rejected, (state, action) => { 106 | state.isLoading = false; 107 | state.isError = true; 108 | state.message = action.payload as string; 109 | }) 110 | .addCase(saveTests.fulfilled, (state, action) => { 111 | state.tests.push(action.payload); 112 | }) 113 | .addCase(deleteTest.pending, (state) => { 114 | state.isLoading = true; 115 | }) 116 | .addCase(deleteTest.rejected, (state, action) => { 117 | state.isLoading = false; 118 | state.isError = true; 119 | state.message = action.payload as string; 120 | }) 121 | .addCase(deleteTest.fulfilled, (state, action) => { 122 | state.isLoading = false; 123 | state.isSuccess = true; 124 | state.tests = state.tests.filter( 125 | (el: Test) => el.id !== action.payload.id 126 | ); 127 | }); 128 | }, 129 | }); 130 | 131 | export const { reset } = testSlice.actions; 132 | 133 | export default testSlice.reducer; 134 | -------------------------------------------------------------------------------- /client/src/images/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Scribe-for-GraphQL/4102d5587fe4862351eb912bc257e95582c6e21d/client/src/images/background.jpg -------------------------------------------------------------------------------- /client/src/images/generateTest.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Scribe-for-GraphQL/4102d5587fe4862351eb912bc257e95582c6e21d/client/src/images/generateTest.gif -------------------------------------------------------------------------------- /client/src/images/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Scribe-for-GraphQL/4102d5587fe4862351eb912bc257e95582c6e21d/client/src/images/github.png -------------------------------------------------------------------------------- /client/src/images/jake.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Scribe-for-GraphQL/4102d5587fe4862351eb912bc257e95582c6e21d/client/src/images/jake.jpg -------------------------------------------------------------------------------- /client/src/images/jason.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Scribe-for-GraphQL/4102d5587fe4862351eb912bc257e95582c6e21d/client/src/images/jason.jpg -------------------------------------------------------------------------------- /client/src/images/linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Scribe-for-GraphQL/4102d5587fe4862351eb912bc257e95582c6e21d/client/src/images/linkedin.png -------------------------------------------------------------------------------- /client/src/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Scribe-for-GraphQL/4102d5587fe4862351eb912bc257e95582c6e21d/client/src/images/logo.png -------------------------------------------------------------------------------- /client/src/images/main-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Scribe-for-GraphQL/4102d5587fe4862351eb912bc257e95582c6e21d/client/src/images/main-logo.png -------------------------------------------------------------------------------- /client/src/images/mason.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Scribe-for-GraphQL/4102d5587fe4862351eb912bc257e95582c6e21d/client/src/images/mason.jpg -------------------------------------------------------------------------------- /client/src/images/pierce.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Scribe-for-GraphQL/4102d5587fe4862351eb912bc257e95582c6e21d/client/src/images/pierce.jpg -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | /* STYLE RESET */ 2 | /* // https://piccalil.li/blog/a-modern-css-reset */ 3 | /* Box sizing rules */ 4 | *, 5 | *::before, 6 | *::after { 7 | box-sizing: border-box; 8 | } 9 | 10 | /* Remove default margin and padding */ 11 | * { 12 | margin: 0; 13 | padding: 0; 14 | font: inherit; 15 | } 16 | 17 | /* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */ 18 | ul[role='list'], 19 | ol[role='list'] { 20 | list-style: none; 21 | } 22 | 23 | /* Set core root defaults */ 24 | html:focus-within { 25 | scroll-behavior: smooth; 26 | } 27 | 28 | html, 29 | body { 30 | height: 100%; 31 | } 32 | 33 | /* Set core body defaults */ 34 | body { 35 | /* min-height: 100vh; */ 36 | text-rendering: optimizeSpeed; 37 | line-height: 1.5; 38 | } 39 | 40 | /* A elements that don't have a class get default styles */ 41 | a:not([class]) { 42 | text-decoration-skip-ink: auto; 43 | } 44 | 45 | /* Make images easier to work with */ 46 | img, 47 | picture, 48 | svg { 49 | max-width: 100%; 50 | display: block; 51 | } 52 | 53 | /* Inherit fonts for inputs and buttons 54 | input, 55 | button, 56 | textarea, 57 | select { 58 | font: inherit; 59 | } */ 60 | 61 | /* Remove all animations, transitions and smooth scroll for people that prefer not to see them */ 62 | @media (prefers-reduced-motion: reduce) { 63 | html:focus-within { 64 | scroll-behavior: auto; 65 | } 66 | 67 | *, 68 | *::before, 69 | *::after { 70 | animation-duration: 0.01ms !important; 71 | animation-iteration-count: 1 !important; 72 | transition-duration: 0.01ms !important; 73 | scroll-behavior: auto !important; 74 | } 75 | } 76 | /* end of reset 77 | #userInput { 78 | font-family: 'Source Code Pro', monospace; 79 | } 80 | 81 | #testOutput { 82 | font-family: 'Source Code Pro', monospace; 83 | } 84 | 85 | .swal2-container { 86 | z-index: 20000 !important; 87 | } */ 88 | -------------------------------------------------------------------------------- /client/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | import './index.css'; 5 | import { Provider } from 'react-redux'; 6 | import { store } from './app/store'; 7 | 8 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /client/src/pages/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props = {}; 4 | 5 | const NotFound = (props: Props) => { 6 | return
NotFound
; 7 | }; 8 | 9 | export default NotFound; 10 | -------------------------------------------------------------------------------- /client/src/pages/SavedTests.tsx: -------------------------------------------------------------------------------- 1 | import { Editor } from '@monaco-editor/react'; 2 | import { Box, Typography } from '@mui/material'; 3 | import React, { useEffect, useRef, useState } from 'react'; 4 | import { useDispatch, useSelector } from 'react-redux'; 5 | import { AppDispatch, RootState } from '../app/store'; 6 | import TestNavBar from '../components/TestNavBar'; 7 | import { deleteTest, getTests, reset, Test } from '../features/testSlice'; 8 | import ContentCopyIcon from '@mui/icons-material/ContentCopy'; 9 | import Swal from 'sweetalert2'; 10 | 11 | type Props = {}; 12 | 13 | const SavedTests = (props: Props) => { 14 | const { tests } = useSelector((state: RootState) => state.tests); 15 | const dispatch = useDispatch(); 16 | const [testType, setTestType] = useState('all-tests'); 17 | const [content, setContent] = useState(''); 18 | const [editorWidth, setEditorWidth] = useState('100%'); 19 | const { user } = useSelector((state: RootState) => state.auth); 20 | console.log('tests: ', tests); 21 | 22 | const handleDelete = (id: any) => { 23 | dispatch(deleteTest(id)); 24 | }; 25 | 26 | const editorRef = useRef(null); 27 | 28 | useEffect(() => { 29 | const handleResize = () => { 30 | if (window.innerWidth > 900) { 31 | setEditorWidth('45%'); 32 | } else if (window.innerWidth > 600) { 33 | setEditorWidth('70%'); 34 | } else { 35 | setEditorWidth('100%'); 36 | } 37 | }; 38 | 39 | handleResize(); 40 | window.addEventListener('resize', handleResize); 41 | 42 | return () => { 43 | window.removeEventListener('resize', handleResize); 44 | }; 45 | }, []); 46 | 47 | useEffect(() => { 48 | dispatch(getTests()); 49 | 50 | return () => { 51 | dispatch(reset()); 52 | }; 53 | }, []); 54 | 55 | const list = 56 | testType === 'all-tests' 57 | ? tests.map((test, idx) => ( 58 |
  • 59 | 65 | 68 |
  • 69 | )) 70 | : tests 71 | .filter((test) => test.test_type === testType) 72 | .map((test, idx) => ( 73 |
  • 74 | 80 | 83 |
  • 84 | )); 85 | 86 | const handleCopy = () => { 87 | navigator.clipboard.writeText(content); 88 | 89 | const Toast = Swal.mixin({ 90 | toast: true, 91 | position: 'top-end', 92 | showConfirmButton: false, 93 | timer: 1500, 94 | timerProgressBar: true, 95 | didOpen: (toast) => { 96 | toast.addEventListener('mouseenter', Swal.stopTimer); 97 | toast.addEventListener('mouseleave', Swal.resumeTimer); 98 | }, 99 | }); 100 | 101 | Toast.fire({ 102 | icon: 'success', 103 | title: 'Tests copied to clipboard', 104 | }); 105 | }; 106 | 107 | const handleEditorDidMount = (editor: any, monaco: any) => { 108 | editorRef.current = editor; 109 | monaco.editor.defineTheme('my-theme', { 110 | base: 'vs-dark', 111 | inherit: true, 112 | rules: [], 113 | colors: { 114 | 'editor.background': '#49405e', 115 | 'editor.lineHighlightBorder': '#44475A', 116 | 'editorCursor.foreground': '#ffffff', 117 | }, 118 | }); 119 | monaco.editor.setTheme('my-theme'); 120 | }; 121 | 122 | return ( 123 | <> 124 | 125 |
    126 |
    127 |
    128 | 137 |
    138 |
      {list}
    139 |
    140 | 141 |
    142 |
    143 | {' '} 144 | 153 | Test Display 154 | 155 | 162 |
    163 | setContent(editorRef.current.getValue())} 169 | value={content} 170 | //@ts-ignore 171 | options={{ 172 | wordWrap: 'on', 173 | minimap: { 174 | enabled: false, 175 | }, 176 | }} 177 | /> 178 |
    179 |
    180 | 181 | ); 182 | }; 183 | 184 | export default SavedTests; 185 | -------------------------------------------------------------------------------- /client/src/pages/Signin.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import * as Components from '../styles/SliderStyle'; 3 | import { SubmitHandler, useForm } from 'react-hook-form'; 4 | import { z } from 'zod'; 5 | import { useSelector, useDispatch } from 'react-redux'; 6 | import { useNavigate } from 'react-router'; 7 | import { RootState, AppDispatch } from '../app/store'; 8 | import { login, register, reset } from '../features/authSlice'; 9 | import { zodResolver } from '@hookform/resolvers/zod'; 10 | import Swal from 'sweetalert2'; 11 | 12 | const loginFormSchema = z.object({ 13 | email: z 14 | .string() 15 | .min(1, 'email required') 16 | .email('Please enter a valid email'), 17 | password: z.string().min(1, 'Please enter your password'), 18 | }); 19 | export type loginFormSchemaType = z.infer; 20 | 21 | const registerFormSchema = z 22 | .object({ 23 | name: z.string().min(1, 'name required'), 24 | email: z 25 | .string() 26 | .min(1, 'Email required') 27 | .email('Please enter a valid email'), 28 | password: z.string().min(1, 'Password required'), 29 | confirmPassword: z.string().min(1, 'Please confirm password'), 30 | }) 31 | .refine((data) => data.password === data.confirmPassword, { 32 | path: ['confirmPassword'], 33 | message: 'Passwords do not match', 34 | }); 35 | 36 | export type registerFormSchemaType = z.infer; 37 | 38 | const signinAndRegister = () => { 39 | const { user, isLoading, isError, isSuccess, message } = useSelector( 40 | (state: RootState) => state.auth 41 | ); 42 | const dispatch = useDispatch(); 43 | const navigate = useNavigate(); 44 | 45 | useEffect(() => { 46 | if (isError) { 47 | const Toast = Swal.mixin({ 48 | toast: true, 49 | position: 'top', 50 | showConfirmButton: false, 51 | timer: 2000, 52 | timerProgressBar: true, 53 | didOpen: (toast) => { 54 | toast.addEventListener('mouseenter', Swal.stopTimer); 55 | toast.addEventListener('mouseleave', Swal.resumeTimer); 56 | }, 57 | }); 58 | //@ts-ignore 59 | Toast.fire({ 60 | icon: 'error', 61 | title: message, 62 | }); 63 | } 64 | 65 | if (user || isSuccess) { 66 | navigate('/test'); 67 | } 68 | 69 | dispatch(reset()); 70 | }, [isError, isSuccess, message, user, dispatch, navigate]); 71 | 72 | const [signIn, toggle] = useState(true); 73 | const [hidePassword, setHidePassword] = useState(true); 74 | 75 | const { 76 | register: registerRegister, 77 | handleSubmit: registerSubmit, 78 | formState: { errors: registerErrors, isSubmitting: registerSubmitting }, 79 | } = useForm({ 80 | resolver: zodResolver(registerFormSchema), 81 | }); 82 | 83 | const { 84 | register: loginRegister, 85 | handleSubmit: loginSubmit, 86 | formState: { errors: loginErrors, isSubmitting: loginSubmitting }, 87 | } = useForm({ 88 | resolver: zodResolver(loginFormSchema), 89 | }); 90 | 91 | const handleRegister: SubmitHandler = async ( 92 | formData: registerFormSchemaType 93 | ) => { 94 | console.log('REGISTER CLICKED'); 95 | dispatch(register(formData)); 96 | }; 97 | 98 | const handleLogin: SubmitHandler = async ( 99 | formData: loginFormSchemaType 100 | ) => { 101 | dispatch(login(formData)); 102 | }; 103 | 104 | return ( 105 |
    106 |
    107 | 108 | 109 | 113 | Create Account 114 | 120 |

    {registerErrors.name?.message}

    121 | 127 |

    {registerErrors.email?.message}

    128 | 134 |

    {registerErrors.password?.message}

    135 | 141 |

    {registerErrors.password?.message}

    142 | Sign Up 143 |
    144 |
    145 | 146 | 147 | 148 | Sign in 149 | 155 |

    {loginErrors.email?.message}

    156 | 162 |

    {loginErrors.password?.message}

    163 | 164 | Forgot your password? 165 | 166 | Sign In 167 |
    168 |
    169 | 170 | 171 | 172 | 173 | Welcome Back! 174 | 175 | Have an account already? Sign in to continue! 176 | 177 | toggle(true)}> 178 | Sign In 179 | 180 | 181 | 182 | 183 | Hello, Friend! 184 | 185 | Create an account and start generating tests! 186 | 187 | toggle(false)}> 188 | Sign Up 189 | 190 | 191 | 192 | 193 |
    194 |
    195 |
    196 | ); 197 | }; 198 | 199 | export default signinAndRegister; 200 | -------------------------------------------------------------------------------- /client/src/pages/Test.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react'; 2 | import Box from '@mui/material/Box'; 3 | import Button from '@mui/material/Button'; 4 | import ContentCopyIcon from '@mui/icons-material/ContentCopy'; 5 | import { generateTypeTest, generateUnitTest } from '../services/testService'; 6 | import Swal from 'sweetalert2'; 7 | import { Editor } from '@monaco-editor/react'; 8 | import TestNavBar from '../components/TestNavBar'; 9 | import { useDispatch, useSelector } from 'react-redux'; 10 | import { RootState, AppDispatch } from '../app/store'; 11 | import { Typography } from '@mui/material'; 12 | import { saveTests } from '../features/testSlice'; 13 | import { useNavigate } from 'react-router'; 14 | 15 | type Props = {}; 16 | 17 | // type Resolvers = { 18 | // queryIntTests?: string; 19 | // mutationIntTests?: string; 20 | // resolverUnitTests?: string; 21 | // } 22 | 23 | const Test = (props: Props) => { 24 | const [outputTest, setOutputTest] = useState(''); 25 | const [editorWidth, setEditorWidth] = useState('100%'); 26 | const [selectedOption, setSelectedOption] = useState('type-tests'); 27 | // const [resolversTests, setResolversTests] = useState(null); 28 | 29 | const dispatch = useDispatch(); 30 | const { user, isLoading, isError, isSuccess, message } = useSelector( 31 | (state: RootState) => state.auth 32 | ); 33 | const navigate = useNavigate(); 34 | const editorRef = useRef(null); 35 | const outputRef = useRef(null); 36 | 37 | /////////// 38 | useEffect(() => { 39 | const handleResize = () => { 40 | if (window.innerWidth > 900) { 41 | setEditorWidth('45%'); 42 | } else if (window.innerWidth > 600) { 43 | setEditorWidth('70%'); 44 | } else { 45 | setEditorWidth('100%'); 46 | } 47 | }; 48 | 49 | handleResize(); 50 | window.addEventListener('resize', handleResize); 51 | 52 | return () => { 53 | window.removeEventListener('resize', handleResize); 54 | }; 55 | }, []); 56 | 57 | const generateTest = async (input: string) => { 58 | try { 59 | console.log('clicked generateTest'); 60 | // console.log('INPUT: ', input); 61 | //@ts-ignore 62 | console.log('unit', await generateUnitTest(input)); 63 | 64 | let test; 65 | let response; 66 | switch (selectedOption) { 67 | case 'type-tests': 68 | test = await generateTypeTest(input); 69 | break; 70 | case 'unit-tests': 71 | // if (resolversTests === null) { 72 | // let resolvers = await generateUnitTest(input); 73 | // setResolversTests(resolvers) 74 | // } 75 | // console.log(resolversTests); 76 | response = await generateUnitTest(input); 77 | test = response.resolverUnitTests; //make unit test function 78 | break; 79 | case 'query-mock-integration-tests': 80 | // if (resolversTests === null) { 81 | // let resolvers = await generateUnitTest(input); 82 | // setResolversTests(resolvers) 83 | // } 84 | // console.log(resolversTests) 85 | response = await generateUnitTest(input); 86 | test = response.queryIntTests; //make integration test func 87 | break; 88 | case 'mutation-mock-integration-tests': 89 | // if (resolversTests === null) { 90 | // let resolvers = await generateUnitTest(input); 91 | // setResolversTests(resolvers) 92 | // } 93 | response = await generateUnitTest(input); 94 | test = response.mutationIntTests; 95 | break; 96 | default: 97 | console.log('default case hit'); 98 | test = await generateTypeTest(input); 99 | } 100 | // const test = await generateTypeTest(input); 101 | console.log('test test test ', test); 102 | if (test.message) { 103 | const Toast = Swal.mixin({ 104 | toast: true, 105 | position: 'top', 106 | showConfirmButton: false, 107 | timer: 2000, 108 | timerProgressBar: true, 109 | didOpen: (toast) => { 110 | toast.addEventListener('mouseenter', Swal.stopTimer); 111 | toast.addEventListener('mouseleave', Swal.resumeTimer); 112 | }, 113 | }); 114 | 115 | Toast.fire({ 116 | icon: 'error', 117 | title: test.message, 118 | }); 119 | 120 | return; 121 | } 122 | setOutputTest(test); 123 | } catch (err: any) { 124 | const message = err.response?.data.message || err.toString(); 125 | window.alert(message); 126 | } 127 | }; 128 | 129 | const handleCopy = () => { 130 | navigator.clipboard.writeText(outputTest); 131 | 132 | const Toast = Swal.mixin({ 133 | toast: true, 134 | position: 'top-end', 135 | showConfirmButton: false, 136 | timer: 1500, 137 | timerProgressBar: true, 138 | didOpen: (toast) => { 139 | toast.addEventListener('mouseenter', Swal.stopTimer); 140 | toast.addEventListener('mouseleave', Swal.resumeTimer); 141 | }, 142 | }); 143 | 144 | Toast.fire({ 145 | icon: 'success', 146 | title: 'Tests copied to clipboard', 147 | }); 148 | }; 149 | 150 | const saveTest = () => { 151 | const Toast = Swal.mixin({ 152 | toast: true, 153 | position: 'top', 154 | showConfirmButton: false, 155 | timer: 1500, 156 | timerProgressBar: true, 157 | didOpen: (toast) => { 158 | toast.addEventListener('mouseenter', Swal.stopTimer); 159 | toast.addEventListener('mouseleave', Swal.resumeTimer); 160 | }, 161 | }); 162 | //@ts-ignore 163 | if (!outputRef.current.getValue()) { 164 | Toast.fire({ 165 | icon: 'error', 166 | title: 'Unable to save empty test', 167 | }); 168 | } else if (!user) { 169 | Swal.fire({ 170 | title: 'Hold Up', 171 | text: 'please sign in to save and manage your tests!', 172 | icon: 'warning', 173 | confirmButtonColor: '#6c6185', 174 | confirmButtonText: 'sign in', 175 | }).then((result) => { 176 | if (result.isConfirmed) { 177 | navigate('/signin'); 178 | } 179 | }); 180 | } else { 181 | console.log('selected option: ', selectedOption); 182 | dispatch( 183 | saveTests({ 184 | test: outputTest, 185 | testType: selectedOption, 186 | }) 187 | ); 188 | const Toast = Swal.mixin({ 189 | toast: true, 190 | position: 'top', 191 | showConfirmButton: false, 192 | timer: 1500, 193 | timerProgressBar: true, 194 | didOpen: (toast) => { 195 | toast.addEventListener('mouseenter', Swal.stopTimer); 196 | toast.addEventListener('mouseleave', Swal.resumeTimer); 197 | }, 198 | }); 199 | 200 | Toast.fire({ 201 | icon: 'success', 202 | title: 'Tests Saved', 203 | }); 204 | } 205 | }; 206 | 207 | const handleEditorDidMountLeft = (editor: any, monaco: any) => { 208 | editorRef.current = editor; 209 | monaco.editor.defineTheme('my-theme', { 210 | base: 'vs-dark', 211 | inherit: true, 212 | rules: [], 213 | colors: { 214 | 'editor.background': '#49405e', 215 | 'editor.lineHighlightBorder': '#44475A', 216 | 'editorCursor.foreground': '#ffffff', 217 | }, 218 | }); 219 | monaco.editor.setTheme('my-theme'); 220 | }; 221 | 222 | const handleEditorDidMountRight = (editor: any) => { 223 | outputRef.current = editor; 224 | }; 225 | 226 | return ( 227 | <> 228 | 229 | 900 || window.innerWidth > 600 240 | ? 'row' 241 | : 'column', 242 | overflow: 'scroll', 243 | }} 244 | > 245 |
    246 | 249 | Input 250 | 251 | 263 |
    264 |
    265 |
    266 | 290 | 297 |
    298 | setOutputTest(outputRef.current.getValue())} 306 | options={{ 307 | wordWrap: 'on', 308 | minimap: { 309 | enabled: false, 310 | }, 311 | }} 312 | /> 313 |
    314 |
    315 | 321 | {/* 328 | */} 331 | 332 | 339 | 342 | 343 | 344 | ); 345 | }; 346 | 347 | export default Test; 348 | -------------------------------------------------------------------------------- /client/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Typography } from '@mui/material'; 2 | import React, { useEffect, useRef, useState } from 'react'; 3 | import NavBar from '../components/NavBar'; 4 | import Editor from '@monaco-editor/react'; 5 | import generateTestGIF from '../images/generateTest.gif'; 6 | import { Link } from 'react-router-dom'; 7 | import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; 8 | import ContentCopyIcon from '@mui/icons-material/ContentCopy'; 9 | import Swal from 'sweetalert2'; 10 | import Footer from '../components/Footer'; 11 | 12 | // const code = 'const a = 0;'; 13 | 14 | type Props = {}; 15 | 16 | const Homepage = (props: Props) => { 17 | const apollo = 'npm install --save-dev @apollo/server apollo-server-testing'; 18 | const jestPackage = 'npm install --save-dev jest @jest/globals'; 19 | const jestViaBabel = 20 | 'npm install --save-dev jest @jest/globals @babel/preset-typescript'; 21 | const jestViaTS = 'npm install --save-dev jest @jest/globals ts-jest'; 22 | 23 | const [boilerPlate, setBoilerPlate] = 24 | useState(`import { ApolloServer } from "apollo-server" 25 | import { createTestClient } from 'apollo-server-testing' 26 | import { typeDefs } from '/* path to schema */' 27 | import { resolvers } from '/* path to resolvers */' 28 | 29 | 30 | export const createTestServer = (context) => { 31 | const server = new ApolloServer({ 32 | typeDefs, 33 | resolvers, 34 | mockEntireSchema: false, // -> since we are passing in a schema and resolvers, we need this to be false 35 | mocks: true, // -> mocks random data according to type definitions in schema 36 | context: () => context, 37 | }); 38 | 39 | 40 | return createTestClient(server); 41 | };`); 42 | const editorRef = useRef(null); 43 | 44 | const handleCopy = (value: string) => { 45 | navigator.clipboard.writeText(value); 46 | 47 | const Toast = Swal.mixin({ 48 | toast: true, 49 | position: 'top-end', 50 | showConfirmButton: false, 51 | timer: 1500, 52 | timerProgressBar: true, 53 | didOpen: (toast) => { 54 | toast.addEventListener('mouseenter', Swal.stopTimer); 55 | toast.addEventListener('mouseleave', Swal.resumeTimer); 56 | }, 57 | }); 58 | 59 | Toast.fire({ 60 | icon: 'success', 61 | title: 'copied to clipboard', 62 | }); 63 | }; 64 | 65 | const handleEditorDidMount = (editor: any, monaco: any) => { 66 | editorRef.current = editor; 67 | monaco.editor.defineTheme('my-theme', { 68 | base: 'vs-dark', 69 | inherit: true, 70 | rules: [], 71 | colors: { 72 | 'editor.background': '#49405e', 73 | 'editor.lineHighlightBorder': '#44475A', 74 | 'editorCursor.foreground': '#ffffff', 75 | }, 76 | }); 77 | monaco.editor.setTheme('my-theme'); 78 | }; 79 | 80 | return ( 81 | <> 82 | 83 |
    91 |
    92 |
    93 |

    Test writing made simple.

    94 |

    95 | Boost development speed and confidence with automated Jest 96 | type-test generation, schema validation, and smart resolver mock 97 | intergration setups. 98 |

    99 |
    100 |
    101 | 102 |
    103 |
    104 |
    105 |

    Get Started

    106 |
    107 | 108 |
    109 |
    110 |
    111 |
    112 |

    Apollo Packages:

    113 | 119 |

    120 | *Choose from relevant Jest Packages 121 |

    122 |

    JS Jest Packages:

    123 | 129 |

    TS Jest Packages via Babel:

    130 | 136 |

    TS Jest Packages via TS-Jest:

    137 | 143 |
    144 |
    145 |

    Install

    146 |

    Required

    147 |

    Packages

    148 |
    149 |
    150 |
    151 |
    152 |

    Test Server Setup

    153 |

    154 | 1. Add a test script to your ‘package.json’ file: E.g. “test”: 155 | “jest --watchAll” 156 |

    157 |

    2. Copy test server boilerplate

    158 | 159 |

    Requirements

    160 |
      161 |
    • 162 | * For type-tests on the basis of schemas, we are expecting a 163 | ‘typeDefs’ declaration, which consists in a valid Schema 164 | Definition Language (SDL) string. 165 |
    • 166 |
    • 167 | * For resolvers, we are expecting a ‘resolvers’ declaration, 168 | which consists in a map of functions that populate data for 169 | individual schema fields. 170 |
    • 171 |
    172 | window.scrollTo({ top: 0 })} 174 | id='home-to-test' 175 | to='/test' 176 | > 177 | Now we're ready to generate some tests! 178 | 179 |
    180 |
    181 |
    182 |
    183 | {' '} 184 | 192 | Test Server Boilerplate 193 | 194 | 201 |
    202 | setBoilerPlate(editorRef.current.getValue())} 210 | options={{ 211 | wordWrap: 'on', 212 | minimap: { 213 | enabled: false, 214 | }, 215 | }} 216 | /> 217 |
    218 |
    219 |
    220 |
    221 |
    222 | 223 | ); 224 | }; 225 | 226 | export default Homepage; 227 | -------------------------------------------------------------------------------- /client/src/services/authService.ts: -------------------------------------------------------------------------------- 1 | import { loginFormSchemaType } from '../pages/Signin'; 2 | import { registerFormSchemaType } from '../pages/Signin'; 3 | import axios from 'axios'; 4 | import { API_URL } from '../utils/constants'; 5 | import { User } from '../features/authSlice'; 6 | 7 | export const registerUser = async (userData: registerFormSchemaType) => { 8 | const { data } = await axios.post(API_URL + 'auth/register', userData, { 9 | withCredentials: true, 10 | }); 11 | 12 | if (data) { 13 | localStorage.setItem('user', JSON.stringify(data)); 14 | } 15 | 16 | return data; 17 | }; 18 | 19 | export const loginUser = async ( 20 | userData: loginFormSchemaType 21 | ): Promise => { 22 | const { data } = await axios.post(API_URL + 'auth/login', userData, { 23 | withCredentials: true, 24 | }); 25 | if (data) { 26 | localStorage.setItem('user', JSON.stringify(data)); 27 | } 28 | return data; 29 | }; 30 | 31 | export const logoutUser = async () => { 32 | localStorage.removeItem('user'); 33 | const { data } = await axios.post(API_URL + 'auth/logout', null, { 34 | withCredentials: true, 35 | }); 36 | return data; 37 | }; 38 | -------------------------------------------------------------------------------- /client/src/services/testService.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { API_URL } from '../utils/constants'; 3 | 4 | export const generateTypeTest = async (userInput: string) => { 5 | console.log('make post request to /typeTest'); 6 | console.log('API', API_URL); 7 | console.log(API_URL + 'typeTest'); 8 | const { data } = await axios.post( 9 | API_URL + 'typeTest', 10 | { schema: userInput }, 11 | { withCredentials: true } 12 | ); 13 | return data; 14 | }; 15 | 16 | export const generateUnitTest = async (userInput: string) => { 17 | const { data } = await axios.post( 18 | API_URL + 'resolverTest', 19 | { resolvers: userInput }, 20 | { withCredentials: true } 21 | ); 22 | return data; 23 | }; 24 | 25 | export const fetchTests = async () => { 26 | const { data } = await axios.get(API_URL + 'users/tests', { 27 | withCredentials: true, 28 | }); 29 | return data; 30 | }; 31 | 32 | export const saveTests = async (testData: any) => { 33 | const { data } = await axios.post( 34 | API_URL + 'users/tests', 35 | { 36 | test: testData.test, 37 | testType: testData.testType, 38 | }, 39 | { 40 | withCredentials: true, 41 | } 42 | ); 43 | 44 | return data; 45 | }; 46 | 47 | export const removeTest = async (id: string) => { 48 | const { data } = await axios.delete(API_URL + `users/tests/${id}`, { 49 | withCredentials: true, 50 | }); 51 | return data; 52 | }; 53 | -------------------------------------------------------------------------------- /client/src/styles/SliderStyle.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | interface SignInOutContainerProps { 4 | signinIn: boolean; 5 | } 6 | interface OverlayContainerProps { 7 | signinIn: boolean; 8 | } 9 | 10 | interface LeftRightOverlayPanelProps { 11 | signinIn: boolean; 12 | } 13 | 14 | export const Container = styled.div` 15 | background-color: #fffefe; 16 | border-radius: 10px; 17 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 18 | position: relative; 19 | overflow: hidden; 20 | width: 900px; 21 | max-width: 100%; 22 | min-height: 500px; 23 | `; 24 | 25 | export const SignUpContainer = styled.div` 26 | position: absolute; 27 | top: 0; 28 | height: 100%; 29 | transition: all 0.6s ease-in-out; 30 | left: 0; 31 | width: 50%; 32 | opacity: 0; 33 | z-index: 1; 34 | ${(props: any) => 35 | props.signinIn !== true 36 | ? ` 37 | transform: translateX(100%); 38 | opacity: 1; 39 | z-index: 5; 40 | ` 41 | : null} 42 | `; 43 | 44 | export const SignInContainer = styled.div` 45 | position: absolute; 46 | top: 0; 47 | height: 100%; 48 | transition: all 0.6s ease-in-out; 49 | left: 0; 50 | width: 50%; 51 | z-index: 2; 52 | ${(props: any) => 53 | props.signinIn !== true ? `transform: translateX(100%);` : null} 54 | `; 55 | 56 | export const Form = styled.form` 57 | background-color: #fffcfd; 58 | display: flex; 59 | align-items: center; 60 | justify-content: center; 61 | flex-direction: column; 62 | padding: 0 50px; 63 | height: 100%; 64 | text-align: center; 65 | `; 66 | 67 | export const Title = styled.h1` 68 | font-weight: bold; 69 | margin: 0; 70 | `; 71 | 72 | export const Input = styled.input` 73 | background-color: #eee; 74 | border: none; 75 | padding: 12px 15px; 76 | margin: 8px 0; 77 | width: 100%; 78 | `; 79 | 80 | export const Button = styled.button` 81 | border-radius: 20px; 82 | border: 1px solid #ae8ea7; 83 | background-color: #ae8ea7; 84 | color: #ffffff; 85 | font-size: 12px; 86 | font-weight: bold; 87 | margin-top: 13px; 88 | padding: 12px 45px; 89 | letter-spacing: 1px; 90 | text-transform: uppercase; 91 | transition: transform 80ms ease-in; 92 | &:active { 93 | transform: scale(0.95); 94 | } 95 | &:focus { 96 | outline: none; 97 | } 98 | `; 99 | export const GhostButton = styled(Button)` 100 | background-color: transparent; 101 | border-color: #ffffff; 102 | `; 103 | 104 | export const Anchor = styled.a` 105 | color: #333; 106 | font-size: 14px; 107 | text-decoration: none; 108 | margin: 15px 0; 109 | `; 110 | export const OverlayContainer = styled.div` 111 | position: absolute; 112 | top: 0; 113 | left: 50%; 114 | width: 50%; 115 | height: 100%; 116 | overflow: hidden; 117 | transition: transform 0.6s ease-in-out; 118 | z-index: 100; 119 | ${(props: any) => 120 | props.signinIn !== true ? `transform: translateX(-100%);` : null} 121 | `; 122 | 123 | export const Overlay = styled.div` 124 | background: #6c6185; 125 | background: -webkit-linear-gradient(to right, #ffcad4, #6c6185); 126 | background: linear-gradient(to right, #ffcad4, #6c6185); 127 | background-repeat: no-repeat; 128 | background-size: cover; 129 | background-position: 0 0; 130 | color: #ffffff; 131 | position: relative; 132 | left: -100%; 133 | height: 100%; 134 | width: 200%; 135 | transform: translateX(0); 136 | transition: transform 0.6s ease-in-out; 137 | ${(props: any) => 138 | props.signinIn !== true ? `transform: translateX(50%);` : null} 139 | `; 140 | 141 | export const OverlayPanel = styled.div` 142 | position: absolute; 143 | display: flex; 144 | align-items: center; 145 | justify-content: center; 146 | flex-direction: column; 147 | padding: 0 40px; 148 | text-align: center; 149 | top: 0; 150 | height: 100%; 151 | width: 50%; 152 | transform: translateX(0); 153 | transition: transform 0.6s ease-in-out; 154 | `; 155 | 156 | export const LeftOverlayPanel = styled( 157 | OverlayPanel 158 | )` 159 | transform: translateX(-20%); 160 | ${(props: any) => 161 | props.signinIn !== true ? `transform: translateX(0);` : null} 162 | `; 163 | 164 | export const RightOverlayPanel = styled( 165 | OverlayPanel 166 | )` 167 | right: 0; 168 | transform: translateX(0); 169 | ${(props: any) => 170 | props.signinIn !== true ? `transform: translateX(20%);` : null} 171 | `; 172 | 173 | export const Paragraph = styled.p` 174 | font-size: 14px; 175 | font-weight: 100; 176 | line-height: 20px; 177 | letter-spacing: 0.5px; 178 | margin: 20px 0 30px; 179 | `; 180 | -------------------------------------------------------------------------------- /client/src/styles/homePage.scss: -------------------------------------------------------------------------------- 1 | .landing-one { 2 | margin-top: 10rem; 3 | margin-left: 3rem; 4 | margin-right: 3rem; 5 | margin-bottom: 1rem; 6 | background: linear-gradient( 7 | 275deg, 8 | rgba(255, 255, 255, 0) 40%, 9 | rgb(255, 255, 255) 100% 10 | ); 11 | border-radius: 15px; 12 | display: flex; 13 | justify-content: space-between; 14 | padding: 3rem; 15 | max-width: 100rem; 16 | 17 | #landing-text-container { 18 | width: 500px; 19 | height: 500px; 20 | min-width: 500px; 21 | h1 { 22 | font-size: 5.5rem; 23 | font-weight: 400; 24 | background: linear-gradient(45deg, #6c6185 10%, #ffcad4 100%); 25 | -webkit-background-clip: text; 26 | -webkit-text-fill-color: transparent; 27 | } 28 | 29 | p { 30 | color: rgba(107, 107, 107, 0.688); 31 | font-size: 1.2rem; 32 | } 33 | } 34 | 35 | img { 36 | margin: 1rem; 37 | border-radius: 15px; 38 | border: white solid 2px; 39 | width: 900px; 40 | box-shadow: 10px 10px 29px 0px rgba(0, 0, 0, 0.41); 41 | -webkit-box-shadow: 10px 10px 29px 0px rgba(0, 0, 0, 0.41); 42 | -moz-box-shadow: 10px 10px 29px 0px rgba(0, 0, 0, 0.41); 43 | } 44 | 45 | #instruction-text-container { 46 | display: flex; 47 | flex-direction: column; 48 | width: 700px; 49 | height: 500px; 50 | 51 | ul { 52 | align-self: flex-start; 53 | list-style-type: none; 54 | font-size: larger; 55 | color: #6c6185; 56 | 57 | li { 58 | display: flex; 59 | align-items: center; 60 | } 61 | } 62 | h1 { 63 | font-size: 5rem; 64 | font-weight: 400; 65 | background: linear-gradient(45deg, #6c6185 10%, #ffcad4 100%); 66 | -webkit-background-clip: text; 67 | -webkit-text-fill-color: transparent; 68 | } 69 | h2 { 70 | color: #6c6185; 71 | font-size: larger; 72 | } 73 | } 74 | } 75 | 76 | #direct-sign { 77 | display: flex; 78 | flex-direction: column; 79 | justify-content: center; 80 | align-items: center; 81 | margin-top: 2rem; 82 | 83 | p { 84 | font-size: 2.5rem; 85 | font-weight: 400; 86 | background: linear-gradient(0deg, #6c6185 25%, #ffcad4 100%); 87 | -webkit-background-clip: text; 88 | -webkit-text-fill-color: transparent; 89 | } 90 | .arrow { 91 | color: #6c6185; 92 | } 93 | .bounce { 94 | -moz-animation: bounce 2s infinite; 95 | -webkit-animation: bounce 2s infinite; 96 | animation: bounce 2s infinite; 97 | } 98 | 99 | @keyframes bounce { 100 | 0%, 101 | 20%, 102 | 50%, 103 | 80%, 104 | 100% { 105 | transform: translateY(0); 106 | } 107 | 40% { 108 | transform: translateY(-30px); 109 | } 110 | 60% { 111 | transform: translateY(-15px); 112 | } 113 | } 114 | } 115 | 116 | .landing-two { 117 | max-width: 100rem; 118 | margin: 1rem 3rem; 119 | background: linear-gradient( 120 | 90deg, 121 | rgba(255, 255, 255, 0) 40%, 122 | rgb(255, 255, 255) 100% 123 | ); 124 | border-radius: 15px; 125 | display: flex; 126 | justify-content: space-between; 127 | padding: 3rem; 128 | 129 | #instruction-display { 130 | display: flex; 131 | flex-direction: column; 132 | align-items: center; 133 | padding: 5px; 134 | } 135 | 136 | h2 { 137 | color: #6c6185; 138 | } 139 | } 140 | 141 | #home-to-test { 142 | margin-top: 2rem; 143 | background: linear-gradient( 144 | 180deg, 145 | $color-primary 50%, 146 | $color-secondary 120% 147 | ); 148 | border-radius: 1rem; 149 | // border-style: none; 150 | border: 1px white solid; 151 | box-sizing: border-box; 152 | color: #ffffff; 153 | cursor: pointer; 154 | flex-shrink: 0; 155 | font-size: 2.1rem; 156 | font-weight: 500; 157 | height: 3.5rem; 158 | text-align: center; 159 | text-shadow: rgba(0, 0, 0, 0.25) 0 3px 8px; 160 | transition: all 0.5s; 161 | user-select: none; 162 | -webkit-user-select: none; 163 | touch-action: manipulation; 164 | box-shadow: 10px 10px 32px -9px rgba(0, 0, 0, 0.75); 165 | -webkit-box-shadow: 10px 10px 32px -9px rgba(0, 0, 0, 0.75); 166 | -moz-box-shadow: 10px 10px 32px -9px rgba(0, 0, 0, 0.75); 167 | } 168 | 169 | #footer { 170 | width: 100%; 171 | max-width: 100%; 172 | display: flex; 173 | margin-top: 5rem; 174 | flex-direction: column; 175 | align-items: center; 176 | background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, #ffffff 50%); 177 | 178 | h1 { 179 | align-self: center; 180 | margin-top: 3rem; 181 | margin-bottom: -2.5rem; 182 | font-size: 4rem; 183 | font-weight: 400; 184 | background: linear-gradient(180deg, #6c6185 10%, #ffcad4 100%); 185 | -webkit-background-clip: text; 186 | -webkit-text-fill-color: transparent; 187 | } 188 | 189 | h2 { 190 | font-weight: bold; 191 | margin-top: 5px; 192 | font-size: larger; 193 | opacity: 50%; 194 | } 195 | 196 | #team-container { 197 | min-width: 110rem; 198 | margin: 4rem 0rem; 199 | display: flex; 200 | justify-content: space-evenly; 201 | flex-wrap: wrap; 202 | } 203 | .member { 204 | border: 2px white solid; 205 | display: flex; 206 | flex-direction: column; 207 | align-items: center; 208 | padding: 1rem; 209 | border-radius: 15px; 210 | background: #ffffff; 211 | box-shadow: inset -20px -20px 60px #d9d9d9, inset 20px 20px 60px #ffffff; 212 | .profile { 213 | width: 170px; 214 | height: 170px; 215 | border-radius: 100px; 216 | border: 2px white solid; 217 | } 218 | .links { 219 | display: flex; 220 | 221 | a { 222 | margin: 0.5rem 2rem; 223 | } 224 | 225 | img { 226 | width: 30px; 227 | height: 30px; 228 | opacity: 50%; 229 | } 230 | } 231 | } 232 | } 233 | .packages { 234 | border-radius: 15px; 235 | background-color: rgb(237, 239, 238); 236 | border: 2px white solid; 237 | padding: 2rem; 238 | width: 43rem; 239 | box-shadow: -8px 10px 23px -2px rgba(0, 0, 0, 0.35); 240 | -webkit-box-shadow: -8px 10px 23px -2px rgba(0, 0, 0, 0.35); 241 | -moz-box-shadow: -8px 10px 23px -2px rgba(0, 0, 0, 0.35); 242 | 243 | h2 { 244 | font-size: 1.5rem; 245 | } 246 | .npm { 247 | display: flex; 248 | border: 2px white solid; 249 | border-radius: 5px; 250 | background-color: #6c6185; 251 | color: white; 252 | font-size: larger; 253 | padding: 4px; 254 | } 255 | .npm:hover { 256 | background-color: #b696ad; 257 | border-color: #b696ad; 258 | cursor: pointer; 259 | } 260 | } 261 | #package-text-container { 262 | display: flex; 263 | flex-direction: column; 264 | justify-content: center; 265 | align-items: flex-end; 266 | margin-left: 20rem; 267 | margin-right: 5rem; 268 | h1 { 269 | font-size: 5.5rem; 270 | font-weight: 400; 271 | background: linear-gradient(345deg, #6c6185 10%, #ffcad4 100%); 272 | -webkit-background-clip: text; 273 | -webkit-text-fill-color: transparent; 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /client/src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './test.scss'; 2 | @import './signin.scss'; 3 | @import './navBar.scss'; 4 | @import './savedTest.scss'; 5 | @import './homePage.scss'; 6 | 7 | html, 8 | /* STYLE RESET */ 9 | /* // https://piccalil.li/blog/a-modern-css-reset */ 10 | /* Box sizing rules */ 11 | *, 12 | *::before, 13 | *::after { 14 | box-sizing: border-box; 15 | } 16 | 17 | /* Remove default margin and padding */ 18 | * { 19 | margin: 0; 20 | padding: 0; 21 | font: inherit; 22 | } 23 | 24 | /* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */ 25 | ul[role='list'], 26 | ol[role='list'] { 27 | list-style: none; 28 | } 29 | 30 | /* Set core root defaults */ 31 | html:focus-within { 32 | scroll-behavior: smooth; 33 | } 34 | 35 | body { 36 | height: 100%; 37 | font-family: 'Roboto', sans-serif; 38 | } 39 | 40 | /* Set core body defaults */ 41 | body { 42 | /* min-height: 100vh; */ 43 | text-rendering: optimizeSpeed; 44 | line-height: 1.5; 45 | background-image: url('/src/images/background.jpg'); 46 | background-size: cover; 47 | background-repeat: no-repeat; 48 | background-position: center center; 49 | background-attachment: fixed; 50 | min-height: 100vh; 51 | z-index: 0; 52 | } 53 | 54 | /* A elements that don't have a class get default styles */ 55 | a:not([class]) { 56 | text-decoration-skip-ink: auto; 57 | } 58 | 59 | /* Make images easier to work with */ 60 | img, 61 | picture, 62 | svg { 63 | max-width: 100%; 64 | display: block; 65 | } 66 | 67 | /* Inherit fonts for inputs and buttons 68 | input, 69 | button, 70 | textarea, 71 | select { 72 | font: inherit; 73 | } */ 74 | 75 | /* Remove all animations, transitions and smooth scroll for people that prefer not to see them */ 76 | @media (prefers-reduced-motion: reduce) { 77 | html:focus-within { 78 | scroll-behavior: auto; 79 | } 80 | 81 | *, 82 | *::before, 83 | *::after { 84 | animation-duration: 0.01ms !important; 85 | animation-iteration-count: 1 !important; 86 | transition-duration: 0.01ms !important; 87 | scroll-behavior: auto !important; 88 | } 89 | } 90 | 91 | .noSelect { 92 | -webkit-touch-callout: none; /* iOS Safari */ 93 | -webkit-user-select: none; /* Safari */ 94 | -khtml-user-select: none; /* Konqueror HTML */ 95 | -moz-user-select: none; /* Old versions of Firefox */ 96 | -ms-user-select: none; /* Internet Explorer/Edge */ 97 | user-select: none; 98 | } 99 | 100 | .noDrag { 101 | user-drag: none; 102 | user-select: none; 103 | -moz-user-select: none; 104 | -webkit-user-drag: none; 105 | -webkit-user-select: none; 106 | -ms-user-select: none; 107 | } 108 | 109 | .swal2-container { 110 | z-index: 20000 !important; 111 | } 112 | -------------------------------------------------------------------------------- /client/src/styles/navBar.scss: -------------------------------------------------------------------------------- 1 | $color-primary: #6c6185; 2 | $color-secondary: #ffcad4; 3 | $color-neutral-lt: #fff; 4 | $color-neutral-med: #ddd; 5 | $color-neutral-dk: #444; 6 | $a-tags: 'a, a:active, a:hover, a:visited'; 7 | $a-tags-no-hov: 'a:link, a:visited, a:active'; 8 | $headings-font: 'Saira Semi Condensed', sans-serif; 9 | $shadow: 0px 3px 10px rgba(0, 0, 0, 0.1); 10 | 11 | @mixin main-gradient { 12 | background: $color-primary; 13 | background: -webkit-linear-gradient(45deg, $color-primary, $color-secondary); 14 | background: linear-gradient(45deg, $color-primary, $color-secondary); 15 | } 16 | 17 | @mixin shadow-box { 18 | background-color: $color-neutral-lt; 19 | box-shadow: $shadow; 20 | } 21 | 22 | @mixin skew { 23 | transform: skew(-20deg); 24 | } 25 | 26 | @mixin unskew { 27 | transform: skew(20deg); 28 | } 29 | 30 | /*-------------Reset-------------*/ 31 | button { 32 | background: none; 33 | box-shadow: none; 34 | border: none; 35 | cursor: pointer; 36 | } 37 | 38 | button:focus, 39 | input:focus { 40 | outline: 0; 41 | } 42 | 43 | html { 44 | scroll-behavior: smooth; 45 | } 46 | 47 | /*-------------Layout-------------*/ 48 | body { 49 | line-height: 1.5em; 50 | padding: 0; 51 | margin: 0; 52 | } 53 | 54 | section { 55 | height: 100vh; 56 | } 57 | 58 | #menu { 59 | margin-top: 0.5em; 60 | } 61 | #home { 62 | background-color: #ddd; 63 | } 64 | 65 | #about { 66 | background-color: #aaa; 67 | } 68 | 69 | #work { 70 | background-color: #888; 71 | } 72 | 73 | #contact { 74 | background-color: #666; 75 | } 76 | 77 | /*-------------Helpers-------------*/ 78 | .skew { 79 | @include skew; 80 | } 81 | 82 | .un-skew { 83 | @include unskew; 84 | } 85 | 86 | /*-------------Nav-------------*/ 87 | #nav-wrapper { 88 | overflow: hidden; 89 | width: 100%; 90 | margin: 0 auto; 91 | position: fixed; 92 | top: 0; 93 | left: 0; 94 | z-index: 100; 95 | } 96 | 97 | #nav { 98 | @include shadow-box; 99 | display: flex; 100 | flex-direction: column; 101 | font-family: $headings-font; 102 | height: 4em; 103 | overflow: hidden; 104 | 105 | &.nav-visible { 106 | height: 100%; 107 | overflow: auto; 108 | } 109 | } 110 | 111 | .nav { 112 | display: flex; 113 | height: 4em; 114 | line-height: 4em; 115 | flex-grow: 1; 116 | } 117 | 118 | .nav-link, 119 | .logo { 120 | padding: 0 1em; 121 | } 122 | 123 | span.gradient { 124 | @include main-gradient; 125 | padding: 0 1em; 126 | position: relative; 127 | right: 1em; 128 | margin-right: auto; 129 | 130 | &:hover { 131 | animation-name: logo-hover; 132 | animation-duration: 0.3s; 133 | animation-fill-mode: forwards; 134 | animation-timing-function: cubic-bezier(0.17, 0.57, 0.31, 0.85); 135 | } 136 | } 137 | 138 | h1.logo { 139 | font-weight: 300; 140 | font-size: 1.75em; 141 | line-height: 2em; 142 | color: $color-neutral-lt; 143 | } 144 | 145 | h1.logo #{$a-tags} { 146 | text-decoration: none; 147 | color: $color-neutral-lt; 148 | } 149 | 150 | .nav-link { 151 | text-transform: uppercase; 152 | text-align: center; 153 | border-top: 0.5px solid $color-neutral-med; 154 | } 155 | 156 | #{$a-tags-no-hov} { 157 | text-decoration: none; 158 | color: $color-primary; 159 | } 160 | 161 | a:hover { 162 | text-decoration: underline; 163 | } 164 | 165 | .right { 166 | display: flex; 167 | flex-direction: column; 168 | height: 100%; 169 | } 170 | 171 | .btn-nav { 172 | color: $color-primary; 173 | padding-left: 2em; 174 | padding-right: 2em; 175 | } 176 | 177 | @media (min-width: 800px) { 178 | #nav-wrapper { 179 | overflow: hidden; 180 | } 181 | 182 | #nav { 183 | overflow: hidden; 184 | flex-direction: row; 185 | } 186 | 187 | .nav-link { 188 | border-top: none; 189 | } 190 | 191 | .right { 192 | overflow: hidden; 193 | flex-direction: row; 194 | justify-content: flex-end; 195 | position: relative; 196 | left: 1.5em; 197 | height: auto; 198 | } 199 | 200 | .btn-nav { 201 | display: none; 202 | } 203 | 204 | .nav #{$a-tags-no-hov} { 205 | &.active { 206 | @include main-gradient; 207 | color: #fff; 208 | } 209 | } 210 | 211 | .nav-link-span { 212 | @include unskew; 213 | display: inline-block; 214 | } 215 | 216 | .nav-link { 217 | @include skew; 218 | color: #777; 219 | text-decoration: none; 220 | 221 | &:last-child { 222 | padding-right: 3em; 223 | } 224 | } 225 | 226 | a:hover.nav-link:not(.active) { 227 | color: white; 228 | background: linear-gradient(45deg, $color-secondary, $color-primary); 229 | } 230 | } 231 | 232 | @keyframes logo-hover { 233 | 20% { 234 | padding-right: 0em; 235 | } 236 | 100% { 237 | padding-right: 5em; 238 | } 239 | } 240 | 241 | #logo-container { 242 | display: flex; 243 | margin-top: 4px; 244 | } 245 | #logo { 246 | margin-top: 3px; 247 | margin-left: 10px; 248 | height: 40px; 249 | width: 40px; 250 | object-fit: contain; 251 | } 252 | -------------------------------------------------------------------------------- /client/src/styles/savedTest.scss: -------------------------------------------------------------------------------- 1 | #saved-test-container { 2 | padding-top: 8rem; 3 | display: flex; 4 | justify-content: space-around; 5 | } 6 | 7 | #test-list { 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | border-radius: 1rem; 12 | min-width: 18rem; 13 | background: rgb(31, 31, 31); 14 | background: linear-gradient( 15 | 0deg, 16 | rgba(255, 255, 255, 0) 0%, 17 | rgba(255, 255, 255, 0.9) 80% 18 | ); 19 | 20 | #saved-test-options { 21 | padding: 8px; 22 | border: 1px solid #cccccc6e; 23 | border-radius: 10px; 24 | font-size: 20px; 25 | width: 200px; 26 | color: rgba(0, 0, 0, 0.474); 27 | } 28 | 29 | .saved-test-list { 30 | color: #898989; 31 | padding: 0.5rem 1rem; 32 | width: 12rem; 33 | border-radius: 50px; 34 | background: linear-gradient(45deg, #e6e6e6, #ffffff); 35 | box-shadow: 20px -20px 60px #d9d9d9, -20px 20px 60px #ffffff; 36 | } 37 | 38 | ul { 39 | list-style-type: none; 40 | } 41 | li { 42 | margin: 1rem; 43 | } 44 | 45 | #delete-btn { 46 | padding: 1px 10px; 47 | margin-left: 2px; 48 | border-radius: 50px; 49 | border: 2px solid rgb(251, 251, 251); 50 | transition: background-color 0.3s ease, color 0.3s ease; 51 | background: linear-gradient(225deg, #e6e6e6, #ffffff); 52 | box-shadow: -20px 20px 60px #d9d9d9, 20px -20px 60px #ffffff; 53 | 54 | &:hover { 55 | background: red; 56 | color: white; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /client/src/styles/signin.scss: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | #signinBackground { 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | min-height: 100vh; 10 | } 11 | 12 | #sliderContainer { 13 | border: 2px white solid; 14 | border-radius: 15px; 15 | font-family: Verdana, Geneva, Tahoma, sans-serif; 16 | -webkit-animation: flip-in-hor-bottom 0.5s 17 | cubic-bezier(0.25, 0.46, 0.45, 0.94) both; 18 | animation: flip-in-hor-bottom 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; 19 | } 20 | 21 | @-webkit-keyframes flip-in-hor-bottom { 22 | 0% { 23 | -webkit-transform: rotateX(80deg); 24 | transform: rotateX(80deg); 25 | opacity: 0; 26 | } 27 | 100% { 28 | -webkit-transform: rotateX(0); 29 | transform: rotateX(0); 30 | opacity: 1; 31 | } 32 | } 33 | @keyframes flip-in-hor-bottom { 34 | 0% { 35 | -webkit-transform: rotateX(80deg); 36 | transform: rotateX(80deg); 37 | opacity: 0; 38 | } 39 | 100% { 40 | -webkit-transform: rotateX(0); 41 | transform: rotateX(0); 42 | opacity: 1; 43 | } 44 | } 45 | 46 | .errorMessage { 47 | color: red; 48 | font-size: small; 49 | margin-right: auto; 50 | } 51 | -------------------------------------------------------------------------------- /client/src/styles/test.scss: -------------------------------------------------------------------------------- 1 | $color-primary: #6c6185; 2 | $color-secondary: #dbb0c1; 3 | $color-neutral-lt: #fff; 4 | $color-neutral-med: #ddd; 5 | $color-neutral-dk: #444; 6 | 7 | .editor-container { 8 | border: 1px white solid; 9 | margin: 5px; 10 | border-radius: 15px; 11 | padding: 8px; 12 | background: linear-gradient(180deg, $color-secondary 0%, #49405e 8%); 13 | -webkit-animation: swing-in-top-fwd 0.5s 14 | cubic-bezier(0.175, 0.885, 0.32, 1.275) both; 15 | animation: swing-in-top-fwd 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) both; 16 | box-shadow: 10px 10px 40px -10px rgba(0, 0, 0, 0.75); 17 | -webkit-box-shadow: 10px 10px 40px -10px rgba(0, 0, 0, 0.75); 18 | -moz-box-shadow: 10px 10px 40px -10px rgba(0, 0, 0, 0.75); 19 | } 20 | 21 | @-webkit-keyframes swing-in-top-fwd { 22 | 0% { 23 | -webkit-transform: rotateX(-100deg); 24 | transform: rotateX(-100deg); 25 | -webkit-transform-origin: top; 26 | transform-origin: top; 27 | opacity: 0; 28 | } 29 | 100% { 30 | -webkit-transform: rotateX(0deg); 31 | transform: rotateX(0deg); 32 | -webkit-transform-origin: top; 33 | transform-origin: top; 34 | opacity: 1; 35 | } 36 | } 37 | @keyframes swing-in-top-fwd { 38 | 0% { 39 | -webkit-transform: rotateX(-100deg); 40 | transform: rotateX(-100deg); 41 | -webkit-transform-origin: top; 42 | transform-origin: top; 43 | opacity: 0; 44 | } 45 | 100% { 46 | -webkit-transform: rotateX(0deg); 47 | transform: rotateX(0deg); 48 | -webkit-transform-origin: top; 49 | transform-origin: top; 50 | opacity: 1; 51 | } 52 | } 53 | 54 | .dropdown-menu { 55 | display: flex; 56 | justify-content: space-between; 57 | margin-bottom: 0.5rem; 58 | 59 | .left-dropdown { 60 | border-radius: 0.4rem; 61 | color: white; 62 | font-family: Verdana, Geneva, Tahoma, sans-serif; 63 | border-color: rgba(255, 255, 255, 0); 64 | background-color: rgba(2, 2, 2, 0); 65 | } 66 | } 67 | 68 | .test-button { 69 | background: linear-gradient( 70 | 180deg, 71 | $color-primary 50%, 72 | $color-secondary 120% 73 | ); 74 | border-radius: 1rem; 75 | // border-style: none; 76 | border: 1px white solid; 77 | box-sizing: border-box; 78 | color: #ffffff; 79 | cursor: pointer; 80 | flex-shrink: 0; 81 | font-family: 'Inter UI', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 82 | 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', 83 | sans-serif; 84 | font-size: 16px; 85 | font-weight: 500; 86 | height: 2.5rem; 87 | text-align: center; 88 | text-shadow: rgba(0, 0, 0, 0.25) 0 3px 8px; 89 | transition: all 0.5s; 90 | user-select: none; 91 | -webkit-user-select: none; 92 | touch-action: manipulation; 93 | box-shadow: 10px 10px 32px -9px rgba(0, 0, 0, 0.75); 94 | -webkit-box-shadow: 10px 10px 32px -9px rgba(0, 0, 0, 0.75); 95 | -moz-box-shadow: 10px 10px 32px -9px rgba(0, 0, 0, 0.75); 96 | } 97 | 98 | .test-button:hover { 99 | -webkit-box-shadow: 0px 0px 46px 3px rgba(255, 202, 212, 1); 100 | -moz-box-shadow: 0px 0px 46px 3px rgba(255, 202, 212, 1); 101 | box-shadow: 0px 0px 46px 3px rgba(255, 202, 212, 1); 102 | transition-duration: 0.1s; 103 | } 104 | 105 | @media (min-width: 768px) { 106 | .test-button { 107 | padding: 0 2.6rem; 108 | } 109 | } 110 | 111 | #copy-button:hover { 112 | border: 2px white solid; 113 | border-radius: 5px; 114 | } 115 | 116 | #copy-button:active { 117 | transform: scale(0.98); 118 | /* Scaling button to 0.98 to its original size */ 119 | box-shadow: 3px 2px 22px 1px rgba(0, 0, 0, 0.24); 120 | /* Lowering the shadow */ 121 | } 122 | -------------------------------------------------------------------------------- /client/src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const API_URL = 'http://localhost:8080/'; 2 | -------------------------------------------------------------------------------- /client/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /client/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /client/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import sass from 'sass'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react()], 8 | css: { 9 | preprocessorOptions: { 10 | scss: { 11 | implementation: sass, 12 | }, 13 | }, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Herman", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /testServer/__tests__/mutationintegration.test.js: -------------------------------------------------------------------------------- 1 | //-> npm install 2 | //-> npm install 3 | // const { expect } = require("@jest/globals"); 4 | import { describe, test, expect } from "@jest/globals"; 5 | import { gql } from "apollo-server"; 6 | import { createTestServer } from "./testServer"; 7 | 8 | const MUTATION_NAME = gql` 9 | mutation { 10 | MUTATIONfn(/*INPUT*/) { 11 | /* DATA SENT BACK */ 12 | } 13 | } 14 | `; 15 | 16 | const QUERY2_NAME = gql` 17 | /* QUERY STRING */ 18 | `; 19 | /*NOTE IN INSTRUCTIONS THAT THE FIRST TEST WILL PASS AUTOMATICALLY WITH MATCH SNAPSHOT */ 20 | describe("mutations", () => { 21 | test("MUTATION_NAME", async () => { 22 | const { mutate } = createTestServer({ 23 | /* CONTEXT OBJECT - MOCK MUTATION CONTEXT REQUIREMENTS HERE */ 24 | }); 25 | const res = await mutate({ query: QUERY_NAME }); 26 | expect(res).toMatchSnapshot(); 27 | }); 28 | 29 | test("QUERY2_NAME", async () => { 30 | const { query } = createTestServer({ 31 | /* CONTEXT OBJECT - MOCK MUTATION CONTEXT REQUITEMENTS HERE */ 32 | }); 33 | const res = await query({ query: QUERY2_NAME }); 34 | expect(res).toMatchSnapshot(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /testServer/__tests__/queryintegration.test.js: -------------------------------------------------------------------------------- 1 | //-> npm install 2 | //-> npm install 3 | const { expect } = require("@jest/globals"); 4 | const gql = require("graphql-tag"); 5 | const createTestServer = require(/*path to testServer*/); 6 | 7 | const QUERY_NAME = gql` 8 | /* QUERY STRING */ 9 | `; 10 | 11 | const QUERY2_NAME = gql` 12 | /* QUERY STRING */ 13 | `; 14 | /*NOTE IN INSTRUCTIONS THAT THE FIRST TEST WILL PASS AUTOMATICALLY WITH MATCH SNAPSHOT */ 15 | describe("queries", () => { 16 | test("QUERY_NAME", async () => { 17 | const { query } = createTestServer({ 18 | /* CONTEXT OBJECT - MOCK QUERY/RESOLVER CONTEXT REQUIREMENTS HERE */ 19 | }); 20 | const res = await query({ query: QUERY_NAME }); 21 | expect(res).toMatchSnapshot(); 22 | }); 23 | 24 | test("QUERY2_NAME", async () => { 25 | const { query } = createTestServer({ 26 | /* CONTEXT OBJECT - MOCK QUERY/RESOLVER CONTEXT REQUIREMENTS HERE */ 27 | }); 28 | const res = await query({ query: QUERY2_NAME }); 29 | expect(res).toMatchSnapshot(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /testServer/__tests__/resolver.test.ts: -------------------------------------------------------------------------------- 1 | // //need proper imports for tests here 2 | // import {describe, expect, test} from '@jest/globals'; 3 | // const { makeExecutableSchema, addMocksToSchema } = require('graphql-tools'); 4 | // const typeDefs = require('../schema') 5 | // const resolvers = require('../schema') 6 | // import * as casual from "casual" 7 | 8 | // // // describe('Resolvers return the correct values', () => { 9 | // // const schema = makeExecutableSchema({ typeDefs, resolvers}) 10 | // // const mocks = { 11 | // // String: () => casual.sentence, 12 | // // Int: () => casual.integer(1,100), 13 | // // Float: () => 22.7, 14 | // // Boolean: () => casual.boolean, 15 | // // /* 16 | // // If you'd like more specific mocks for any of your fields, add them below, using this as an example: 17 | 18 | // // User: () => ({ 19 | // // id: casual.uuid, 20 | // // name: casual.name, 21 | // // email: casual.email, 22 | // // age: casual.integer(18,100), 23 | // // }), 24 | 25 | // // Please refer to the npm package 'casual' for random javascript generator calls. 26 | // // */ 27 | // // } 28 | // // const preserveResolvers = true; 29 | // // const mockedSchema = addMocksToSchema({ 30 | // // schema, 31 | // // mocks, 32 | // // preserveResolvers 33 | // // }) 34 | 35 | // // }) 36 | -------------------------------------------------------------------------------- /testServer/__tests__/resolverMock.test.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Scribe-for-GraphQL/4102d5587fe4862351eb912bc257e95582c6e21d/testServer/__tests__/resolverMock.test.ts -------------------------------------------------------------------------------- /testServer/__tests__/resolvers.test.js: -------------------------------------------------------------------------------- 1 | //-> npm install 2 | const { expect } = require("@jest/globals"); 3 | const resolvers = require(/*path to resolvers*/); 4 | 5 | describe("resolvers return the correct values", () => { 6 | test("RESOLVER_NAME", () => { 7 | const result = resolvers.RESOLVER_NAME(/*resolver mock parameters*/); 8 | 9 | expect(result).toEqual(/*expected result*/); 10 | }); 11 | test("RESOLVER_NAME2", () => { 12 | const result = resolvers.RESOLVER_NAME(/*resolver mock parameters*/); 13 | 14 | expect(result).toEqual(/*expected result*/); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /testServer/__tests__/schema.test.ts: -------------------------------------------------------------------------------- 1 | //> npm install graphql-tools @jest/globals jest babel-jest 2 | //> for typescript tests, npm install ts-jest @types/jest 3 | import { describe, expect, test } from "@jest/globals"; 4 | const { makeExecutableSchema, addMocksToSchema } = require("graphql-tools"); 5 | const typeDefs = require("../schema"); 6 | 7 | describe("Schema Types Are Correct", () => { 8 | const schema = makeExecutableSchema({ typeDefs }); 9 | test("Book should have the correct types", () => { 10 | const type = schema.getType("Book"); 11 | expect(type).toBeDefined(); 12 | 13 | expect(type.getFields().id.type.name).toBe("ID"); 14 | expect(type.getFields().title.type.name).toBe("String"); 15 | expect(type.getFields().author.type.name).toBe("Author"); 16 | expect(type.getFields().genre.type.name).toBe("String"); 17 | }); 18 | test("Author should have the correct types", () => { 19 | const type = schema.getType("Author"); 20 | expect(type).toBeDefined(); 21 | 22 | expect(type.getFields().id.type.name).toBe("ID"); 23 | expect(type.getFields().name.type.name).toBe("String"); 24 | expect(JSON.stringify(type.getFields().books.type)).toBe( 25 | JSON.stringify("[Book]") 26 | ); 27 | }); 28 | test("User should have the correct types", () => { 29 | const type = schema.getType("User"); 30 | expect(type).toBeDefined(); 31 | 32 | expect(type.getFields().id.type.name).toBe("ID"); 33 | expect(type.getFields().name.type.name).toBe("String"); 34 | expect(type.getFields().email.type.name).toBe("String"); 35 | expect(type.getFields().age.type.name).toBe("Int"); 36 | expect(type.getFields().favoriteBook.type.name).toBe("Book"); 37 | }); 38 | test("Query should have the correct types", () => { 39 | const type = schema.getType("Query"); 40 | expect(type).toBeDefined(); 41 | 42 | expect(JSON.stringify(type.getFields().allBooks.type)).toBe( 43 | JSON.stringify("[Book]") 44 | ); 45 | expect(type.getFields().book.type.name).toBe("Book"); 46 | expect(JSON.stringify(type.getFields().allAuthors.type)).toBe( 47 | JSON.stringify("[Author]") 48 | ); 49 | expect(type.getFields().author.type.name).toBe("Author"); 50 | expect(JSON.stringify(type.getFields().allUsers.type)).toBe( 51 | JSON.stringify("[User]") 52 | ); 53 | expect(type.getFields().user.type.name).toBe("User"); 54 | }); 55 | test("Mutation should have the correct types", () => { 56 | const type = schema.getType("Mutation"); 57 | expect(type).toBeDefined(); 58 | 59 | expect(JSON.stringify(type.getFields().createBook.type)).toBe( 60 | JSON.stringify("Book!") 61 | ); 62 | expect(JSON.stringify(type.getFields().updateBook.type)).toBe( 63 | JSON.stringify("Book!") 64 | ); 65 | expect(JSON.stringify(type.getFields().deleteBook.type)).toBe( 66 | JSON.stringify("Book!") 67 | ); 68 | expect(JSON.stringify(type.getFields().createUser.type)).toBe( 69 | JSON.stringify("User!") 70 | ); 71 | expect(JSON.stringify(type.getFields().updateUser.type)).toBe( 72 | JSON.stringify("User!") 73 | ); 74 | expect(JSON.stringify(type.getFields().deleteUser.type)).toBe( 75 | JSON.stringify("User!") 76 | ); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /testServer/__tests__/testServer.js: -------------------------------------------------------------------------------- 1 | const { ApolloServer } = require("apollo-server"); 2 | const { createTestClient } = require("apollo-server-testing"); 3 | const typeDefs = require(/* path to schema */); 4 | const resolvers = require(/* path to resolvers */); 5 | 6 | export const createTestServer = (cntext) => { 7 | const server = new ApolloServer({ 8 | typeDefs, 9 | resolvers, 10 | mockEntireSchema: false, // -> since we are passing in a schema and resolvers, we need this to be false 11 | mocks: true, // -> mocks random data according to type definitions in schema 12 | context: () => cntext, 13 | }); 14 | 15 | return createTestClient(server); 16 | }; 17 | 18 | //INCLUDE IN INSTRUCTIONS 19 | -------------------------------------------------------------------------------- /testServer/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', {targets: {node: 'current'}}], 4 | '@babel/preset-typescript', 5 | ], 6 | }; -------------------------------------------------------------------------------- /testServer/graphql-test1/schema.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Scribe-for-GraphQL/4102d5587fe4862351eb912bc257e95582c6e21d/testServer/graphql-test1/schema.ts -------------------------------------------------------------------------------- /testServer/graphql-test1/server.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Scribe-for-GraphQL/4102d5587fe4862351eb912bc257e95582c6e21d/testServer/graphql-test1/server.ts -------------------------------------------------------------------------------- /testServer/graphql-test2/schema.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Scribe-for-GraphQL/4102d5587fe4862351eb912bc257e95582c6e21d/testServer/graphql-test2/schema.ts -------------------------------------------------------------------------------- /testServer/graphql-test2/server.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Scribe-for-GraphQL/4102d5587fe4862351eb912bc257e95582c6e21d/testServer/graphql-test2/server.ts -------------------------------------------------------------------------------- /testServer/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type {Config} from "@jest/types" 2 | const config: Config.InitialOptions = { 3 | preset: "ts-jest", 4 | testEnvironment: "node", 5 | } 6 | 7 | export default config -------------------------------------------------------------------------------- /testServer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@apollo/server": "^4.7.1", 4 | "apollo-server": "^3.11.1", 5 | "apollo-server-testing": "^2.25.3", 6 | "casual": "^1.6.2", 7 | "cors": "^2.8.5", 8 | "express": "^4.18.2", 9 | "graphql": "^16.6.0", 10 | "graphql-tools": "^8.3.19", 11 | "node-fetch": "^2.6.9", 12 | "ts-node": "^10.9.1" 13 | }, 14 | "devDependencies": { 15 | "@babel/core": "^7.21.0", 16 | "@babel/preset-env": "^7.20.2", 17 | "@babel/preset-typescript": "^7.21.0", 18 | "@jest/globals": "^29.4.3", 19 | "@types/jest": "^29.4.0", 20 | "@types/node": "^18.14.0", 21 | "babel-jest": "^29.4.3", 22 | "jest": "^29.4.3", 23 | "ts-jest": "^29.0.5" 24 | }, 25 | "name": "testserver", 26 | "version": "1.0.0", 27 | "main": "babel.config.js", 28 | "directories": { 29 | "test": "tests" 30 | }, 31 | "scripts": { 32 | "test": "jest --config jest.config.ts ./__tests__" 33 | }, 34 | "keywords": [], 35 | "author": "", 36 | "license": "ISC", 37 | "description": "" 38 | } 39 | -------------------------------------------------------------------------------- /testServer/schema.ts: -------------------------------------------------------------------------------- 1 | declare var require: NodeRequire; 2 | declare var module: NodeModule; 3 | const graphql = require("graphql"); 4 | const { gql } = require("apollo-server"); 5 | const { makeExecutableSchema } = require("graphql-tools"); 6 | // const fetch = require('node-fetch') 7 | const { 8 | GraphQLObjectType, 9 | GraphQLString, 10 | GraphQLInt, 11 | GraphQLFloat, 12 | GraphQLList, 13 | GraphQLSchema, 14 | GraphQLBoolean, 15 | } = graphql; 16 | 17 | const typeDefs = ` 18 | type Book { 19 | id: ID 20 | title: String 21 | author: Author 22 | genre: String 23 | } 24 | 25 | type Author { 26 | id: ID 27 | name: String 28 | books: [Book] 29 | } 30 | 31 | type User { 32 | id: ID 33 | name: String 34 | email: String 35 | age: Int 36 | favoriteBook: Book 37 | } 38 | 39 | type Query { 40 | allBooks: [Book] 41 | book(id: ID): Book 42 | allAuthors: [Author] 43 | author(id: ID): Author 44 | allUsers: [User] 45 | user(id: ID): User 46 | } 47 | 48 | type Mutation { 49 | createBook(title: String!, authorId: ID!, genre: String!): Book! 50 | updateBook(id: ID!, title: String, authorId: ID, genre: String): Book! 51 | deleteBook(id: ID!): Book! 52 | createUser( 53 | name: String! 54 | email: String! 55 | age: Int 56 | favoriteBookId: ID 57 | ): User! 58 | updateUser( 59 | id: ID! 60 | name: String 61 | email: String 62 | age: Int 63 | favoriteBookId: ID 64 | ): User! 65 | deleteUser(id: ID!): User! 66 | } 67 | `; 68 | 69 | const resolvers = { 70 | Query: { 71 | allBooks: () => books, 72 | book: (parent, { id }) => books.find((book) => book.id === id), 73 | allAuthors: () => authors, 74 | author: (parent, { id }) => authors.find((author) => author.id === id), 75 | allUsers: () => users, 76 | user: (parent, { id }) => users.find((user) => user.id === id), 77 | }, 78 | Mutation: { 79 | createBook: (parent, { title, authorId, genre }) => { 80 | const book = { id: String(books.length + 1), title, authorId, genre }; 81 | books.push(book); 82 | return book; 83 | }, 84 | updateBook: (parent, { id, title, authorId, genre }) => { 85 | const bookIndex = books.findIndex((book) => book.id === id); 86 | if (bookIndex === -1) throw new Error(`No book with id ${id}`); 87 | const book = { ...books[bookIndex], title, authorId, genre }; 88 | books[bookIndex] = book; 89 | return book; 90 | }, 91 | deleteBook: (parent, { id }) => { 92 | const bookIndex = books.findIndex((book) => book.id === id); 93 | if (bookIndex === -1) throw new Error(`No book with id ${id}`); 94 | const deletedBook = books.splice(bookIndex, 1)[0]; 95 | return deletedBook; 96 | }, 97 | createUser: (parent, { name, email, age, favoriteBookId }) => { 98 | const user = { 99 | id: String(users.length + 1), 100 | name, 101 | email, 102 | age, 103 | favoriteBookId, 104 | }; 105 | users.push(user); 106 | return user; 107 | }, 108 | updateUser: (parent, { id, name, email, age, favoriteBookId }) => { 109 | const userIndex = users.findIndex((user) => user.id === id); 110 | if (userIndex === -1) throw new Error(`No user with id ${id}`); 111 | const user = { ...users[userIndex], name, email, age, favoriteBookId }; 112 | users[userIndex] = user; 113 | return user; 114 | }, 115 | deleteUser: (parent, { id }) => { 116 | const userIndex = users.findIndex((user) => user.id === id); 117 | if (userIndex === -1) throw new Error(`No user with id ${id}`); 118 | const deletedUser = users.splice(userIndex, 1)[0]; 119 | return deletedUser; 120 | }, 121 | }, 122 | Book: { 123 | author: (parent) => authors.find((author) => author.id === parent.authorId), 124 | }, 125 | Author: { 126 | books: (parent) => books.filter((book) => book.authorId === parent.id), 127 | }, 128 | User: { 129 | favoriteBook: (parent) => { 130 | if (!parent.favoriteBookId) return null; 131 | return books.find((book) => book.id === parent.favoriteBookId); 132 | }, 133 | }, 134 | }; 135 | 136 | const books = [ 137 | { id: "1", title: "The Great Gatsby", authorId: "1", genre: "Fiction" }, 138 | { id: "2", title: "To Kill a Mockingbird", authorId: "2", genre: "Fiction" }, 139 | { id: "3", title: "The Catcher in the Rye", authorId: "3", genre: "Fiction" }, 140 | { id: "4", title: "1984", authorId: "4", genre: "Fiction" }, 141 | { id: "5", title: "Pride and Prejudice", authorId: "5", genre: "Romance" }, 142 | ]; 143 | 144 | const authors = [ 145 | { id: "1", name: "F. Scott Fitzgerald" }, 146 | { id: "2", name: "Harper Lee" }, 147 | { id: "3", name: "J.D. Salinger" }, 148 | { id: "4", name: "George Orwell" }, 149 | { id: "5", name: "Jane Austen" }, 150 | ]; 151 | 152 | const users = [ 153 | { 154 | id: "1", 155 | name: "John", 156 | email: "john@example.com", 157 | age: 30, 158 | favoriteBookId: "1", 159 | }, 160 | { 161 | id: "2", 162 | name: "Jane", 163 | email: "jane@example.com", 164 | age: 25, 165 | favoriteBookId: "2", 166 | }, 167 | { 168 | id: "3", 169 | name: "Bob", 170 | email: "bob@example.com", 171 | age: 35, 172 | favoriteBookId: "3", 173 | }, 174 | ]; 175 | 176 | /* ROOT QUERY */ 177 | const schema = makeExecutableSchema({ 178 | typeDefs: typeDefs, 179 | resolvers: resolvers, 180 | }); 181 | 182 | module.exports = schema; 183 | -------------------------------------------------------------------------------- /testServer/testServer.ts: -------------------------------------------------------------------------------- 1 | 2 | import express from 'express' 3 | const { ApolloServer } = require("apollo-server"); 4 | const typeDefs = require("./schema"); 5 | 6 | const server = new ApolloServer({ typeDefs }); 7 | //@ts-ignore 8 | app.use('/graphql', server) 9 | //@ts-ignore 10 | app.listen(4000, () => { 11 | console.log('testServer running on port 4000') 12 | }) --------------------------------------------------------------------------------