├── packages ├── server │ ├── .env.example │ ├── src │ │ ├── loaders │ │ │ ├── index.ts │ │ │ └── userLoader.ts │ │ ├── type │ │ │ ├── MutationType.ts │ │ │ └── QueryType.ts │ │ ├── schema.ts │ │ ├── modules │ │ │ └── main │ │ │ │ ├── mutation │ │ │ │ ├── index.ts │ │ │ │ ├── UserDeleteMutation.ts │ │ │ │ ├── UserUpdateMutation.ts │ │ │ │ ├── UserLoginMutation.ts │ │ │ │ └── UserRegisterMutation.ts │ │ │ │ ├── OrganizationType.ts │ │ │ │ └── UserType.ts │ │ ├── models │ │ │ ├── Organization.ts │ │ │ └── User.ts │ │ ├── database.ts │ │ ├── index.js │ │ ├── auth.ts │ │ └── app.ts │ ├── esbuild.config.js │ ├── webpack.config.js │ ├── .gitignore │ ├── webpack │ │ ├── webpack.prod.js │ │ ├── webpack.common.js │ │ └── webpack.dev.js │ ├── .babelrc.js │ ├── .eslintrc │ ├── jest.config.js │ ├── README.md │ ├── yarn-error.log │ ├── package.json │ ├── data │ │ └── schema.graphql │ └── tsconfig.json ├── mdx │ ├── src │ │ ├── deck.js │ │ ├── img │ │ │ ├── json.png │ │ │ ├── me.jpg │ │ │ ├── cursor.png │ │ │ ├── fields.png │ │ │ ├── github.png │ │ │ ├── react.png │ │ │ ├── graphql.png │ │ │ ├── mutation.png │ │ │ ├── twitter.png │ │ │ ├── delete-user.png │ │ │ ├── fetch-users.png │ │ │ ├── query-type.png │ │ │ ├── api-response.png │ │ │ ├── documentation.png │ │ │ ├── fetch-graphql.png │ │ │ ├── organization.png │ │ │ ├── graphql-request.png │ │ │ ├── graphql-response.png │ │ │ ├── graphql-vs-rest.jpeg │ │ │ ├── book-pagination-1.png │ │ │ ├── book-pagination-2.png │ │ │ ├── documentation-user.png │ │ │ ├── graphql-objectType.png │ │ │ ├── mutation-response.png │ │ │ ├── mutation-delete-user.png │ │ │ └── organization-response.png │ │ ├── VarLetConst.js │ │ ├── theme.js │ │ ├── Thanks.js │ │ ├── Img.js │ │ ├── Cover.js │ │ ├── Intro.js │ │ └── deck.mdx │ ├── .gitignore │ └── package.json └── client │ ├── src │ ├── auth.tsx │ ├── App.css │ ├── assets │ │ └── images │ │ │ ├── home.png │ │ │ ├── login.png │ │ │ ├── backend.png │ │ │ ├── editUser.png │ │ │ ├── frontend.png │ │ │ ├── location.png │ │ │ ├── register.png │ │ │ └── yin-yang.png │ ├── relay │ │ ├── index.tsx │ │ ├── Environment.tsx │ │ ├── ExecuteEnvironment.tsx │ │ ├── cacheHandler.tsx │ │ ├── createQueryRendererModern.tsx │ │ ├── fetchQuery.tsx │ │ ├── relayTransactionLogger.tsx │ │ ├── helpers.tsx │ │ └── fetchWithRetries.tsx │ ├── index.tsx │ ├── routes │ │ ├── Home │ │ │ ├── styles.tsx │ │ │ └── index.tsx │ │ ├── Edit │ │ │ ├── styles.tsx │ │ │ └── index.tsx │ │ ├── Register │ │ │ ├── styles.tsx │ │ │ └── index.tsx │ │ └── Login │ │ │ ├── styles.tsx │ │ │ └── index.tsx │ ├── components │ │ ├── Input │ │ │ ├── styles.tsx │ │ │ └── index.tsx │ │ ├── PrivateRouter.tsx │ │ └── UserList │ │ │ ├── styles.tsx │ │ │ └── index.tsx │ ├── App.tsx │ ├── ErrorBoundary.tsx │ └── Router.tsx │ ├── jest-eslint.config.js │ ├── tsconfig.json │ ├── .eslintrc │ ├── jest.config.js │ ├── babel.config.js │ ├── README.md │ ├── package.json │ └── webpack.config.js ├── babel.config.js ├── .gitignore ├── LICENSE ├── .eslintrc ├── package.json ├── README.md └── tsconfig.json /packages/server/.env.example: -------------------------------------------------------------------------------- 1 | PORT= -------------------------------------------------------------------------------- /packages/mdx/src/deck.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | 3 | ]; 4 | -------------------------------------------------------------------------------- /packages/server/src/loaders/index.ts: -------------------------------------------------------------------------------- 1 | export { userLoader } from "./userLoader"; 2 | -------------------------------------------------------------------------------- /packages/client/src/auth.tsx: -------------------------------------------------------------------------------- 1 | export default () => localStorage.getItem('token'); 2 | -------------------------------------------------------------------------------- /packages/mdx/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .DS_Store 4 | intro-react 5 | /public -------------------------------------------------------------------------------- /packages/mdx/src/img/json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/mdx/src/img/json.png -------------------------------------------------------------------------------- /packages/mdx/src/img/me.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/mdx/src/img/me.jpg -------------------------------------------------------------------------------- /packages/client/src/App.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | font-family:Arial, Helvetica, sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /packages/mdx/src/img/cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/mdx/src/img/cursor.png -------------------------------------------------------------------------------- /packages/mdx/src/img/fields.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/mdx/src/img/fields.png -------------------------------------------------------------------------------- /packages/mdx/src/img/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/mdx/src/img/github.png -------------------------------------------------------------------------------- /packages/mdx/src/img/react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/mdx/src/img/react.png -------------------------------------------------------------------------------- /packages/mdx/src/img/graphql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/mdx/src/img/graphql.png -------------------------------------------------------------------------------- /packages/mdx/src/img/mutation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/mdx/src/img/mutation.png -------------------------------------------------------------------------------- /packages/mdx/src/img/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/mdx/src/img/twitter.png -------------------------------------------------------------------------------- /packages/mdx/src/img/delete-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/mdx/src/img/delete-user.png -------------------------------------------------------------------------------- /packages/mdx/src/img/fetch-users.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/mdx/src/img/fetch-users.png -------------------------------------------------------------------------------- /packages/mdx/src/img/query-type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/mdx/src/img/query-type.png -------------------------------------------------------------------------------- /packages/mdx/src/img/api-response.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/mdx/src/img/api-response.png -------------------------------------------------------------------------------- /packages/mdx/src/img/documentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/mdx/src/img/documentation.png -------------------------------------------------------------------------------- /packages/mdx/src/img/fetch-graphql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/mdx/src/img/fetch-graphql.png -------------------------------------------------------------------------------- /packages/mdx/src/img/organization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/mdx/src/img/organization.png -------------------------------------------------------------------------------- /packages/mdx/src/img/graphql-request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/mdx/src/img/graphql-request.png -------------------------------------------------------------------------------- /packages/mdx/src/img/graphql-response.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/mdx/src/img/graphql-response.png -------------------------------------------------------------------------------- /packages/mdx/src/img/graphql-vs-rest.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/mdx/src/img/graphql-vs-rest.jpeg -------------------------------------------------------------------------------- /packages/client/src/assets/images/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/client/src/assets/images/home.png -------------------------------------------------------------------------------- /packages/client/src/assets/images/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/client/src/assets/images/login.png -------------------------------------------------------------------------------- /packages/mdx/src/img/book-pagination-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/mdx/src/img/book-pagination-1.png -------------------------------------------------------------------------------- /packages/mdx/src/img/book-pagination-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/mdx/src/img/book-pagination-2.png -------------------------------------------------------------------------------- /packages/mdx/src/img/documentation-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/mdx/src/img/documentation-user.png -------------------------------------------------------------------------------- /packages/mdx/src/img/graphql-objectType.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/mdx/src/img/graphql-objectType.png -------------------------------------------------------------------------------- /packages/mdx/src/img/mutation-response.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/mdx/src/img/mutation-response.png -------------------------------------------------------------------------------- /packages/client/src/assets/images/backend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/client/src/assets/images/backend.png -------------------------------------------------------------------------------- /packages/client/src/assets/images/editUser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/client/src/assets/images/editUser.png -------------------------------------------------------------------------------- /packages/client/src/assets/images/frontend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/client/src/assets/images/frontend.png -------------------------------------------------------------------------------- /packages/client/src/assets/images/location.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/client/src/assets/images/location.png -------------------------------------------------------------------------------- /packages/client/src/assets/images/register.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/client/src/assets/images/register.png -------------------------------------------------------------------------------- /packages/client/src/assets/images/yin-yang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/client/src/assets/images/yin-yang.png -------------------------------------------------------------------------------- /packages/mdx/src/img/mutation-delete-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/mdx/src/img/mutation-delete-user.png -------------------------------------------------------------------------------- /packages/mdx/src/img/organization-response.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wellers0n/yin-yang-playground/HEAD/packages/mdx/src/img/organization-response.png -------------------------------------------------------------------------------- /packages/client/src/relay/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as createQueryRendererModern } from './createQueryRendererModern'; 2 | export { default as Environment } from './Environment'; -------------------------------------------------------------------------------- /packages/client/jest-eslint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | runner: 'jest-runner-eslint', 3 | displayName: 'lint', 4 | testMatch: ['/src/**/*.js'], 5 | watchPlugins: ['jest-runner-eslint/watch-fix'], 6 | }; -------------------------------------------------------------------------------- /packages/server/src/type/MutationType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType } from 'graphql'; 2 | 3 | import CompanyMutations from '../modules/main/mutation'; 4 | 5 | export default new GraphQLObjectType({ 6 | name: 'MutationType', 7 | fields: () => ({ 8 | ...CompanyMutations, 9 | }), 10 | }); -------------------------------------------------------------------------------- /packages/server/src/schema.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from "graphql"; 2 | import QueryType from "./type/QueryType"; 3 | import MutationType from "./type/MutationType"; 4 | 5 | const schema = new GraphQLSchema({ 6 | query: QueryType, 7 | mutation: MutationType, 8 | }); 9 | 10 | export default schema; 11 | -------------------------------------------------------------------------------- /packages/server/esbuild.config.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import esbuildServe from "esbuild-serve"; 4 | 5 | esbuildServe( 6 | { 7 | logLevel: "info", 8 | target: "node", 9 | entryPoints: ["src/index.js"], 10 | bundle: true, 11 | outfile: "dist/main.js", 12 | }, 13 | { root: "dist", port: 5002 } 14 | ); -------------------------------------------------------------------------------- /packages/client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import 'core-js/stable'; 2 | import 'regenerator-runtime/runtime'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | 6 | import App from './App'; 7 | import './App.css' 8 | 9 | const root = document.createElement('div'); 10 | document.body.appendChild(root); 11 | 12 | ReactDOM.render(, root); -------------------------------------------------------------------------------- /packages/server/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: 0 */ 2 | /* eslint import/no-dynamic-require: 0 */ 3 | 4 | module.exports = (env) => { 5 | let mode = 'dev'; 6 | 7 | if (env.prod) { 8 | mode = 'prod'; 9 | } else if (env.common) { 10 | mode = 'common'; 11 | } 12 | 13 | return require(`./webpack/webpack.${mode}`); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/client/src/routes/Home/styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components" 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | ` 7 | 8 | export const Title = styled.div` 9 | display: flex; 10 | padding: 15px 10px; 11 | font-size: 20px; 12 | color: #003825; 13 | p { 14 | margin-left: 5px; 15 | color: #1aebb7 16 | } 17 | ` -------------------------------------------------------------------------------- /packages/server/src/modules/main/mutation/index.ts: -------------------------------------------------------------------------------- 1 | import UserLoginMutation from "./UserLoginMutation"; 2 | import UserRegisterMutation from "./UserRegisterMutation"; 3 | import UserDeleteMutation from "./UserDeleteMutation"; 4 | import UserUpdateMutation from "./UserUpdateMutation"; 5 | 6 | export default { 7 | UserLoginMutation, 8 | UserRegisterMutation, 9 | UserDeleteMutation, 10 | UserUpdateMutation, 11 | }; 12 | -------------------------------------------------------------------------------- /packages/mdx/src/VarLetConst.js: -------------------------------------------------------------------------------- 1 | var name = "Wellerson"; 2 | let age = 21; 3 | const location = "Brazil, RJ"; 4 | 5 | name = "Moreira"; 6 | age = 22; 7 | // location = 'Brazil' 8 | 9 | // with scope global 10 | 11 | for (var i = 0; i < array.length; i++) { 12 | console.log(array[i]); 13 | } 14 | 15 | i; 16 | 17 | // without scope global 18 | for (let i = 0; i < array.length; i++) { 19 | console.log(array[i]); 20 | } 21 | -------------------------------------------------------------------------------- /packages/server/src/loaders/userLoader.ts: -------------------------------------------------------------------------------- 1 | import DataLoader from "dataloader"; 2 | import UserModel from "../models/User"; 3 | 4 | export const userLoader = new DataLoader(async (ids) => { 5 | const users = await UserModel.find({ _id: { $in: ids } }); 6 | 7 | const userMap = {}; 8 | 9 | users.forEach((user) => { 10 | userMap[user._id] = user; 11 | }); 12 | 13 | return ids.map((id: string) => userMap[id]); 14 | }); 15 | -------------------------------------------------------------------------------- /packages/mdx/src/theme.js: -------------------------------------------------------------------------------- 1 | import { themes } from "mdx-deck"; 2 | 3 | const { future } = themes; 4 | 5 | export default { 6 | ...future, 7 | colors: { 8 | text: '#c6d1dd', 9 | background: "#272425", 10 | }, 11 | // Customize your presentation theme here. 12 | // 13 | // Read the docs for more info: 14 | // https://github.com/jxnblk/mdx-deck/blob/master/docs/theming.md 15 | // https://github.com/jxnblk/mdx-deck/blob/master/docs/themes.md 16 | }; 17 | -------------------------------------------------------------------------------- /packages/server/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.sublime-project 3 | *.sublime-workspace 4 | .idea/ 5 | .vscode/ 6 | package-lock.json 7 | 8 | lib-cov 9 | *.seed 10 | *.log 11 | *.csv 12 | *.dat 13 | *.out 14 | *.pid 15 | *.gz 16 | *.map 17 | 18 | pids 19 | logs 20 | results 21 | 22 | node_modules 23 | npm-debug.log 24 | 25 | dump.rdb 26 | bundle.js 27 | 28 | # jest stuff 29 | test-results 30 | 31 | dist 32 | coverage 33 | .nyc_output 34 | flow-coverage 35 | 36 | .reify-cache/ -------------------------------------------------------------------------------- /packages/server/src/models/Organization.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const { Schema } = mongoose; 4 | 5 | const Organization = new Schema({ 6 | name: { 7 | type: String, 8 | required: "name is requerid", 9 | }, 10 | description: { 11 | type: String, 12 | required: "description is requerid", 13 | }, 14 | email: { 15 | type: String, 16 | required: "email is requerid", 17 | }, 18 | }); 19 | 20 | export default mongoose.model("Organization", Organization); 21 | -------------------------------------------------------------------------------- /packages/server/src/database.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | export default function connectDatabase() { 4 | return new Promise((resolve, reject) => { 5 | mongoose.Promise = global.Promise; 6 | mongoose.connection 7 | .on("error", (error) => reject(error)) 8 | .on("close", () => console.log("Database connection closed.")) 9 | .once("open", () => resolve(mongoose.connections[0])); 10 | 11 | mongoose.connect( 12 | "mongodb://localhost:27017/yin-yang", 13 | ); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /packages/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": [ 5 | "esnext", 6 | "dom" 7 | ] /* Specify library files to be included in the compilation. */, 8 | "allowJs": false /* Allow javascript files to be compiled. */, 9 | "checkJs": false /* Report errors in .js files. */, 10 | "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, 11 | "types": ["react-dom/experimental"] 12 | }, 13 | "include": ["./src/**/*"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/server/webpack/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | const webpack = require('webpack'); 3 | const merge = require('webpack-merge'); 4 | const WebpackNodeExternals = require('webpack-node-externals'); 5 | 6 | const common = require('./webpack.common'); 7 | 8 | module.exports = merge.smart(common, { 9 | mode: 'production', 10 | externals: [WebpackNodeExternals()], 11 | plugins: [ 12 | new webpack.DefinePlugin({ 13 | 'process.env.NODE_ENV': JSON.stringify('production'), 14 | }), 15 | ], 16 | }); 17 | -------------------------------------------------------------------------------- /packages/server/src/index.js: -------------------------------------------------------------------------------- 1 | import "@babel/polyfill"; 2 | import { createServer } from "http"; 3 | import connectDatabase from "./database"; 4 | import app from "./app"; 5 | 6 | (async () => { 7 | try { 8 | await connectDatabase(); 9 | } catch (error) { 10 | console.log("Could not connect to database", { error }); 11 | throw error; 12 | } 13 | const server = createServer(app.callback()); 14 | server.listen(5001, () => { 15 | console.log( 16 | `SERVER ON: http://localhost:${5001}/graphql` 17 | ); 18 | }); 19 | })(); 20 | -------------------------------------------------------------------------------- /packages/client/src/components/Input/styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | width: 100%; 6 | flex-direction: column; 7 | margin: 20px 0; 8 | `; 9 | 10 | export const Title = styled.div` 11 | font-size: 18px; 12 | color: #363636; 13 | margin-bottom: 8px; 14 | 15 | ` 16 | 17 | export const Input = styled.input` 18 | height: 50px; 19 | border: 1px solid #F2F2F2; 20 | padding-left: 8px; 21 | &::placeholder { 22 | color: #DFDFDF 23 | } 24 | ` 25 | 26 | 27 | -------------------------------------------------------------------------------- /packages/client/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "jest": true 7 | }, 8 | "parserOptions": { 9 | "ecmaVersion": 6, 10 | "sourceType": "module", 11 | "ecmaFeatures": { 12 | "jsx": true, 13 | "modules": true 14 | } 15 | }, 16 | "settings": { 17 | "react": { 18 | "createClass": "createReactClass", 19 | "pragma": "React", 20 | "version": "16.6" 21 | } 22 | }, 23 | "plugins": ["react"], 24 | "extends": ["eslint:recommended", "plugin:react/recommended"] 25 | } 26 | -------------------------------------------------------------------------------- /packages/server/.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | '@babel/preset-typescript', 12 | ], 13 | plugins: [ 14 | '@babel/plugin-proposal-object-rest-spread', 15 | '@babel/plugin-proposal-class-properties', 16 | '@babel/plugin-proposal-export-default-from', 17 | '@babel/plugin-proposal-export-namespace-from', 18 | '@babel/plugin-transform-async-to-generator', 19 | '@babel/plugin-syntax-async-generators', 20 | ], 21 | }; -------------------------------------------------------------------------------- /packages/mdx/src/Thanks.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components' 3 | 4 | export const Root = styled.div([], { 5 | width: '50vw', 6 | height: '70vh', 7 | }); 8 | 9 | const ThanksText = styled.span` 10 | font-size: 50px; 11 | color: #ffffff; 12 | `; 13 | 14 | export const Center = styled.div` 15 | display: flex; 16 | flex: 1; 17 | flex-direction: row; 18 | align-items: center; 19 | justify-content: center; 20 | `; 21 | 22 | 23 | export const Thanks = () => ( 24 | 25 |
26 | Thanks! 27 |
28 |
29 | ); -------------------------------------------------------------------------------- /packages/server/src/auth.ts: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import User from "./models/User"; 3 | 4 | export async function getUser(token: string | undefined) { 5 | if (!token) return { user: null }; 6 | 7 | try { 8 | const decodedToken: any = jwt.verify(token, "batman"); 9 | 10 | const user = await User.findOne({ _id: decodedToken.id }); 11 | 12 | return { 13 | user, 14 | }; 15 | } catch (err) { 16 | return { user: null }; 17 | } 18 | } 19 | 20 | type UserType = { 21 | _id: string; 22 | }; 23 | 24 | export function generateToken(user: UserType) { 25 | return jwt.sign({ id: user._id }, "batman"); 26 | } 27 | -------------------------------------------------------------------------------- /packages/client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from "react"; 2 | import { RelayEnvironmentProvider } from "react-relay/hooks"; 3 | import RelayEnvironment from "./relay/Environment"; 4 | import ErrorBoundary from "./ErrorBoundary"; 5 | import { hot } from "react-hot-loader"; 6 | import Router from "./Router"; 7 | 8 | function App() { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | } 19 | 20 | export default hot(module)(App); 21 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // const { workspaces = [] } = require('./package.json'); 2 | 3 | module.exports = { 4 | presets: [ 5 | '@babel/preset-flow', 6 | [ 7 | '@babel/preset-env', 8 | { 9 | targets: { 10 | node: 'current', 11 | }, 12 | }, 13 | ], 14 | '@babel/preset-typescript', 15 | ], 16 | plugins: [ 17 | '@babel/plugin-proposal-object-rest-spread', 18 | '@babel/plugin-proposal-class-properties', 19 | '@babel/plugin-proposal-export-default-from', 20 | '@babel/plugin-proposal-export-namespace-from', 21 | '@babel/plugin-transform-async-to-generator', 22 | '@babel/plugin-syntax-async-generators', 23 | ], 24 | }; -------------------------------------------------------------------------------- /packages/client/src/components/PrivateRouter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Route, Redirect, RouteProps } from "react-router-dom"; 3 | import auth from "../auth"; 4 | 5 | const PrivateRouter: React.FunctionComponent = ({ 6 | component: Component, 7 | ...rest 8 | }) => { 9 | return ( 10 | 13 | auth() ? ( 14 |
15 | 16 |
17 | ) : ( 18 | 19 | ) 20 | } 21 | /> 22 | ); 23 | }; 24 | 25 | export default PrivateRouter; 26 | -------------------------------------------------------------------------------- /packages/server/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "parser": "babel-eslint", 4 | "rules": { 5 | "no-console": 0, 6 | "quotes": "off", 7 | "max-len": [1, 120, 2], 8 | "no-param-reassign": [2, { "props": false }], 9 | "no-continue": 0, 10 | "no-underscore-dangle": 0, 11 | "generator-star-spacing": 0, 12 | "object-curly-newline": 0, 13 | "import/prefer-default-export": 0, 14 | "function-paren-newline": 0, 15 | "import/no-extraneous-dependencies": 0 16 | }, 17 | "env": { 18 | "jest": true 19 | }, 20 | "settings": { 21 | "import/resolver": { 22 | "node": true, 23 | "eslint-import-resolver-typescript": true 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/mdx/src/Img.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components' 3 | import { width } from 'styled-system'; 4 | 5 | const StyledImg = styled.img` 6 | ${width} 7 | `; 8 | 9 | const Root = styled.div([], { 10 | 11 | display: "flex", 12 | justifyContent: "center", 13 | alignItems: "center", 14 | flexDirection: 'column' 15 | }); 16 | 17 | export const Center = styled.div` 18 | display: flex; 19 | flex: 1; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | `; 24 | 25 | export const Img = ({ src, ...props}) => ( 26 | 27 |
28 | 29 |
30 |
31 | ); 32 | -------------------------------------------------------------------------------- /packages/server/src/models/User.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import DataLoader from "dataloader"; 3 | 4 | const { Schema } = mongoose; 5 | 6 | const User = new Schema({ 7 | name: { 8 | type: String, 9 | required: "name is requerid", 10 | index: true, 11 | }, 12 | description: { 13 | type: String, 14 | required: "description is requerid", 15 | }, 16 | email: { 17 | type: String, 18 | required: "email is requerid", 19 | index: true, 20 | }, 21 | password: { 22 | type: String, 23 | required: "password is requerid", 24 | }, 25 | organizationIds: { 26 | type: Array, 27 | index: true, 28 | }, 29 | }); 30 | 31 | export default mongoose.model("User", User); 32 | -------------------------------------------------------------------------------- /packages/server/webpack/webpack.common.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | 3 | module.exports = { 4 | entry: { 5 | index: ['./src/index.js'], 6 | }, 7 | output: { 8 | filename: '[name].js', 9 | path: resolve('dist'), 10 | }, 11 | target: 'node', 12 | node: { 13 | __filename: false, 14 | __dirname: false, 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.(js|ts|tsx)$/, 20 | exclude: /node_modules/, 21 | use: ['babel-loader'], 22 | }, 23 | { 24 | test: /\.mjs$/, 25 | include: /node_modules/, 26 | type: 'javascript/auto', 27 | }, 28 | ], 29 | }, 30 | resolve: { 31 | extensions: ['.js', '.ts', '.tsx'], 32 | }, 33 | }; -------------------------------------------------------------------------------- /packages/client/src/components/Input/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Container, Title, Input } from "./styles"; 3 | 4 | type Props = { 5 | title: string; 6 | placeholder?: string; 7 | type: "text" | "password" | "description"; 8 | value: string; 9 | onChange: Function; 10 | }; 11 | 12 | const InputComponent = (props: Props) => { 13 | const { title, placeholder = "", type = "text", value, onChange } = props; 14 | return ( 15 | 16 | {title} 17 | onChange(e.target.value)} 22 | /> 23 | 24 | ); 25 | }; 26 | 27 | export default InputComponent; 28 | -------------------------------------------------------------------------------- /packages/client/src/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type Props = { 4 | children: React.ReactNode; 5 | }; 6 | type State = { 7 | error: Error | null; 8 | }; 9 | 10 | export default class ErrorBoundary extends React.Component { 11 | state = { error: null }; 12 | 13 | static getDerivedStateFromError(error: any) { 14 | return { 15 | error, 16 | }; 17 | } 18 | 19 | render() { 20 | if (this.state.error != null) { 21 | return ( 22 |
23 |
Error: {this.state.error.message}
24 |
25 |
{JSON.stringify(this.state.error.source, null, 2)}
26 |
27 |
28 | ); 29 | } 30 | return this.props.children; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/client/src/relay/Environment.tsx: -------------------------------------------------------------------------------- 1 | // import { installRelayDevTools } from 'relay-devtools'; 2 | import { Environment, Network, RecordSource, Store } from 'relay-runtime'; 3 | import { relayTransactionLogger } from './relayTransactionLogger'; 4 | 5 | import cacheHandler from './cacheHandler'; 6 | 7 | const __DEV__ = process.env.NODE_ENV === 'development'; 8 | // if (__DEV__) { 9 | // installRelayDevTools(); 10 | // } 11 | 12 | const network = Network.create(cacheHandler); 13 | 14 | const source = new RecordSource(); 15 | const store = new Store(source); 16 | 17 | // export const inspector = new RecordSourceInspector(source); 18 | 19 | const env = new Environment({ 20 | network, 21 | store, 22 | log: __DEV__ ? relayTransactionLogger : null, 23 | }); 24 | 25 | export default env; -------------------------------------------------------------------------------- /packages/client/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | notify: false, 3 | bail: false, 4 | displayName: 'web-app', 5 | transformIgnorePatterns: ['/node_modules/', './dist'], 6 | testEnvironment: 'jest-environment-jsdom-fifteen', 7 | setupFilesAfterEnv: ['test/setupFilesAfterEnv.ts'], 8 | coverageReporters: ['lcov', 'html'], 9 | transform: { 10 | '^.+\\.tsx?$': ['babel-jest', { cwd: __dirname }], 11 | }, 12 | rootDir: './', 13 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', 14 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 15 | automock: false, 16 | moduleNameMapper: { 17 | '.(css)$': '/test_utils/stub.js', 18 | '.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 19 | '/test_utils/stub.js', 20 | '.(woff|woff2|otf|ttf|eot|csv)$': '/test_utils/stub.js', 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /packages/client/src/relay/ExecuteEnvironment.tsx: -------------------------------------------------------------------------------- 1 | const canUseDOM = !!( 2 | typeof window !== "undefined" && 3 | window.document && 4 | window.document.createElement 5 | ); 6 | 7 | /** 8 | * Simple, lightweight module assisting with the detection and context of 9 | * Worker. Helps avoid circular dependencies and allows code to reason about 10 | * whether or not they are in a Worker, even if they never include the main 11 | * `ReactWorker` dependency. 12 | */ 13 | const ExecutionEnvironment = { 14 | canUseDOM: canUseDOM, 15 | 16 | canUseWorkers: typeof Worker !== "undefined", 17 | 18 | canUseEventListeners: 19 | canUseDOM && !!(window.addEventListener || (window as any).attachEvent), 20 | 21 | canUseViewport: canUseDOM && !!window.screen, 22 | 23 | isInWorker: !canUseDOM, // For now, this is true - might change in the future. 24 | }; 25 | 26 | export default ExecutionEnvironment; 27 | -------------------------------------------------------------------------------- /packages/server/webpack/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const merge = require("webpack-merge"); 3 | const WebpackNodeExternals = require("webpack-node-externals"); 4 | const AutoReloadServerPlugin = require("auto-reload-webpack-plugin"); 5 | 6 | const common = require("./webpack.common"); 7 | 8 | module.exports = merge.merge(common, { 9 | mode: "development", 10 | watch: true, 11 | stats: "errors-only", 12 | entry: { 13 | index: ["webpack/hot/poll?1000"], 14 | }, 15 | externals: [ 16 | WebpackNodeExternals({ 17 | allowlist: ["webpack/hot/poll?1000"], 18 | }), 19 | ], 20 | plugins: [ 21 | new AutoReloadServerPlugin({ 22 | filePath: "dist/index.js", 23 | }), 24 | new webpack.HotModuleReplacementPlugin(), 25 | new webpack.DefinePlugin({ 26 | "process.env.NODE_ENV": JSON.stringify("development"), 27 | }), 28 | ], 29 | }); 30 | -------------------------------------------------------------------------------- /packages/mdx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yin-yang/mdx", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "private": true, 7 | "scripts": { 8 | "build": "mdx-deck build src/deck.mdx", 9 | "help": "mdx-deck", 10 | "image": "mdx-deck screenshot src/deck.mdx", 11 | "pdf": "mdx-deck pdf src/deck.mdx", 12 | "copy:static": "cp -r src/img dist", 13 | "start": "mdx-deck src/deck.mdx", 14 | "publish:deck": "yarn build && yarn copy:static && vercel dist/" 15 | }, 16 | "dependencies": { 17 | "babel": "^6.23.0", 18 | "babel-runtime": "^6.26.0", 19 | "codemirror": "^5.62.0", 20 | "mdx-code": "^1.1.4", 21 | "mdx-deck": "^1.10.2", 22 | "mdx-deck-live-code": "^1.0.1", 23 | "qrcode.react": "^0.9.3", 24 | "raw-loader": "^4.0.2", 25 | "react-codemirror2": "^5.1.0", 26 | "rebass": "^4.0.7", 27 | "vercel": "^23.0.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/server/jest.config.js: -------------------------------------------------------------------------------- 1 | const pack = require('./package.json'); 2 | 3 | module.exports = { 4 | displayName: pack.name, 5 | name: pack.name, 6 | testEnvironment: '/test/environment/mongodb', 7 | testPathIgnorePatterns: ['/node_modules/', './dist'], 8 | setupFilesAfterEnv: ['/test/setupTestFramework.js'], 9 | globalSetup: '/test/setup.js', 10 | globalTeardown: '/test/teardown.js', 11 | resetModules: false, 12 | reporters: [ 13 | 'default', 14 | [ 15 | 'jest-junit', 16 | { 17 | suiteName: 'GraphQL Dataloader Boilerplate Tests', 18 | output: './test-results/jest/results.xml', 19 | }, 20 | ], 21 | ], 22 | transform: { 23 | '^.+\\.(js|ts|tsx)?$': '/test/babel-transformer', 24 | }, 25 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(js?|ts?)$', 26 | moduleFileExtensions: ['ts', 'js'], 27 | 28 | }; 29 | -------------------------------------------------------------------------------- /packages/client/src/routes/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { graphql, useLazyLoadQuery } from "react-relay"; 3 | import { Container, Title } from "./styles"; 4 | 5 | const UserList = React.lazy(() => import("../../components/UserList")); 6 | 7 | import { HomeQuery } from "./__generated__/HomeQuery.graphql"; 8 | 9 | const Home = (props: any) => { 10 | const data = useLazyLoadQuery( 11 | graphql` 12 | query HomeQuery { 13 | ...UserList_query 14 | me { 15 | id 16 | name 17 | } 18 | } 19 | `, 20 | { }, 21 | { fetchPolicy: "network-only" } 22 | ); 23 | 24 | const { me } = data; 25 | 26 | return ( 27 | 28 | 29 | Seja bem vindo <p>{me?.name}</p> 30 | 31 |
32 | 33 |
34 |
35 | ); 36 | }; 37 | 38 | export default Home; 39 | -------------------------------------------------------------------------------- /packages/server/src/modules/main/mutation/UserDeleteMutation.ts: -------------------------------------------------------------------------------- 1 | import User from "../../../models/User"; 2 | import { GraphQLString, GraphQLNonNull, GraphQLList } from "graphql"; 3 | import { mutationWithClientMutationId } from "graphql-relay"; 4 | import UserType from "../UserType"; 5 | 6 | export default mutationWithClientMutationId({ 7 | name: "UserDelete", 8 | inputFields: { 9 | id: { 10 | type: new GraphQLNonNull(GraphQLString), 11 | }, 12 | }, 13 | mutateAndGetPayload: async ({ id }, ctx) => { 14 | await User.deleteOne({ _id: id }); 15 | const users = await User.find({ email: { $ne: ctx.user.email } }); 16 | 17 | return { 18 | message: "User deleted with successfully", 19 | users, 20 | }; 21 | }, 22 | outputFields: { 23 | users: { 24 | type: GraphQLList(UserType), 25 | resolve: ({ users }) => users, 26 | }, 27 | message: { 28 | type: GraphQLString, 29 | resolve: ({ message }) => message, 30 | }, 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.sublime-project 3 | *.sublime-workspace 4 | .idea/ 5 | 6 | lib-cov 7 | *.seed 8 | *.log 9 | *.csv 10 | *.dat 11 | *.out 12 | *.pid 13 | *.gz 14 | *.map 15 | 16 | pids 17 | logs 18 | results 19 | test-results 20 | 21 | node_modules 22 | npm-debug.log 23 | 24 | dump.rdb 25 | bundle.js 26 | 27 | build 28 | dist 29 | coverage 30 | .nyc_output 31 | .env 32 | 33 | graphql.*.json 34 | junit.xml 35 | 36 | .vs 37 | 38 | test/globalConfig.json 39 | distTs 40 | 41 | # Random things to ignore 42 | ignore/ 43 | package-lock.json 44 | /yarn-offline-cache 45 | .cache 46 | 47 | **/node_modules 48 | .idea 49 | yarn-error.log 50 | .vs/ 51 | 52 | # IDE/Editor GraphQL Extensions 53 | .gqlconfig 54 | 55 | # Relay modern 56 | __generated__/ 57 | 58 | # Flow log 59 | flow.log 60 | 61 | # Random things to ignore 62 | yarn-offline-cache 63 | 64 | hard-source-cache/ 65 | **/hard-source-cache/ 66 | 67 | # Cypress 68 | cypress/videos 69 | cypress/screenshots 70 | 71 | differencify_reports 72 | .parcel-cache 73 | 74 | .vscode 75 | .yarn 76 | .yarnrc 77 | -------------------------------------------------------------------------------- /packages/server/src/modules/main/OrganizationType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType, GraphQLString, GraphQLID } from "graphql"; 2 | import { connectionDefinitions, globalIdField } from "graphql-relay"; 3 | 4 | const OrganizationType = new GraphQLObjectType({ 5 | name: "Organization", 6 | description: "Organization data", 7 | fields: () => ({ 8 | id: globalIdField("Organization"), 9 | _id: { 10 | type: GraphQLID, 11 | resolve: (organization) => organization._id, 12 | }, 13 | name: { 14 | type: GraphQLString, 15 | resolve: (organization) => organization.name, 16 | }, 17 | email: { 18 | type: GraphQLString, 19 | resolve: (organization) => organization.email, 20 | }, 21 | description: { 22 | type: GraphQLString, 23 | resolve: (organization) => organization.description, 24 | }, 25 | }), 26 | }); 27 | 28 | export default OrganizationType; 29 | 30 | export const OrganizationConnection = connectionDefinitions({ 31 | name: "Organization", 32 | nodeType: OrganizationType, 33 | }); 34 | -------------------------------------------------------------------------------- /packages/client/src/Router.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from "react"; 2 | import { BrowserRouter, Route, Switch } from "react-router-dom"; 3 | import PrivateRouter from "./components/PrivateRouter"; 4 | 5 | const Login = React.lazy(() => import("./routes/Login")); 6 | const Register = React.lazy(() => import("./routes/Register")); 7 | const Home = React.lazy(() => import("./routes/Home")); 8 | const Edit = React.lazy(() => import("./routes/Edit")); 9 | 10 | const router = () => { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | const NotFoundScreen = () => { 27 | return
page not found
; 28 | }; 29 | 30 | export default router; 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Wellerson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb", 4 | "plugin:@typescript-eslint/recommended", 5 | "prettier", 6 | "prettier/react", 7 | "plugin:import/errors", 8 | "plugin:import/warnings", 9 | "plugin:import/typescript" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "project": "./tsconfig.json" 14 | }, 15 | "plugins": ["@typescript-eslint", "prettier", "import"], 16 | "rules": { 17 | "@typescript-eslint/indent": [2, 2], 18 | "no-console": "off", 19 | "import/no-unresolved": [2, { "commonjs": true, "amd": true }], 20 | "import/named": 2, 21 | "import/namespace": 2, 22 | "import/default": 2, 23 | "import/export": 2 24 | }, 25 | "env": { 26 | "jest": true 27 | }, 28 | "settings": { 29 | "import/extensions": [".js", ".jsx", ".ts", ".tsx"], 30 | "import/resolver": { 31 | "node": { 32 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 33 | }, 34 | "typescript": { 35 | "directory": "./tsconfig.json" 36 | }, 37 | "eslint-import-resolver-typescript": true, 38 | "@typescript-eslint/parser": [".ts", ".tsx"] 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/mdx/src/Cover.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { space, width } from "styled-system"; 4 | 5 | const Img = styled.img` 6 | ${width} 7 | `; 8 | 9 | const Root = styled.div([], { 10 | width: "50vw", 11 | height: "70vh", 12 | display: "flex", 13 | justifyContent: "center", 14 | alignItems: "center", 15 | flexDirection: 'column' 16 | }); 17 | 18 | export const Center = styled.div` 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | `; 24 | 25 | const Title = styled.span` 26 | font-size: 50px; 27 | color: #c6d1dd; 28 | ${space} 29 | `; 30 | 31 | const Subtitle = styled.span` 32 | font-size: 40px; 33 | color: #fdaa4c; 34 | ${space} 35 | `; 36 | 37 | const MeName = styled.span` 38 | font-size: 30px; 39 | color: #57ad50; 40 | ${space} 41 | `; 42 | 43 | const ImgCenter = styled.div` 44 | display: flex; 45 | `; 46 | 47 | export const Cover = () => ( 48 | 49 |
50 | 51 | 52 | 53 | GraphQL 54 |
55 |
56 | ); 57 | -------------------------------------------------------------------------------- /packages/client/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-react', 4 | [ 5 | '@babel/preset-env', 6 | { 7 | modules: false, 8 | }, 9 | ], 10 | '@babel/preset-typescript', 11 | ], 12 | plugins: [ 13 | 'react-hot-loader/babel', 14 | 'babel-plugin-styled-components', 15 | [ 16 | 'relay', 17 | { 18 | schema: '../server/data/schema.json', 19 | }, 20 | ], 21 | '@babel/plugin-proposal-object-rest-spread', 22 | '@babel/plugin-proposal-class-properties', 23 | '@babel/plugin-proposal-export-default-from', 24 | '@babel/plugin-proposal-export-namespace-from', 25 | ], 26 | env: { 27 | test: { 28 | presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'], 29 | plugins: [ 30 | '@babel/plugin-transform-runtime', 31 | 'dynamic-import-node', 32 | '@babel/plugin-syntax-dynamic-import', 33 | '@babel/plugin-proposal-object-rest-spread', 34 | '@babel/plugin-proposal-class-properties', 35 | '@babel/plugin-proposal-export-default-from', 36 | '@babel/plugin-proposal-export-namespace-from', 37 | ], 38 | }, 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /packages/client/src/relay/cacheHandler.tsx: -------------------------------------------------------------------------------- 1 | import { Variables, UploadableMap, CacheConfig } from "relay-runtime"; 2 | 3 | import { RequestNode, QueryResponseCache } from "relay-runtime"; 4 | 5 | import fetchQuery from "./fetchQuery"; 6 | import { isMutation, isQuery, forceFetch } from "./helpers"; 7 | 8 | const oneMinute = 60 * 1000; 9 | const relayResponseCache = new QueryResponseCache({ 10 | size: 250, 11 | ttl: oneMinute, 12 | }); 13 | 14 | const cacheHandler = async ( 15 | request: RequestNode, 16 | variables: Variables, 17 | cacheConfig: CacheConfig, 18 | uploadables: UploadableMap 19 | ) => { 20 | const queryID = request.text; 21 | 22 | if (isMutation(request)) { 23 | relayResponseCache.clear(); 24 | return fetchQuery(request, variables, uploadables); 25 | } 26 | 27 | const fromCache = relayResponseCache.get(queryID, variables); 28 | if (isQuery(request) && fromCache !== null && !forceFetch(cacheConfig)) { 29 | return fromCache; 30 | } 31 | 32 | const fromServer = await fetchQuery(request, variables, uploadables); 33 | if (fromServer) { 34 | relayResponseCache.set(queryID, variables, fromServer); 35 | } 36 | 37 | return fromServer; 38 | }; 39 | 40 | export default cacheHandler; 41 | -------------------------------------------------------------------------------- /packages/server/src/modules/main/mutation/UserUpdateMutation.ts: -------------------------------------------------------------------------------- 1 | import User from "../../../models/User"; 2 | import { GraphQLString, GraphQLNonNull, GraphQLList } from "graphql"; 3 | import { mutationWithClientMutationId } from "graphql-relay"; 4 | import UserType from "../UserType"; 5 | 6 | export default mutationWithClientMutationId({ 7 | name: "UserUpdate", 8 | inputFields: { 9 | id: { 10 | type: new GraphQLNonNull(GraphQLString), 11 | }, 12 | name: { 13 | type: new GraphQLNonNull(GraphQLString), 14 | }, 15 | description: { 16 | type: new GraphQLNonNull(GraphQLString), 17 | }, 18 | email: { 19 | type: new GraphQLNonNull(GraphQLString), 20 | }, 21 | }, 22 | mutateAndGetPayload: async ({ id, name, email, description }, ctx) => { 23 | await User.updateOne({ _id: id }, { name, description, email }); 24 | const users = await User.find({ email: { $ne: ctx.user.email } }); 25 | 26 | return { 27 | message: "User update with successfully", 28 | users, 29 | }; 30 | }, 31 | outputFields: { 32 | users: { 33 | type: GraphQLList(UserType), 34 | resolve: ({ users }) => users, 35 | }, 36 | message: { 37 | type: GraphQLString, 38 | resolve: ({ message }) => message, 39 | }, 40 | }, 41 | }); 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yin-yang-playground", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "https://github.com/Wellers0n/code-challenge-entria.git", 6 | "author": "Wellerson ", 7 | "license": "MIT", 8 | "private": true, 9 | "scripts": { 10 | "start": "yarn start:server & yarn start:client", 11 | "start:client": "yarn workspace @yin-yang/client run start", 12 | "start:server": "yarn workspace @yin-yang/server run start" 13 | }, 14 | "lint-staged": { 15 | "*.js": [ 16 | "prettier --write --single-quote true --trailing-comma all --print-width 100", 17 | "yarn jest:lint --passWithNoTests", 18 | "git add" 19 | ], 20 | "*.ts": [ 21 | "prettier --write --single-quote true --trailing-comma all --print-width 100", 22 | "eslint --fix", 23 | "git add" 24 | ], 25 | "*.tsx": [ 26 | "prettier --write --single-quote true --trailing-comma all --print-width 100", 27 | "eslint --fix", 28 | "git add" 29 | ], 30 | "*.yml": [ 31 | "prettier --write", 32 | "git add" 33 | ] 34 | }, 35 | "pre-commit": "lint-staged", 36 | "workspaces": { 37 | "packages": [ 38 | "packages/client", 39 | "packages/server" 40 | ] 41 | }, 42 | "dependencies": { 43 | "esbuild": "^0.15.15" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/server/src/modules/main/mutation/UserLoginMutation.ts: -------------------------------------------------------------------------------- 1 | import User from "../../../models/User"; 2 | import { GraphQLString, GraphQLNonNull } from "graphql"; 3 | import { mutationWithClientMutationId } from "graphql-relay"; 4 | import bcrypt from "bcryptjs"; 5 | import { generateToken } from "../../../auth"; 6 | 7 | export default mutationWithClientMutationId({ 8 | name: "UserLogin", 9 | inputFields: { 10 | email: { 11 | type: new GraphQLNonNull(GraphQLString), 12 | }, 13 | password: { 14 | type: new GraphQLNonNull(GraphQLString), 15 | }, 16 | }, 17 | mutateAndGetPayload: async ({ email, password }) => { 18 | const user = await User.findOne({ email: email.toLowerCase() }); 19 | 20 | if (!user) { 21 | return { 22 | message: "Invalid credentials", 23 | }; 24 | } 25 | 26 | const correctPassword = bcrypt.compareSync(password, user.password); 27 | 28 | if (!correctPassword) { 29 | return { 30 | message: "Invalid credentials", 31 | }; 32 | } 33 | 34 | return { 35 | message: 'Login with successfully', 36 | token: generateToken(user), 37 | }; 38 | }, 39 | outputFields: { 40 | token: { 41 | type: GraphQLString, 42 | resolve: ({ token }) => token, 43 | }, 44 | message: { 45 | type: GraphQLString, 46 | resolve: ({ message }) => message, 47 | }, 48 | }, 49 | }); 50 | -------------------------------------------------------------------------------- /packages/server/src/type/QueryType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType, GraphQLString } from "graphql"; 2 | // types 3 | import UserType, { UserConnection } from "../modules/main/UserType"; 4 | import { connectionArgs, connectionFromArray } from "graphql-relay"; 5 | 6 | // models 7 | import userModel from "../models/User"; 8 | 9 | export default new GraphQLObjectType({ 10 | name: "QueryType", 11 | description: "Get users[], user and me", 12 | fields: () => ({ 13 | me: { 14 | type: UserType, 15 | resolve: (parentValue, args, ctx) => { 16 | return ctx.user ? ctx.loaders.userLoader.load(ctx.user._id) : null; 17 | }, 18 | }, 19 | user: { 20 | type: UserType, 21 | args: { 22 | id: { 23 | type: GraphQLString, 24 | }, 25 | }, 26 | resolve: (parentValue, args, ctx) => { 27 | return ctx.loaders.userLoader.load(args.id); 28 | }, 29 | }, 30 | users: { 31 | type: UserConnection.connectionType, 32 | args: { 33 | ...connectionArgs, 34 | }, 35 | resolve: async (parentValue, args, ctx) => { 36 | const users = await userModel.find({ email: { $ne: ctx.user.email } }); 37 | const ids = users.map((user: any) => user._id); 38 | const batchUsers = await ctx.loaders.userLoader.loadMany(ids); 39 | return connectionFromArray(batchUsers, args); 40 | }, 41 | }, 42 | }), 43 | }); 44 | -------------------------------------------------------------------------------- /packages/client/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

5 | 6 | 7 | 8 | 9 | 10 | 11 |

12 | 13 |

14 |

Frontend

15 |

16 | 17 |
18 | 19 | ## Client package 20 | 21 | The client uses relay and react to build front-end 22 | 23 | ## Screens 24 | 25 | ### Login 26 | 27 | 28 | ### Register 29 | 30 | 31 | ### Home 32 | 33 | 34 | ### Edit User 35 | 36 | 37 | ## Initing in the your PC 38 | 39 | - For clone the project `git clone https://github.com/Wellers0n/yin-yang-playground.git` 40 | - Enter in the folder `cd yin-yang-playground/packages/client` 41 | - To install project dependency: `yarn install` 42 | - After the installation of the dependencies `yarn start` in the default directory 43 | -------------------------------------------------------------------------------- /packages/client/src/relay/createQueryRendererModern.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import hoistStatics from 'hoist-non-react-statics'; 3 | import { QueryRenderer, GraphQLTaggedNode } from 'react-relay'; 4 | import { Variables } from 'relay-runtime' 5 | 6 | import Environment from './Environment'; 7 | 8 | type Config = { 9 | query: GraphQLTaggedNode, 10 | queriesParams?: (props: Object) => object, 11 | variables?: Variables, 12 | hideSplash?: boolean, 13 | }; 14 | 15 | export default function createQueryRenderer

