diff --git a/apollo-graphql/client/package-lock.json b/apollo-graphql/client/package-lock.json index d4b182f..4753449 100644 --- a/apollo-graphql/client/package-lock.json +++ b/apollo-graphql/client/package-lock.json @@ -13,6 +13,7 @@ "@testing-library/react": "^12.1.2", "@testing-library/user-event": "^13.5.0", "graphql": "^16.6.0", + "graphql-ws": "^5.12.1", "react": "^18.1.0", "react-dom": "^18.1.0", "react-scripts": "5.0.0", @@ -7925,6 +7926,17 @@ "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, + "node_modules/graphql-ws": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.12.1.tgz", + "integrity": "sha512-umt4f5NnMK46ChM2coO36PTFhHouBrK9stWWBczERguwYrGnPNxJ9dimU6IyOBfOkC6Izhkg4H8+F51W/8CYDg==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "graphql": ">=0.11 <=16" + } + }, "node_modules/gzip-size": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", @@ -21805,6 +21817,12 @@ "tslib": "^2.1.0" } }, + "graphql-ws": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.12.1.tgz", + "integrity": "sha512-umt4f5NnMK46ChM2coO36PTFhHouBrK9stWWBczERguwYrGnPNxJ9dimU6IyOBfOkC6Izhkg4H8+F51W/8CYDg==", + "requires": {} + }, "gzip-size": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", diff --git a/apollo-graphql/client/package.json b/apollo-graphql/client/package.json index c124437..2c22edc 100644 --- a/apollo-graphql/client/package.json +++ b/apollo-graphql/client/package.json @@ -8,6 +8,7 @@ "@testing-library/react": "^12.1.2", "@testing-library/user-event": "^13.5.0", "graphql": "^16.6.0", + "graphql-ws": "^5.12.1", "react": "^18.1.0", "react-dom": "^18.1.0", "react-scripts": "5.0.0", diff --git a/apollo-graphql/client/src/App.js b/apollo-graphql/client/src/App.js index e1d08ab..d49db1f 100644 --- a/apollo-graphql/client/src/App.js +++ b/apollo-graphql/client/src/App.js @@ -1,18 +1,26 @@ import { useState } from 'react'; -import { useQuery, useApolloClient } from '@apollo/client'; +import { useQuery, useApolloClient, useSubscription } from '@apollo/client'; import './App.css'; -import { ALL_PERSONS } from './queries/queries'; +import { ALL_PERSONS, PERSON_ADDED } from './queries/queries'; import { Persons } from './Persons'; import { PersonForm } from './forms/PersonForm'; import { PhoneForm } from './forms/PhoneForm'; import { Notify } from './components/Notify'; import { LoginForm } from './forms/LoginForm'; +import { updateCache } from './utils/updateCache'; function App() { const [token, setToken] = useState(null); const [errorMsg, setErrorMsg] = useState(''); const result = useQuery(ALL_PERSONS); const client = useApolloClient(); + useSubscription(PERSON_ADDED, { + onData: ({ data }) => { + const addedPerson = data.data.personAdded + notify(`${addedPerson.name} added`) + updateCache(client.cache, { query: ALL_PERSONS }, addedPerson) + } + }) const notify = (message) => { setErrorMsg(message); diff --git a/apollo-graphql/client/src/forms/LoginForm.js b/apollo-graphql/client/src/forms/LoginForm.js index 3812d66..ed792c4 100644 --- a/apollo-graphql/client/src/forms/LoginForm.js +++ b/apollo-graphql/client/src/forms/LoginForm.js @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import { useMutation } from '@apollo/client'; -import { LOGIN } from '../queries'; +import { LOGIN } from '../queries/queries'; export const LoginForm = props => { const [username, setUsername] = useState(''); diff --git a/apollo-graphql/client/src/forms/PersonForm.js b/apollo-graphql/client/src/forms/PersonForm.js index 2d974e6..c0c0557 100644 --- a/apollo-graphql/client/src/forms/PersonForm.js +++ b/apollo-graphql/client/src/forms/PersonForm.js @@ -1,6 +1,7 @@ import { useState } from 'react'; import { useMutation } from '@apollo/client'; import { ALL_PERSONS, ADD_PERSON } from '../queries/queries'; +import { updateCache } from '../utils/updateCache'; export const PersonForm = props => { const [name, setName] = useState(''); @@ -15,11 +16,7 @@ export const PersonForm = props => { props.setError(message); }, update: (cache, response) => { - cache.updateQuery({ query: ALL_PERSONS }, ({ allPersons }) => { - return { - allPersons: allPersons.concat(response.data.addNewPerson), - } - }) + updateCache(cache, { query: ALL_PERSONS }, response.data.addPerson) } }); diff --git a/apollo-graphql/client/src/forms/PhoneForm.js b/apollo-graphql/client/src/forms/PhoneForm.js index 30f1303..31c1444 100644 --- a/apollo-graphql/client/src/forms/PhoneForm.js +++ b/apollo-graphql/client/src/forms/PhoneForm.js @@ -1,13 +1,12 @@ -import { useState , useEffect } from 'react' -import { useMutation } from '@apollo/client' - -import { EDIT_NUMBER } from '../queries' +import { useState , useEffect } from 'react'; +import { useMutation } from '@apollo/client'; +import { UPDATE_PHONE } from '../queries/queries'; export const PhoneForm = ({ setError }) => { const [name, setName] = useState('') const [phone, setPhone] = useState('') - const [ changeNumber, result ] = useMutation(EDIT_NUMBER, { + const [ changeNumber, result ] = useMutation(UPDATE_PHONE, { onError: (error) => { const errors = error.graphQLErrors[0].extensions.error.errors const messages = Object.values(errors).map(e => e.message).join('\n') diff --git a/apollo-graphql/client/src/index.js b/apollo-graphql/client/src/index.js index a803b8b..c3a6691 100644 --- a/apollo-graphql/client/src/index.js +++ b/apollo-graphql/client/src/index.js @@ -6,9 +6,13 @@ import { ApolloClient, ApolloProvider, InMemoryCache, - createHttpLink + createHttpLink, + split } from '@apollo/client'; import { setContext } from '@apollo/client/link/context'; +import { getMainDefinition } from '@apollo/client/utilities'; +import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; +import { createClient } from 'graphql-ws'; const authLink = setContext((_, { headers }) => { const token = localStorage.getItem('phonenumbers-user-token') @@ -23,9 +27,23 @@ const authLink = setContext((_, { headers }) => { const httpLink = createHttpLink({ uri: 'http://localhost:4000' }); +const wsLink = new GraphQLWsLink( + createClient({ url: 'ws://localhost:4000' }) +); +const splitLink = split( + ({ query }) => { + const definition = getMainDefinition(query) + return ( + definition.kind === 'OperationDefinition' && + definition.operation === 'subscription' + ) + }, + wsLink, + authLink.concat(httpLink) +) const client = new ApolloClient({ - link: authLink.concat(httpLink), + link: splitLink, cache: new InMemoryCache(), }) diff --git a/apollo-graphql/client/src/queries/queries.js b/apollo-graphql/client/src/queries/queries.js index c2e7ebf..045dfc3 100644 --- a/apollo-graphql/client/src/queries/queries.js +++ b/apollo-graphql/client/src/queries/queries.js @@ -59,3 +59,11 @@ export const LOGIN = gql` } } `; + +export const PERSON_ADDED = gql` + subscription { + personAdded { + ${PERSON} + } + } +`; diff --git a/apollo-graphql/client/src/utils/updateCache.js b/apollo-graphql/client/src/utils/updateCache.js new file mode 100644 index 0000000..beaaf21 --- /dev/null +++ b/apollo-graphql/client/src/utils/updateCache.js @@ -0,0 +1,17 @@ +// function that takes care of manipulating cache +export const updateCache = (cache, query, addedPerson) => { + // helper that is used to eliminate saving same person twice + const uniqByName = (a) => { + let seen = new Set(); + return a.filter((item) => { + let k = item.name; + return seen.has(k) ? false : seen.add(k); + }); + } +; + cache.updateQuery(query, ({ allPersons }) => { + return { + allPersons: uniqByName(allPersons.concat(addedPerson)), + }; + }); +}; diff --git a/apollo-graphql/server/models/person.js b/apollo-graphql/server/models/person.js index 40cc1bd..ccddce6 100644 --- a/apollo-graphql/server/models/person.js +++ b/apollo-graphql/server/models/person.js @@ -20,6 +20,12 @@ const schema = new mongoose.Schema({ required: true, minlength: 3 }, + friendOf: [ + { + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + } + ], }); module.exports = mongoose.model('Person', schema); \ No newline at end of file diff --git a/apollo-graphql/server/package-lock.json b/apollo-graphql/server/package-lock.json index 4665a70..a09cd52 100644 --- a/apollo-graphql/server/package-lock.json +++ b/apollo-graphql/server/package-lock.json @@ -10,9 +10,16 @@ "license": "MIT", "dependencies": { "@apollo/server": "^4.6.0", + "@graphql-tools/schema": "^9.0.18", + "cors": "^2.8.5", "dotenv": "^16.0.3", + "express": "^4.18.2", "graphql": "^16.6.0", - "mongoose": "^7.0.4" + "graphql-subscriptions": "^2.0.0", + "graphql-ws": "^5.12.1", + "jsonwebtoken": "^9.0.0", + "mongoose": "^7.0.4", + "ws": "^8.13.0" } }, "node_modules/@apollo/cache-control-types": { @@ -502,6 +509,11 @@ "node": ">=14.20.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -618,6 +630,14 @@ "node": ">=12" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -794,6 +814,28 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, + "node_modules/graphql-subscriptions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/graphql-subscriptions/-/graphql-subscriptions-2.0.0.tgz", + "integrity": "sha512-s6k2b8mmt9gF9pEfkxsaO1lTxaySfKoEJzEfmwguBbQ//Oq23hIXCfR1hm4kdh5hnR20RdwB+s3BCb+0duHSZA==", + "dependencies": { + "iterall": "^1.3.0" + }, + "peerDependencies": { + "graphql": "^15.7.2 || ^16.0.0" + } + }, + "node_modules/graphql-ws": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.12.1.tgz", + "integrity": "sha512-umt4f5NnMK46ChM2coO36PTFhHouBrK9stWWBczERguwYrGnPNxJ9dimU6IyOBfOkC6Izhkg4H8+F51W/8CYDg==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "graphql": ">=0.11 <=16" + } + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -860,6 +902,50 @@ "node": ">= 0.10" } }, + "node_modules/iterall": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz", + "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "dependencies": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kareem": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz", @@ -868,6 +954,11 @@ "node": ">=12.0.0" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -1270,6 +1361,31 @@ "node": ">=6" } }, + "node_modules/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", @@ -1477,6 +1593,31 @@ "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } + }, + "node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } } diff --git a/apollo-graphql/server/package.json b/apollo-graphql/server/package.json index 8a9c3e0..f4cac09 100644 --- a/apollo-graphql/server/package.json +++ b/apollo-graphql/server/package.json @@ -10,8 +10,15 @@ "license": "MIT", "dependencies": { "@apollo/server": "^4.6.0", + "@graphql-tools/schema": "^9.0.18", + "cors": "^2.8.5", "dotenv": "^16.0.3", + "express": "^4.18.2", "graphql": "^16.6.0", - "mongoose": "^7.0.4" + "graphql-subscriptions": "^2.0.0", + "graphql-ws": "^5.12.1", + "jsonwebtoken": "^9.0.0", + "mongoose": "^7.0.4", + "ws": "^8.13.0" } } diff --git a/apollo-graphql/server/resolvers/resolvers.js b/apollo-graphql/server/resolvers/resolvers.js new file mode 100644 index 0000000..a9a9211 --- /dev/null +++ b/apollo-graphql/server/resolvers/resolvers.js @@ -0,0 +1,138 @@ +const { GraphQLError } = require('graphql'); +const jwt = require('jsonwebtoken'); +const { PubSub } = require('graphql-subscriptions'); +const pubsub = new PubSub(); +const Person = require('../models/person'); +const User = require('../models/user'); + +const resolvers = { + Query: { + me: (root, args, context) => context.currentUser, + personCount: async () => Person.collection.countDocuments(), + allPersons: async (root, args) => { + if (!args.phone) { + return Person.find({}).populate('friendOf'); + } + return Person.find({ + phone: { + $exists: args.phone === 'YES' + } + }).populate('friendOf'); + }, + findPerson: async (root, args) => + await Person.findOne({ name: args.name}).populate('friendOf') + }, + Person: { + address: (root) => ({ street: root.street, city: root.city }), + friendOf: async (root) => { + // return list of users + const friends = await User.find({ + friends: { + $in: [root._id] + } + }); + return friends; + } + }, + Mutation: { + addPerson: async (root, args, context) => { + const currentUser = context.currentUser; + if (!currentUser) { + throw new GraphQLError('not authenticated', { + extensions: { + code: 'BAD_USER_INPUT', + } + }) + } + const person = new Person({ ...args }); + try { + await person.save(); + currentUser.friends = currentUser.friends.concat(person) + await currentUser.save() + } catch (error) { + throw new GraphQLError('Saving person failed', { + extensions: { + code: 'BAD_USER_INPUT', + invalidArgs: args.name, + error + } + }); + } + pubsub.publish('PERSON_ADDED', { personAdded: person }); + return person; + }, + editPhone: async (root, args, context) => { + if (!context.currentUser) { + throw new GraphQLError('not authenticated', { + extensions: { + code: 'BAD_USER_INPUT', + } + }); + } + const person = await Person.findOne({ name: args.name }); + person.phone = args.phone; + try { + await person.save(); + } catch (error) { + throw new GraphQLError('Saving person failed', { + extensions: { + code: 'BAD_USER_INPUT', + invalidArgs: args.name, + error + } + }); + } + return person; + }, + addAsFriend: async (root, args, context) => { + const currentUser = context.currentUser; + if (!currentUser) { + throw new GraphQLError('wrong credentials', { + extensions: { code: 'BAD_USER_INPUT' } + }) + } + + const person = Person.findOne({ name: args.name }); + if (!currentUser.friends.some(f=> f._id.toString() === person._id.toString())) { + currentUser.friends = currentUser.friends.concat(person); + } + return await currentUser.save(); + }, + createUser: async (root, args) => { + const user = new User({ username: args.username }); + return user.save().catch(e => { + throw new GraphQLError('User creation failed', { + extensions: { + code: 'BAD_USER_INPUT', + invalidArgs: args.name, + error: e + } + }); + }); + }, + login: async (root, args) => { + const user = await User.findOne({ username: args.username }); + // hard coded password: 'secret' + // change to password hash and store in user collection + if ( !user || args.password !== 'secret' ) { + throw new GraphQLError('wrong credentials', { + extensions: { + code: 'BAD_USER_INPUT' + } + }) + } + const userForToken = { + username: user.username, + id: user._id, + } + return { value: jwt.sign(userForToken, process.env.JWT_SECRET) } + } + }, + Subscription: { + personAdded: { + subscribe: () => pubsub.asyncIterator('PERSON_ADDED') + }, + }, +} + +module.exports = resolvers; diff --git a/apollo-graphql/server/schemas/schemas.js b/apollo-graphql/server/schemas/schemas.js new file mode 100644 index 0000000..1d4c147 --- /dev/null +++ b/apollo-graphql/server/schemas/schemas.js @@ -0,0 +1,67 @@ +const typeDefs = ` + type Address { + street: String! + city: String! + } + + type Person { + name: String! + phone: String + address: Address! + friendOf: [User!]! + id: ID! + } + + type User { + username: String! + friends: [Person!]! + id: ID! + } + + type Token { + value: String! + } + + enum YesNo { + YES NO + } + + type Query { + me: User! + personCount: Int! + allPersons(hasPhone: YesNo): [Person!]! + findPerson(name: String!): Person + } + + type Mutation { + addPerson( + name: String! + phone: String + street: String! + city: String! + ): Person + + editPhone( + name: String! + phone: String! + ): Person + + addAsFriend( + name: String! + ): User + + createUser( + username: String! + ): User + + login( + username: String! + password: String! + ): Token + } + + type Subscription { + personAdded: Person! + } +` +module.exports = typeDefs; diff --git a/apollo-graphql/server/server.js b/apollo-graphql/server/server.js index 345bf0c..d08023a 100644 --- a/apollo-graphql/server/server.js +++ b/apollo-graphql/server/server.js @@ -1,12 +1,19 @@ const config = require('./utils/config'); +const { expressMiddleware } = require('@apollo/server/express4') +const { ApolloServerPluginDrainHttpServer } = require('@apollo/server/plugin/drainHttpServer') +const { makeExecutableSchema } = require('@graphql-tools/schema') +const { WebSocketServer } = require('ws') +const { useServer } = require('graphql-ws/lib/use/ws') +const express = require('express') +const cors = require('cors') +const http = require('http') +const jwt = require('jsonwebtoken') const { ApolloServer } = require('@apollo/server'); -const { startStandaloneServer } = require('@apollo/server/standalone'); -const { v1: uuid } = require('uuid'); -const { GraphQLError } = require('graphql'); const mongoose = require('mongoose'); -const Person = require('./models/person'); const User = require('./models/user'); const logger = require('./utils/logger'); +const typeDefs = require('./schemas/schemas'); +const resolvers = require('./resolvers/resolvers'); mongoose.set('strictQuery', false); logger.info('connecting to mongoDB...') @@ -19,223 +26,60 @@ mongoose.connect(config.MONGODB_URI) logger.error('error connecting to MongoDB:', error.message) }); -// let persons = [ -// { -// name: "Arto Hellas", -// phone: "040-123543", -// street: "Tapiolankatu 5 A", -// city: "Espoo", -// id: "3d594650-3436-11e9-bc57-8b80ba54c431" -// }, -// { -// name: "Matti Luukkainen", -// phone: "040-432342", -// street: "Malminkaari 10 A", -// city: "Helsinki", -// id: '3d599470-3436-11e9-bc57-8b80ba54c431' -// }, -// { -// name: "Venla Ruuska", -// street: "Nallemäentie 22 C", -// city: "Helsinki", -// id: '3d599471-3436-11e9-bc57-8b80ba54c431' -// }, -// ] +mongoose.set('debug', true); -const typeDefs = ` - type Address { - street: String! - city: String! - } - - type Person { - name: String! - phone: String - address: Address! - id: ID! - } - - type User { - username: String! - friends: [Person!]! - id: ID! - } - - type Token { - value: String! - } - - enum YesNo { - YES NO - } - - type Query { - personCount: Int! - allPersons(hasPhone: YesNo): [Person!]! - findPerson(name: String!): Person - } - - type Mutation { - addPerson( - name: String! - phone: String - street: String! - city: String! - ): Person - - editPhone( - name: String! - phone: String! - ): Person - - addAsFriend( - name: String! - ): User - - createUser( - username: String! - ): User - - login( - username: String! - password: String! - ): Token - } -` - -const resolvers = { - Query: { - me: (root, args, context) => context.currentUser, - personCount: async () => Person.collection.countDocuments(), - allPersons: async (root, args) => { - if (!args.phone) { - return Person.find({}); - } - return Person.find({ - phone: { - $exists: args.phone === 'YES' - } - }); - }, - findPerson: async (root, args) => - Person.findOne({ name: args.name}) - }, - Person: { - address: (root) => ({ street: root.street, city: root.city }) - }, - Mutation: { - addPerson: async (root, args, context) => { - const currentUser = context.currentUser; - if (!currentUser) { - throw new GraphQLError('not authenticated', { - extensions: { - code: 'BAD_USER_INPUT', - } - }) - } - const person = new Person({ ...args }); - try { - await person.save(); - currentUser.friends = currentUser.friends.concat(person) - await currentUser.save() - } catch (error) { - throw new GraphQLError('Saving person failed', { - extensions: { - code: 'BAD_USER_INPUT', - invalidArgs: args.name, - error - } - }); - } - return person; - }, - editPhone: async (root, args, context) => { - if (!context.currentUser) { - throw new GraphQLError('not authenticated', { - extensions: { - code: 'BAD_USER_INPUT', - } - }); - } - const person = await Person.findOne({ name: args.name }); - person.phone = args.phone; - try { - await person.save(); - } catch (error) { - throw new GraphQLError('Saving person failed', { - extensions: { - code: 'BAD_USER_INPUT', - invalidArgs: args.name, - error - } - }); - } - return person; - }, - addAsFriend: async (root, args, context) => { - const currentUser = context.currentUser; - if (!currentUser) { - throw new GraphQLError('wrong credentials', { - extensions: { code: 'BAD_USER_INPUT' } - }) - } - - const person = Person.findOne({ name: args.name }); - if (!currentUser.friends.some(f=> f._id.toString() === person._id.toString())) { - currentUser.friends = currentUser.friends.concat(person); - } - return await currentUser.save(); - }, - createUser: async (root, args) => { - const user = new User({ username: args.username }); - return user.save().catch(e => { - throw new GraphQLError('User creation failed', { - extensions: { - code: 'BAD_USER_INPUT', - invalidArgs: args.name, - error: e - } - }); - }); - }, - login: async (root, args) => { - const user = await User.findOne({ username: args.username }); - // hard coded password: 'secret' - // change to password hash and store in user collection - if ( !user || args.password !== 'secret' ) { - throw new GraphQLError('wrong credentials', { - extensions: { - code: 'BAD_USER_INPUT' - } - }) - } - const userForToken = { - username: user.username, - id: user._id, +const start = async () => { + const app = express(); + const httpServer = http.createServer(app); + const wsServer = new WebSocketServer({ + server: httpServer, + path: '/', + }) + + const schema = makeExecutableSchema({ typeDefs, resolvers }) + const serverCleanup = useServer({ schema }, wsServer) + + const server = new ApolloServer({ + schema, + plugins: [ + ApolloServerPluginDrainHttpServer({ httpServer }), + { + async serverWillStart() { + return { + async drainServer() { + await serverCleanup.dispose(); + }, + }; + } } - return { value: jwt.sign(userForToken, process.env.JWT_SECRET) } - } - } -} - -const server = new ApolloServer({ - typeDefs, - resolvers, -}) + ], + }); -startStandaloneServer(server, { - listen: { port: config.PORT }, - context: async ({ req, res }) => { - const auth = req ? req.headers.authorization : null - if (auth && auth.startsWith('Bearer ')) { - const decodedToken = jwt.verify( - auth.substring(7), process.env.JWT_SECRET - ) - const currentUser = await User - .findById(decodedToken.id).populate('friends') - return { currentUser } - } - }, -}).then(({ url }) => { - console.log(`Server ready at ${url}`) -}) + await server.start(); + + app.use( + '/', + cors(), + express.json(), + expressMiddleware(server, { + context: async ({ req }) => { + const auth = req ? req.headers.authorization : null + if (auth && auth.startsWith('Bearer ')) { + const decodedToken = jwt.verify(auth.substring(7), process.env.JWT_SECRET) + const currentUser = await User.findById(decodedToken.id).populate( + 'friends' + ) + return { currentUser } + } + }, + }), + ); + + const PORT = config.PORT; + + httpServer.listen(PORT, () => + console.log(`Server is now running on http://localhost:${PORT}`) + ); +}; + +start();