Skip to content

Instantly share code, notes, and snippets.

@GGrassiant
Created December 26, 2020 21:57
Show Gist options
  • Select an option

  • Save GGrassiant/437cfbbdee7205c979ebe65d820ad101 to your computer and use it in GitHub Desktop.

Select an option

Save GGrassiant/437cfbbdee7205c979ebe65d820ad101 to your computer and use it in GitHub Desktop.
MERN-Mongo GQL
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 }
});
};
// 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
email
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
}
}
}
}
`;
// FILE TO INSTRUCT REACT THROUGH APOLLO HOW TO HANDLE QUERIES WITH GRAPHQL
export const ME_QUERY = `
{
me {
_id
name
email
picture
}
}
`;
export const GET_PINS_QUERY = `
{
getPins {
_id
createdAt
title
image
content
latitude
longitude
author {
_id
name
email
picture
}
comments {
text
createdAt
author {
_id
name
picture
}
}
}
}
`;
// 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
email
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
}
}
`;
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();
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);
// 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
}
}
};
/// 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}`);
});
// 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
}
`;
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
name: String,
email: String,
picture: String
});
module.exports = mongoose.model('User', UserSchema);
// 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