( 16 | FragmentComponent: any, 17 | Component: React.ComponentType

, 18 | config: Config, 19 | ) { 20 | const { query, queriesParams } = config; 21 | 22 | class QueryRendererWrapper extends React.Component<{}> { 23 | render() { 24 | const variables = queriesParams ? queriesParams(this.props) : config.variables; 25 | 26 | return ( 27 | { 32 | if (error) { 33 | return {error.toString()}; 34 | } 35 | 36 | if (props) { 37 | return ; 38 | } 39 | 40 | return loading; 41 | }} 42 | /> 43 | ); 44 | } 45 | } 46 | 47 | return hoistStatics(QueryRendererWrapper, Component); 48 | } -------------------------------------------------------------------------------- /packages/client/src/routes/Edit/styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | height: 100vh; 6 | width: 100%; 7 | `; 8 | 9 | export const Aside = styled.div` 10 | flex: 1; 11 | background-color: #1aebb7; 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: center; 15 | align-items: center; 16 | `; 17 | 18 | export const Wrapper = styled.div` 19 | flex: 2; 20 | background-color: #fff; 21 | display: flex; 22 | justify-content: center; 23 | align-items: center; 24 | flex-direction: column; 25 | `; 26 | 27 | export const Img = styled.img` 28 | width: 100px; 29 | height: 120px; 30 | `; 31 | 32 | export const LogoTitle = styled.h1` 33 | color: black; 34 | margin-bottom: 20px; 35 | margin-top: 10px; 36 | `; 37 | 38 | export const LogoDescription = styled.h3` 39 | color: #003825; 40 | width: 300px; 41 | text-align: center; 42 | `; 43 | 44 | export const Title = styled.h2` 45 | color: black; 46 | `; 47 | 48 | export const Box = styled.div` 49 | width: 50%; 50 | height: 100%; 51 | margin-top: 20px; 52 | display: flex; 53 | justify-content: center; 54 | align-items: center; 55 | flex-direction: column; 56 | `; 57 | 58 | export const Button = styled.button` 59 | background-color: #1aebb7; 60 | height: 50px; 61 | border: none; 62 | width: 100%; 63 | margin-top: 60px; 64 | border-radius: 5px; 65 | font-weight: bold; 66 | font-size: 16px; 67 | ` -------------------------------------------------------------------------------- /packages/client/src/routes/Register/styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | height: 100vh; 6 | width: 100%; 7 | `; 8 | 9 | export const Aside = styled.div` 10 | flex: 1; 11 | background-color: #1aebb7; 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: center; 15 | align-items: center; 16 | `; 17 | 18 | export const Wrapper = styled.div` 19 | flex: 2; 20 | background-color: #fff; 21 | display: flex; 22 | justify-content: center; 23 | align-items: center; 24 | flex-direction: column; 25 | `; 26 | 27 | export const Img = styled.img` 28 | width: 100px; 29 | height: 120px; 30 | `; 31 | 32 | export const LogoTitle = styled.h1` 33 | color: black; 34 | margin-bottom: 20px; 35 | margin-top: 10px; 36 | `; 37 | 38 | export const LogoDescription = styled.h3` 39 | color: #003825; 40 | width: 300px; 41 | text-align: center; 42 | `; 43 | 44 | export const Title = styled.h2` 45 | color: black; 46 | `; 47 | 48 | export const Box = styled.div` 49 | width: 50%; 50 | height: 100%; 51 | margin-top: 20px; 52 | display: flex; 53 | justify-content: center; 54 | align-items: center; 55 | flex-direction: column; 56 | `; 57 | 58 | export const Button = styled.button` 59 | background-color: #1aebb7; 60 | height: 50px; 61 | border: none; 62 | width: 100%; 63 | margin-top: 60px; 64 | border-radius: 5px; 65 | font-weight: bold; 66 | font-size: 16px; 67 | ` -------------------------------------------------------------------------------- /packages/server/src/app.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv-safe"; 2 | import Koa, { Request, Response } from "koa"; 3 | import Router from "koa-router"; 4 | import logger from "koa-logger"; 5 | import json from "koa-json"; 6 | import bodyparser from "koa-bodyparser"; 7 | import cors from "kcors"; 8 | import koaPlayground from "graphql-playground-middleware-koa"; 9 | import schema from "./schema"; 10 | import { getUser } from "./auth"; 11 | import { userLoader } from "./loaders"; 12 | 13 | const graphqlHTTP = require("koa-graphql"); 14 | 15 | // init router and koa 16 | const app = new Koa(); 17 | const router = new Router(); 18 | 19 | // init doenv 20 | dotenv.config(); 21 | 22 | // middlewares 23 | app.use(logger()); 24 | app.use(cors()); 25 | app.use(json()); 26 | app.use(bodyparser()); 27 | app.use(router.routes()); 28 | app.use(router.allowedMethods()); 29 | 30 | const graphqlSettingsPerReq = async (req: Request, ctx: Response) => { 31 | const { user } = await getUser(req.header.authorization); 32 | 33 | return { 34 | graphiql: true, 35 | schema, 36 | rootValue: { 37 | request: ctx.req, 38 | }, 39 | context: { 40 | user, 41 | req, 42 | loaders: { 43 | userLoader, 44 | }, 45 | }, 46 | }; 47 | }; 48 | 49 | const graphqlServer = graphqlHTTP(graphqlSettingsPerReq); 50 | 51 | router.all("/graphql", graphqlServer); 52 | router.all( 53 | "/graphiql", 54 | koaPlayground({ 55 | endpoint: "/graphql", 56 | }) 57 | ); 58 | 59 | export default app; 60 | -------------------------------------------------------------------------------- /packages/server/src/modules/main/UserType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType, GraphQLString, GraphQLID } from "graphql"; 2 | import { connectionDefinitions, globalIdField } from "graphql-relay"; 3 | import { OrganizationConnection } from "./OrganizationType"; 4 | import Organization from "../../models/Organization"; 5 | import { connectionArgs, connectionFromArray } from "graphql-relay"; 6 | 7 | const UserType = new GraphQLObjectType({ 8 | name: "User", 9 | description: "User data", 10 | fields: () => ({ 11 | id: globalIdField("User"), 12 | _id: { 13 | type: GraphQLID, 14 | resolve: (user) => user._id, 15 | }, 16 | name: { 17 | type: GraphQLString, 18 | resolve: (user) => user.name, 19 | }, 20 | email: { 21 | type: GraphQLString, 22 | resolve: (user) => user.email, 23 | }, 24 | password: { 25 | type: GraphQLString, 26 | resolve: (user) => user.password, 27 | }, 28 | description: { 29 | type: GraphQLString, 30 | resolve: (user) => user.description, 31 | }, 32 | organizations: { 33 | type: OrganizationConnection.connectionType, 34 | args: { 35 | ...connectionArgs, 36 | }, 37 | resolve: async (user, args) => { 38 | const data = await Organization.find({ 39 | _id: { $in: user.organizationIds }, 40 | }); 41 | return connectionFromArray(data, args); 42 | }, 43 | }, 44 | }), 45 | }); 46 | 47 | export default UserType; 48 | 49 | export const UserConnection = connectionDefinitions({ 50 | name: "User", 51 | nodeType: UserType, 52 | }); 53 | -------------------------------------------------------------------------------- /packages/client/src/routes/Login/styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | height: 100vh; 6 | width: 100%; 7 | `; 8 | 9 | export const Aside = styled.div` 10 | flex: 1; 11 | background-color: #1aebb7; 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: center; 15 | align-items: center; 16 | `; 17 | 18 | export const WrapperLogin = styled.div` 19 | flex: 2; 20 | background-color: #fff; 21 | display: flex; 22 | justify-content: center; 23 | align-items: center; 24 | flex-direction: column; 25 | `; 26 | 27 | export const Img = styled.img` 28 | width: 100px; 29 | height: 120px; 30 | `; 31 | 32 | export const LogoTitle = styled.h1` 33 | color: black; 34 | margin-bottom: 20px; 35 | margin-top: 10px; 36 | `; 37 | 38 | export const LogoDescription = styled.h3` 39 | color: #003825; 40 | width: 300px; 41 | text-align: center; 42 | `; 43 | 44 | export const Title = styled.h2` 45 | color: black; 46 | `; 47 | 48 | export const Box = styled.div` 49 | width: 50%; 50 | height: 100%; 51 | margin-top: 20px; 52 | display: flex; 53 | justify-content: center; 54 | align-items: center; 55 | flex-direction: column; 56 | `; 57 | 58 | export const Register = styled.button` 59 | color: #363636; 60 | background-color: transparent; 61 | border: none; 62 | font-size: 14px; 63 | align-self: flex-end; 64 | font-weight: bold; 65 | `; 66 | 67 | export const Button = styled.button` 68 | background-color: #1aebb7; 69 | height: 50px; 70 | border: none; 71 | width: 100%; 72 | margin-top: 60px; 73 | border-radius: 5px; 74 | font-weight: bold; 75 | font-size: 16px; 76 | ` -------------------------------------------------------------------------------- /packages/client/src/components/UserList/styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | padding: 30px; 7 | justify-content: center; 8 | align-items: center; 9 | `; 10 | 11 | export const List = styled.div` 12 | display: flex; 13 | flex-wrap: wrap; 14 | `; 15 | 16 | export const Box = styled.div` 17 | display: flex; 18 | height: 200px; 19 | width: 500px; 20 | margin: 25px; 21 | border: solid 1px #f1f1f1; 22 | box-shadow: 1px 3px 5px; 23 | border-radius: 10px; 24 | .left { 25 | display: flex; 26 | flex-direction: column; 27 | justify-content: space-evenly; 28 | align-items: center; 29 | border-radius: 10px 0 0 10px; 30 | flex: 1; 31 | background-color: #1aebb7; 32 | svg { 33 | font-size: 1.5em; 34 | } 35 | } 36 | 37 | .right { 38 | display: flex; 39 | flex-direction: column; 40 | justify-content: space-around; 41 | align-items: center; 42 | flex: 2; 43 | } 44 | `; 45 | 46 | export const Name = styled.div` 47 | font-size: 1.6em; 48 | color: #003825; 49 | font-weight: bold; 50 | `; 51 | 52 | export const Description = styled.div` 53 | font-size: 1.2em; 54 | color: #003825; 55 | `; 56 | 57 | export const Email = styled.div` 58 | font-size: 1em; 59 | color: #003825; 60 | align-self: flex-end; 61 | margin-right: 10px; 62 | margin-bottom: -15px; 63 | `; 64 | 65 | 66 | export const Button = styled.button` 67 | width: 200px; 68 | height: 50px; 69 | background-color: #1aebb7; 70 | color: #003825; 71 | border: 1px solid #003825; 72 | border-radius: 15px; 73 | font-size: 15px; 74 | font-weight: bold; 75 | } 76 | ` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

