├── .env ├── .gitignore ├── Makefile ├── README.md ├── back-post ├── .editorconfig ├── Dockerfile ├── docker-compose.yml ├── jest.config.ts ├── package.json ├── src │ ├── __tests__ │ │ └── providerVerify.test.ts │ ├── models │ │ ├── Post.ts │ │ └── User.ts │ ├── prisma │ │ ├── client.ts │ │ ├── migrations │ │ │ ├── 20220721185348_init │ │ │ │ └── migration.sql │ │ │ └── migration_lock.toml │ │ └── schema.prisma │ ├── resolvers │ │ └── Resolvers.ts │ ├── schema.gql │ └── server.ts ├── tsconfig.json └── yarn.lock ├── docker-compose.yml └── post-ui ├── .editorconfig ├── Dockerfile ├── docker-compose.yml ├── jest.config.js ├── package.json ├── pact ├── pactClient.ts └── pactProvider.ts ├── public ├── favicon.ico ├── index.html ├── manifest.json └── robots.txt ├── src ├── App.css ├── App.jsx ├── __tests__ │ └── consumer.test.js ├── components │ ├── AddPostModal.jsx │ └── Card.jsx ├── graphql │ ├── mutations │ │ ├── createPost.js │ │ └── createUser.js │ └── queries │ │ └── getUserById.js ├── index.css ├── index.jsx └── pages │ ├── NotFound.jsx │ ├── Profile.jsx │ └── Signup.jsx └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | ADMIN_USER=qa 2 | ADMIN_PASSWORD=password -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | back-post/node_modules 2 | post-ui/node_modules 3 | .vscode 4 | post-ui/pact/logs 5 | post-ui/pact/pacts -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # My Graphql Contract Testing 3 | # 4 | 5 | back_post: 6 | @echo "Installing dependencies and Running backend ....." 7 | @cd back-post && yarn install && yarn dev 8 | 9 | post_ui: 10 | @echo "Installing dependencies and Running frontend ....." 11 | @cd post-ui && yarn install && yarn start 12 | 13 | run_contract_generate: 14 | @echo "Contract Testing Generation ....." 15 | @cd post-ui && yarn test 16 | 17 | run_contract_publish: 18 | @echo "Contract Testing Publish ....." 19 | @cd post-ui && yarn contract:publish 20 | 21 | run_contract_verify: 22 | @echo "Contract Testing Verify ....." 23 | @cd back-post && yarn test 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Contract-Graphql 2 | 3 |

