From e1b0d4c9fd2168ea772c8544ff7aee5e43a5ed84 Mon Sep 17 00:00:00 2001 From: Michiel Leyman Date: Tue, 27 Jul 2021 11:28:27 +0200 Subject: [PATCH 1/4] feat(common): add applicantsChanged subscription --- common/src/graphql/queries/index.ts | 2 +- .../src/graphql/subscriptions/applicantsChanged.ts | 14 ++++++++++++++ common/src/graphql/subscriptions/index.ts | 1 + 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 common/src/graphql/subscriptions/applicantsChanged.ts create mode 100644 common/src/graphql/subscriptions/index.ts diff --git a/common/src/graphql/queries/index.ts b/common/src/graphql/queries/index.ts index 6eaa73b..f94f6e5 100644 --- a/common/src/graphql/queries/index.ts +++ b/common/src/graphql/queries/index.ts @@ -1 +1 @@ -export { default as me } from './me'; +export { default as ME } from './me'; diff --git a/common/src/graphql/subscriptions/applicantsChanged.ts b/common/src/graphql/subscriptions/applicantsChanged.ts new file mode 100644 index 0000000..3f22963 --- /dev/null +++ b/common/src/graphql/subscriptions/applicantsChanged.ts @@ -0,0 +1,14 @@ +import gql from 'graphql-tag'; + +export default gql` + subscription ApplicantsSub { + applicantsChanged { + id + suggestions + projects + firstname + lastname + isAlumni + } + } +`; diff --git a/common/src/graphql/subscriptions/index.ts b/common/src/graphql/subscriptions/index.ts new file mode 100644 index 0000000..2ca0826 --- /dev/null +++ b/common/src/graphql/subscriptions/index.ts @@ -0,0 +1 @@ +export { default as APPLICANTS_CHANGED } from './applicantsChanged'; From 6eb59b088753baa36dc41f1a5d306b53962aa393 Mon Sep 17 00:00:00 2001 From: Michiel Leyman Date: Tue, 27 Jul 2021 14:32:57 +0200 Subject: [PATCH 2/4] feat(common): add applicant queries to common --- common/src/graphql/queries/applicantList.ts | 15 +++++++++++++++ common/src/graphql/queries/index.ts | 1 + .../graphql/subscriptions/applicantsChanged.ts | 8 ++++++-- common/src/index.ts | 1 + 4 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 common/src/graphql/queries/applicantList.ts diff --git a/common/src/graphql/queries/applicantList.ts b/common/src/graphql/queries/applicantList.ts new file mode 100644 index 0000000..e9d18ac --- /dev/null +++ b/common/src/graphql/queries/applicantList.ts @@ -0,0 +1,15 @@ +import gql from 'graphql-tag'; + +export default gql` + query ApplicantList { + applicants { + id + isAlumni + firstname + lastname + suggestions { + id + } + } + } +`; diff --git a/common/src/graphql/queries/index.ts b/common/src/graphql/queries/index.ts index f94f6e5..e7c6cfb 100644 --- a/common/src/graphql/queries/index.ts +++ b/common/src/graphql/queries/index.ts @@ -1 +1,2 @@ export { default as ME } from './me'; +export { default as APPLICANT_LIST } from './applicantList'; diff --git a/common/src/graphql/subscriptions/applicantsChanged.ts b/common/src/graphql/subscriptions/applicantsChanged.ts index 3f22963..a60d65d 100644 --- a/common/src/graphql/subscriptions/applicantsChanged.ts +++ b/common/src/graphql/subscriptions/applicantsChanged.ts @@ -4,8 +4,12 @@ export default gql` subscription ApplicantsSub { applicantsChanged { id - suggestions - projects + suggestions { + id + } + projects { + id + } firstname lastname isAlumni diff --git a/common/src/index.ts b/common/src/index.ts index 07da892..272e0e0 100644 --- a/common/src/index.ts +++ b/common/src/index.ts @@ -6,3 +6,4 @@ export * from './types/Project'; export * from './types/Profile'; export * from './types/Skill'; export * as queries from './graphql/queries'; +export * as subscriptions from './graphql/subscriptions'; From dde41e329cde43a437383b6d7608d2c1c0d1caeb Mon Sep 17 00:00:00 2001 From: Michiel Leyman Date: Tue, 27 Jul 2021 14:33:09 +0200 Subject: [PATCH 3/4] feat: retrieve all applicants --- client/package.json | 1 + client/src/components/AppWrapper.jsx | 2 +- client/src/components/StudentList.jsx | 3 ++- client/src/hooks/index.js | 1 + client/src/hooks/useAuth.js | 2 +- client/src/hooks/useStudentList.js | 8 ++++++++ client/src/pages/index.jsx | 6 ++++-- client/src/pages/login.jsx | 4 ---- client/src/urql-client.js | 26 ++++++++++++++++++++++++-- 9 files changed, 42 insertions(+), 11 deletions(-) create mode 100644 client/src/hooks/useStudentList.js diff --git a/client/package.json b/client/package.json index f0a1100..b26dd11 100644 --- a/client/package.json +++ b/client/package.json @@ -35,6 +35,7 @@ "react-dnd": "^11.0.0", "react-dnd-html5-backend": "^11.0.0", "react-dom": "^17.0.2", + "subscriptions-transport-ws": "^0.9.19", "urql": "^2.0.4", "zustand": "^3.5.5" }, diff --git a/client/src/components/AppWrapper.jsx b/client/src/components/AppWrapper.jsx index 631cb0b..2943f47 100644 --- a/client/src/components/AppWrapper.jsx +++ b/client/src/components/AppWrapper.jsx @@ -3,7 +3,7 @@ import { useAuth } from '@/hooks'; const AppWrapper = ({ children }) => { const { isLoading } = useAuth(); - if (isLoading) return

; + // if (isLoading) return

; return { children }; }; diff --git a/client/src/components/StudentList.jsx b/client/src/components/StudentList.jsx index 7310ddb..01792c7 100644 --- a/client/src/components/StudentList.jsx +++ b/client/src/components/StudentList.jsx @@ -1,11 +1,12 @@ import React, { useState } from 'react'; -import { useStudents } from '@/hooks'; +import { useStudents, useStudentList } from '@/hooks'; import Filters from './Filters'; import StudentCard from './StudentCard'; import styles from '../assets/styles/dashboard.module.css'; const StudentList = ({ showOnly }) => { + const { applicants, isLoading: applicantsLoading } = useStudentList(); const { students, isLoading } = useStudents(); const [filtered, setFiltered] = useState([]); diff --git a/client/src/hooks/index.js b/client/src/hooks/index.js index f2393c5..bf52276 100644 --- a/client/src/hooks/index.js +++ b/client/src/hooks/index.js @@ -1,4 +1,5 @@ export { default as useAuth } from './useAuth'; export { default as useStudents } from './useStudents'; export { default as useSuggestions } from './useSuggestions'; +export { default as useStudentList } from './useStudentList'; export { default as useRequireAuth } from './useRequireAuth'; diff --git a/client/src/hooks/useAuth.js b/client/src/hooks/useAuth.js index 5cd6a31..e36f0c3 100644 --- a/client/src/hooks/useAuth.js +++ b/client/src/hooks/useAuth.js @@ -16,7 +16,7 @@ const useStore = create((set) => ({ })); export default function useAuth() { - const [{ data, fetching }] = useQuery({ query: queries.me }); + const [{ data, fetching }] = useQuery({ query: queries.ME }); const { user, isLoggingIn, setUser, finishLoading, login, isLoading } = useStore(); const router = useRouter(); diff --git a/client/src/hooks/useStudentList.js b/client/src/hooks/useStudentList.js new file mode 100644 index 0000000..f614e92 --- /dev/null +++ b/client/src/hooks/useStudentList.js @@ -0,0 +1,8 @@ +import { useQuery } from 'urql'; +import { queries } from 'common'; + +export default function useStudents() { + const [{ data, fetching }] = useQuery({ query: queries.APPLICANT_LIST }); + + return { isLoading: fetching, applicants: data ? data.applicants : null }; +} diff --git a/client/src/pages/index.jsx b/client/src/pages/index.jsx index d9e9d2d..f5142b0 100644 --- a/client/src/pages/index.jsx +++ b/client/src/pages/index.jsx @@ -1,11 +1,13 @@ import { useRequireAuth } from '@/hooks'; import StudentDetail from '@/components/StudentDetail'; import SidebarLayout from '@/components/SidebarLayout'; +import { useQuery, useSubscription } from 'urql'; +import { subscriptions, queries } from 'common'; function Index() { - const user = useRequireAuth(); + // const user = useRequireAuth(); - if (!user) return

; + // if (!user) return

; return ; } diff --git a/client/src/pages/login.jsx b/client/src/pages/login.jsx index c692fa5..4b1c291 100644 --- a/client/src/pages/login.jsx +++ b/client/src/pages/login.jsx @@ -2,15 +2,11 @@ import { useEffect } from 'react'; import { useRouter } from 'next/router'; import { useAuth } from '@/hooks'; import { API_URL } from '@/constants'; -import { useQuery } from 'urql'; -import { queries } from 'common'; import SocialButton from '../components/SocialButton'; import styles from '../assets/styles/pending.module.css'; export default function Login() { - const router = useRouter(); - const [result] = useQuery({ query: queries.me }); // const { user } = useAuth(); /* diff --git a/client/src/urql-client.js b/client/src/urql-client.js index 512f4c9..0c8a4d5 100644 --- a/client/src/urql-client.js +++ b/client/src/urql-client.js @@ -1,15 +1,37 @@ import { API_URL } from '@/constants'; import { cacheExchange } from '@urql/exchange-graphcache'; -import { createClient, ssrExchange, dedupExchange, fetchExchange } from 'urql'; +import { + createClient, + ssrExchange, + dedupExchange, + fetchExchange, + subscriptionExchange +} from 'urql'; +import { SubscriptionClient } from 'subscriptions-transport-ws'; // Use a normalized cache const cache = cacheExchange({}); const isServerSide = typeof window === 'undefined'; const ssrCache = ssrExchange({ isClient: !isServerSide }); + +const subscriptionClient = !isServerSide + ? new SubscriptionClient(`ws${API_URL.replace(/^http?/, '')}/graphql`, { + reconnect: true + }) + : null; + const client = createClient({ url: `${API_URL}/graphql`, - exchanges: [dedupExchange, cache, fetchExchange, ssrCache] + exchanges: [ + dedupExchange, + cache, + fetchExchange, + ssrCache, + subscriptionExchange({ + forwardSubscription: (operation) => subscriptionClient.request(operation) + }) + ] }); export { client, ssrCache }; From 045acd54b8c1ddf2cbba66073dca358f88f63caf Mon Sep 17 00:00:00 2001 From: Michiel Leyman Date: Tue, 27 Jul 2021 14:47:29 +0200 Subject: [PATCH 4/4] feat: listen for applicant changes --- client/src/components/StudentList.jsx | 3 +- client/src/hooks/index.js | 1 + client/src/hooks/useApplicantSubscription.js | 9 ++ package-lock.json | 30 +++-- server/prisma/seed.ts | 74 ++++++------ server/src/schema.gql | 114 ++----------------- 6 files changed, 82 insertions(+), 149 deletions(-) create mode 100644 client/src/hooks/useApplicantSubscription.js diff --git a/client/src/components/StudentList.jsx b/client/src/components/StudentList.jsx index 01792c7..e710bb0 100644 --- a/client/src/components/StudentList.jsx +++ b/client/src/components/StudentList.jsx @@ -1,11 +1,12 @@ import React, { useState } from 'react'; -import { useStudents, useStudentList } from '@/hooks'; +import { useStudents, useStudentList, useApplicantSubscription } from '@/hooks'; import Filters from './Filters'; import StudentCard from './StudentCard'; import styles from '../assets/styles/dashboard.module.css'; const StudentList = ({ showOnly }) => { + useApplicantSubscription(); const { applicants, isLoading: applicantsLoading } = useStudentList(); const { students, isLoading } = useStudents(); const [filtered, setFiltered] = useState([]); diff --git a/client/src/hooks/index.js b/client/src/hooks/index.js index bf52276..f91770f 100644 --- a/client/src/hooks/index.js +++ b/client/src/hooks/index.js @@ -1,3 +1,4 @@ +export { default as useApplicantSubscription } from './useApplicantSubscription'; export { default as useAuth } from './useAuth'; export { default as useStudents } from './useStudents'; export { default as useSuggestions } from './useSuggestions'; diff --git a/client/src/hooks/useApplicantSubscription.js b/client/src/hooks/useApplicantSubscription.js new file mode 100644 index 0000000..0f178d8 --- /dev/null +++ b/client/src/hooks/useApplicantSubscription.js @@ -0,0 +1,9 @@ +import { useSubscription } from 'urql'; +import { subscriptions } from 'common'; + +export default function useApplicantSubscription() { + const [{ result, fetching }] = useSubscription({ query: subscriptions.APPLICANTS_CHANGED }); + + console.log(result); + return null; +} diff --git a/package-lock.json b/package-lock.json index 16ed74f..c1b8036 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "react-dnd": "^11.0.0", "react-dnd-html5-backend": "^11.0.0", "react-dom": "^17.0.2", + "subscriptions-transport-ws": "^0.9.19", "urql": "^2.0.4", "zustand": "^3.5.5" }, @@ -9687,6 +9688,12 @@ "url": "https://github.com/sponsors/jaydenseric" } }, + "node_modules/faker": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/faker/-/faker-5.5.3.tgz", + "integrity": "sha512-wLTv2a28wjUyWkbnX7u/ABZBkUkIF2fCd73V6P2oFqEGEktDfzWx4UxrSqtPRw0xPRAcjeAOIiJWqZm3pP4u3g==", + "dev": true + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -18777,9 +18784,9 @@ } }, "node_modules/ws": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.1.tgz", - "integrity": "sha512-2c6faOUH/nhoQN6abwMloF7Iyl0ZS2E9HGtsiLrWn0zOOMWlhtDmdf/uihDt6jnuCxgtwGBNy6Onsoy2s2O2Ow==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz", + "integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==", "engines": { "node": ">=8.3.0" }, @@ -19071,6 +19078,7 @@ "eslint": "^7.22.0", "eslint-config-prettier": "^8.1.0", "eslint-plugin-prettier": "^3.3.1", + "faker": "^5.5.3", "jest": "^26.6.3", "prettier": "^2.2.1", "prisma": "^2.26.0", @@ -24038,6 +24046,7 @@ "react-dnd": "^11.0.0", "react-dnd-html5-backend": "^11.0.0", "react-dom": "^17.0.2", + "subscriptions-transport-ws": "^0.9.19", "typescript": "^4.3.5", "urql": "^2.0.4", "zustand": "^3.5.5" @@ -26702,6 +26711,12 @@ "resolved": "https://registry.npmjs.org/extract-files/-/extract-files-9.0.0.tgz", "integrity": "sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ==" }, + "faker": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/faker/-/faker-5.5.3.tgz", + "integrity": "sha512-wLTv2a28wjUyWkbnX7u/ABZBkUkIF2fCd73V6P2oFqEGEktDfzWx4UxrSqtPRw0xPRAcjeAOIiJWqZm3pP4u3g==", + "dev": true + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -31669,10 +31684,11 @@ "@typescript-eslint/eslint-plugin": "^4.19.0", "@typescript-eslint/parser": "^4.19.0", "apollo-server-express": "^2.25.2", - "common": "*", + "common": "^0.0.1", "eslint": "^7.22.0", "eslint-config-prettier": "^8.1.0", "eslint-plugin-prettier": "^3.3.1", + "faker": "^5.5.3", "graphql": "^15.5.1", "graphql-tools": "^7.0.5", "graphql-type-json": "^0.3.2", @@ -33800,9 +33816,9 @@ } }, "ws": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.1.tgz", - "integrity": "sha512-2c6faOUH/nhoQN6abwMloF7Iyl0ZS2E9HGtsiLrWn0zOOMWlhtDmdf/uihDt6jnuCxgtwGBNy6Onsoy2s2O2Ow==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz", + "integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==", "requires": {} }, "xml-name-validator": { diff --git a/server/prisma/seed.ts b/server/prisma/seed.ts index d3de07e..48c0c77 100644 --- a/server/prisma/seed.ts +++ b/server/prisma/seed.ts @@ -6,45 +6,45 @@ const prisma = new PrismaClient(); async function main() { console.log('seeding...'); - // for (let i = 0; i < 10; i++) { - // await prisma.user.upsert({ - // where: { email: faker.internet.email() }, - // update: {}, - // create: { - // uuid: faker.datatype.uuid(), - // email: faker.internet.email(), - // firstname: faker.name.firstName(), - // lastname: faker.name.lastName(), - // role: Math.random() < 0.5 ? Role.COACH : Role.USER - // } - // }); + for (let i = 0; i < 10; i++) { + // await prisma.user.upsert({ + // where: { email: faker.internet.email() }, + // update: {}, + // create: { + // uuid: faker.datatype.uuid(), + // email: faker.internet.email(), + // firstname: faker.name.firstName(), + // lastname: faker.name.lastName(), + // role: Math.random() < 0.5 ? Role.COACH : Role.USER + // } + // }); - // const genders = ['male', 'female', 'nonbinary']; + const genders = ['male', 'female', 'nonbinary']; - // await prisma.applicant.upsert({ - // where: { email: faker.internet.email() }, - // update: {}, - // create: { - // uuid: faker.datatype.uuid(), - // email: faker.internet.email(), - // firstname: faker.name.firstName(), - // lastname: faker.name.lastName(), - // gender: genders[Math.floor(Math.random() * genders.length)], - // nationality: 'Belgian', - // phone: faker.phone.phoneNumber(), - // address: { - // create: { - // addressLine: faker.address.streetAddress(), - // city: faker.address.cityName(), - // postalCode: faker.address.zipCode(), - // state: faker.address.state(), - // country: faker.address.country() - // } - // }, - // isAlumni: Math.random() < 0.5 - // } - // }); - // } + await prisma.applicant.upsert({ + where: { email: faker.internet.email() }, + update: {}, + create: { + uuid: faker.datatype.uuid(), + email: faker.internet.email(), + firstname: faker.name.firstName(), + lastname: faker.name.lastName(), + gender: genders[Math.floor(Math.random() * genders.length)], + nationality: 'Belgian', + phone: faker.phone.phoneNumber(), + address: { + create: { + addressLine: faker.address.streetAddress(), + city: faker.address.cityName(), + postalCode: faker.address.zipCode(), + state: faker.address.state(), + country: faker.address.country() + } + }, + isAlumni: Math.random() < 0.5 + } + }); + } } main() diff --git a/server/src/schema.gql b/server/src/schema.gql index 31f8c40..7e8ec28 100644 --- a/server/src/schema.gql +++ b/server/src/schema.gql @@ -2,12 +2,6 @@ # THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) # ------------------------------------------------------ -type Skillset { - id: Int! - name: String! - level: String! -} - type Address { id: Int! addressLine: String! @@ -17,6 +11,13 @@ type Address { country: String! } +type Profile { + id: Float! + name: String! + image_url: String + applicants: [Applicant!] +} + type User { id: Int! uuid: String! @@ -39,11 +40,6 @@ A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date """ scalar DateTime -type Skill { - id: Int! - name: String! -} - type Project { id: Int! uuid: String! @@ -54,20 +50,10 @@ type Project { leadCoach: User coaches: [User!] applicants: [Applicant!] - profiles: [Profile!] - skills: [Skill!] createdAt: DateTime! updatedAt: DateTime! } -type Profile { - id: Float! - name: String! - image_url: String - applicants: [Applicant!] - projects: [Project!] -} - type Suggestion { id: Int! status: String! @@ -93,14 +79,11 @@ type Applicant { suggestions: [Suggestion!] projects: [Project!] profiles: [Profile!] - skillset: [Skillset!] } type Query { - applicants(where: FilterApplicantInput): [Applicant!]! + applicants: [Applicant!]! applicant(uuid: String!): Applicant! - skills: [Skill!]! - skill(id: Float!): Skill! users: [User!]! user(uuid: String!): User! suggestions: [Suggestion!]! @@ -112,86 +95,12 @@ type Query { logout: User } -input FilterApplicantInput { - AND: [FilterApplicantInput!] - OR: [FilterApplicantInput!] - NOT: [FilterApplicantInput!] - email: StringFilter - firstname: StringFilter - lastname: StringFilter - callname: StringFilter - gender: StringFilter - phone: StringFilter - nationality: StringFilter - address: FilterAddressInput - isAlumni: Boolean - profiles_every: FilterProfileInput - projects_every: FilterProjectInput - suggestions_every: FilterSuggestionInput - skills_every: FilterSkillInput -} - -input StringFilter { - equals: String - in: [String!] - notIn: [String!] - lt: String - lte: String - gt: String - gte: String - contains: String - startsWith: String - endsWith: String -} - -input FilterAddressInput { - AND: FilterAddressInput - OR: FilterAddressInput - NOT: FilterAddressInput - addressLine: StringFilter - postalCode: StringFilter - city: StringFilter - state: StringFilter - country: StringFilter -} - -input FilterProfileInput { - AND: [FilterProfileInput!] - OR: [FilterProfileInput!] - NOT: [FilterProfileInput!] - name: StringFilter -} - -input FilterProjectInput { - AND: FilterProjectInput - OR: FilterProjectInput - NOT: FilterProjectInput - name: StringFilter - client: StringFilter - leadCoachId: Float -} - -input FilterSuggestionInput { - AND: FilterSuggestionInput - OR: FilterSuggestionInput - NOT: FilterSuggestionInput - status: StringFilter -} - -input FilterSkillInput { - AND: FilterSkillInput - OR: FilterSkillInput - NOT: FilterSkillInput - name: StringFilter -} - type Mutation { createApplicant(input: CreateApplicantInput!): Applicant! updateApplicant(input: UpdateApplicantInput!, uuid: String!): Applicant! deleteApplicant(uuid: String!): Boolean! addApplicantToProject(projectId: Int!, applicantId: Int!): Boolean! removeApplicantFromProject(projectId: Int!, applicantId: Int!): Boolean! - addSkillToApplicant(level: String!, skill: String!, applicantId: Int!): Boolean! updateUser(input: UpdateUserInput!, uuid: String!): User! deleteUser(uuid: String!): Boolean! addUserToProject(projectId: Int!, userId: Int!): Boolean! @@ -202,14 +111,11 @@ type Mutation { createProject(input: CreateProjectInput!): Project! updateProject(input: UpdateProjectInput!, uuid: String!): Project! deleteProject(uuid: String!): Boolean! - addSkillToProject(skill: String!, projectId: Int!): Boolean! createProfile(input: CreateProfileInput!): Profile! updateProfile(input: UpdateProfileInput!, id: Float!): Profile! deleteProfile(id: Float!): Boolean! - addProfileToApplicant(profileId: Int!, applicantId: Int!): Boolean! - removeProfileToApplicant(profileId: Int!, applicantId: Int!): Boolean! - addProfileToProject(profileId: Int!, projectId: Int!): Boolean! - removeProfileToProject(profileId: Int!, projectId: Int!): Boolean! + addProfileToApplicant(projectId: Int!, applicantId: Int!): Boolean! + removeProfileToApplicant(projectId: Int!, applicantId: Int!): Boolean! } input CreateApplicantInput {