5 | 6 | 7 | 8 | 9 | 10 | 11 |

12 | 13 |

14 |

yin-yang-playground

15 |

16 | 17 |
18 | 19 | ## Fullstack typescript/javascript 20 | 21 | yin-yang-playground uses a stack full `JS/TS` on the frontend and backend, I'm making this project to improve 22 | my skills with graphql and relay. 23 | 24 | ##### [Frontend](https://github.com/Wellers0n/yin-yang-playground/tree/master/packages/client) 25 | ##### [Backend](https://github.com/Wellers0n/yin-yang-playground/tree/master/packages/server) 26 | 27 | ## Initing in the your PC 28 | 29 | - For clone the project `git clone https://github.com/Wellers0n/yin-yang-playground.git` 30 | - Enter in the folder `cd yin-yang-playground/` 31 | - To install project dependency: `yarn install` 32 | - After the installation of the dependencies `yarn start` in the default directory 33 | 34 | ## Stack used 35 | 36 | [ReactJS](https://reactjs.org/)
37 | [KoaJS](https://koajs.com/)
38 | [MongoDB](https://www.mongodb.com/)
39 | [GraphQL](https://graphql.org/)
40 | [Relay](https://relay.dev/)
41 | [Yarn](https://yarnpkg.com/en/)
42 | [WorkSpaces](https://yarnpkg.com/lang/en/docs/workspaces/)
43 | [Styled-Components](https://www.styled-components.com/)
44 | -------------------------------------------------------------------------------- /packages/server/src/modules/main/mutation/UserRegisterMutation.ts: -------------------------------------------------------------------------------- 1 | import User from "../../../models/User"; 2 | import bcrypt from 'bcryptjs'; 3 | import { GraphQLString, GraphQLNonNull, GraphQLBoolean } from "graphql"; 4 | import { mutationWithClientMutationId, offsetToCursor } from "graphql-relay"; 5 | import { generateToken } from "../../../auth"; 6 | import { UserConnection } from '../UserType'; 7 | 8 | export default mutationWithClientMutationId({ 9 | name: "userRegister", 10 | inputFields: { 11 | name: { 12 | type: new GraphQLNonNull(GraphQLString), 13 | }, 14 | email: { 15 | type: new GraphQLNonNull(GraphQLString), 16 | }, 17 | password: { 18 | type: new GraphQLNonNull(GraphQLString), 19 | }, 20 | description: { 21 | type: new GraphQLNonNull(GraphQLString), 22 | }, 23 | }, 24 | mutateAndGetPayload: async ({ name, email, password, description }) => { 25 | const user = await User.findOne({ email }); 26 | 27 | if (user) { 28 | return { 29 | message: "User exist", 30 | error: false, 31 | }; 32 | } 33 | 34 | const userCreated = await User.create({ 35 | name, 36 | email, 37 | password: bcrypt.hashSync(password, 8), 38 | description, 39 | }); 40 | 41 | return { 42 | user: userCreated, 43 | token: generateToken(userCreated), 44 | message: "User created successfully", 45 | error: false, 46 | }; 47 | }, 48 | outputFields: { 49 | userEdge: { 50 | type: UserConnection.edgeType, 51 | resolve: ({ user }) => ({ 52 | cursor: offsetToCursor(user.id), 53 | node: user, 54 | }), 55 | }, 56 | message: { 57 | type: GraphQLString, 58 | resolve: ({ message }) => message, 59 | }, 60 | error: { 61 | type: GraphQLBoolean, 62 | resolve: ({ error }) => error, 63 | }, 64 | token: { 65 | type: GraphQLString, 66 | resolve: ({ token }) => token, 67 | }, 68 | } 69 | }); 70 | -------------------------------------------------------------------------------- /packages/client/src/relay/fetchQuery.tsx: -------------------------------------------------------------------------------- 1 | import { Variables, UploadableMap } from "relay-runtime"; 2 | 3 | import { RequestNode } from "relay-runtime"; 4 | 5 | import { handleData, getRequestBody, getHeaders, isMutation } from "./helpers"; 6 | import fetchWithRetries from "./fetchWithRetries"; 7 | 8 | export const GRAPHQL_URL = "http://localhost:5001/graphql"; 9 | 10 | export const TOKEN_KEY = "token"; 11 | 12 | export function getToken() { 13 | // get token from cookie or session token instead 14 | return localStorage.getItem(TOKEN_KEY); 15 | } 16 | 17 | // Define a function that fetches the results of a request (query/mutation/etc) 18 | // and returns its results as a Promise: 19 | const fetchQuery = async ( 20 | request: RequestNode, 21 | variables: Variables, 22 | uploadables: UploadableMap 23 | ) => { 24 | try { 25 | const body = getRequestBody(request, variables, uploadables); 26 | const headers = { 27 | ...getHeaders(uploadables), 28 | Authorization: getToken(), 29 | }; 30 | 31 | const response = await fetchWithRetries(GRAPHQL_URL, { 32 | method: "POST", 33 | headers, 34 | body, 35 | fetchTimeout: 20000, 36 | retryDelays: [1000, 3000, 5000], 37 | }); 38 | 39 | const data = await handleData(response); 40 | 41 | if (response.status === 401) { 42 | throw data.errors; 43 | } 44 | 45 | if (isMutation(request) && data.errors) { 46 | throw data; 47 | } 48 | 49 | if (!data.data) { 50 | throw data.errors; 51 | } 52 | 53 | return data; 54 | } catch (err) { 55 | // eslint-disable-next-line 56 | console.log("err: ", err); 57 | 58 | const timeoutRegexp = new RegExp(/Still no successful response after/); 59 | const serverUnavailableRegexp = new RegExp(/Failed to fetch/); 60 | if ( 61 | timeoutRegexp.test(err.message) || 62 | serverUnavailableRegexp.test(err.message) 63 | ) { 64 | throw new Error("Serviço indisponível. Tente novamente mais tarde."); 65 | } 66 | 67 | throw err; 68 | } 69 | }; 70 | 71 | export default fetchQuery; 72 | -------------------------------------------------------------------------------- /packages/client/src/relay/relayTransactionLogger.tsx: -------------------------------------------------------------------------------- 1 | import { LogEvent } from "relay-runtime"; 2 | // export type LogEvent = 3 | // | { 4 | // name: 'queryresource.fetch', 5 | // operation: OperationDescriptor, 6 | // // FetchPolicy from relay-experimental 7 | // fetchPolicy: string, 8 | // // RenderPolicy from relay-experimental 9 | // renderPolicy: string, 10 | // hasFullQuery: boolean, 11 | // shouldFetch: boolean, 12 | // } 13 | // | { 14 | // name: 'execute.info', 15 | // transactionID: number, 16 | // info: mixed, 17 | // } 18 | // | { 19 | // name: 'execute.start', 20 | // transactionID: number, 21 | // params: RequestParameters, 22 | // variables: Variables, 23 | // } 24 | // | { 25 | // name: 'execute.next', 26 | // transactionID: number, 27 | // response: GraphQLResponse, 28 | // } 29 | // | { 30 | // name: 'execute.error', 31 | // transactionID: number, 32 | // error: Error, 33 | // } 34 | // | { 35 | // name: 'execute.complete', 36 | // transactionID: number, 37 | // } 38 | // | { 39 | // name: 'execute.unsubscribe', 40 | // transactionID: number, 41 | // }; 42 | 43 | // TODO - improve this 44 | export const relayTransactionLogger = (event: LogEvent) => { 45 | // eslint-disable-next-line 46 | console.log("RELAY: ", event); 47 | return; 48 | 49 | // if (event.name === 'execute.start') { 50 | // const { transactionID, params, variables } = event; 51 | // 52 | // const error = false; 53 | // 54 | // console.groupCollapsed && 55 | // console.groupCollapsed(`%c${transactionID}`, error ? 'color:red' : ''); 56 | // 57 | // const log = {params, variables}; 58 | // Object.keys(log).map(key => { 59 | // console.log(`${key}:`, log[key]); 60 | // }); 61 | // } 62 | // 63 | // if (event.name === 'execute.error') { 64 | // 65 | // } 66 | // 67 | // if (event.name === 'execute.next') { 68 | // const { response } = event; 69 | // 70 | // console.log(`response:`, response); 71 | // } 72 | }; 73 | -------------------------------------------------------------------------------- /packages/client/src/relay/helpers.tsx: -------------------------------------------------------------------------------- 1 | import { Variables, UploadableMap, CacheConfig } from "relay-runtime"; 2 | import { RequestNode } from "relay-runtime"; 3 | 4 | export const isMutation = (request: RequestNode) => 5 | request.operationKind === "mutation"; 6 | export const isQuery = (request: RequestNode) => 7 | request.operationKind === "query"; 8 | export const forceFetch = (cacheConfig: CacheConfig) => 9 | !!(cacheConfig && cacheConfig.force); 10 | 11 | export const handleData = (response: any) => { 12 | const contentType = response.headers.get("content-type"); 13 | if (contentType && contentType.indexOf("application/json") !== -1) { 14 | return response.json(); 15 | } 16 | 17 | return response.text(); 18 | }; 19 | 20 | function getRequestBodyWithUploadables( 21 | request: RequestNode, 22 | variables: Variables, 23 | uploadables: UploadableMap 24 | ) { 25 | let formData = new FormData(); 26 | formData.append("name", request.name); 27 | formData.append("query", request.text); 28 | formData.append("variables", JSON.stringify(variables)); 29 | 30 | Object.keys(uploadables).forEach((key) => { 31 | if (Object.prototype.hasOwnProperty.call(uploadables, key)) { 32 | formData.append(key, uploadables[key]); 33 | } 34 | }); 35 | 36 | return formData; 37 | } 38 | 39 | function getRequestBodyWithoutUplodables( 40 | request: RequestNode, 41 | variables: Variables 42 | ) { 43 | return JSON.stringify({ 44 | name: request.name, // used by graphql mock on tests 45 | query: request.text, // GraphQL text from input 46 | variables, 47 | }); 48 | } 49 | 50 | export function getRequestBody( 51 | request: RequestNode, 52 | variables: Variables, 53 | uploadables?: UploadableMap 54 | ) { 55 | if (uploadables) { 56 | return getRequestBodyWithUploadables(request, variables, uploadables); 57 | } 58 | 59 | return getRequestBodyWithoutUplodables(request, variables); 60 | } 61 | 62 | export const getHeaders = (uploadables?: UploadableMap) => { 63 | if (uploadables) { 64 | return { 65 | Accept: "*/*", 66 | }; 67 | } 68 | 69 | return { 70 | Accept: "application/json", 71 | "Content-type": "application/json", 72 | }; 73 | }; 74 | -------------------------------------------------------------------------------- /packages/server/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

5 | 6 | 7 | 8 | 9 | 10 | 11 |

12 | 13 |

14 |

Backend

15 |

16 | 17 |
18 | 19 | ## Server package 20 | 21 | The client uses graphql and koa to build back-end 22 | 23 | ## Routes 24 | 25 | ### Queries 26 | 27 | #### me 28 | ```graphql 29 | me { 30 | id 31 | _id 32 | name 33 | description 34 | email 35 | } 36 | ``` 37 | 38 | #### User 39 | ```graphql 40 | user { 41 | id 42 | _id 43 | name 44 | description 45 | email 46 | } 47 | ``` 48 | 49 | #### Users[Array] 50 | ```graphql 51 | users { 52 | id 53 | _id 54 | name 55 | description 56 | email 57 | } 58 | ``` 59 | 60 | ### Mutations 61 | 62 | #### UserLoginMutation 63 | ```graphql 64 | UserLoginMutation(input: {email: "test@gmail.com", password: "test"}) { 65 | token 66 | message 67 | } 68 | ``` 69 | 70 | #### UserRegisterMutation 71 | ```graphql 72 | UserRegisterMutation(input: {email: "test@gmail.com", password: "test", description: "test", name: "test name"}) { 73 | token 74 | message 75 | } 76 | ``` 77 | 78 | #### UserDeleteMutation 79 | ```graphql 80 | UserDeleteMutation(input: {id: 1}) { 81 | users 82 | message 83 | } 84 | ``` 85 | 86 | #### UserUpdateMutation 87 | ```graphql 88 | UserUpdateMutation(input: {id:1, email: "test@gmail.com", description: "test", name: "test name"}) { 89 | users 90 | message 91 | } 92 | ``` 93 | 94 | ## Initing in the your PC 95 | 96 | - For clone the project `git clone https://github.com/Wellers0n/yin-yang-playground.git` 97 | - Enter in the folder `cd yin-yang-playground/packages/server` 98 | - To install project dependency: `yarn install` 99 | - After the installation of the dependencies `yarn start` in the default directory 100 | -------------------------------------------------------------------------------- /packages/mdx/src/Intro.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { space, width, fontSize, color } from "styled-system"; 4 | import { Flex } from "rebass"; 5 | 6 | // Add styled-system functions to your component 7 | const Box = styled.div` 8 | ${space} 9 | ${width} 10 | ${fontSize} 11 | ${color} 12 | `; 13 | 14 | const IconImage = styled.img` 15 | max-height: 60px; 16 | max-width: 60px; 17 | `; 18 | 19 | const Link = styled.a` 20 | text-decoration: none; 21 | color: #c6d1dd; 22 | `; 23 | 24 | const Me = styled.img` 25 | max-width: 150px; 26 | max-height: 150px; 27 | `; 28 | 29 | const MeName = styled.span` 30 | font-size: 50px; 31 | color: #c6d1dd; 32 | margin-left: 60px; 33 | `; 34 | 35 | const Row = styled.div` 36 | display: flex; 37 | flex: 1; 38 | flex-direction: row; 39 | align-items: center; 40 | margin-bottom: 40px; 41 | `; 42 | 43 | export const Center = styled.div` 44 | display: flex; 45 | flex: 1; 46 | flex-direction: row; 47 | align-items: center; 48 | justify-content: center; 49 | `; 50 | 51 | const SpaceBetween = styled.div` 52 | display: flex; 53 | flex: 1; 54 | width: 100%; 55 | flex-direction: row; 56 | align-items: center; 57 | justify-content: space-around; 58 | `; 59 | 60 | export const Root = styled.div([], { 61 | width: "50vw", 62 | height: "70vh", 63 | display: "flex", 64 | justifyContent: "center", 65 | alignItems: "center", 66 | flexDirection: 'column' 67 | }); 68 | 69 | const Username = styled.span` 70 | font-size: 14px; 71 | margin-left: 20px; 72 | `; 73 | 74 | const SocialMediaLink = ({ name, link, username }) => ( 75 |

76 | 77 | 78 | 79 | {username} 80 | 81 | 82 |
83 | ); 84 | 85 | export const Intro = () => ( 86 | 87 | 88 | 89 | Wellerson 90 | 91 | 92 | 97 | 102 | 103 | 104 | 105 | Developer 106 | 107 | 108 | ); 109 | -------------------------------------------------------------------------------- /packages/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yin-yang/client", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "author": "wellerson", 6 | "license": "MIT", 7 | "private": true, 8 | "scripts": { 9 | "build": "cross-env NODE_ENV=production wp --config ./webpack.config", 10 | "jest:lint": "jest --config jest-eslint.config.js", 11 | "now-build": "cross-env NODE_ENV=production wp --config ./webpack.config", 12 | "relay": "relay-compiler --watchman false --src ./src --schema ../server/data/schema.graphql --language typescript", 13 | "start": "cross-env NODE_ENV=development wp --config ./webpack.config", 14 | "test": "jest --coverage", 15 | "ts:check": "tsc" 16 | }, 17 | "devDependencies": { 18 | "@babel/core": "^7.14.6", 19 | "@babel/preset-env": "^7.14.7", 20 | "@babel/preset-react": "^7.14.5", 21 | "@babel/preset-typescript": "^7.14.5", 22 | "@testing-library/jest-dom": "^5.14.1", 23 | "@testing-library/react": "^12.0.0", 24 | "@types/hoist-non-react-statics": "^3.3.1", 25 | "@types/http-proxy-middleware": "^1.0.0", 26 | "@types/jest": "^26.0.23", 27 | "@types/node": "^16.0.0", 28 | "@types/react": "^17.0.11", 29 | "@types/react-dom": "^17.0.8", 30 | "@types/react-relay": "^11.0.1", 31 | "@types/react-router-dom": "^5.1.7", 32 | "@types/rebass": "^4.0.8", 33 | "@types/relay-runtime": "^11.0.0", 34 | "@types/relay-test-utils": "^6.0.4", 35 | "babel-core": "^6.26.3", 36 | "babel-eslint": "^10.1.0", 37 | "babel-jest": "^27.0.5", 38 | "babel-loader": "^8.2.2", 39 | "babel-plugin-relay": "^11.0.2", 40 | "core-js": "^3.15.1", 41 | "cross-env": "^7.0.3", 42 | "css-loader": "^5.2.6", 43 | "html-webpack-plugin": "^5.3.2", 44 | "mini-css-extract-plugin": "^1.6.1", 45 | "react-hot-loader": "^4.13.0", 46 | "relay-compiler": "^11.0.2", 47 | "relay-compiler-language-typescript": "^14.0.0", 48 | "style-loader": "^3.0.0", 49 | "typescript": "^4.3.4", 50 | "url-loader": "^4.1.1", 51 | "webpack": "^5.40.0", 52 | "webpack-nano": "^1.1.1", 53 | "webpack-plugin-serve": "^1.4.1" 54 | }, 55 | "dependencies": { 56 | "@fortawesome/fontawesome-svg-core": "^1.2.35", 57 | "@fortawesome/free-solid-svg-icons": "^5.15.3", 58 | "@fortawesome/react-fontawesome": "^0.1.14", 59 | "hoist-non-react-statics": "^3.3.2", 60 | "mini-css-extract-plugin": "^1.6.1", 61 | "react": "^17.0.2", 62 | "react-dom": "^17.0.2", 63 | "react-relay": "^11.0.2", 64 | "react-router": "^5.2.0", 65 | "react-router-dom": "^5.2.0", 66 | "rebass": "^4.0.7", 67 | "relay-runtime": "^11.0.2", 68 | "styled-components": "^5.3.0", 69 | "styled-system": "^5.1.5" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/client/src/routes/Login/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { RouteComponentProps } from "react-router"; 3 | import { 4 | Container, 5 | Aside, 6 | WrapperLogin, 7 | Img, 8 | LogoTitle, 9 | LogoDescription, 10 | Title, 11 | Box, 12 | Register, 13 | Button, 14 | } from "./styles"; 15 | import Logo from "../../assets/images/location.png"; 16 | import { graphql, commitMutation } from "react-relay"; 17 | import Environment from "../../relay/Environment"; 18 | import { LoginQueryResponse } from "./__generated__/LoginQuery.graphql"; 19 | // components 20 | import Input from "../../components/Input"; 21 | 22 | const Login = (props: RouteComponentProps) => { 23 | const { history } = props; 24 | const [email, setEmail] = useState(""); 25 | const [password, setPassword] = useState(""); 26 | 27 | const mutation = graphql` 28 | mutation LoginQuery($input: UserLoginInput!) { 29 | UserLoginMutation(input: $input) { 30 | token 31 | message 32 | } 33 | } 34 | `; 35 | 36 | const submit = () => { 37 | commitMutation(Environment, { 38 | mutation, 39 | variables: { input: { email, password } }, 40 | onCompleted: (response: LoginQueryResponse, errors: any) => { 41 | if (errors) return console.log(errors); 42 | 43 | const token = response.UserLoginMutation?.token; 44 | if (token) { 45 | localStorage.setItem("token", token); 46 | return history.push("/home"); 47 | } 48 | }, 49 | onError: (err) => console.error(err), 50 | }); 51 | }; 52 | 53 | return ( 54 | 55 | 62 | 63 | 64 | Log in 65 | setEmail(value)} 69 | type={"text"} 70 | placeholder={"joe@email.com"} 71 | /> 72 | setPassword(value)} 75 | title={"Password"} 76 | type={"password"} 77 | placeholder={"Enter your password"} 78 | /> 79 | history.push("/register")}> 80 | Register 81 | 82 | 83 | 84 | 85 | 86 | ); 87 | }; 88 | 89 | export default Login; 90 | -------------------------------------------------------------------------------- /packages/client/src/routes/Register/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { RouteComponentProps } from "react-router"; 3 | import { 4 | Container, 5 | Aside, 6 | Wrapper, 7 | Img, 8 | LogoTitle, 9 | LogoDescription, 10 | Title, 11 | Box, 12 | Button, 13 | } from "./styles"; 14 | import Logo from "../../assets/images/location.png"; 15 | import { graphql, commitMutation } from "react-relay"; 16 | import Environment from "../../relay/Environment"; 17 | import { RegisterQueryResponse } from "./__generated__/RegisterQuery.graphql"; 18 | 19 | // components 20 | import Input from "../../components/Input"; 21 | 22 | const Register = (props: RouteComponentProps) => { 23 | const { history } = props; 24 | const [email, setEmail] = useState(""); 25 | const [password, setPassword] = useState(""); 26 | const [name, setName] = useState(""); 27 | const [description, setDescription] = useState(""); 28 | 29 | const mutation = graphql` 30 | mutation RegisterQuery($input: userRegisterInput!) { 31 | UserRegisterMutation(input: $input) { 32 | token 33 | message 34 | } 35 | } 36 | `; 37 | 38 | const submit = () => { 39 | commitMutation(Environment, { 40 | mutation, 41 | variables: { input: { email, password, name, description } }, 42 | onCompleted: (response: RegisterQueryResponse, errors: any) => { 43 | if (errors) return console.log(errors); 44 | 45 | const token = response.UserRegisterMutation?.token; 46 | if (token) { 47 | localStorage.setItem("token", token); 48 | return history.push("/home"); 49 | } 50 | }, 51 | onError: (err) => console.error(err), 52 | }); 53 | }; 54 | 55 | return ( 56 | 57 | 64 | 65 | 66 | Register 67 | setEmail(value)} 71 | type={"text"} 72 | placeholder={"joe@email.com"} 73 | /> 74 | setPassword(value)} 77 | title={"Password"} 78 | type={"password"} 79 | placeholder={"Enter your password"} 80 | /> 81 | setName(value)} 84 | title={"Name"} 85 | type={"text"} 86 | placeholder={"Enter your name"} 87 | /> 88 | setDescription(value)} 91 | title={"Description"} 92 | type={"text"} 93 | placeholder={"Enter your description"} 94 | /> 95 | 96 | 97 | 98 | 99 | ); 100 | }; 101 | 102 | export default Register; 103 | -------------------------------------------------------------------------------- /packages/client/src/routes/Edit/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { RouteComponentProps, useHistory } from "react-router"; 3 | import { Container, Wrapper, Title, Box, Button } from "./styles"; 4 | import { graphql, commitMutation, useLazyLoadQuery } from "react-relay"; 5 | import Environment from "../../relay/Environment"; 6 | import { EditQuery } from "./__generated__/EditQuery.graphql"; 7 | import { EditMutationResponse } from "./__generated__/EditMutation.graphql"; 8 | 9 | // components 10 | import Input from "../../components/Input"; 11 | 12 | const Edit = (props: RouteComponentProps) => { 13 | const { id } = props.location.state; 14 | 15 | const data = useLazyLoadQuery( 16 | graphql` 17 | query EditQuery($id: String) { 18 | user(id: $id) { 19 | id 20 | name 21 | description 22 | email 23 | } 24 | } 25 | `, 26 | { id }, 27 | { fetchPolicy: "network-only" } 28 | ); 29 | 30 | const history = useHistory(); 31 | 32 | const { user } = data; 33 | 34 | const [email, setEmail] = useState(user?.email || ""); 35 | const [name, setName] = useState(user?.name || ""); 36 | const [description, setDescription] = useState(user?.description || ""); 37 | 38 | const mutation = graphql` 39 | mutation EditMutation($input: UserUpdateInput!) { 40 | UserUpdateMutation(input: $input) { 41 | users { 42 | _id 43 | name 44 | description 45 | email 46 | } 47 | message 48 | } 49 | } 50 | `; 51 | 52 | function updater(store: any) { 53 | const root = store.getRoot(); 54 | 55 | const newUsers = store 56 | .getRootField("UserUpdateMutation") 57 | .getLinkedRecords("users"); 58 | 59 | root.setLinkedRecords(newUsers, "users"); 60 | } 61 | 62 | const submit = () => { 63 | commitMutation(Environment, { 64 | mutation, 65 | variables: { input: { id, email, description, name } }, 66 | updater, 67 | onCompleted: (response: EditMutationResponse, errors: any) => { 68 | if (errors) return console.log(errors); 69 | history.push("/home"); 70 | }, 71 | onError: (err) => console.error(err), 72 | }); 73 | }; 74 | 75 | return ( 76 | 77 | 78 | 79 | Edit 80 | setEmail(value)} 84 | type={"text"} 85 | placeholder={"joe@email.com"} 86 | /> 87 | setName(value)} 90 | title={"Name"} 91 | type={"text"} 92 | placeholder={"Enter your name"} 93 | /> 94 | setDescription(value)} 97 | title={"Description"} 98 | type={"text"} 99 | placeholder={"Enter your description"} 100 | /> 101 | 102 | 103 | 104 | 105 | ); 106 | }; 107 | 108 | export default Edit; 109 | -------------------------------------------------------------------------------- /packages/client/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("path"); 2 | 3 | const webpack = require("webpack"); 4 | const { WebpackPluginServe: Serve } = require("webpack-plugin-serve"); 5 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 6 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 7 | 8 | const isDev = process.env.NODE_ENV === "development"; 9 | const outputPath = resolve(__dirname, "dist"); 10 | 11 | const entry = isDev 12 | ? ["./src/index.tsx", "webpack-plugin-serve/client"] 13 | : "./src/index.tsx"; 14 | 15 | const plugins = [ 16 | new HtmlWebpackPlugin(), 17 | new webpack.DefinePlugin({ 18 | "process.env": { 19 | NODE_ENV: JSON.stringify(process.env.NODE_ENV), 20 | }, 21 | }), 22 | ]; 23 | 24 | if (isDev) { 25 | plugins.push( 26 | new Serve({ 27 | port: 3000, 28 | hmr: true, 29 | historyFallback: true, 30 | static: [outputPath], 31 | }) 32 | ); 33 | } else { 34 | plugins.push(new MiniCssExtractPlugin()); 35 | } 36 | 37 | module.exports = { 38 | entry, 39 | mode: process.env.NODE_ENV, 40 | devtool: "eval-cheap-source-map", 41 | resolve: { 42 | extensions: [".js", ".jsx", ".ts", ".tsx"], 43 | }, 44 | module: { 45 | rules: [ 46 | { 47 | test: /\.(ts|tsx|js|jsx)$/, 48 | exclude: /node_modules/, 49 | use: ["babel-loader"], 50 | }, 51 | { 52 | test: /\.css$/, 53 | use: !isDev 54 | ? [MiniCssExtractPlugin.loader, "css-loader"] 55 | : ["style-loader", "css-loader"], 56 | }, 57 | { 58 | test: /\.woff(\?.*)?$/, 59 | use: { 60 | loader: "url-loader", 61 | options: { 62 | name: "fonts/[name].[ext]", 63 | mimetype: "application/font-woff", 64 | }, 65 | }, 66 | }, 67 | { 68 | test: /\.woff2(\?.*)?$/, 69 | use: { 70 | loader: "url-loader", 71 | options: { 72 | name: "fonts/[name].[ext]", 73 | mimetype: "application/font-woff2", 74 | }, 75 | }, 76 | }, 77 | { 78 | test: /\.(otf)(\?.*)?$/, 79 | use: { 80 | loader: "file-loader", 81 | options: { 82 | name: "fonts/[name].[ext]", 83 | }, 84 | }, 85 | }, 86 | { 87 | test: /\.ttf(\?.*)?$/, 88 | use: { 89 | loader: "url-loader", 90 | options: { 91 | name: "fonts/[name].[ext]", 92 | mimetype: "application/octet-stream", 93 | }, 94 | }, 95 | }, 96 | { 97 | test: /\.svg(\?.*)?$/, 98 | use: { 99 | loader: "url-loader", 100 | options: { 101 | name: "images/[name].[ext]", 102 | mimetype: "image/svg+xml", 103 | }, 104 | }, 105 | }, 106 | { 107 | test: /\.(png|jpg)(\?.*)?$/, 108 | use: { 109 | loader: "url-loader", 110 | options: { 111 | name: "images/[name].[ext]", 112 | }, 113 | }, 114 | }, 115 | ], 116 | }, 117 | output: { 118 | path: outputPath, 119 | publicPath: "/", 120 | filename: !isDev ? "bundle.[contenthash].js" : "bundle.js", 121 | }, 122 | plugins, 123 | watch: isDev, 124 | }; 125 | -------------------------------------------------------------------------------- /packages/server/yarn-error.log: -------------------------------------------------------------------------------- 1 | Arguments: 2 | /Users/wellerson/.nvm/versions/node/v12.21.0/bin/node /opt/homebrew/bin/yarn 3 | 4 | PATH: 5 | /opt/apache-maven-3.6.3/bin:/Users/wellerson/.nvm/versions/node/v12.21.0/bin:/opt/homebrew/bin:/usr/local/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/apache-maven-3.6.3/bin:/Users/wellerson/.nvm/versions/node/v12.21.0/bin:/opt/homebrew/bin 6 | 7 | Yarn version: 8 | 1.22.10 9 | 10 | Node version: 11 | 12.21.0 12 | 13 | Platform: 14 | darwin arm64 15 | 16 | Trace: 17 | SyntaxError: /Users/wellerson/Projects/open-source/code-challenge-entria/packages/server/package.json: Unexpected token } in JSON at position 2094 18 | at JSON.parse () 19 | at /opt/homebrew/lib/node_modules/yarn/lib/cli.js:1625:59 20 | at Generator.next () 21 | at step (/opt/homebrew/lib/node_modules/yarn/lib/cli.js:310:30) 22 | at /opt/homebrew/lib/node_modules/yarn/lib/cli.js:321:13 23 | 24 | npm manifest: 25 | { 26 | "name": "@challenge/server", 27 | "version": "1.0.0", 28 | "main": "index.js", 29 | "license": "MIT", 30 | "private": true, 31 | "devDependencies": { 32 | "@babel/cli": "^7.14.5", 33 | "@babel/core": "^7.14.6", 34 | "@babel/node": "^7.14.7", 35 | "@babel/plugin-proposal-class-properties": "^7.14.5", 36 | "@babel/plugin-proposal-decorators": "^7.14.5", 37 | "@babel/plugin-proposal-do-expressions": "^7.14.5", 38 | "@babel/plugin-proposal-export-default-from": "^7.14.5", 39 | "@babel/plugin-proposal-export-namespace-from": "^7.14.5", 40 | "@babel/plugin-proposal-function-bind": "^7.14.5", 41 | "@babel/plugin-proposal-function-sent": "^7.14.5", 42 | "@babel/plugin-proposal-json-strings": "^7.14.5", 43 | "@babel/plugin-proposal-logical-assignment-operators": "^7.14.5", 44 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5", 45 | "@babel/plugin-proposal-numeric-separator": "^7.14.5", 46 | "@babel/plugin-proposal-object-rest-spread": "^7.14.7", 47 | "@babel/plugin-proposal-optional-chaining": "^7.14.5", 48 | "@babel/plugin-proposal-pipeline-operator": "^7.14.5", 49 | "@babel/plugin-proposal-throw-expressions": "^7.14.5", 50 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 51 | "@babel/plugin-syntax-import-meta": "^7.10.4", 52 | "@babel/plugin-transform-async-to-generator": "^7.14.5", 53 | "@babel/preset-env": "^7.14.7", 54 | "@babel/preset-typescript": "^7.14.5", 55 | "@types/jest": "^26.0.23", 56 | "@types/kcors": "^2.2.3", 57 | "@types/koa": "^2.13.3", 58 | "@types/koa-bodyparser": "^4.3.1", 59 | "@types/koa-json": "^2.0.18", 60 | "@types/koa-logger": "^3.1.1", 61 | "@types/koa-router": "^7.4.2", 62 | "babel-eslint": "^10.1.0", 63 | "eslint": "^7.29.0", 64 | "eslint-config-airbnb-base": "^14.2.1", 65 | "eslint-import-resolver-typescript": "^2.4.0", 66 | "eslint-plugin-import": "^2.23.4", 67 | "get-graphql-schema": "^2.1.2", 68 | "jest": "^27.0.4", 69 | "jest-fetch-mock": "^3.0.3", 70 | "jest-junit": "^12.2.0", 71 | "jest-runner-eslint": "^0.10.1", 72 | "nodemon": "^2.0.7", 73 | "ts-jest": "^27.0.3", 74 | "ts-node": "^10.0.0", 75 | "typescript": "^4.3.4", 76 | "typescript-eslint-parser": "^22.0.0" 77 | }, 78 | } 79 | 80 | yarn manifest: 81 | No manifest 82 | 83 | Lockfile: 84 | No lockfile 85 | -------------------------------------------------------------------------------- /packages/client/src/components/UserList/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | Container, 4 | Box, 5 | Name, 6 | Description, 7 | Email, 8 | List, 9 | Button, 10 | } from "./styles"; 11 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 12 | import { faEdit, faTrash, faSpinner } from "@fortawesome/free-solid-svg-icons"; 13 | import { useHistory } from "react-router-dom"; 14 | import { graphql, usePaginationFragment, commitMutation } from "react-relay"; 15 | import Environment from "../../relay/Environment"; 16 | import { UserListQuery } from "./__generated__/UserListQuery.graphql"; 17 | import { UserList_query$key } from "./__generated__/UserList_query.graphql"; 18 | import { UserListMutationResponse } from "./__generated__/UserListMutation.graphql"; 19 | 20 | type Props = { 21 | query: UserList_query$key; 22 | }; 23 | 24 | const UserList = (props: Props) => { 25 | const { 26 | data, 27 | loadNext, 28 | loadPrevious, 29 | hasNext, 30 | hasPrevious, 31 | isLoadingNext, 32 | isLoadingPrevious, 33 | refetch, // For refetching connection 34 | } = usePaginationFragment( 35 | graphql` 36 | fragment UserList_query on QueryType 37 | @argumentDefinitions( 38 | first: { type: "Int", defaultValue: 3 } 39 | cursor: { type: "String" } 40 | ) 41 | @refetchable(queryName: "UserListQuery") { 42 | users(first: $first, after: $cursor) 43 | @connection(key: "UserList_users", filters: []) { 44 | edges { 45 | node { 46 | _id 47 | id 48 | name 49 | description 50 | email 51 | } 52 | } 53 | } 54 | } 55 | `, 56 | props.query 57 | ); 58 | 59 | let history = useHistory(); 60 | 61 | const { users } = data; 62 | 63 | const mutation = graphql` 64 | mutation UserListMutation($input: UserDeleteInput!) { 65 | UserDeleteMutation(input: $input) { 66 | users { 67 | _id 68 | name 69 | description 70 | email 71 | } 72 | message 73 | } 74 | } 75 | `; 76 | 77 | function updater(store: any) { 78 | const root = store.getRoot(); 79 | 80 | const newUsers = store 81 | .getRootField("UserDeleteMutation") 82 | .getLinkedRecords("users"); 83 | 84 | root.setLinkedRecords(newUsers, "users"); 85 | } 86 | 87 | const deleteSubmit = (id: string | null | undefined) => { 88 | commitMutation(Environment, { 89 | mutation, 90 | variables: { input: { id } }, 91 | updater, 92 | onCompleted: (response: UserListMutationResponse, errors: any) => { 93 | if (errors) return console.log(errors); 94 | }, 95 | onError: (err) => console.error(err), 96 | }); 97 | }; 98 | 99 | return ( 100 | 101 | 102 | {users?.edges?.map((value, index) => { 103 | return ( 104 | 105 |
106 | 108 | history.push(`/edit/${value?.node?._id}`, { 109 | id: value?.node?._id, 110 | }) 111 | } 112 | icon={faEdit} 113 | /> 114 | deleteSubmit(value?.node?._id)} 117 | /> 118 |
119 |
120 | {value?.node?.name} 121 | {value?.node?.description} 122 | {value?.node?.email} 123 |
124 |
125 | ); 126 | })} 127 |
128 | 135 |
136 | ); 137 | }; 138 | 139 | export default UserList; 140 | -------------------------------------------------------------------------------- /packages/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yin-yang/server", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "private": true, 7 | "scripts": { 8 | "dev": "yarn clear && yarn build && webpack --env dev", 9 | "clear": "rimraf ./dist", 10 | "build": "esbuild src/index.js --bundle --platform=node --target=node18.12.1 --outfile=dist/index.js", 11 | "tslint": "tslint --project .", 12 | "test": "jest", 13 | "test:watch": "jest --watchAll --coverage" 14 | }, 15 | "devDependencies": { 16 | "@babel/cli": "^7.14.5", 17 | "@babel/core": "^7.14.6", 18 | "@babel/node": "^7.14.7", 19 | "@babel/plugin-proposal-class-properties": "^7.14.5", 20 | "@babel/plugin-proposal-decorators": "^7.14.5", 21 | "@babel/plugin-proposal-do-expressions": "^7.14.5", 22 | "@babel/plugin-proposal-export-default-from": "^7.14.5", 23 | "@babel/plugin-proposal-export-namespace-from": "^7.14.5", 24 | "@babel/plugin-proposal-function-bind": "^7.14.5", 25 | "@babel/plugin-proposal-function-sent": "^7.14.5", 26 | "@babel/plugin-proposal-json-strings": "^7.14.5", 27 | "@babel/plugin-proposal-logical-assignment-operators": "^7.14.5", 28 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5", 29 | "@babel/plugin-proposal-numeric-separator": "^7.14.5", 30 | "@babel/plugin-proposal-object-rest-spread": "^7.14.7", 31 | "@babel/plugin-proposal-optional-chaining": "^7.14.5", 32 | "@babel/plugin-proposal-pipeline-operator": "^7.14.5", 33 | "@babel/plugin-proposal-throw-expressions": "^7.14.5", 34 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 35 | "@babel/plugin-syntax-import-meta": "^7.10.4", 36 | "@babel/plugin-transform-async-to-generator": "^7.14.5", 37 | "@babel/polyfill": "^7.12.1", 38 | "@babel/preset-env": "^7.14.7", 39 | "@babel/preset-typescript": "^7.14.5", 40 | "@types/bcryptjs": "^2.4.2", 41 | "@types/dotenv-safe": "^8.1.1", 42 | "@types/graphql": "^14.5.0", 43 | "@types/graphql-relay": "^0.7.0", 44 | "@types/jest": "^26.0.23", 45 | "@types/jsonwebtoken": "^8.5.2", 46 | "@types/kcors": "^2.2.3", 47 | "@types/koa": "^2.13.3", 48 | "@types/koa-bodyparser": "^4.3.1", 49 | "@types/koa-json": "^2.0.18", 50 | "@types/koa-logger": "^3.1.1", 51 | "@types/koa-router": "^7.4.2", 52 | "@types/mongoose": "^5.11.97", 53 | "@types/node-fetch": "^2.5.10", 54 | "auto-reload-webpack-plugin": "^1.1.0", 55 | "babel-core": "^6.26.3", 56 | "babel-eslint": "^10.1.0", 57 | "babel-loader": "^8.2.2", 58 | "eslint": "^7.29.0", 59 | "eslint-config-airbnb-base": "^14.2.1", 60 | "eslint-import-resolver-typescript": "^2.4.0", 61 | "eslint-plugin-import": "^2.23.4", 62 | "get-graphql-schema": "^2.1.2", 63 | "jest": "^27.0.4", 64 | "jest-fetch-mock": "^3.0.3", 65 | "jest-junit": "^12.2.0", 66 | "jest-runner-eslint": "^0.10.1", 67 | "lint-staged": "^11.0.0", 68 | "nodemon": "^2.0.7", 69 | "prettier": "^2.3.1", 70 | "rimraf": "^3.0.2", 71 | "ts-jest": "^27.0.3", 72 | "ts-node": "^10.0.0", 73 | "typescript": "^4.3.4", 74 | "typescript-eslint-parser": "^22.0.0", 75 | "webpack": "^5.40.0", 76 | "webpack-cli": "^4.7.2", 77 | "webpack-merge": "^5.8.0", 78 | "webpack-node-externals": "^3.0.0" 79 | }, 80 | "dependencies": { 81 | "babel-core": "^6.26.3", 82 | "bcryptjs": "^2.4.3", 83 | "core-js": "^3.15.0", 84 | "dataloader": "^2.0.0", 85 | "dotenv-safe": "^8.2.0", 86 | "graphql": "^15.5.1", 87 | "graphql-playground-middleware-koa": "^1.6.21", 88 | "graphql-relay": "^0.7.0", 89 | "graphql-subscriptions": "^1.2.1", 90 | "jsonwebtoken": "^8.5.1", 91 | "kcors": "^2.2.2", 92 | "koa": "^2.13.1", 93 | "koa-bodyparser": "^4.3.0", 94 | "koa-compose": "^4.1.0", 95 | "koa-convert": "^2.0.0", 96 | "koa-graphql": "^0.8.0", 97 | "koa-graphql-batch": "^1.1.0", 98 | "koa-json": "^2.0.2", 99 | "koa-logger": "^3.2.1", 100 | "koa-router": "^10.0.0", 101 | "mongoose": "^6.7.2", 102 | "node-fetch": "^2.6.1", 103 | "pretty-error": "^3.0.4", 104 | "regenerator-runtime": "^0.13.7", 105 | "subscriptions-transport-ws": "^0.9.19" 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /packages/client/src/relay/fetchWithRetries.tsx: -------------------------------------------------------------------------------- 1 | import ExecutionEnvironment from './ExecuteEnvironment'; 2 | 3 | export type InitWithRetries = { 4 | body?: BodyInit | null, 5 | cache?: RequestCache, 6 | credentials?: RequestCredentials, 7 | headers?: HeadersInit, 8 | method?: string, 9 | mode?: RequestMode, 10 | fetchTimeout?: number | null, 11 | retryDelays?: Array | null, 12 | }; 13 | 14 | const DEFAULT_TIMEOUT = 15000; 15 | const DEFAULT_RETRIES = [1000, 3000]; 16 | 17 | /** 18 | * Makes a POST request to the server with the given data as the payload. 19 | * Automatic retries are done based on the values in `retryDelays`. 20 | */ 21 | function fetchWithRetries(uri: string, initWithRetries?: InitWithRetries | null): Promise { 22 | const { fetchTimeout = null, retryDelays = null, ...init } = initWithRetries || {}; 23 | const _fetchTimeout = fetchTimeout !== null ? fetchTimeout : DEFAULT_TIMEOUT; 24 | const _retryDelays = retryDelays !== null ? retryDelays : DEFAULT_RETRIES; 25 | 26 | let requestsAttempted = 0; 27 | let requestStartTime = 0; 28 | return new Promise((resolve, reject) => { 29 | /** 30 | * Sends a request to the server that will timeout after `fetchTimeout`. 31 | * If the request fails or times out a new request might be scheduled. 32 | */ 33 | function sendTimedRequest(): void { 34 | requestsAttempted++; 35 | requestStartTime = Date.now(); 36 | let isRequestAlive = true; 37 | const request = fetch(uri, init); 38 | const requestTimeout = setTimeout(() => { 39 | isRequestAlive = false; 40 | if (shouldRetry(requestsAttempted)) { 41 | // eslint-disable-next-line 42 | console.log(false, 'fetchWithRetries: HTTP timeout, retrying.'); 43 | retryRequest(); 44 | } else { 45 | reject( 46 | new Error(`fetchWithRetries(): Failed to get response from server, tried ${requestsAttempted} times.`), 47 | ); 48 | } 49 | }, _fetchTimeout); 50 | 51 | request 52 | .then(response => { 53 | clearTimeout(requestTimeout); 54 | if (isRequestAlive) { 55 | // We got a response, we can clear the timeout. 56 | if (response.status >= 200 && response.status < 300) { 57 | // Got a response code that indicates success, resolve the promise. 58 | resolve(response); 59 | } else if (response.status === 401) { 60 | resolve(response); 61 | } else if (shouldRetry(requestsAttempted)) { 62 | // Fetch was not successful, retrying. 63 | // TODO(#7595849): Only retry on transient HTTP errors. 64 | // eslint-disable-next-line 65 | console.log(false, 'fetchWithRetries: HTTP error, retrying.'), retryRequest(); 66 | } else { 67 | // Request was not successful, giving up. 68 | const error: any = new Error( 69 | `fetchWithRetries(): Still no successful response after ${requestsAttempted} retries, giving up.`, 70 | ); 71 | error.response = response; 72 | reject(error); 73 | } 74 | } 75 | }) 76 | .catch(error => { 77 | clearTimeout(requestTimeout); 78 | if (shouldRetry(requestsAttempted)) { 79 | retryRequest(); 80 | } else { 81 | reject(error); 82 | } 83 | }); 84 | } 85 | 86 | /** 87 | * Schedules another run of sendTimedRequest based on how much time has 88 | * passed between the time the last request was sent and now. 89 | */ 90 | function retryRequest(): void { 91 | const retryDelay = _retryDelays[requestsAttempted - 1]; 92 | const retryStartTime = requestStartTime + retryDelay; 93 | // Schedule retry for a configured duration after last request started. 94 | setTimeout(sendTimedRequest, retryStartTime - Date.now()); 95 | } 96 | 97 | /** 98 | * Checks if another attempt should be done to send a request to the server. 99 | */ 100 | function shouldRetry(attempt: number): boolean { 101 | return ExecutionEnvironment.canUseDOM && attempt <= _retryDelays.length; 102 | } 103 | 104 | sendTimedRequest(); 105 | }); 106 | } 107 | 108 | export default fetchWithRetries; -------------------------------------------------------------------------------- /packages/server/data/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: QueryType 3 | mutation: MutationType 4 | } 5 | 6 | """Exposes a URL that specifies the behaviour of this scalar.""" 7 | directive @specifiedBy( 8 | """The URL that specifies the behaviour of this scalar.""" 9 | url: String! 10 | ) on SCALAR 11 | 12 | type MutationType { 13 | UserLoginMutation(input: UserLoginInput!): UserLoginPayload 14 | UserRegisterMutation(input: userRegisterInput!): userRegisterPayload 15 | UserDeleteMutation(input: UserDeleteInput!): UserDeletePayload 16 | UserUpdateMutation(input: UserUpdateInput!): UserUpdatePayload 17 | } 18 | 19 | """Organization data""" 20 | type Organization { 21 | """The ID of an object""" 22 | id: ID! 23 | _id: ID 24 | name: String 25 | email: String 26 | description: String 27 | } 28 | 29 | """A connection to a list of items.""" 30 | type OrganizationConnection { 31 | """Information to aid in pagination.""" 32 | pageInfo: PageInfo! 33 | 34 | """A list of edges.""" 35 | edges: [OrganizationEdge] 36 | } 37 | 38 | """An edge in a connection.""" 39 | type OrganizationEdge { 40 | """The item at the end of the edge""" 41 | node: Organization 42 | 43 | """A cursor for use in pagination""" 44 | cursor: String! 45 | } 46 | 47 | """Information about pagination in a connection.""" 48 | type PageInfo { 49 | """When paginating forwards, are there more items?""" 50 | hasNextPage: Boolean! 51 | 52 | """When paginating backwards, are there more items?""" 53 | hasPreviousPage: Boolean! 54 | 55 | """When paginating backwards, the cursor to continue.""" 56 | startCursor: String 57 | 58 | """When paginating forwards, the cursor to continue.""" 59 | endCursor: String 60 | } 61 | 62 | """Get users[], user and me""" 63 | type QueryType { 64 | me: User 65 | user(id: String): User 66 | users( 67 | """Returns the items in the list that come after the specified cursor.""" 68 | after: String 69 | 70 | """Returns the first n items from the list.""" 71 | first: Int 72 | 73 | """Returns the items in the list that come before the specified cursor.""" 74 | before: String 75 | 76 | """Returns the last n items from the list.""" 77 | last: Int 78 | ): UserConnection 79 | } 80 | 81 | """User data""" 82 | type User { 83 | """The ID of an object""" 84 | id: ID! 85 | _id: ID 86 | name: String 87 | email: String 88 | password: String 89 | description: String 90 | organizations( 91 | """Returns the items in the list that come after the specified cursor.""" 92 | after: String 93 | 94 | """Returns the first n items from the list.""" 95 | first: Int 96 | 97 | """Returns the items in the list that come before the specified cursor.""" 98 | before: String 99 | 100 | """Returns the last n items from the list.""" 101 | last: Int 102 | ): OrganizationConnection 103 | } 104 | 105 | """A connection to a list of items.""" 106 | type UserConnection { 107 | """Information to aid in pagination.""" 108 | pageInfo: PageInfo! 109 | 110 | """A list of edges.""" 111 | edges: [UserEdge] 112 | } 113 | 114 | input UserDeleteInput { 115 | id: String! 116 | clientMutationId: String 117 | } 118 | 119 | type UserDeletePayload { 120 | users: [User] 121 | message: String 122 | clientMutationId: String 123 | } 124 | 125 | """An edge in a connection.""" 126 | type UserEdge { 127 | """The item at the end of the edge""" 128 | node: User 129 | 130 | """A cursor for use in pagination""" 131 | cursor: String! 132 | } 133 | 134 | input UserLoginInput { 135 | email: String! 136 | password: String! 137 | clientMutationId: String 138 | } 139 | 140 | type UserLoginPayload { 141 | token: String 142 | message: String 143 | clientMutationId: String 144 | } 145 | 146 | input userRegisterInput { 147 | name: String! 148 | email: String! 149 | password: String! 150 | description: String! 151 | clientMutationId: String 152 | } 153 | 154 | type userRegisterPayload { 155 | userEdge: UserEdge 156 | message: String 157 | error: Boolean 158 | token: String 159 | clientMutationId: String 160 | } 161 | 162 | input UserUpdateInput { 163 | id: String! 164 | name: String! 165 | description: String! 166 | email: String! 167 | clientMutationId: String 168 | } 169 | 170 | type UserUpdatePayload { 171 | users: [User] 172 | message: String 173 | clientMutationId: String 174 | } 175 | -------------------------------------------------------------------------------- /packages/mdx/src/deck.mdx: -------------------------------------------------------------------------------- 1 | import Code from "mdx-code"; 2 | import { Head, Image, Appear } from "mdx-deck"; 3 | 4 | export { default as theme } from "./theme"; 5 | 6 | import { Cover } from "./Cover"; 7 | import { Intro } from "./Intro"; 8 | import { Img } from "./Img"; 9 | import { Thanks } from "./Thanks"; 10 | 11 | 12 | GraphQL 13 | 14 | 15 | 16 | 17 | --- 18 | 19 | 20 | 21 | --- 22 | 23 | # Motivation 24 | 25 |
    26 |
    Data fetching
    27 |
    Pagination with cursor
    28 |
    Documentation
    29 |
30 | 31 | --- 32 | 33 | # Overview 34 | 35 |
    36 |
    What is GraphQL ?
    37 |
    Under fetching
    38 |
    Over fetching
    39 |
    Query
    40 |
    Mutation
    41 |
    URL version
    42 |
    GraphQL Object Type
    43 |
    Types
    44 |
    Request numbers
    45 |
    Cursor
    46 |
    Documentation
    47 |
    Code
    48 |
49 | 50 | --- 51 | 52 | ## GraphQL vs REST 53 | 54 | 55 | 56 | --- 57 | 58 | ## What is an Under fetching 59 | 60 | Under fetching is when you get less data than you need 61 | 62 | --- 63 | 64 | ## What is an Over fetching 65 | 66 | It's when you get more data than you need (like some fields on the JSON that you don't need) 67 | 68 | --- 69 | 70 | ## How does a query work? 71 | 72 |
We need to get some data
73 | 74 |
    75 |
    Name
    76 |
    Email
    77 |
    Description
    78 |
79 | 80 | --- 81 | 82 | ### REST api request 83 | 84 |
https://fix.dev/api/user/60f291031a6d0b68dc2a149b/
85 | 86 | 87 | --- 88 | 89 | ### GraphQL Query type 90 | 91 | 92 | 93 | --- 94 | 95 | ### GraphQL api request 96 | 97 |
GraphQL query
98 | 99 | 100 | 101 | --- 102 | 103 | ### GraphQL api response 104 | 105 |
GraphQL response
106 | 107 | 108 | 109 | --- 110 | 111 | ### How does a mutation work? 112 | 113 | ###### rest vs graphql 114 | 115 | --- 116 | 117 | ### Delete an user 118 | 119 | 120 | 121 | --- 122 | 123 | ### define a mutation 124 | 125 | 126 | 127 | 128 | --- 129 | 130 | ### Using mutation 131 | 132 | 133 | 134 | 135 | --- 136 | ### Mutation response 137 | 138 | 139 | 140 | 141 | --- 142 | 143 | ### fetch N endpoints 144 | 145 | --- 146 | 147 | ### Get users list and user 148 | 149 | 150 | 151 | 152 | --- 153 | 154 | ### With Graphql 155 | 156 | 157 | 158 | --- 159 | 160 | ### URL version 161 | 162 |
    163 |
    https://fix.dev/api/v1/user/1/
    164 |
    https://fix.dev/api/v2/user/2/
    165 |
    https://fix.dev/api/v3/user/3/
    166 |
167 | 168 | --- 169 | 170 | ### GraphQL Object Type 171 | 172 | 173 | 174 | --- 175 | 176 | ### Types 177 | 178 | 179 | 180 | --- 181 | 182 | ### Request numbers 183 | 184 | 185 | 186 | --- 187 | 188 | ### With GraphQL 189 | 190 | 191 | 192 | --- 193 | 194 | ### Response 195 | 196 | 197 | 198 | --- 199 | 200 | ### Pagination 201 | 202 | #### Offset vs Cursor 203 | 204 | --- 205 | 206 | ### Cursor 207 | 208 | 209 | 210 | --- 211 | 212 | 213 | ### Delete Sam Drunk photos #1 and #2 214 | 215 | 216 | 217 | --- 218 | 219 | ### Page 2 no cat photos 220 | 221 | 222 | 223 | --- 224 | 225 | ### Documentation 226 | 227 | 228 | 229 | --- 230 | 231 | ### Documentation user 232 | 233 | 234 | 235 | --- 236 | 237 | 238 | ## References 239 | 240 | - [Is offset pagination dead?](https://uxdesign.cc/why-facebook-says-cursor-pagination-is-the-greatest-d6b98d86b6c0) 241 | - [Explaining GraphQL Connections](https://www.apollographql.com/blog/graphql/explaining-graphql-connections/) 242 | - [How to implement Pagination and Mutation in GraphQL](https://buddy.works/tutorials/how-to-implement-pagination-and-mutation-in-graphql) 243 | - [A deep dive into the Relay store](https://yashmahalwal.medium.com/a-deep-dive-into-the-relay-store-9388affd2c2b) 244 | 245 | --- 246 | 247 | # Thanks -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | "lib": ["esnext"], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | // "outDir": "./distTs", /* Redirect output structure to the directory. */ 15 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "composite": true, /* Enable project compilation */ 17 | // "removeComments": true, /* Do not emit comments to output. */ 18 | "noEmit": true, /* Do not emit outputs. */ 19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 22 | 23 | /* Strict Type-Checking Options */ 24 | "strict": true, /* Enable all strict type-checking options. */ 25 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 26 | // "strictNullChecks": true, /* Enable strict null checks. */ 27 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 28 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 29 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 30 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 31 | 32 | /* Additional Checks */ 33 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 34 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 35 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 36 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 37 | 38 | /* Module Resolution Options */ 39 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 40 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 41 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 42 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 43 | // "typeRoots": [], /* List of folders to include type definitions from. */ 44 | // "types": [], /* Type declaration files to be included in compilation. */ 45 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 46 | "resolveJsonModule": true, 47 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 48 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 49 | 50 | /* Source Map Options */ 51 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 52 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 53 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 54 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 55 | 56 | /* Experimental Options */ 57 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 58 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 59 | }, 60 | "include": [ 61 | "packages/**/*" 62 | ], 63 | } -------------------------------------------------------------------------------- /packages/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | "lib": [ /* Specify library files to be included in the compilation. */ 7 | "esnext" 8 | ], 9 | // "allowJs": true, /* Allow javascript files to be compiled. */ 10 | // "checkJs": true, /* Report errors in .js files. */ 11 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 12 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 13 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 14 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 15 | // "outFile": "./", /* Concatenate and emit output to single file. */ 16 | "outDir": "./distTs", /* Redirect output structure to the directory. */ 17 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 18 | // "composite": true, /* Enable project compilation */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 31 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 32 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 33 | 34 | /* Additional Checks */ 35 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 36 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 37 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 38 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 39 | 40 | /* Module Resolution Options */ 41 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 42 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 43 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 44 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 45 | // "typeRoots": [], /* List of folders to include type definitions from. */ 46 | // "types": [], /* Type declaration files to be included in compilation. */ 47 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 48 | "resolveJsonModule": true, 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | 52 | /* Source Map Options */ 53 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 54 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 55 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 56 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 57 | 58 | /* Experimental Options */ 59 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 60 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 61 | }, 62 | "include": [ 63 | "src/**/*" 64 | ] 65 | } --------------------------------------------------------------------------------