4 | 5 | ![](https://img.shields.io/badge/license-MIT-green) 6 | ![](https://img.shields.io/badge/language-Typescript-blue) 7 | ![](https://img.shields.io/badge/language-Nodejs-orange) 8 | 9 | 10 | > A ideia deste projeto é mostrar como usar o pact broker (criação, publicação e validação de um contrato) tendo um **consumer** 11 | e um **provider** que interagem através do GraphQL 12 | 13 | Para Saber mais sobre teste de contrato, recomendo ler/assistir:
14 | - [Pact docs](https://docs.pact.io/)
15 | - [Tio Martin Fowler](https://martinfowler.com/articles/consumerDrivenContracts.html)
16 | - [DevTests #38: Teste de contrato com Pact num contexto GraphQL](https://www.youtube.com/watch?v=KtCwZ5h8LZ8&t=3784s&ab_channel=DevTestsBR) 17 | 18 | 19 | ## Dependências 20 | 21 | - Nodejs 16ˆ 22 | - Docker 23 | - Docker Compose 24 | 25 | ## Executando o Pact Broker 26 | 27 | ``` 28 | docker-compose up -d 29 | ``` 30 | 31 | > Acesse o pact broker em: http://localhost:9292.
32 | As credenciais estão no .env
33 | 34 | 35 | ## Executando o backend 36 | 37 | 38 | Instalando as dependências e iniciando o backend 39 | 40 | ``` 41 | cd back-post 42 | docker-compose up -d postgres 43 | yarn install 44 | yarn prisma migrate dev --name init 45 | yarn dev 46 | ``` 47 | 48 | > O playground do GraphiQl estará disponível em http://localhost:4000 49 | 50 | ## Executando o frontend 51 | 52 | 53 | Instalando as dependências e iniciando o frontend 54 | 55 | ``` 56 | make post_ui 57 | ``` 58 | > Acesse o Front em: http://localhost:3000. 59 | 60 | ## Criação, Publicação e Validação de um Contrato 61 | 62 | ``` 63 | make run_contract_generate 64 | ``` 65 | 66 | > O contrato será gerado em: **./contract-graphql/post-ui/pact/pacts** 67 | 68 | ``` 69 | make run_contract_publish 70 | ``` 71 | 72 | > O contrato gerado será publicado no pact broker em: http://localhost:9292 73 | 74 | ``` 75 | make run_contract_verify 76 | ``` 77 | 78 | > O contrato será validado entre o frontend e o backend. **P.S.: É necessário ter o backend rodando.** 79 | -------------------------------------------------------------------------------- /back-post/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true -------------------------------------------------------------------------------- /back-post/Dockerfile: -------------------------------------------------------------------------------- 1 | # pull official base image 2 | FROM node:16-alpine3.14 3 | 4 | # set working directory 5 | WORKDIR /app 6 | 7 | # add `/app/node_modules/.bin` to $PATH 8 | ENV PATH /app/node_modules/.bin:$PATH 9 | 10 | # install app dependencies 11 | COPY package.json ./ 12 | COPY yarn.lock ./ 13 | RUN yarn install --silent 14 | 15 | # add app 16 | COPY . ./ 17 | 18 | # start app 19 | CMD ["yarn", "dev"] 20 | 21 | -------------------------------------------------------------------------------- /back-post/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | backend: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | ports: 9 | - "4000:4000" 10 | restart: always 11 | depends_on: 12 | - postgres 13 | postgres: 14 | image: postgres 15 | restart: always 16 | environment: 17 | POSTGRES_USER: test-user 18 | POSTGRES_PASSWORD: test-password 19 | POSTGRES_DB: postgres 20 | ports: 21 | - '5432:5432' 22 | -------------------------------------------------------------------------------- /back-post/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property and type check, visit: 3 | * https://jestjs.io/docs/en/configuration.html 4 | */ 5 | 6 | export default { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | bail: true, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "/private/var/folders/vb/ccc3njz55yq8cpdr_v_06n300000gq/T/jest_dz", 15 | 16 | // Automatically clear mock calls and instances between every test 17 | clearMocks: true, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | // collectCoverage: false, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | // collectCoverageFrom: undefined, 24 | 25 | // The directory where Jest should output its coverage files 26 | coverageDirectory: "coverage", 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | // coveragePathIgnorePatterns: [ 30 | // "/node_modules/" 31 | // ], 32 | 33 | // Indicates which provider should be used to instrument code for coverage 34 | coverageProvider: "v8", 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: undefined, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: undefined, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // Force coverage collection from ignored files using an array of glob patterns 54 | // forceCoverageMatch: [], 55 | 56 | // A path to a module which exports an async function that is triggered once before all test suites 57 | // globalSetup: undefined, 58 | 59 | // A path to a module which exports an async function that is triggered once after all test suites 60 | // globalTeardown: undefined, 61 | 62 | // A set of global variables that need to be available in all test environments 63 | // globals: {}, 64 | 65 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 66 | // maxWorkers: "50%", 67 | 68 | // An array of directory names to be searched recursively up from the requiring module's location 69 | // moduleDirectories: [ 70 | // "node_modules" 71 | // ], 72 | 73 | // An array of file extensions your modules use 74 | // moduleFileExtensions: [ 75 | // "js", 76 | // "json", 77 | // "jsx", 78 | // "ts", 79 | // "tsx", 80 | // "node" 81 | // ], 82 | 83 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 84 | // moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 85 | // prefix: "/src", 86 | // }), 87 | 88 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 89 | // modulePathIgnorePatterns: [], 90 | 91 | // Activates notifications for test results 92 | // notify: false, 93 | 94 | // An enum that specifies notification mode. Requires { notify: true } 95 | // notifyMode: "failure-change", 96 | 97 | // A preset that is used as a base for Jest's configuration 98 | preset: "ts-jest", 99 | 100 | // Run tests from one or more projects 101 | // projects: undefined, 102 | 103 | // Use this configuration option to add custom reporters to Jest 104 | // reporters: undefined, 105 | 106 | // Automatically reset mock state between every test 107 | // resetMocks: false, 108 | 109 | // Reset the module registry before running each individual test 110 | // resetModules: false, 111 | 112 | // A path to a custom resolver 113 | // resolver: undefined, 114 | 115 | // Automatically restore mock state between every test 116 | // restoreMocks: false, 117 | 118 | // The root directory that Jest should scan for tests and modules within 119 | // rootDir: undefined, 120 | 121 | // A list of paths to directories that Jest should use to search for files in 122 | // roots: [ 123 | // "" 124 | // ], 125 | 126 | // Allows you to use a custom runner instead of Jest's default test runner 127 | // runner: "jest-runner", 128 | 129 | // The paths to modules that run some code to configure or set up the testing environment before each test 130 | // setupFiles: [], 131 | 132 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 133 | // setupFilesAfterEnv: [], 134 | 135 | // The number of seconds after which a test is considered as slow and reported as such in the results. 136 | // slowTestThreshold: 5, 137 | 138 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 139 | // snapshotSerializers: [], 140 | 141 | // The test environment that will be used for testing 142 | testEnvironment: "node", 143 | 144 | // Options that will be passed to the testEnvironment 145 | // testEnvironmentOptions: {}, 146 | 147 | // Adds a location field to test results 148 | // testLocationInResults: false, 149 | 150 | // The glob patterns Jest uses to detect test files 151 | testMatch: ["./**/*.test.ts"], 152 | 153 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 154 | // testPathIgnorePatterns: [ 155 | // "/node_modules/" 156 | // ], 157 | 158 | // The regexp pattern or array of patterns that Jest uses to detect test files 159 | // testRegex: [], 160 | 161 | // This option allows the use of a custom results processor 162 | // testResultsProcessor: undefined, 163 | 164 | // This option allows use of a custom test runner 165 | // testRunner: "jasmine2", 166 | 167 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 168 | // testURL: "http://localhost", 169 | 170 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 171 | // timers: "real", 172 | 173 | // A map from regular expressions to paths to transformers 174 | // transform: undefined, 175 | 176 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 177 | // transformIgnorePatterns: [ 178 | // "/node_modules/", 179 | // "\\.pnp\\.[^\\/]+$" 180 | // ], 181 | testTimeout: 20000, 182 | 183 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 184 | // unmockedModulePathPatterns: undefined, 185 | 186 | // Indicates whether each individual test should be reported during the run 187 | // verbose: undefined, 188 | 189 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 190 | // watchPathIgnorePatterns: [], 191 | 192 | // Whether to use watchman for file crawling 193 | // watchman: true, 194 | }; 195 | -------------------------------------------------------------------------------- /back-post/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "backend using graphql", 5 | "author": "Thialison Souza", 6 | "type": "module", 7 | "engines": { 8 | "node": ">=16.13.1" 9 | }, 10 | "license": "MIT", 11 | "scripts": { 12 | "dev": "ts-node-dev --respawn --transpile-only src/server.ts", 13 | "test": "jest src/__tests__/providerVerify.test.ts --runInBand --forceExit" 14 | }, 15 | "dependencies": { 16 | "@pact-foundation/pact": "^10.1.4", 17 | "@prisma/client": "^4.1.0", 18 | "@types/uuid": "^8.3.4", 19 | "apollo-server": "^3.6.7", 20 | "apollo-server-express": "^3.10.0", 21 | "class-validator": "^0.13.2", 22 | "cors": "^2.8.5", 23 | "express": "^4.18.1", 24 | "graphql": "^15.3.0", 25 | "jest": "^28.1.0", 26 | "prisma": "^4.1.0", 27 | "reflect-metadata": "^0.1.13", 28 | "type-graphql": "^1.1.1", 29 | "uuid": "^8.3.2" 30 | }, 31 | "prisma": { 32 | "schema": "src/prisma/schema.prisma" 33 | }, 34 | "devDependencies": { 35 | "@types/cors": "^2.8.12", 36 | "@types/jest": "^27.5.1", 37 | "@types/mocha": "^9.1.1", 38 | "@types/node": "^17.0.31", 39 | "eslint": "^8.14.0", 40 | "ts-jest": "^28.0.2", 41 | "ts-node-dev": "^1.1.8", 42 | "typescript": "^4.6.4" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /back-post/src/__tests__/providerVerify.test.ts: -------------------------------------------------------------------------------- 1 | import { Verifier } from "@pact-foundation/pact"; 2 | 3 | describe("Pact Verification", () => { 4 | it("validates the expectations of Matching Service", async () => { 5 | const opts = { 6 | providerBaseUrl: "http://localhost:4000/graphql", 7 | pactBrokerUrl: "http://localhost:9292/", 8 | provider: "back-post", 9 | pactBrokerUsername: "qa", 10 | pactBrokerPassword: "password", 11 | consumerVersionTags: ["prod"], 12 | enablePending: false, 13 | publishVerificationResult: true, 14 | verbose: true, 15 | providerVersion: "1.0.0", 16 | }; 17 | 18 | return new Verifier(opts).verifyProvider().then((output) => { 19 | console.log(output); 20 | }); 21 | }, 70000); 22 | }); 23 | -------------------------------------------------------------------------------- /back-post/src/models/Post.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType } from "type-graphql"; 2 | import { User } from "./User"; 3 | 4 | @ObjectType() 5 | export class Post { 6 | @Field((_type) => ID) 7 | id!: string; 8 | 9 | @Field() 10 | title!: string; 11 | 12 | @Field() 13 | message?: string; 14 | 15 | @Field((_type) => User) 16 | user?: User; 17 | } 18 | -------------------------------------------------------------------------------- /back-post/src/models/User.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, Int, ObjectType } from "type-graphql"; 2 | import { Post } from "./Post"; 3 | 4 | @ObjectType() 5 | export class User { 6 | @Field((_type) => ID) 7 | id!: string; 8 | 9 | @Field() 10 | name!: string; 11 | 12 | password!: string; 13 | 14 | @Field((_type) => Int) 15 | age?: number; 16 | 17 | @Field((_type) => [Post]) 18 | posts?: Post[]; 19 | } 20 | -------------------------------------------------------------------------------- /back-post/src/prisma/client.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | export const prisma = new PrismaClient(); 4 | -------------------------------------------------------------------------------- /back-post/src/prisma/migrations/20220721185348_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" TEXT NOT NULL, 4 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 5 | "updatedAt" TIMESTAMP(3) NOT NULL, 6 | "name" TEXT NOT NULL, 7 | "password" TEXT, 8 | "age" INTEGER, 9 | 10 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 11 | ); 12 | 13 | -- CreateTable 14 | CREATE TABLE "Post" ( 15 | "id" TEXT NOT NULL, 16 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 17 | "updatedAt" TIMESTAMP(3) NOT NULL, 18 | "title" TEXT NOT NULL, 19 | "message" TEXT, 20 | "userId" TEXT, 21 | 22 | CONSTRAINT "Post_pkey" PRIMARY KEY ("id") 23 | ); 24 | 25 | -- AddForeignKey 26 | ALTER TABLE "Post" ADD CONSTRAINT "Post_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; 27 | -------------------------------------------------------------------------------- /back-post/src/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /back-post/src/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "postgresql" 10 | url = "postgresql://test-user:test-password@localhost:5432/mydb?schema=public" 11 | } 12 | 13 | model User { 14 | id String @id @default(uuid()) 15 | createdAt DateTime @default(now()) 16 | updatedAt DateTime @updatedAt 17 | name String 18 | password String? 19 | age Int? 20 | posts Post[] 21 | } 22 | 23 | model Post { 24 | id String @id @default(uuid()) 25 | createdAt DateTime @default(now()) 26 | updatedAt DateTime @updatedAt 27 | title String 28 | message String? 29 | user User? @relation(fields: [userId], references: [id]) 30 | userId String? 31 | } 32 | -------------------------------------------------------------------------------- /back-post/src/resolvers/Resolvers.ts: -------------------------------------------------------------------------------- 1 | import { Arg, Mutation, Query, Resolver } from "type-graphql"; 2 | import { User } from "../models/User"; 3 | import { v4 as uuidv4 } from "uuid"; 4 | import { Post } from "../models/Post"; 5 | import { prisma } from "../prisma/client"; 6 | 7 | @Resolver() 8 | export class Resolvers { 9 | @Query(() => User) 10 | async getUserById(@Arg("userId") userId: string) { 11 | const foundUser = await prisma.user.findUnique({ 12 | where: { 13 | id: userId, 14 | }, 15 | include: { 16 | posts: true, 17 | }, 18 | }); 19 | 20 | if (!foundUser) { 21 | return new Error("User does not exist"); 22 | } 23 | 24 | return foundUser; 25 | } 26 | 27 | @Query(() => [Post]) 28 | async getPostsByUser(@Arg("userId") userId: string) { 29 | const foundUser = await prisma.user.findUnique({ 30 | where: { 31 | id: userId, 32 | }, 33 | }); 34 | 35 | if (!foundUser) { 36 | return new Error("User does not exist"); 37 | } 38 | 39 | const foundPostFromUser = await prisma.post.findMany({ 40 | where: { 41 | userId, 42 | }, 43 | include: { 44 | user: true, 45 | }, 46 | }); 47 | 48 | if (!foundPostFromUser) { 49 | return new Error("User does have posts"); 50 | } 51 | 52 | return foundPostFromUser; 53 | } 54 | 55 | @Mutation(() => User) 56 | async createUser( 57 | @Arg("name") name: string, 58 | @Arg("password") password: string, 59 | @Arg("age") age: number 60 | ) { 61 | const newUser = { id: uuidv4(), name, password, age }; 62 | 63 | console.log(newUser); 64 | 65 | return prisma.user.create({ 66 | data: newUser, 67 | }); 68 | } 69 | 70 | @Mutation(() => Post) 71 | async createPost( 72 | @Arg("userId") userId: string, 73 | @Arg("title") title: string, 74 | @Arg("message") message?: string 75 | ) { 76 | const foundUser = await prisma.user.findUnique({ 77 | where: { 78 | id: userId, 79 | }, 80 | }); 81 | 82 | if (!foundUser) { 83 | return new Error("User does not exist"); 84 | } 85 | 86 | const post = { 87 | id: uuidv4(), 88 | title, 89 | message, 90 | userId: foundUser.id, 91 | }; 92 | 93 | return prisma.post.create({ 94 | data: post, 95 | }); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /back-post/src/schema.gql: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------- 2 | # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! 3 | # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! 4 | # ----------------------------------------------- 5 | 6 | type Mutation { 7 | createPost(message: String!, title: String!, userId: String!): Post! 8 | createUser(age: Float!, name: String!, password: String!): User! 9 | } 10 | 11 | type Post { 12 | id: ID! 13 | message: String! 14 | title: String! 15 | user: User! 16 | } 17 | 18 | type Query { 19 | getPostsByUser(userId: String!): [Post!]! 20 | getUserById(userId: String!): User! 21 | } 22 | 23 | type User { 24 | age: Int! 25 | id: ID! 26 | name: String! 27 | posts: [Post!]! 28 | } 29 | -------------------------------------------------------------------------------- /back-post/src/server.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | 3 | import path from "path"; 4 | import { buildSchema } from "type-graphql"; 5 | import { Resolvers } from "./resolvers/Resolvers"; 6 | import { createServer } from "http"; 7 | import express from "express"; 8 | import { ApolloServer } from "apollo-server-express"; 9 | import { prisma } from "./prisma/client"; 10 | import cors from "cors"; 11 | 12 | export async function startServer() { 13 | const app = express(); 14 | const httpServer = createServer(app); 15 | 16 | app.use( 17 | cors({ 18 | origin: "*", 19 | }) 20 | ); 21 | 22 | const schema = await buildSchema({ 23 | resolvers: [Resolvers], 24 | emitSchemaFile: path.resolve(__dirname, "schema.gql"), 25 | }); 26 | 27 | const apolloServer = new ApolloServer({ 28 | schema, 29 | }); 30 | 31 | await apolloServer.start(); 32 | 33 | apolloServer.applyMiddleware({ 34 | app, 35 | path: "/graphql", 36 | }); 37 | 38 | httpServer.listen({ port: process.env.PORT || 4000 }, () => 39 | console.log(`Server listening on localhost:4000${apolloServer.graphqlPath}`) 40 | ); 41 | } 42 | 43 | startServer() 44 | .catch((e) => { 45 | throw e; 46 | }) 47 | .finally(async () => { 48 | await prisma.$disconnect(); 49 | }); 50 | -------------------------------------------------------------------------------- /back-post/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2018", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | "lib": ["es2018", "esnext.asynciterable"], 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "commonjs", /* Specify what module code is generated. */ 28 | // "rootDir": "./", /* Specify the root folder within your source files. */ 29 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | // "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 68 | 69 | /* Interop Constraints */ 70 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 71 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 72 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 73 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 74 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 75 | 76 | /* Type Checking */ 77 | "strict": true, /* Enable all strict type-checking options. */ 78 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 79 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 80 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 81 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 82 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 83 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 84 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 85 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 86 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 87 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 88 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 89 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 90 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 91 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 92 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 93 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 94 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 95 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 96 | 97 | /* Completeness */ 98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 99 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | pact-broker: 5 | image: pactfoundation/pact-broker:2.87.0.2 6 | ports: 7 | - "9292:9292" 8 | depends_on: 9 | - postgres 10 | environment: 11 | PACT_BROKER_BASIC_AUTH_ENABLED: 'true' 12 | PACT_BROKER_BASIC_AUTH_USERNAME: ${ADMIN_USER} 13 | PACT_BROKER_BASIC_AUTH_PASSWORD: ${ADMIN_PASSWORD} 14 | PACT_BROKER_BASE_URL: 'http://localhost:9292' 15 | PACT_BROKER_DATABASE_URL: "postgres://postgres:password@postgres/postgres" 16 | PACT_BROKER_LOG_LEVEL: INFO 17 | PACT_BROKER_SQL_LOG_LEVEL: DEBUG 18 | PACT_BROKER_DATABASE_CONNECT_MAX_RETRIES: "5" 19 | postgres: 20 | image: postgres 21 | ports: 22 | - "5434:5432" 23 | healthcheck: 24 | test: psql postgres --command "select 1" -U postgres 25 | environment: 26 | POSTGRES_USER: postgres 27 | POSTGRES_PASSWORD: password 28 | POSTGRES_DB: postgres 29 | -------------------------------------------------------------------------------- /post-ui/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | -------------------------------------------------------------------------------- /post-ui/Dockerfile: -------------------------------------------------------------------------------- 1 | # pull official base image 2 | FROM node:16-alpine3.14 3 | 4 | # set working directory 5 | WORKDIR /app 6 | 7 | # add `/app/node_modules/.bin` to $PATH 8 | ENV PATH /app/node_modules/.bin:$PATH 9 | 10 | # install app dependencies 11 | COPY package.json ./ 12 | COPY yarn.lock ./ 13 | RUN yarn 14 | 15 | # add app 16 | COPY . ./ 17 | 18 | # start app 19 | CMD ["yarn", "start"] 20 | 21 | -------------------------------------------------------------------------------- /post-ui/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | frontend: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | ports: 9 | - "3000:3000" 10 | restart: always 11 | -------------------------------------------------------------------------------- /post-ui/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest/presets/js-with-ts', 3 | testEnvironment: 'jsdom' 4 | }; -------------------------------------------------------------------------------- /post-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "post-ui", 3 | "version": "1.0.0", 4 | "author": "Thialison Souza", 5 | "engines": { 6 | "node": ">=16.13.1" 7 | }, 8 | "private": true, 9 | "scripts": { 10 | "start": "react-scripts start", 11 | "build": "react-scripts build", 12 | "test": "react-scripts test src/__tests__/**.test.js --watchAll=false --runInBand --detectOpenHandles --forceExit", 13 | "contract:publish": "pact-broker publish ./pact/pacts --tag=prod --consumer-app-version=1.0.0 --broker-username=qa --broker-password=password --broker-base-url=http://localhost:9292" 14 | }, 15 | "dependencies": { 16 | "@apollo/client": "^3.6.2", 17 | "@emotion/react": "^11.9.0", 18 | "@emotion/styled": "^11.8.1", 19 | "@mui/icons-material": "^5.6.2", 20 | "@mui/material": "^5.6.4", 21 | "@pact-foundation/pact": "^9.17.3", 22 | "@testing-library/jest-dom": "^5.11.4", 23 | "@testing-library/react": "^11.1.0", 24 | "@testing-library/user-event": "^12.1.10", 25 | "bootstrap": "5.1.3", 26 | "cross-fetch": "^3.1.5", 27 | "graphql": "^16.4.0", 28 | "onsenui": "^2.12.0", 29 | "react": "^17.0.2", 30 | "react-bootstrap": "^2.0.0", 31 | "react-dom": "^17.0.2", 32 | "react-onsenui": "^1.12.0", 33 | "react-router": "^5.2.1", 34 | "react-router-dom": "^5.3.0", 35 | "react-scripts": "4.0.3", 36 | "ts-jest": "^29.0.3", 37 | "web-vitals": "^1.0.1" 38 | }, 39 | "eslintConfig": { 40 | "extends": [ 41 | "react-app", 42 | "react-app/jest" 43 | ] 44 | }, 45 | "browserslist": { 46 | "production": [ 47 | ">0.2%", 48 | "not dead", 49 | "not op_mini all" 50 | ], 51 | "development": [ 52 | "last 1 chrome version", 53 | "last 1 firefox version", 54 | "last 1 safari version" 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /post-ui/pact/pactClient.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client"; 2 | import fetch from "cross-fetch" 3 | 4 | export const pactClient = () => { 5 | return new ApolloClient({ 6 | cache: new InMemoryCache({ 7 | addTypename: false, 8 | }), 9 | link: createHttpLink({ 10 | fetch, 11 | headers: {}, 12 | uri: `http://localhost:5000/`, 13 | }), 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /post-ui/pact/pactProvider.ts: -------------------------------------------------------------------------------- 1 | import { Pact } from "@pact-foundation/pact"; 2 | import path from "path"; 3 | 4 | export default new Pact({ 5 | port: 5000, 6 | log: path.resolve(process.cwd(), "pact/logs", "pact.log"), 7 | dir: path.resolve(process.cwd(), "pact/pacts"), 8 | consumer: "post-ui", 9 | provider: "back-post", 10 | pactfileWriteMode: "overwrite", 11 | }); 12 | -------------------------------------------------------------------------------- /post-ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thialison/contract-graphql/10ef44a8bbb2f4acfc1f8e4ca98954df6df88bc0/post-ui/public/favicon.ico -------------------------------------------------------------------------------- /post-ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Post App 28 | 29 | 30 | 31 |

32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /post-ui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /post-ui/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /post-ui/src/App.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | .App { 8 | width: 25rem; 9 | margin: 2.5rem auto; 10 | } 11 | -------------------------------------------------------------------------------- /post-ui/src/App.jsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import { Route, Switch } from "react-router"; 3 | import "bootstrap/dist/css/bootstrap.min.css"; 4 | import Profile from "./pages/Profile.jsx"; 5 | import Signup from "./pages/Signup.jsx"; 6 | import NotFound from "./pages/NotFound.jsx"; 7 | 8 | function App() { 9 | return ( 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | ); 19 | } 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /post-ui/src/__tests__/consumer.test.js: -------------------------------------------------------------------------------- 1 | import SIGNUP from "../graphql/mutations/createUser"; 2 | import { GraphQLInteraction, Matchers } from "@pact-foundation/pact"; 3 | import { print } from "graphql/language/printer"; 4 | import Provider from "../../pact/pactProvider"; 5 | import { pactClient } from "../../pact/pactClient"; 6 | 7 | const provider = Provider; 8 | 9 | beforeAll(async () => { 10 | await provider.setup(); 11 | }); 12 | 13 | afterAll(() => provider.finalize()); 14 | 15 | afterEach(() => provider.verify()); 16 | 17 | const mutationString = print(SIGNUP); 18 | 19 | const mutationCreateUser = () => { 20 | return pactClient() 21 | .mutate({ 22 | mutation: SIGNUP, 23 | variables: { 24 | name: "Fake Author", 25 | password: "password", 26 | age: 32, 27 | }, 28 | }) 29 | .then((result) => { 30 | return result; 31 | }) 32 | .catch((err) => { 33 | console.log(err); 34 | }); 35 | }; 36 | 37 | describe("mutation createUser on /graphql", () => { 38 | beforeEach(() => { 39 | const createMessageMutation = new GraphQLInteraction() 40 | .uponReceiving("a CreateUser mutation") 41 | .withMutation(mutationString) 42 | .withVariables({ 43 | name: "Fake Author", 44 | password: "password", 45 | age: 32, 46 | }) 47 | .withOperation("Signup") 48 | .withRequest({ 49 | path: "/", 50 | method: "POST", 51 | }) 52 | .willRespondWith({ 53 | status: 200, 54 | headers: { 55 | "Content-Type": "application/json; charset=utf-8", 56 | }, 57 | body: { 58 | data: { 59 | createUser: { 60 | id: Matchers.like("374d700f-521d-4a00-a992-d37261fcdc9d"), 61 | name: Matchers.like("Fake Author"), 62 | age: Matchers.like(32), 63 | }, 64 | }, 65 | }, 66 | }); 67 | return provider.addInteraction(createMessageMutation); 68 | }); 69 | 70 | it("returns the correct response", async () => { 71 | const response = await mutationCreateUser(); 72 | console.log(response); 73 | }, 70000); 74 | }); 75 | -------------------------------------------------------------------------------- /post-ui/src/components/AddPostModal.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Modal, Form } from "react-bootstrap"; 3 | import { useParams } from "react-router"; 4 | import Box from "@mui/material/Box"; 5 | import Button from "@mui/material/Button"; 6 | import Alert from "@mui/material/Alert"; 7 | import { useMutation } from "@apollo/client"; 8 | import CREATE_POST from "../graphql/mutations/createPost" 9 | 10 | export default function AddPostModal() { 11 | const { id } = useParams(); 12 | 13 | const [show, setShow] = useState(false); 14 | const [error, setError] = useState(null); 15 | 16 | const [createPost] = useMutation(CREATE_POST, { 17 | onError(err) { 18 | console.log(err); 19 | setError(err.message); 20 | }, 21 | }); 22 | 23 | const handleClose = () => { 24 | setShow(false); 25 | setError("") 26 | } 27 | const handleShow = () => setShow(true); 28 | 29 | const [message, setMessage] = useState(""); 30 | const [title, setTitle] = useState(""); 31 | 32 | const refreshPage = () => { 33 | window.location.reload(); 34 | }; 35 | 36 | const handleClick = () => { 37 | if (!message || !title) { 38 | setError("Fill the Gaps"); 39 | } else { 40 | createPost({ 41 | variables: { 42 | userId: id, 43 | title, 44 | message, 45 | }, 46 | }); 47 | handleClose(); 48 | refreshPage(); 49 | } 50 | }; 51 | 52 | return ( 53 | <> 54 | :not(style)": { m: 1 } }} onClick={handleShow}> 55 | 56 | 57 | 58 | 64 | 65 |

ADD POST

66 |
67 | 68 |
69 | 70 | Title 71 | { 76 | setTitle(e.target.value); 77 | setError(""); 78 | }} 79 | /> 80 | 81 | 82 | 86 | Message 87 | { 92 | setMessage(e.target.value); 93 | setError(""); 94 | }} 95 | /> 96 | 97 | {error && Please, fill the gaps} 98 |
99 |
100 | 101 | 104 | 112 | 113 |
114 | 115 | ); 116 | } 117 | -------------------------------------------------------------------------------- /post-ui/src/components/Card.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Card from "@mui/material/Card"; 3 | import CardContent from "@mui/material/CardContent"; 4 | import Typography from "@mui/material/Typography"; 5 | 6 | const formatedDate = new Date( 7 | +new Date() - Math.floor(Math.random() * 10000000000) 8 | ); 9 | 10 | export default function BasicCard({ title, content, user }) { 11 | return ( 12 | 13 | 14 | 15 | {title} 16 | 17 | 18 |
19 | {user ? ( 20 |
21 | Created At {`${formatedDate}`.split(" ").splice(0, 3).join(" ")}{" "} 22 | by {user} 23 |
24 | ) : ( 25 |
26 | )} 27 |
28 |
29 | {content} 30 |
31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /post-ui/src/graphql/mutations/createPost.js: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | const createPost = gql` 4 | mutation CreatePost($userId: String!, $message: String!, $title: String!) { 5 | createPost(userId: $userId, message: $message, title: $title) { 6 | id 7 | title 8 | message 9 | } 10 | } 11 | `; 12 | 13 | export default createPost; 14 | -------------------------------------------------------------------------------- /post-ui/src/graphql/mutations/createUser.js: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | const createUser = gql` 4 | mutation Signup($name: String!, $password: String!, $age: Float!) { 5 | createUser(name: $name, password: $password, age: $age) { 6 | id 7 | name 8 | age 9 | } 10 | } 11 | `; 12 | 13 | export default createUser; 14 | -------------------------------------------------------------------------------- /post-ui/src/graphql/queries/getUserById.js: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | const getUserById = gql` 4 | query ($userId: String!) { 5 | getUserById(userId: $userId) { 6 | id 7 | name 8 | age 9 | posts { 10 | title 11 | message 12 | } 13 | } 14 | } 15 | `; 16 | 17 | export default getUserById; 18 | -------------------------------------------------------------------------------- /post-ui/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /post-ui/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App.jsx"; 5 | import { BrowserRouter } from "react-router-dom"; 6 | import { ApolloProvider, ApolloClient, InMemoryCache } from "@apollo/client"; 7 | 8 | const client = new ApolloClient({ 9 | uri: "http://localhost:4000/graphql", 10 | cache: new InMemoryCache(), 11 | }); 12 | 13 | ReactDOM.render( 14 | 15 | 16 | 17 | 18 | 19 | 20 | , 21 | document.getElementById("root") 22 | ); 23 | -------------------------------------------------------------------------------- /post-ui/src/pages/NotFound.jsx: -------------------------------------------------------------------------------- 1 | export default function NotFound() { 2 | return ( 3 |
4 |

5 | BAD REQUEST 6 |

7 | 14 | {"> "} 404 PAGE {" <"} 15 | 16 |

17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /post-ui/src/pages/Profile.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useQuery } from "@apollo/client"; 3 | import { useParams } from "react-router"; 4 | import AddPostModal from "../components/AddPostModal.jsx"; 5 | import BasicCard from "../components/Card.jsx"; 6 | import NotFound from "./NotFound"; 7 | import Alert from "@mui/material/Alert"; 8 | import GET_USER_BY_ID from "../graphql/queries/getUserById" 9 | 10 | export default function Profile() { 11 | const { id } = useParams(); 12 | 13 | const [closeSucess, setCloseSucess] = useState( 14 | Boolean(localStorage.getItem("signupCompleted")) 15 | ); 16 | 17 | const { data, error, loading } = useQuery(GET_USER_BY_ID, { 18 | variables: { userId: id }, 19 | }); 20 | 21 | if (error) return
{NotFound()}
; 22 | 23 | if (loading) return
LOADING...
; 24 | 25 | const { getUserById } = data; 26 | 27 | const hasPosts = getUserById.posts?.length !== 0; 28 | 29 | return ( 30 |
31 | {closeSucess ? ( 32 | <> 33 | { 35 | setCloseSucess(false); 36 | localStorage.removeItem("signupCompleted"); 37 | }} 38 | > 39 | SIGN UP done successfully 40 | 41 |



42 | 43 | ) : ( 44 |
45 | )} 46 | 47 |
54 |
55 |

{getUserById.name}

56 |
Age: {getUserById.age}
57 |
58 | 59 |
60 |
61 |
62 |

My posts

63 |
64 | {hasPosts ? ( 65 | getUserById.posts?.map((post) => { 66 | return ( 67 | 72 | ); 73 | }) 74 | ) : ( 75 |
76 | {" "} 77 |

78 | USER DOES NOT HAVE POSTS...{" "} 79 |
80 | )} 81 |
82 |
83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /post-ui/src/pages/Signup.jsx: -------------------------------------------------------------------------------- 1 | import Button from "@mui/material/Button"; 2 | import React, { useState } from "react"; 3 | import { Form } from "react-bootstrap"; 4 | import OutlinedInput from "@mui/material/OutlinedInput"; 5 | import InputLabel from "@mui/material/InputLabel"; 6 | import InputAdornment from "@mui/material/InputAdornment"; 7 | import FormControl from "@mui/material/FormControl"; 8 | import Visibility from "@mui/icons-material/Visibility"; 9 | import VisibilityOff from "@mui/icons-material/VisibilityOff"; 10 | import IconButton from "@mui/material/IconButton"; 11 | import Box from "@mui/material/Box"; 12 | import TextField from "@mui/material/TextField"; 13 | import Alert from "@mui/material/Alert"; 14 | import { useHistory } from "react-router-dom"; 15 | import { useMutation } from "@apollo/client"; 16 | import SIGNUP from "../graphql/mutations/createUser" 17 | 18 | const Signup = () => { 19 | const [values, setValues] = React.useState({ 20 | password: "", 21 | showPassword: false, 22 | }); 23 | 24 | const [name, setName] = useState(""); 25 | const [age, setAge] = useState(""); 26 | const [error, setError] = useState(null); 27 | 28 | const [createUser, { data }] = useMutation(SIGNUP, { 29 | onError(err) { 30 | console.log(err); 31 | setError(err.message); 32 | }, 33 | }); 34 | 35 | const handleChange = (prop) => (event) => { 36 | setValues({ ...values, [prop]: event.target.value }); 37 | setError(""); 38 | }; 39 | 40 | const handleClickShowPassword = () => { 41 | setValues({ 42 | ...values, 43 | showPassword: !values.showPassword, 44 | }); 45 | }; 46 | 47 | const handleMouseDownPassword = (event) => { 48 | event.preventDefault(); 49 | }; 50 | 51 | const handleClick = () => { 52 | if (!name) { 53 | setError("Name is required"); 54 | return; 55 | } 56 | if (!values.password) { 57 | setError("Password is required"); 58 | return; 59 | } 60 | if (!age) { 61 | setError("Age is required"); 62 | return; 63 | } 64 | createUser({ 65 | variables: { 66 | name: name, 67 | password: values.password, 68 | age: parseFloat(age), 69 | }, 70 | }); 71 | }; 72 | 73 | let history = useHistory(); 74 | 75 | if (data?.createUser?.id) { 76 | localStorage.setItem("signupCompleted", true); 77 | history.push(`/profile/${data.createUser.id}`); 78 | } 79 | 80 | return ( 81 |
82 |

90 | {" "} 91 | POST UI 92 |

93 |
94 | 103 | { 105 | setName(e.target.value); 106 | setError(""); 107 | }} 108 | fullWidth 109 | label="Your Name" 110 | id="name" 111 | /> 112 | 113 | 114 | 115 | Password 116 | 117 | 124 | 130 | {values.showPassword ? : } 131 | 132 | 133 | } 134 | label="Password" 135 | /> 136 | 137 | 143 | { 151 | setAge(e.target.value); 152 | setError(""); 153 | }} 154 | /> 155 | 156 | {error && {error}} 157 |

158 | 161 |
162 |
163 | ); 164 | } 165 | 166 | export default Signup 167 | --------------------------------------------------------------------------------