MERN-Mongo GQL
Created
December 26, 2020 21:57
-
-
Save GGrassiant/437cfbbdee7205c979ebe65d820ad101 to your computer and use it in GitHub Desktop.
MERN-Mongo GQL
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { useState, useEffect } from "react"; | |
| import { GraphQLClient } from "graphql-request"; | |
| export const BASE_URL = | |
| process.env.NODE_ENV === "production" | |
| ? "https://geopins-mtl.herokuapp.com/graphql" | |
| : "http://localhost:4000/graphql"; | |
| export const useClient = () => { | |
| const [idToken, setIdToken] = useState(""); | |
| useEffect(() => { | |
| // get token from current logged-in user | |
| const token = window.gapi.auth2 | |
| .getAuthInstance() | |
| .currentUser.get() | |
| .getAuthResponse().id_token; | |
| setIdToken(token); | |
| }, []); | |
| return new GraphQLClient(BASE_URL, { | |
| headers: { authorization: idToken } | |
| }); | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // FILE TO INSTRUCT REACT THROUGH APOLLO HOW TO HANDLE MUTATIONS WITH GRAPHQL | |
| export const CREATE_PIN_MUTATION = ` | |
| mutation($title: String!, $image: String!, $content: String!, $latitude: Float!, $longitude: Float!) { | |
| createPin(input: { | |
| title: $title, | |
| image: $image, | |
| content: $content, | |
| latitude: $latitude, | |
| longitude: $longitude | |
| }) { | |
| _id | |
| createdAt | |
| title | |
| image | |
| content | |
| latitude | |
| longitude | |
| author { | |
| _id | |
| name | |
| picture | |
| } | |
| } | |
| } | |
| `; | |
| export const DELETE_PIN_MUTATION = ` | |
| mutation($pinId: ID!) { | |
| deletePin(pinId: $pinId) { | |
| _id | |
| } | |
| } | |
| `; | |
| export const CREATE_COMMENT_MUTATION = ` | |
| mutation($pinId: ID!, $text: String!){ | |
| createComment(pinId: $pinId, text: $text) { | |
| _id | |
| createdAt | |
| title | |
| content | |
| image | |
| latitude | |
| longitude | |
| author { | |
| _id | |
| name | |
| } | |
| comments { | |
| text | |
| createdAt | |
| author { | |
| name | |
| picture | |
| } | |
| } | |
| } | |
| } | |
| `; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // FILE TO INSTRUCT REACT THROUGH APOLLO HOW TO HANDLE QUERIES WITH GRAPHQL | |
| export const ME_QUERY = ` | |
| { | |
| me { | |
| _id | |
| name | |
| picture | |
| } | |
| } | |
| `; | |
| export const GET_PINS_QUERY = ` | |
| { | |
| getPins { | |
| _id | |
| createdAt | |
| title | |
| image | |
| content | |
| latitude | |
| longitude | |
| author { | |
| _id | |
| name | |
| picture | |
| } | |
| comments { | |
| text | |
| createdAt | |
| author { | |
| _id | |
| name | |
| picture | |
| } | |
| } | |
| } | |
| } | |
| `; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // FILE TO INSTRUCT REACT THROUGH APOLLO HOW TO HANDLE SUBSCRIPTION WITH GRAPHQL | |
| import gql from 'graphql-tag'; | |
| // same values as what's return from the mutation except for Added we return comments {} even though it's an empty array at the beginning | |
| export const PIN_ADDED_SUBSCRIPTION = gql` | |
| subscription { | |
| pinAdded { | |
| _id | |
| createdAt | |
| title | |
| image | |
| content | |
| latitude | |
| longitude | |
| author { | |
| _id | |
| name | |
| picture | |
| } | |
| comments { | |
| text | |
| createdAt | |
| author { | |
| name | |
| picture | |
| } | |
| } | |
| } | |
| } | |
| `; | |
| export const PIN_UPDATED_SUBSCRIPTION = gql` | |
| subscription { | |
| pinUpdated { | |
| _id | |
| createdAt | |
| title | |
| content | |
| image | |
| latitude | |
| longitude | |
| author { | |
| _id | |
| name | |
| } | |
| comments { | |
| text | |
| createdAt | |
| author { | |
| name | |
| picture | |
| } | |
| } | |
| } | |
| } | |
| `; | |
| export const PIN_DELETED_SUBSCRIPTION = gql` | |
| subscription { | |
| pinDeleted { | |
| _id | |
| } | |
| } | |
| `; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React, { useContext, useReducer } from "react"; | |
| import ReactDOM from "react-dom"; | |
| import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; | |
| import App from "./pages/App"; | |
| import Splash from "./pages/Splash"; | |
| import ProtectedRoute from "./ProtectedRoute"; | |
| import Context from "./context"; | |
| import reducer from "./reducer"; | |
| import "mapbox-gl/dist/mapbox-gl.css"; | |
| import * as serviceWorker from "./serviceWorker"; | |
| // for subscriptions | |
| import { ApolloProvider } from "react-apollo"; | |
| import { ApolloClient } from 'apollo-client'; | |
| import { WebSocketLink } from "apollo-link-ws"; | |
| import { InMemoryCache } from "apollo-cache-inmemory"; | |
| const wsLink = new WebSocketLink({ | |
| uri: process.env.NODE_ENV === 'production' ? 'wss://geopins-mtl.herokuapp.com/graphql' : 'ws://localhost:4000/graphql', | |
| options: { reconnect: true }}); | |
| // for the Apollo Provider around the React app | |
| const client = new ApolloClient({ | |
| link: wsLink, | |
| cache: new InMemoryCache() | |
| }); | |
| const Root = () => { | |
| const initialState = useContext(Context); | |
| const [state, dispatch] = useReducer(reducer, initialState); | |
| // state is the state of the app after reducer has run, with a fist value of initial state provided by the Context | |
| // dispatch from React is going to dispatch state and actions to components | |
| // This is done through wrapping the routes with Context.Provider and passing the state and dispatch | |
| return ( | |
| <Router> | |
| <ApolloProvider client={client}> | |
| <Context.Provider value={ { state, dispatch } }> | |
| <Switch> | |
| <ProtectedRoute exact path="/" component={App} /> | |
| <Route path="/login" component={Splash} /> | |
| </Switch> | |
| </Context.Provider> | |
| </ApolloProvider> | |
| </Router> | |
| ); | |
| }; | |
| ReactDOM.render(<Root />, document.getElementById("root")); | |
| // If you want your app to work offline and load faster, you can change | |
| // unregister() to register() below. Note this comes with some pitfalls. | |
| // Learn more about service workers: http://bit.ly/CRA-PWA | |
| serviceWorker.unregister(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const mongoose = require("mongoose"); | |
| const PinSchema = new mongoose.Schema( | |
| { | |
| title: String, | |
| content: String, | |
| image: String, | |
| latitude: Number, | |
| longitude: Number, | |
| author: { type: mongoose.Schema.ObjectId, ref: "User" }, | |
| comments: [ | |
| { | |
| text: String, | |
| createdAt: { type: Date, default: Date.now }, | |
| author: { type: mongoose.Schema.ObjectId, ref: "User" } | |
| } | |
| ] | |
| }, | |
| { timestamps: true } | |
| ); | |
| module.exports = mongoose.model("Pin", PinSchema); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // FILE TO HANDLE GRAPHQL QL REQUEST FROM THE APP | |
| // HAS ACCESS TO THE SCHEMA/TYPE DEFS, THE MODELS AND THE DB | |
| // ~ LIKE A CONTROLLER | |
| const { AuthenticationError, PubSub } = require('apollo-server'); // PubSub is for subscription | |
| const Pin = require("./models/Pin"); | |
| // live data broadcast | |
| const pubsub = new PubSub(); | |
| const PIN_ADDED = 'PIN_ADDED'; | |
| const PIN_DELETED = 'PIN_DELETED'; | |
| const PIN_UDPDATED = 'PIN_UPDATED'; | |
| // guard clause to check if we have a user before executing the query | |
| // context is defined in the server.js when creating the Apollo server | |
| const authenticated = next => (root, args, context, info) => { | |
| if(!context.currentUser) { | |
| throw new AuthenticationError('You must be logged in') | |
| } | |
| return next(root, args, context, info); // yield to the query if there is a user | |
| }; | |
| module.exports = { | |
| Query: { | |
| me: authenticated((root, args, context) => context.currentUser), // wrap the query with a higher-order guard clause function | |
| getPins: async (root, args, context) => { | |
| const pins = await Pin.find({}) | |
| .populate('author') // populate the authors for all pins | |
| .populate('comments.author'); // populate the comments.author for all pins | |
| return pins | |
| } | |
| }, | |
| Mutation: { | |
| createPin: authenticated(async (root, args, context) => { | |
| const newPin = await new Pin({ | |
| ...args.input, | |
| author: context.currentUser._id // add the author to the pin | |
| }).save(); | |
| // populate in Mongoose is a bit like a join an populates the field 'author: User' of the newly created pin as per the Model | |
| const pinAdded = await Pin.populate(newPin, "author"); | |
| // subscription, will be passed to Subcription resolvers | |
| pubsub.publish(PIN_ADDED, {pinAdded}); // passing the destructured pinAdded from data | |
| // local | |
| return pinAdded; | |
| }), | |
| deletePin: authenticated(async (root, args, context) => { | |
| const pinDeleted = await Pin.findOneAndDelete({_id: args.pinId}).exec(); | |
| // subscription | |
| pubsub.publish(PIN_DELETED, {pinDeleted}); | |
| // local | |
| return pinDeleted; | |
| }), | |
| createComment: authenticated(async (root, args, context) => { | |
| const newComment = {text: args.text, author: context.currentUser._id}; | |
| const pinUpdated = await Pin.findOneAndUpdate( | |
| {_id: args.pinId}, // get the pin from the db | |
| {$push: {comments: newComment}}, // push the newly created comment in the array of ids | |
| {new: true} // return the latest version of this document from Mongo | |
| ).populate('author') // populate the author for the pin | |
| .populate('comments.author'); // populate the comments.author for this updated pin | |
| // subscription | |
| pubsub.publish(PIN_UDPDATED, {pinUpdated}); | |
| // local | |
| return pinUpdated; | |
| }), | |
| }, | |
| Subscription: { | |
| pinAdded: { | |
| subscribe: () => pubsub.asyncIterator(PIN_ADDED) // passed by the createPin resolver | |
| }, | |
| pinDeleted: { | |
| subscribe: () => pubsub.asyncIterator(PIN_DELETED) // passed by the createPin resolver | |
| }, | |
| pinUpdated: { | |
| subscribe: () => pubsub.asyncIterator(PIN_UDPDATED) // passed by the createPin resolver | |
| } | |
| } | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /// Libs | |
| const { ApolloServer } = require ('apollo-server'); | |
| const mongoose = require('mongoose'); | |
| require('dotenv').config(); | |
| // Utils | |
| const typeDefs = require('./typeDefs'); | |
| const resolvers = require('./resolvers'); | |
| const { findOrCreateUser } = require('./controllers/userController'); | |
| mongoose | |
| .connect(process.env.MONGO_URI, { | |
| useNewUrlParser: true, // remove deprecation warning | |
| useUnifiedTopology: true, // remove deprecation warning | |
| useFindAndModify: false | |
| }) | |
| .then(() => console.log('DB connected')) | |
| .catch((error) => console.log(error)); | |
| const server = new ApolloServer({ | |
| cors: { | |
| origin: '*', // <- allow request from all domains | |
| credentials: true}, // <- enable CORS response for requests with credentials (cookies, http authentication) | |
| typeDefs, // making the graphql queries and type definitions available in the GraphQL Apollo Server | |
| resolvers, // making the resolver available in the server to execute queries and mutation | |
| // creating context | |
| context: async ({ req }) => { // grab the request from client's login page in React | |
| let authToken = null; | |
| let currentUser = null; | |
| try { | |
| authToken = req.headers.authorization; | |
| if (authToken) { | |
| currentUser = await findOrCreateUser(authToken); | |
| } | |
| } catch (err) { | |
| console.error(`unable to authenticate user with token ${authToken}`); | |
| } | |
| return { currentUser }; // returning the user to make it available to resolvers to execute a query whether we have a user or not | |
| } | |
| }); | |
| server.listen({ port: process.env.PORT || 4000 }).then(({url}) => { | |
| console.log(`listening on ${url}`); | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // FILE TO DEFINE THE GRAPHQL SCHEMA, QUERIES, MUTATIONS AND SUBSCRIPTION IN THE APP | |
| const { gql } = require('apollo-server'); | |
| module.exports = gql` | |
| type User { | |
| _id: ID | |
| name: String | |
| email: String | |
| picture: String | |
| } | |
| type Pin { | |
| _id: ID | |
| createdAt: String | |
| title: String | |
| content: String | |
| image: String | |
| latitude: Float | |
| longitude: Float | |
| author: User | |
| comments: [Comment] | |
| } | |
| type Comment { | |
| text: String | |
| createdAt: String | |
| author: User | |
| } | |
| input CreatePinInput { | |
| title: String | |
| image: String | |
| content: String | |
| latitude: Float | |
| longitude: Float | |
| } | |
| type Query { | |
| me: User | |
| getPins: [Pin!] | |
| } | |
| type Mutation { | |
| createPin(input: CreatePinInput!): Pin | |
| deletePin(pinId: ID!): Pin | |
| createComment(pinId: ID!, text: String!): Pin | |
| } | |
| type Subscription { | |
| pinAdded: Pin | |
| pinDeleted: Pin | |
| pinUpdated: Pin | |
| } | |
| `; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const mongoose = require('mongoose'); | |
| const UserSchema = new mongoose.Schema({ | |
| name: String, | |
| email: String, | |
| picture: String | |
| }); | |
| module.exports = mongoose.model('User', UserSchema); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Libs | |
| const { OAuth2Client } = require('google-auth-library'); | |
| // Utils | |
| const User = require('../models/User'); | |
| const client = new OAuth2Client(process.env.OAUTH_CLIENT_ID); | |
| exports.findOrCreateUser = async token => { | |
| // verify authToken | |
| const googleUser = await verifyAuthToken(token); | |
| // check if user exists | |
| const user = await checkIfUserExists(googleUser.email); | |
| // if so, return them or create a new user in the DB | |
| return user ? user : createNewUser(googleUser); | |
| }; | |
| const verifyAuthToken = async token => { | |
| try { | |
| const ticket = await client.verifyIdToken({ | |
| idToken: token, | |
| audience: process.env.OAUTH_CLIENT_ID | |
| }); | |
| return ticket.getPayload() // the user | |
| } catch (err) { | |
| console.log("Error verifying auth token", err) | |
| } | |
| }; | |
| const checkIfUserExists = async email => await User.findOne({ email }).exec(); | |
| const createNewUser = googleUser => { | |
| const { name, email, picture } = googleUser; | |
| const user = { name, email, picture }; | |
| return new User(user).save(); | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment