├── .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 | 
6 | 
7 | 
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 |
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 |
162 |
163 | );
164 | }
165 |
166 | export default Signup
167 |
--------------------------------------------------------------------------------