├── .env.example ├── .eslintrc ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── cover.png ├── final ├── README.md ├── db.js ├── index.js ├── models │ ├── index.js │ ├── note.js │ └── user.js ├── resolvers │ ├── index.js │ ├── mutation.js │ ├── note.js │ ├── query.js │ └── user.js ├── schema.js └── util │ ├── gravatar.js │ └── seed │ ├── index.js │ ├── notes.js │ └── users.js ├── package-lock.json ├── package.json ├── solutions ├── 01-Web-Server │ ├── index.js │ └── util │ │ └── gravatar.js ├── 02-First-GraphQL │ ├── index.js │ └── util │ │ └── gravatar.js ├── 03-Database │ ├── db.js │ ├── index.js │ ├── models │ │ ├── index.js │ │ └── note.js │ └── util │ │ └── gravatar.js ├── 04-CRUD │ ├── db.js │ ├── index.js │ ├── models │ │ ├── index.js │ │ └── note.js │ ├── resolvers │ │ ├── index.js │ │ ├── mutation.js │ │ └── query.js │ ├── schema.js │ └── util │ │ └── gravatar.js ├── 05-Authentication │ ├── db.js │ ├── index.js │ ├── models │ │ ├── index.js │ │ ├── note.js │ │ └── user.js │ ├── resolvers │ │ ├── index.js │ │ ├── mutation.js │ │ └── query.js │ ├── schema.js │ └── util │ │ └── gravatar.js ├── 06-User-Actions │ ├── db.js │ ├── index.js │ ├── models │ │ ├── index.js │ │ ├── note.js │ │ └── user.js │ ├── resolvers │ │ ├── index.js │ │ ├── mutation.js │ │ ├── note.js │ │ ├── query.js │ │ └── user.js │ ├── schema.js │ └── util │ │ └── gravatar.js ├── 07-Details │ ├── db.js │ ├── index.js │ ├── models │ │ ├── index.js │ │ ├── note.js │ │ └── user.js │ ├── resolvers │ │ ├── index.js │ │ ├── mutation.js │ │ ├── note.js │ │ ├── query.js │ │ └── user.js │ ├── schema.js │ └── util │ │ └── gravatar.js └── README.md └── src ├── index.js └── util └── gravatar.js /.env.example: -------------------------------------------------------------------------------- 1 | ## Database 2 | DB_HOST= 3 | TEST_DB= 4 | 5 | ## Authentication 6 | JWT_SECRET= 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "prettier"], 3 | "parserOptions": { 4 | "ecmaVersion": 2017 5 | }, 6 | "env": { 7 | "es6": true, 8 | "node": true, 9 | "jest": true 10 | }, 11 | "rules": { 12 | "no-console": "off", 13 | "no-unused-vars": "off" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # VSCode settings 64 | .vscode/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # JavaScript Everywhere API 4 | 5 | This repository contains code examples for the API chapters of [_JavaScript Everywhere_](https://www.jseverywhere.io/) by Adam D. Scott, published by O'Reilly Media 6 | 7 | ## Getting Help 8 | 9 | The best place to get help is our Spectrum channel, [spectrum.chat/jseverywhere](https://spectrum.chat/jseverywhere). 10 | 11 | ## Directory Structure 12 | 13 | - `/src` If you are following along with the book, this is the directory where you should perform your development. 14 | - `/solutions` This directory contains the solutions for each chapter. If you get stuck, these are available for you to consult. 15 | - `/final` This directory contains the final working project 16 | 17 | ## To Use the Final Project Files 18 | 19 | If you're developing a UI and would like to use the completed project, copy the files to the completed files to the `src` as follows: 20 | 21 | ``` 22 | cp -rf ./final/* ./src/ 23 | ``` 24 | 25 | ## Seed Data 26 | 27 | To seed data for local development: `npm run seed`. The password for all of the seeded users is `password`. 28 | 29 | Each time this command is run, it will generate 10 users and 25 notes. 30 | 31 | ## Related Repositories 32 | 33 | - [Web 💻 ](https://github.com/javascripteverywhere/web) 34 | - [Mobile 🤳](https://github.com/javascripteverywhere/mobile) 35 | - [Desktop 🖥️](https://github.com/javascripteverywhere/desktop) 36 | 37 | ## Code of Conduct 38 | 39 | In the interest of fostering an open and welcoming environment, I pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.. 40 | 41 | This project pledges to follow the [Contributor's Covenant](http://contributor-covenant.org/version/1/4/). 42 | 43 | ## License 44 | 45 | Copyright 2019 Adam D. Scott 46 | 47 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 48 | 49 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 50 | 51 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 52 | -------------------------------------------------------------------------------- /cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javascripteverywhere/api/fad62b4c7eca820d52270e0cc56cde5bfa9c1386/cover.png -------------------------------------------------------------------------------- /final/README.md: -------------------------------------------------------------------------------- 1 | # JavaScript Everywhere API Solutions 2 | 3 | > The completed API code example for JavaScript Everywhere by Adam Scott, published by O'Reilly Media 4 | 5 | This directory contains the final API example found in the book. For the starter project, visit the `/src` directory and for chapter by chapter solutions, visit `/solutions`. 6 | 7 | 8 | ## Getting Help 9 | 10 | The best place to get help is our Spectrum channel, [spectrum.chat/jseverywhere](https://spectrum.chat/jseverywhere). 11 | -------------------------------------------------------------------------------- /final/db.js: -------------------------------------------------------------------------------- 1 | // Require the mongose library 2 | const mongoose = require('mongoose'); 3 | 4 | module.exports = { 5 | connect: DB_HOST => { 6 | // Use the Mongo driver's updated URL string parser 7 | mongoose.set('useNewUrlParser', true); 8 | // Use `findOneAndUpdate()` in place of findAndModify() 9 | mongoose.set('useFindAndModify', false); 10 | // Use `createIndex()` in place of `ensureIndex()` 11 | mongoose.set('useCreateIndex', true); 12 | // Use the new server discovery & monitoring engine 13 | mongoose.set('useUnifiedTopology', true); 14 | // Connect to the DB 15 | mongoose.connect(DB_HOST); 16 | // Log an error if we fail to connect 17 | mongoose.connection.on('error', err => { 18 | console.error(err); 19 | console.log( 20 | 'MongoDB connection error. Please make sure MongoDB is running.' 21 | ); 22 | process.exit(); 23 | }); 24 | }, 25 | 26 | close: () => { 27 | mongoose.connection.close(); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /final/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { ApolloServer } = require('apollo-server-express'); 3 | const jwt = require('jsonwebtoken'); 4 | const helmet = require('helmet'); 5 | const cors = require('cors'); 6 | const depthLimit = require('graphql-depth-limit'); 7 | const { createComplexityLimitRule } = require('graphql-validation-complexity'); 8 | require('dotenv').config(); 9 | 10 | const db = require('./db'); 11 | const models = require('./models'); 12 | const typeDefs = require('./schema'); 13 | const resolvers = require('./resolvers'); 14 | 15 | // Run our server on a port specified in our .env file or port 4000 16 | const port = process.env.PORT || 4000; 17 | const DB_HOST = process.env.DB_HOST; 18 | 19 | const app = express(); 20 | 21 | db.connect(DB_HOST); 22 | 23 | // Security middleware 24 | app.use(helmet()); 25 | // CORS middleware 26 | app.use(cors()); 27 | 28 | // get the user info from a JWT 29 | const getUser = token => { 30 | if (token) { 31 | try { 32 | // return the user information from the token 33 | return jwt.verify(token, process.env.JWT_SECRET); 34 | } catch (err) { 35 | // if there's a problem with the token, throw an error 36 | throw new Error('Session invalid'); 37 | } 38 | } 39 | }; 40 | 41 | // Apollo Server setup 42 | // updated to include `validationRules` 43 | const server = new ApolloServer({ 44 | typeDefs, 45 | resolvers, 46 | validationRules: [depthLimit(5), createComplexityLimitRule(1000)], 47 | context: async ({ req }) => { 48 | // get the user token from the headers 49 | const token = req.headers.authorization; 50 | // try to retrieve a user with the token 51 | const user = getUser(token); 52 | // add the db models and the user to the context 53 | return { models, user }; 54 | } 55 | }); 56 | 57 | // Apply the Apollo GraphQL middleware and set the path to /api 58 | server.applyMiddleware({ app, path: '/api' }); 59 | 60 | app.listen({ port }, () => 61 | console.log( 62 | `GraphQL Server running at http://localhost:${port}${server.graphqlPath}` 63 | ) 64 | ); 65 | -------------------------------------------------------------------------------- /final/models/index.js: -------------------------------------------------------------------------------- 1 | const Note = require('./note'); 2 | const User = require('./user'); 3 | 4 | const models = { 5 | Note, 6 | User 7 | }; 8 | 9 | module.exports = models; 10 | -------------------------------------------------------------------------------- /final/models/note.js: -------------------------------------------------------------------------------- 1 | // Require the mongose library 2 | const mongoose = require('mongoose'); 3 | 4 | // Define the note's database schema 5 | const noteSchema = new mongoose.Schema( 6 | { 7 | content: { 8 | type: String, 9 | required: true 10 | }, 11 | // reference the author's object ID 12 | author: { 13 | type: mongoose.Schema.Types.ObjectId, 14 | ref: 'User', 15 | required: true 16 | }, 17 | favoriteCount: { 18 | type: Number, 19 | default: 0 20 | }, 21 | favoritedBy: [ 22 | { 23 | type: mongoose.Schema.Types.ObjectId, 24 | ref: 'User' 25 | } 26 | ] 27 | }, 28 | { 29 | // Assigns createdAt and updatedAt fields with a Date type 30 | timestamps: true 31 | } 32 | ); 33 | 34 | // Define the 'Note' model with the schema 35 | const Note = mongoose.model('Note', noteSchema); 36 | // Export the module 37 | module.exports = Note; 38 | -------------------------------------------------------------------------------- /final/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const UserSchema = new mongoose.Schema( 4 | { 5 | username: { 6 | type: String, 7 | required: true, 8 | index: { unique: true } 9 | }, 10 | email: { 11 | type: String, 12 | required: true, 13 | index: { unique: true } 14 | }, 15 | password: { 16 | type: String, 17 | required: true 18 | }, 19 | avatar: { 20 | type: String 21 | } 22 | }, 23 | { 24 | // Assigns createdAt and updatedAt fields with a Date type 25 | timestamps: true 26 | } 27 | ); 28 | 29 | const User = mongoose.model('User', UserSchema); 30 | module.exports = User; 31 | -------------------------------------------------------------------------------- /final/resolvers/index.js: -------------------------------------------------------------------------------- 1 | const Query = require('./query'); 2 | const Mutation = require('./mutation'); 3 | const Note = require('./note'); 4 | const User = require('./user'); 5 | const { GraphQLDateTime } = require('graphql-iso-date'); 6 | 7 | module.exports = { 8 | Query, 9 | Mutation, 10 | Note, 11 | User, 12 | DateTime: GraphQLDateTime 13 | }; 14 | -------------------------------------------------------------------------------- /final/resolvers/mutation.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt'); 2 | const jwt = require('jsonwebtoken'); 3 | const { 4 | AuthenticationError, 5 | ForbiddenError 6 | } = require('apollo-server-express'); 7 | const mongoose = require('mongoose'); 8 | require('dotenv').config(); 9 | 10 | const gravatar = require('../util/gravatar'); 11 | 12 | module.exports = { 13 | newNote: async (parent, args, { models, user }) => { 14 | if (!user) { 15 | throw new AuthenticationError('You must be signed in to create a note'); 16 | } 17 | 18 | return await models.Note.create({ 19 | content: args.content, 20 | author: mongoose.Types.ObjectId(user.id), 21 | favoriteCount: 0 22 | }); 23 | }, 24 | deleteNote: async (parent, { id }, { models, user }) => { 25 | // if not a user, throw an Authentication Error 26 | if (!user) { 27 | throw new AuthenticationError('You must be signed in to delete a note'); 28 | } 29 | 30 | // find the note 31 | const note = await models.Note.findById(id); 32 | // if the note owner and current user don't match, throw a forbidden error 33 | if (note && String(note.author) !== user.id) { 34 | throw new ForbiddenError("You don't have permissions to delete the note"); 35 | } 36 | 37 | try { 38 | // if everything checks out, remove the note 39 | await note.remove(); 40 | return true; 41 | } catch (err) { 42 | // if there's an error along the way, return false 43 | return false; 44 | } 45 | }, 46 | updateNote: async (parent, { content, id }, { models, user }) => { 47 | // if not a user, throw an Authentication Error 48 | if (!user) { 49 | throw new AuthenticationError('You must be signed in to update a note'); 50 | } 51 | 52 | // find the note 53 | const note = await models.Note.findById(id); 54 | // if the note owner and current user don't match, throw a forbidden error 55 | if (note && String(note.author) !== user.id) { 56 | throw new ForbiddenError("You don't have permissions to update the note"); 57 | } 58 | 59 | // Update the note in the db and return the updated note 60 | return await models.Note.findOneAndUpdate( 61 | { 62 | _id: id 63 | }, 64 | { 65 | $set: { 66 | content 67 | } 68 | }, 69 | { 70 | new: true 71 | } 72 | ); 73 | }, 74 | toggleFavorite: async (parent, { id }, { models, user }) => { 75 | // if no user context is passed, throw auth error 76 | if (!user) { 77 | throw new AuthenticationError(); 78 | } 79 | 80 | // check to see if the user has already favorited the note 81 | let noteCheck = await models.Note.findById(id); 82 | const hasUser = noteCheck.favoritedBy.indexOf(user.id); 83 | 84 | // if the user exists in the list 85 | // pull them from the list and reduce the favoriteCount by 1 86 | if (hasUser >= 0) { 87 | return await models.Note.findByIdAndUpdate( 88 | id, 89 | { 90 | $pull: { 91 | favoritedBy: mongoose.Types.ObjectId(user.id) 92 | }, 93 | $inc: { 94 | favoriteCount: -1 95 | } 96 | }, 97 | { 98 | // Set new to true to return the updated doc 99 | new: true 100 | } 101 | ); 102 | } else { 103 | // if the user doesn't exists in the list 104 | // add them to the list and increment the favoriteCount by 1 105 | return await models.Note.findByIdAndUpdate( 106 | id, 107 | { 108 | $push: { 109 | favoritedBy: mongoose.Types.ObjectId(user.id) 110 | }, 111 | $inc: { 112 | favoriteCount: 1 113 | } 114 | }, 115 | { 116 | new: true 117 | } 118 | ); 119 | } 120 | }, 121 | signUp: async (parent, { username, email, password }, { models }) => { 122 | // normalize email address 123 | email = email.trim().toLowerCase(); 124 | // hash the password 125 | const hashed = await bcrypt.hash(password, 10); 126 | // create the gravatar url 127 | const avatar = gravatar(email); 128 | try { 129 | const user = await models.User.create({ 130 | username, 131 | email, 132 | avatar, 133 | password: hashed 134 | }); 135 | 136 | // create and return the json web token 137 | return jwt.sign({ id: user._id }, process.env.JWT_SECRET); 138 | } catch (err) { 139 | // if there's a problem creating the account, throw an error 140 | throw new Error('Error creating account'); 141 | } 142 | }, 143 | 144 | signIn: async (parent, { username, email, password }, { models }) => { 145 | if (email) { 146 | // normalize email address 147 | email = email.trim().toLowerCase(); 148 | } 149 | 150 | const user = await models.User.findOne({ 151 | $or: [{ email }, { username }] 152 | }); 153 | 154 | // if no user is found, throw an authentication error 155 | if (!user) { 156 | throw new AuthenticationError('Error signing in'); 157 | } 158 | 159 | // if the passwords don't match, throw an authentication error 160 | const valid = await bcrypt.compare(password, user.password); 161 | if (!valid) { 162 | throw new AuthenticationError('Error signing in'); 163 | } 164 | 165 | // create and return the json web token 166 | return jwt.sign({ id: user._id }, process.env.JWT_SECRET); 167 | } 168 | }; 169 | -------------------------------------------------------------------------------- /final/resolvers/note.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Resolve the author info for a note when requested 3 | author: async (note, args, { models }) => { 4 | return await models.User.findById(note.author); 5 | }, 6 | // Resolved the favoritedBy info for a note when requested 7 | favoritedBy: async (note, args, { models }) => { 8 | return await models.User.find({ _id: { $in: note.favoritedBy } }); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /final/resolvers/query.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | notes: async (parent, args, { models }) => { 3 | return await models.Note.find().limit(100); 4 | }, 5 | note: async (parent, args, { models }) => { 6 | return await models.Note.findById(args.id); 7 | }, 8 | user: async (parent, args, { models }) => { 9 | return await models.User.findOne({ username: args.username }); 10 | }, 11 | users: async (parent, args, { models }) => { 12 | return await models.User.find({}).limit(100); 13 | }, 14 | me: async (parent, args, { models, user }) => { 15 | return await models.User.findById(user.id); 16 | }, 17 | noteFeed: async (parent, { cursor }, { models }) => { 18 | // hard code the limit to 10 items 19 | const limit = 10; 20 | // set the default hasNextPage value to false 21 | let hasNextPage = false; 22 | // if no cursor is passed the default query will be empty 23 | // this will pull the newest notes from the db 24 | let cursorQuery = {}; 25 | 26 | // if there is a cursor 27 | // our query will look for notes with an ObjectId less than that of the cursor 28 | if (cursor) { 29 | cursorQuery = { _id: { $lt: cursor } }; 30 | } 31 | 32 | // find the limit + 1 of notes in our db, sorted newest to oldest 33 | let notes = await models.Note.find(cursorQuery) 34 | .sort({ _id: -1 }) 35 | .limit(limit + 1); 36 | 37 | // if the number of notes we find exceeds our limit 38 | // set hasNextPage to true & trim the notes to the limit 39 | if (notes.length > limit) { 40 | hasNextPage = true; 41 | notes = notes.slice(0, -1); 42 | } 43 | 44 | // the new cursor will be the Mongo ObjectID of the last item in the feed array 45 | const newCursor = notes[notes.length - 1]._id; 46 | 47 | return { 48 | notes, 49 | cursor: newCursor, 50 | hasNextPage 51 | }; 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /final/resolvers/user.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Resolve the list of notes for a user when requested 3 | notes: async (user, args, { models }) => { 4 | return await models.Note.find({ author: user._id }).sort({ _id: -1 }); 5 | }, 6 | // Resolve the list of favorites for a user when requested 7 | favorites: async (user, args, { models }) => { 8 | return await models.Note.find({ favoritedBy: user._id }).sort({ _id: -1 }); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /final/schema.js: -------------------------------------------------------------------------------- 1 | const { gql } = require('apollo-server-express'); 2 | 3 | module.exports = gql` 4 | scalar DateTime 5 | 6 | type Note { 7 | id: ID! 8 | content: String! 9 | author: User! 10 | favoriteCount: Int! 11 | favoritedBy: [User] 12 | createdAt: DateTime! 13 | updatedAt: DateTime! 14 | } 15 | 16 | type User { 17 | id: ID! 18 | username: String! 19 | email: String! 20 | avatar: String 21 | notes: [Note!]! 22 | favorites: [Note!]! 23 | } 24 | 25 | type NoteFeed { 26 | notes: [Note]! 27 | cursor: String! 28 | hasNextPage: Boolean! 29 | } 30 | 31 | type Query { 32 | notes: [Note!]! 33 | note(id: ID): Note! 34 | user(username: String!): User 35 | users: [User!]! 36 | me: User! 37 | noteFeed(cursor: String): NoteFeed 38 | } 39 | 40 | type Mutation { 41 | newNote(content: String!): Note 42 | updateNote(id: ID!, content: String!): Note! 43 | deleteNote(id: ID!): Boolean! 44 | toggleFavorite(id: ID!): Note! 45 | signUp(username: String!, email: String!, password: String!): String! 46 | signIn(username: String, email: String, password: String!): String! 47 | } 48 | `; 49 | -------------------------------------------------------------------------------- /final/util/gravatar.js: -------------------------------------------------------------------------------- 1 | /* Take in an email and generate a Gravatar url */ 2 | /* https://gravatar.com/site/implement/ */ 3 | const md5 = require('md5'); 4 | 5 | const gravatar = email => { 6 | const hash = md5(email); 7 | return `https://www.gravatar.com/avatar/${hash}.jpg?d=identicon`; 8 | }; 9 | 10 | module.exports = gravatar; 11 | -------------------------------------------------------------------------------- /final/util/seed/index.js: -------------------------------------------------------------------------------- 1 | /* Helper file for seeding user data during testing or local development */ 2 | 3 | const models = require('../../models'); 4 | const seedUsers = require('./users'); 5 | const seedNotes = require('./notes'); 6 | const db = require('../../db'); 7 | require('dotenv').config(); 8 | 9 | const DB_HOST = process.env.DB_HOST; 10 | 11 | const seed = async () => { 12 | console.log('Seeding data...'); 13 | db.connect(DB_HOST); 14 | const users = await models.User.create(await seedUsers()); 15 | await models.Note.create(await seedNotes(users)); 16 | console.log('Data successfully seeded'); 17 | process.exit(0); 18 | }; 19 | 20 | seed(); 21 | 22 | // module.exports = seed; 23 | -------------------------------------------------------------------------------- /final/util/seed/notes.js: -------------------------------------------------------------------------------- 1 | /* Helper file for testing or local dev 2 | /* Generates 25 fake notes */ 3 | 4 | const faker = require('faker'); 5 | const mongoose = require('mongoose'); 6 | const fetch = require('node-fetch'); 7 | 8 | const seedNotes = async users => { 9 | console.log('Seeding notes...'); 10 | let notes = []; 11 | 12 | // generate notes 13 | for (var i = 0; i < 25; i++) { 14 | // pick a random user from the array 15 | let random = [Math.floor(Math.random() * users.length)]; 16 | let content; 17 | 18 | // grab content from the lorem markdownum api 19 | const response = await fetch( 20 | 'https://jaspervdj.be/lorem-markdownum/markdown.txt' 21 | ); 22 | 23 | // if the response is ok, use the content else generate a fake ipsum paragraph 24 | if (response.ok) { 25 | content = await response.text(); 26 | } else { 27 | content = faker.lorem.paragraph(); 28 | } 29 | 30 | let note = { 31 | content, 32 | favoriteCount: 0, 33 | favoritedBy: [], 34 | author: mongoose.Types.ObjectId(users[random]._id) 35 | }; 36 | notes.push(note); 37 | } 38 | return notes; 39 | }; 40 | 41 | module.exports = seedNotes; 42 | -------------------------------------------------------------------------------- /final/util/seed/users.js: -------------------------------------------------------------------------------- 1 | /* Helper file for testing or local dev 2 | /* Generates 10 fake users */ 3 | 4 | const faker = require('faker'); 5 | const bcrypt = require('bcrypt'); 6 | 7 | const gravatar = require('../gravatar'); 8 | 9 | const seedUsers = async () => { 10 | console.log('Seeding users...'); 11 | let users = []; 12 | 13 | // generate 10 user profiles 14 | for (var i = 0; i < 10; i++) { 15 | let user = { 16 | username: faker.internet.userName(), 17 | password: await bcrypt.hash('password', 10), 18 | email: faker.internet.email() 19 | }; 20 | user.avatar = gravatar(user.email); 21 | users.push(user); 22 | } 23 | return users; 24 | }; 25 | 26 | module.exports = seedUsers; 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notedly-api", 3 | "version": "1.0.0", 4 | "description": "API code examples for JavaScript Everywhere by Adam Scott, published by O'Reilly Media", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "start": "nodemon src/index.js", 8 | "dev": "nodemon src/index.js", 9 | "final": "nodemon final/index.js", 10 | "seed": "node final/util/seed/index.js", 11 | "lint": "eslint \"src/**/*.js\"" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git@github.com:javascripteverywhere/api.git" 16 | }, 17 | "nodemonConfig": { 18 | "ignore": [ 19 | "**.test.js" 20 | ] 21 | }, 22 | "keywords": [], 23 | "author": "Adam Scott", 24 | "license": "MIT", 25 | "dependencies": { 26 | "apollo-server-express": "2.4.0", 27 | "bcrypt": "3.0.6", 28 | "cors": "2.8.5", 29 | "dotenv": "6.1.0", 30 | "express": "4.16.4", 31 | "express-session": "1.15.6", 32 | "graphql": "^14.1.1", 33 | "graphql-depth-limit": "1.1.0", 34 | "graphql-iso-date": "3.6.1", 35 | "graphql-validation-complexity": "0.2.4", 36 | "helmet": "3.21.2", 37 | "jsonwebtoken": "8.5.1", 38 | "marked": "0.7.0", 39 | "md5": "2.2.1", 40 | "mongoose": "5.7.13", 41 | "nodemon": "1.18.7", 42 | "passport": "0.4.0", 43 | "passport-github2": "0.1.11" 44 | }, 45 | "devDependencies": { 46 | "eslint": "5.13.0", 47 | "eslint-config-prettier": "4.0.0", 48 | "eslint-plugin-prettier": "3.0.1", 49 | "faker": "4.1.0", 50 | "node-fetch": "2.5.0", 51 | "prettier": "1.18.2" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /solutions/01-Web-Server/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const app = express(); 4 | const port = process.env.PORT || 4000; 5 | 6 | app.get('/', (req, res) => res.send('Hello World!!!')); 7 | 8 | app.listen(port, () => 9 | console.log(`Server running at http://localhost:${port}`) 10 | ); 11 | -------------------------------------------------------------------------------- /solutions/01-Web-Server/util/gravatar.js: -------------------------------------------------------------------------------- 1 | /* Take in an email and generate a Gravatar url */ 2 | /* https://gravatar.com/site/implement/ */ 3 | const md5 = require('md5'); 4 | 5 | const gravatar = email => { 6 | const hash = md5(email); 7 | return `https://www.gravatar.com/avatar/${hash}.jpg?d=identicon`; 8 | }; 9 | 10 | module.exports = gravatar; 11 | -------------------------------------------------------------------------------- /solutions/02-First-GraphQL/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { ApolloServer, gql } = require('apollo-server-express'); 3 | 4 | // Run the server on a port specified in our .env file or port 4000 5 | const port = process.env.PORT || 4000; 6 | 7 | let notes = [ 8 | { 9 | id: '1', 10 | content: 'This is a note', 11 | author: 'Adam Scott' 12 | }, 13 | { 14 | id: '2', 15 | content: 'This is another note', 16 | author: 'Harlow Everly' 17 | }, 18 | { 19 | id: '3', 20 | content: 'Oh hey look, another note!', 21 | author: 'Riley Harrison' 22 | } 23 | ]; 24 | 25 | // Construct a schema, using GraphQL's schema language 26 | const typeDefs = gql` 27 | type Note { 28 | id: ID 29 | content: String 30 | author: String 31 | } 32 | 33 | type Query { 34 | hello: String 35 | notes: [Note] 36 | note(id: ID): Note 37 | } 38 | 39 | type Mutation { 40 | newNote(content: String!): Note 41 | } 42 | `; 43 | 44 | // Provide resolver functions for our schema fields 45 | const resolvers = { 46 | Query: { 47 | hello: () => 'Hello world!', 48 | notes: () => notes, 49 | note: (parent, args) => { 50 | return notes.find(note => note.id === args.id); 51 | } 52 | }, 53 | Mutation: { 54 | newNote: (parent, args) => { 55 | let noteValue = { 56 | id: String(notes.length + 1), 57 | content: args.content, 58 | author: 'Adam Scott' 59 | }; 60 | notes.push(noteValue); 61 | return noteValue; 62 | } 63 | } 64 | }; 65 | 66 | const app = express(); 67 | 68 | // Apollo Server setup 69 | const server = new ApolloServer({ typeDefs, resolvers }); 70 | 71 | // Apply the Apollo GraphQL middleware and set the path to /api 72 | server.applyMiddleware({ app, path: '/api' }); 73 | 74 | app.listen({ port }, () => 75 | console.log( 76 | `GraphQL Server running at http://localhost:${port}${server.graphqlPath}` 77 | ) 78 | ); 79 | -------------------------------------------------------------------------------- /solutions/02-First-GraphQL/util/gravatar.js: -------------------------------------------------------------------------------- 1 | /* Take in an email and generate a Gravatar url */ 2 | /* https://gravatar.com/site/implement/ */ 3 | const md5 = require('md5'); 4 | 5 | const gravatar = email => { 6 | const hash = md5(email); 7 | return `https://www.gravatar.com/avatar/${hash}.jpg?d=identicon`; 8 | }; 9 | 10 | module.exports = gravatar; 11 | -------------------------------------------------------------------------------- /solutions/03-Database/db.js: -------------------------------------------------------------------------------- 1 | // Require the mongose library 2 | const mongoose = require('mongoose'); 3 | 4 | module.exports = { 5 | connect: DB_HOST => { 6 | // Use the Mongo driver's updated URL string parser 7 | mongoose.set('useNewUrlParser', true); 8 | // Use `findOneAndUpdate()` in place of findAndModify() 9 | mongoose.set('useFindAndModify', false); 10 | // Use `createIndex()` in place of `ensureIndex()` 11 | mongoose.set('useCreateIndex', true); 12 | // Use the new server discovery & monitoring engine 13 | mongoose.set('useUnifiedTopology', true); 14 | // Connect to the DB 15 | mongoose.connect(DB_HOST); 16 | // Log an error if we fail to connect 17 | mongoose.connection.on('error', err => { 18 | console.error(err); 19 | console.log( 20 | 'MongoDB connection error. Please make sure MongoDB is running.' 21 | ); 22 | process.exit(); 23 | }); 24 | }, 25 | 26 | close: () => { 27 | mongoose.connection.close(); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /solutions/03-Database/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { ApolloServer, gql } = require('apollo-server-express'); 3 | require('dotenv').config(); 4 | 5 | const db = require('./db'); 6 | const models = require('./models'); 7 | 8 | // Run our server on a port specified in our .env file or port 4000 9 | const port = process.env.PORT || 4000; 10 | const DB_HOST = process.env.DB_HOST; 11 | 12 | // Construct a schema, using GraphQL's schema language 13 | const typeDefs = gql` 14 | type Note { 15 | id: ID 16 | content: String 17 | author: String 18 | } 19 | 20 | type Query { 21 | hello: String 22 | notes: [Note] 23 | note(id: ID): Note 24 | } 25 | 26 | type Mutation { 27 | newNote(content: String!): Note 28 | } 29 | `; 30 | 31 | // Provide resolver functions for our schema fields 32 | const resolvers = { 33 | Query: { 34 | hello: () => 'Hello world!', 35 | notes: async () => { 36 | return await models.Note.find(); 37 | }, 38 | note: async (parent, args) => { 39 | return await models.Note.findById(args.id); 40 | } 41 | }, 42 | Mutation: { 43 | newNote: async (parent, args) => { 44 | return await models.Note.create({ 45 | content: args.content, 46 | author: 'Adam Scott' 47 | }); 48 | } 49 | } 50 | }; 51 | 52 | const app = express(); 53 | 54 | db.connect(DB_HOST); 55 | 56 | // Apollo Server setup 57 | const server = new ApolloServer({ typeDefs, resolvers }); 58 | 59 | // Apply the Apollo GraphQL middleware and set the path to /api 60 | server.applyMiddleware({ app, path: '/api' }); 61 | 62 | app.listen({ port }, () => 63 | console.log( 64 | `GraphQL Server running at http://localhost:${port}${server.graphqlPath}` 65 | ) 66 | ); 67 | -------------------------------------------------------------------------------- /solutions/03-Database/models/index.js: -------------------------------------------------------------------------------- 1 | const Note = require('./note'); 2 | 3 | const models = { 4 | Note 5 | }; 6 | 7 | module.exports = models; 8 | -------------------------------------------------------------------------------- /solutions/03-Database/models/note.js: -------------------------------------------------------------------------------- 1 | // Require the mongose library 2 | const mongoose = require('mongoose'); 3 | 4 | // Define the note's database schema 5 | const noteSchema = new mongoose.Schema( 6 | { 7 | content: { 8 | type: String, 9 | required: true 10 | }, 11 | author: { 12 | type: String, 13 | required: true 14 | } 15 | }, 16 | { 17 | // Assigns createdAt and updatedAt fields with a Date type 18 | timestamps: true 19 | } 20 | ); 21 | 22 | // Define the 'Note' model with the schema 23 | const Note = mongoose.model('Note', noteSchema); 24 | // Export the module 25 | module.exports = Note; 26 | -------------------------------------------------------------------------------- /solutions/03-Database/util/gravatar.js: -------------------------------------------------------------------------------- 1 | /* Take in an email and generate a Gravatar url */ 2 | /* https://gravatar.com/site/implement/ */ 3 | const md5 = require('md5'); 4 | 5 | const gravatar = email => { 6 | const hash = md5(email); 7 | return `https://www.gravatar.com/avatar/${hash}.jpg?d=identicon`; 8 | }; 9 | 10 | module.exports = gravatar; 11 | -------------------------------------------------------------------------------- /solutions/04-CRUD/db.js: -------------------------------------------------------------------------------- 1 | // Require the mongose library 2 | const mongoose = require('mongoose'); 3 | 4 | module.exports = { 5 | connect: DB_HOST => { 6 | // Use the Mongo driver's updated URL string parser 7 | mongoose.set('useNewUrlParser', true); 8 | // Use `findOneAndUpdate()` in place of findAndModify() 9 | mongoose.set('useFindAndModify', false); 10 | // Use `createIndex()` in place of `ensureIndex()` 11 | mongoose.set('useCreateIndex', true); 12 | // Use the new server discovery & monitoring engine 13 | mongoose.set('useUnifiedTopology', true); 14 | // Connect to the DB 15 | mongoose.connect(DB_HOST); 16 | // Log an error if we fail to connect 17 | mongoose.connection.on('error', err => { 18 | console.error(err); 19 | console.log( 20 | 'MongoDB connection error. Please make sure MongoDB is running.' 21 | ); 22 | process.exit(); 23 | }); 24 | }, 25 | 26 | close: () => { 27 | mongoose.connection.close(); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /solutions/04-CRUD/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { ApolloServer } = require('apollo-server-express'); 3 | require('dotenv').config(); 4 | 5 | const db = require('./db'); 6 | const models = require('./models'); 7 | const typeDefs = require('./schema'); 8 | const resolvers = require('./resolvers'); 9 | 10 | // Run our server on a port specified in our .env file or port 4000 11 | const port = process.env.PORT || 4000; 12 | const DB_HOST = process.env.DB_HOST; 13 | 14 | const app = express(); 15 | 16 | db.connect(DB_HOST); 17 | 18 | // Apollo Server setup 19 | const server = new ApolloServer({ 20 | typeDefs, 21 | resolvers, 22 | context: () => { 23 | // add the db models to the context 24 | return { models }; 25 | } 26 | }); 27 | 28 | // Apply the Apollo GraphQL middleware and set the path to /api 29 | server.applyMiddleware({ app, path: '/api' }); 30 | 31 | app.listen({ port }, () => 32 | console.log( 33 | `GraphQL Server running at http://localhost:${port}${server.graphqlPath}` 34 | ) 35 | ); 36 | -------------------------------------------------------------------------------- /solutions/04-CRUD/models/index.js: -------------------------------------------------------------------------------- 1 | const Note = require('./note'); 2 | 3 | const models = { 4 | Note 5 | }; 6 | 7 | module.exports = models; 8 | -------------------------------------------------------------------------------- /solutions/04-CRUD/models/note.js: -------------------------------------------------------------------------------- 1 | // Require the mongose library 2 | const mongoose = require('mongoose'); 3 | 4 | // Define the note's database schema 5 | const noteSchema = new mongoose.Schema( 6 | { 7 | content: { 8 | type: String, 9 | required: true 10 | }, 11 | author: { 12 | type: String, 13 | required: true 14 | } 15 | }, 16 | { 17 | // Assigns createdAt and updatedAt fields with a Date type 18 | timestamps: true 19 | } 20 | ); 21 | 22 | // Define the 'Note' model with the schema 23 | const Note = mongoose.model('Note', noteSchema); 24 | // Export the module 25 | module.exports = Note; 26 | -------------------------------------------------------------------------------- /solutions/04-CRUD/resolvers/index.js: -------------------------------------------------------------------------------- 1 | const Query = require('./query'); 2 | const Mutation = require('./mutation'); 3 | const { GraphQLDateTime } = require('graphql-iso-date'); 4 | 5 | module.exports = { 6 | Query, 7 | Mutation, 8 | DateTime: GraphQLDateTime 9 | }; 10 | -------------------------------------------------------------------------------- /solutions/04-CRUD/resolvers/mutation.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | newNote: async (parent, args, { models }) => { 3 | return await models.Note.create({ 4 | content: args.content, 5 | author: 'Adam Scott' 6 | }); 7 | }, 8 | deleteNote: async (parent, { id }, { models }) => { 9 | try { 10 | await models.Note.findOneAndRemove({ _id: id }); 11 | return true; 12 | } catch (err) { 13 | return false; 14 | } 15 | }, 16 | updateNote: async (parent, { content, id }, { models }) => { 17 | try { 18 | return await models.Note.findOneAndUpdate( 19 | { 20 | _id: id 21 | }, 22 | { 23 | $set: { 24 | content 25 | } 26 | }, 27 | { 28 | new: true 29 | } 30 | ); 31 | } catch (err) { 32 | throw new Error('Error updating note'); 33 | } 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /solutions/04-CRUD/resolvers/query.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | notes: async (parent, args, { models }) => { 3 | return await models.Note.find(); 4 | }, 5 | note: async (parent, args, { models }) => { 6 | return await models.Note.findById(args.id); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /solutions/04-CRUD/schema.js: -------------------------------------------------------------------------------- 1 | const { gql } = require('apollo-server-express'); 2 | 3 | module.exports = gql` 4 | scalar DateTime 5 | 6 | type Note { 7 | id: ID! 8 | content: String! 9 | author: String! 10 | createdAt: DateTime! 11 | updatedAt: DateTime! 12 | } 13 | 14 | type Query { 15 | notes: [Note!]! 16 | note(id: ID): Note! 17 | } 18 | 19 | type Mutation { 20 | newNote(content: String!): Note 21 | updateNote(id: ID!, content: String!): Note! 22 | deleteNote(id: ID!): Boolean! 23 | } 24 | `; 25 | -------------------------------------------------------------------------------- /solutions/04-CRUD/util/gravatar.js: -------------------------------------------------------------------------------- 1 | /* Take in an email and generate a Gravatar url */ 2 | /* https://gravatar.com/site/implement/ */ 3 | const md5 = require('md5'); 4 | 5 | const gravatar = email => { 6 | const hash = md5(email); 7 | return `https://www.gravatar.com/avatar/${hash}.jpg?d=identicon`; 8 | }; 9 | 10 | module.exports = gravatar; 11 | -------------------------------------------------------------------------------- /solutions/05-Authentication/db.js: -------------------------------------------------------------------------------- 1 | // Require the mongose library 2 | const mongoose = require('mongoose'); 3 | 4 | module.exports = { 5 | connect: DB_HOST => { 6 | // Use the Mongo driver's updated URL string parser 7 | mongoose.set('useNewUrlParser', true); 8 | // Use `findOneAndUpdate()` in place of findAndModify() 9 | mongoose.set('useFindAndModify', false); 10 | // Use `createIndex()` in place of `ensureIndex()` 11 | mongoose.set('useCreateIndex', true); 12 | // Use the new server discovery & monitoring engine 13 | mongoose.set('useUnifiedTopology', true); 14 | // Connect to the DB 15 | mongoose.connect(DB_HOST); 16 | // Log an error if we fail to connect 17 | mongoose.connection.on('error', err => { 18 | console.error(err); 19 | console.log( 20 | 'MongoDB connection error. Please make sure MongoDB is running.' 21 | ); 22 | process.exit(); 23 | }); 24 | }, 25 | 26 | close: () => { 27 | mongoose.connection.close(); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /solutions/05-Authentication/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { ApolloServer } = require('apollo-server-express'); 3 | const jwt = require('jsonwebtoken'); 4 | require('dotenv').config(); 5 | 6 | const db = require('./db'); 7 | const models = require('./models'); 8 | const typeDefs = require('./schema'); 9 | const resolvers = require('./resolvers'); 10 | 11 | // Run our server on a port specified in our .env file or port 4000 12 | const port = process.env.PORT || 4000; 13 | const DB_HOST = process.env.DB_HOST; 14 | 15 | const app = express(); 16 | 17 | db.connect(DB_HOST); 18 | 19 | // get the user info from a JWT 20 | const getUser = token => { 21 | if (token) { 22 | try { 23 | // return the user information from the token 24 | return jwt.verify(token, process.env.JWT_SECRET); 25 | } catch (err) { 26 | // if there's a problem with the token, throw an error 27 | throw new Error('Session invalid'); 28 | } 29 | } 30 | }; 31 | 32 | // Apollo Server setup 33 | const server = new ApolloServer({ 34 | typeDefs, 35 | resolvers, 36 | context: ({ req }) => { 37 | // get the user token from the headers 38 | const token = req.headers.authorization; 39 | // try to retrieve a user with the token 40 | const user = getUser(token); 41 | // for now, let's log the user to the console: 42 | console.log(user); 43 | // add the db models and the user to the context 44 | return { models, user }; 45 | } 46 | }); 47 | 48 | // Apply the Apollo GraphQL middleware and set the path to /api 49 | server.applyMiddleware({ app, path: '/api' }); 50 | 51 | app.listen({ port }, () => 52 | console.log( 53 | `GraphQL Server running at http://localhost:${port}${server.graphqlPath}` 54 | ) 55 | ); 56 | -------------------------------------------------------------------------------- /solutions/05-Authentication/models/index.js: -------------------------------------------------------------------------------- 1 | const Note = require('./note'); 2 | const User = require('./user'); 3 | 4 | const models = { 5 | Note, 6 | User 7 | }; 8 | 9 | module.exports = models; 10 | -------------------------------------------------------------------------------- /solutions/05-Authentication/models/note.js: -------------------------------------------------------------------------------- 1 | // Require the mongose library 2 | const mongoose = require('mongoose'); 3 | 4 | // Define the note's database schema 5 | const noteSchema = new mongoose.Schema( 6 | { 7 | content: { 8 | type: String, 9 | required: true 10 | }, 11 | author: { 12 | type: String, 13 | required: true 14 | } 15 | }, 16 | { 17 | // Assigns createdAt and updatedAt fields with a Date type 18 | timestamps: true 19 | } 20 | ); 21 | 22 | // Define the 'Note' model with the schema 23 | const Note = mongoose.model('Note', noteSchema); 24 | // Export the module 25 | module.exports = Note; 26 | -------------------------------------------------------------------------------- /solutions/05-Authentication/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const UserSchema = new mongoose.Schema( 4 | { 5 | username: { 6 | type: String, 7 | required: true, 8 | index: { unique: true } 9 | }, 10 | email: { 11 | type: String, 12 | required: true, 13 | index: { unique: true } 14 | }, 15 | password: { 16 | type: String, 17 | required: true 18 | }, 19 | avatar: { 20 | type: String 21 | } 22 | }, 23 | { 24 | // Assigns createdAt and updatedAt fields with a Date type 25 | timestamps: true 26 | } 27 | ); 28 | 29 | const User = mongoose.model('User', UserSchema); 30 | module.exports = User; 31 | -------------------------------------------------------------------------------- /solutions/05-Authentication/resolvers/index.js: -------------------------------------------------------------------------------- 1 | const Query = require('./query'); 2 | const Mutation = require('./mutation'); 3 | const { GraphQLDateTime } = require('graphql-iso-date'); 4 | 5 | module.exports = { 6 | Query, 7 | Mutation, 8 | DateTime: GraphQLDateTime 9 | }; 10 | -------------------------------------------------------------------------------- /solutions/05-Authentication/resolvers/mutation.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt'); 2 | const jwt = require('jsonwebtoken'); 3 | const { 4 | AuthenticationError, 5 | ForbiddenError 6 | } = require('apollo-server-express'); 7 | require('dotenv').config(); 8 | 9 | const gravatar = require('../util/gravatar'); 10 | 11 | module.exports = { 12 | newNote: async (parent, args, { models }) => { 13 | return await models.Note.create({ 14 | content: args.content, 15 | author: 'Adam Scott' 16 | }); 17 | }, 18 | deleteNote: async (parent, { id }, { models }) => { 19 | try { 20 | await models.Note.findOneAndRemove({ _id: id }); 21 | return true; 22 | } catch (err) { 23 | return false; 24 | } 25 | }, 26 | updateNote: async (parent, { content, id }, { models }) => { 27 | try { 28 | return await models.Note.findOneAndUpdate( 29 | { 30 | _id: id 31 | }, 32 | { 33 | $set: { 34 | content 35 | } 36 | }, 37 | { 38 | new: true 39 | } 40 | ); 41 | } catch (err) { 42 | throw new Error('Error updating note'); 43 | } 44 | }, 45 | signUp: async (parent, { username, email, password }, { models }) => { 46 | // normalize email address 47 | email = email.trim().toLowerCase(); 48 | // hash the password 49 | const hashed = await bcrypt.hash(password, 10); 50 | // create the gravatar url 51 | const avatar = gravatar(email); 52 | try { 53 | const user = await models.User.create({ 54 | username, 55 | email, 56 | avatar, 57 | password: hashed 58 | }); 59 | 60 | // create and return the json web token 61 | return jwt.sign({ id: user._id }, process.env.JWT_SECRET); 62 | } catch (err) { 63 | // if there's a problem creating the account, throw an error 64 | throw new Error('Error creating account'); 65 | } 66 | }, 67 | signIn: async (parent, { username, email, password }, { models }) => { 68 | if (email) { 69 | // normalize email address 70 | email = email.trim().toLowerCase(); 71 | } 72 | 73 | const user = await models.User.findOne({ 74 | $or: [{ email }, { username }] 75 | }); 76 | 77 | // if no user is found, throw an authentication error 78 | if (!user) { 79 | throw new AuthenticationError('Error signing in'); 80 | } 81 | 82 | // if the passwords don't match, throw an authentication error 83 | const valid = await bcrypt.compare(password, user.password); 84 | if (!valid) { 85 | throw new AuthenticationError('Error signing in'); 86 | } 87 | 88 | // create and return the json web token 89 | return jwt.sign({ id: user._id }, process.env.JWT_SECRET); 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /solutions/05-Authentication/resolvers/query.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | notes: async (parent, args, { models }) => { 3 | return await models.Note.find(); 4 | }, 5 | note: async (parent, args, { models }) => { 6 | return await models.Note.findById(args.id); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /solutions/05-Authentication/schema.js: -------------------------------------------------------------------------------- 1 | const { gql } = require('apollo-server-express'); 2 | 3 | module.exports = gql` 4 | scalar DateTime 5 | 6 | type Note { 7 | id: ID! 8 | content: String! 9 | author: User! 10 | favoriteCount: Int! 11 | favoritedBy: [User!] 12 | createdAt: DateTime! 13 | updatedAt: DateTime! 14 | } 15 | 16 | type User { 17 | id: ID! 18 | username: String! 19 | email: String! 20 | avatar: String 21 | notes: [Note!]! 22 | favorites: [Note!]! 23 | } 24 | 25 | type Query { 26 | notes: [Note!]! 27 | note(id: ID): Note! 28 | user(username: String!): User 29 | users: [User!]! 30 | me: User! 31 | } 32 | 33 | type Mutation { 34 | newNote(content: String!): Note 35 | updateNote(id: ID!, content: String!): Note! 36 | deleteNote(id: ID!): Boolean! 37 | toggleFavorite(id: ID!): Note! 38 | signUp(username: String!, email: String!, password: String!): String! 39 | signIn(username: String, email: String, password: String!): String! 40 | } 41 | `; 42 | -------------------------------------------------------------------------------- /solutions/05-Authentication/util/gravatar.js: -------------------------------------------------------------------------------- 1 | /* Take in an email and generate a Gravatar url */ 2 | /* https://gravatar.com/site/implement/ */ 3 | const md5 = require('md5'); 4 | 5 | const gravatar = email => { 6 | const hash = md5(email); 7 | return `https://www.gravatar.com/avatar/${hash}.jpg?d=identicon`; 8 | }; 9 | 10 | module.exports = gravatar; 11 | -------------------------------------------------------------------------------- /solutions/06-User-Actions/db.js: -------------------------------------------------------------------------------- 1 | // Require the mongose library 2 | const mongoose = require('mongoose'); 3 | 4 | module.exports = { 5 | connect: DB_HOST => { 6 | // Use the Mongo driver's updated URL string parser 7 | mongoose.set('useNewUrlParser', true); 8 | // Use `findOneAndUpdate()` in place of findAndModify() 9 | mongoose.set('useFindAndModify', false); 10 | // Use `createIndex()` in place of `ensureIndex()` 11 | mongoose.set('useCreateIndex', true); 12 | // Use the new server discovery & monitoring engine 13 | mongoose.set('useUnifiedTopology', true); 14 | // Connect to the DB 15 | mongoose.connect(DB_HOST); 16 | // Log an error if we fail to connect 17 | mongoose.connection.on('error', err => { 18 | console.error(err); 19 | console.log( 20 | 'MongoDB connection error. Please make sure MongoDB is running.' 21 | ); 22 | process.exit(); 23 | }); 24 | }, 25 | 26 | close: () => { 27 | mongoose.connection.close(); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /solutions/06-User-Actions/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { ApolloServer } = require('apollo-server-express'); 3 | const jwt = require('jsonwebtoken'); 4 | require('dotenv').config(); 5 | 6 | const db = require('./db'); 7 | const models = require('./models'); 8 | const typeDefs = require('./schema'); 9 | const resolvers = require('./resolvers'); 10 | 11 | // Run our server on a port specified in our .env file or port 4000 12 | const port = process.env.PORT || 4000; 13 | const DB_HOST = process.env.DB_HOST; 14 | 15 | const app = express(); 16 | 17 | db.connect(DB_HOST); 18 | 19 | // get the user info from a JWT 20 | const getUser = token => { 21 | if (token) { 22 | try { 23 | // return the user information from the token 24 | return jwt.verify(token, process.env.JWT_SECRET); 25 | } catch (err) { 26 | // if there's a problem with the token, throw an error 27 | throw new Error('Session invalid'); 28 | } 29 | } 30 | }; 31 | 32 | // Apollo Server setup 33 | const server = new ApolloServer({ 34 | typeDefs, 35 | resolvers, 36 | context: ({ req }) => { 37 | // get the user token from the headers 38 | const token = req.headers.authorization; 39 | // try to retrieve a user with the token 40 | const user = getUser(token); 41 | // add the db models and the user to the context 42 | return { models, user }; 43 | } 44 | }); 45 | 46 | // Apply the Apollo GraphQL middleware and set the path to /api 47 | server.applyMiddleware({ app, path: '/api' }); 48 | 49 | app.listen({ port }, () => 50 | console.log( 51 | `GraphQL Server running at http://localhost:${port}${server.graphqlPath}` 52 | ) 53 | ); 54 | -------------------------------------------------------------------------------- /solutions/06-User-Actions/models/index.js: -------------------------------------------------------------------------------- 1 | const Note = require('./note'); 2 | const User = require('./user'); 3 | 4 | const models = { 5 | Note, 6 | User 7 | }; 8 | 9 | module.exports = models; 10 | -------------------------------------------------------------------------------- /solutions/06-User-Actions/models/note.js: -------------------------------------------------------------------------------- 1 | // Require the mongose library 2 | const mongoose = require('mongoose'); 3 | 4 | // Define the note's database schema 5 | const noteSchema = new mongoose.Schema( 6 | { 7 | content: { 8 | type: String, 9 | required: true 10 | }, 11 | // reference the author's object ID 12 | author: { 13 | type: mongoose.Schema.Types.ObjectId, 14 | ref: 'User', 15 | required: true 16 | }, 17 | favoriteCount: { 18 | type: Number, 19 | default: 0 20 | }, 21 | favoritedBy: [ 22 | { 23 | type: mongoose.Schema.Types.ObjectId, 24 | ref: 'User' 25 | } 26 | ] 27 | }, 28 | { 29 | // Assigns createdAt and updatedAt fields with a Date type 30 | timestamps: true 31 | } 32 | ); 33 | 34 | // Define the 'Note' model with the schema 35 | const Note = mongoose.model('Note', noteSchema); 36 | // Export the module 37 | module.exports = Note; 38 | -------------------------------------------------------------------------------- /solutions/06-User-Actions/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const UserSchema = new mongoose.Schema( 4 | { 5 | username: { 6 | type: String, 7 | required: true, 8 | index: { unique: true } 9 | }, 10 | email: { 11 | type: String, 12 | required: true, 13 | index: { unique: true } 14 | }, 15 | password: { 16 | type: String, 17 | required: true 18 | }, 19 | avatar: { 20 | type: String 21 | } 22 | }, 23 | { 24 | // Assigns createdAt and updatedAt fields with a Date type 25 | timestamps: true 26 | } 27 | ); 28 | 29 | const User = mongoose.model('User', UserSchema); 30 | module.exports = User; 31 | -------------------------------------------------------------------------------- /solutions/06-User-Actions/resolvers/index.js: -------------------------------------------------------------------------------- 1 | const Query = require('./query'); 2 | const Mutation = require('./mutation'); 3 | const Note = require('./note'); 4 | const User = require('./user'); 5 | const { GraphQLDateTime } = require('graphql-iso-date'); 6 | 7 | module.exports = { 8 | Query, 9 | Mutation, 10 | Note, 11 | User, 12 | DateTime: GraphQLDateTime 13 | }; 14 | -------------------------------------------------------------------------------- /solutions/06-User-Actions/resolvers/mutation.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt'); 2 | const jwt = require('jsonwebtoken'); 3 | const { 4 | AuthenticationError, 5 | ForbiddenError 6 | } = require('apollo-server-express'); 7 | const mongoose = require('mongoose'); 8 | require('dotenv').config(); 9 | 10 | const gravatar = require('../util/gravatar'); 11 | 12 | module.exports = { 13 | newNote: async (parent, args, { models, user }) => { 14 | if (!user) { 15 | throw new AuthenticationError('You must be signed in to create a note'); 16 | } 17 | 18 | return await models.Note.create({ 19 | content: args.content, 20 | author: mongoose.Types.ObjectId(user.id), 21 | favoriteCount: 0 22 | }); 23 | }, 24 | deleteNote: async (parent, { id }, { models, user }) => { 25 | // if not a user, throw an Authentication Error 26 | if (!user) { 27 | throw new AuthenticationError('You must be signed in to delete a note'); 28 | } 29 | 30 | // find the note 31 | const note = await models.Note.findById(id); 32 | // if the note owner and current user don't match, throw a forbidden error 33 | if (note && String(note.author) !== user.id) { 34 | throw new ForbiddenError("You don't have permissions to delete the note"); 35 | } 36 | 37 | try { 38 | // if everything checks out, remove the note 39 | await note.remove(); 40 | return true; 41 | } catch (err) { 42 | // if there's an error along the way, return false 43 | return false; 44 | } 45 | }, 46 | updateNote: async (parent, { content, id }, { models, user }) => { 47 | // if not a user, throw an Authentication Error 48 | if (!user) { 49 | throw new AuthenticationError('You must be signed in to update a note'); 50 | } 51 | 52 | // find the note 53 | const note = await models.Note.findById(id); 54 | // if the note owner and current user don't match, throw a forbidden error 55 | if (note && String(note.author) !== user.id) { 56 | throw new ForbiddenError("You don't have permissions to update the note"); 57 | } 58 | 59 | // Update the note in the db and return the updated note 60 | return await models.Note.findOneAndUpdate( 61 | { 62 | _id: id 63 | }, 64 | { 65 | $set: { 66 | content 67 | } 68 | }, 69 | { 70 | new: true 71 | } 72 | ); 73 | }, 74 | toggleFavorite: async (parent, { id }, { models, user }) => { 75 | // if no user context is passed, throw auth error 76 | if (!user) { 77 | throw new AuthenticationError(); 78 | } 79 | 80 | // check to see if the user has already favorited the note 81 | let noteCheck = await models.Note.findById(id); 82 | const hasUser = noteCheck.favoritedBy.indexOf(user.id); 83 | 84 | // if the user exists in the list 85 | // pull them from the list and reduce the favoriteCount by 1 86 | if (hasUser >= 0) { 87 | return await models.Note.findByIdAndUpdate( 88 | id, 89 | { 90 | $pull: { 91 | favoritedBy: mongoose.Types.ObjectId(user.id) 92 | }, 93 | $inc: { 94 | favoriteCount: -1 95 | } 96 | }, 97 | { 98 | // Set new to true to return the updated doc 99 | new: true 100 | } 101 | ); 102 | } else { 103 | // if the user doesn't exists in the list 104 | // add them to the list and increment the favoriteCount by 1 105 | return await models.Note.findByIdAndUpdate( 106 | id, 107 | { 108 | $push: { 109 | favoritedBy: mongoose.Types.ObjectId(user.id) 110 | }, 111 | $inc: { 112 | favoriteCount: 1 113 | } 114 | }, 115 | { 116 | new: true 117 | } 118 | ); 119 | } 120 | }, 121 | signUp: async (parent, { username, email, password }, { models }) => { 122 | // normalize email address 123 | email = email.trim().toLowerCase(); 124 | // hash the password 125 | const hashed = await bcrypt.hash(password, 10); 126 | // create the gravatar url 127 | const avatar = gravatar(email); 128 | try { 129 | const user = await models.User.create({ 130 | username, 131 | email, 132 | avatar, 133 | password: hashed 134 | }); 135 | 136 | // create and return the json web token 137 | return jwt.sign({ id: user._id }, process.env.JWT_SECRET); 138 | } catch (err) { 139 | // if there's a problem creating the account, throw an error 140 | throw new Error('Error creating account'); 141 | } 142 | }, 143 | 144 | signIn: async (parent, { username, email, password }, { models }) => { 145 | if (email) { 146 | // normalize email address 147 | email = email.trim().toLowerCase(); 148 | } 149 | 150 | const user = await models.User.findOne({ 151 | $or: [{ email }, { username }] 152 | }); 153 | 154 | // if no user is found, throw an authentication error 155 | if (!user) { 156 | throw new AuthenticationError('Error signing in'); 157 | } 158 | 159 | // if the passwords don't match, throw an authentication error 160 | const valid = await bcrypt.compare(password, user.password); 161 | if (!valid) { 162 | throw new AuthenticationError('Error signing in'); 163 | } 164 | 165 | // create and return the json web token 166 | return jwt.sign({ id: user._id }, process.env.JWT_SECRET); 167 | } 168 | }; 169 | -------------------------------------------------------------------------------- /solutions/06-User-Actions/resolvers/note.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Resolve the author info for a note when requested 3 | author: async (note, args, { models }) => { 4 | return await models.User.findById(note.author); 5 | }, 6 | // Resolved the favoritedBy info for a note when requested 7 | favoritedBy: async (note, args, { models }) => { 8 | return await models.User.find({ _id: { $in: note.favoritedBy } }); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /solutions/06-User-Actions/resolvers/query.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | notes: async (parent, args, { models }) => { 3 | return await models.Note.find(); 4 | }, 5 | note: async (parent, args, { models }) => { 6 | return await models.Note.findById(args.id); 7 | }, 8 | user: async (parent, args, { models }) => { 9 | return await models.User.findOne({ username: args.username }); 10 | }, 11 | users: async (parent, args, { models }) => { 12 | return await models.User.find({}); 13 | }, 14 | me: async (parent, args, { models, user }) => { 15 | return await models.User.findById(user.id); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /solutions/06-User-Actions/resolvers/user.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Resolve the list of notes for a user when requested 3 | notes: async (user, args, { models }) => { 4 | return await models.Note.find({ author: user._id }).sort({ _id: -1 }); 5 | }, 6 | // Resolve the list of favorites for a user when requested 7 | favorites: async (user, args, { models }) => { 8 | return await models.Note.find({ favoritedBy: user._id }).sort({ _id: -1 }); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /solutions/06-User-Actions/schema.js: -------------------------------------------------------------------------------- 1 | const { gql } = require('apollo-server-express'); 2 | 3 | module.exports = gql` 4 | scalar DateTime 5 | 6 | type Note { 7 | id: ID! 8 | content: String! 9 | author: User! 10 | favoriteCount: Int! 11 | favoritedBy: [User] 12 | createdAt: DateTime! 13 | updatedAt: DateTime! 14 | } 15 | 16 | type User { 17 | id: ID! 18 | username: String! 19 | email: String! 20 | avatar: String 21 | notes: [Note!]! 22 | favorites: [Note!]! 23 | } 24 | 25 | type Query { 26 | notes: [Note!]! 27 | note(id: ID): Note! 28 | user(username: String!): User 29 | users: [User!]! 30 | me: User! 31 | } 32 | 33 | type Mutation { 34 | newNote(content: String!): Note 35 | updateNote(id: ID!, content: String!): Note! 36 | deleteNote(id: ID!): Boolean! 37 | toggleFavorite(id: ID!): Note! 38 | signUp(username: String!, email: String!, password: String!): String! 39 | signIn(username: String, email: String, password: String!): String! 40 | } 41 | `; 42 | -------------------------------------------------------------------------------- /solutions/06-User-Actions/util/gravatar.js: -------------------------------------------------------------------------------- 1 | /* Take in an email and generate a Gravatar url */ 2 | /* https://gravatar.com/site/implement/ */ 3 | const md5 = require('md5'); 4 | 5 | const gravatar = email => { 6 | const hash = md5(email); 7 | return `https://www.gravatar.com/avatar/${hash}.jpg?d=identicon`; 8 | }; 9 | 10 | module.exports = gravatar; 11 | -------------------------------------------------------------------------------- /solutions/07-Details/db.js: -------------------------------------------------------------------------------- 1 | // Require the mongose library 2 | const mongoose = require('mongoose'); 3 | 4 | module.exports = { 5 | connect: DB_HOST => { 6 | // Use the Mongo driver's updated URL string parser 7 | mongoose.set('useNewUrlParser', true); 8 | // Use `findOneAndUpdate()` in place of findAndModify() 9 | mongoose.set('useFindAndModify', false); 10 | // Use `createIndex()` in place of `ensureIndex()` 11 | mongoose.set('useCreateIndex', true); 12 | // Use the new server discovery & monitoring engine 13 | mongoose.set('useUnifiedTopology', true); 14 | // Connect to the DB 15 | mongoose.connect(DB_HOST); 16 | // Log an error if we fail to connect 17 | mongoose.connection.on('error', err => { 18 | console.error(err); 19 | console.log( 20 | 'MongoDB connection error. Please make sure MongoDB is running.' 21 | ); 22 | process.exit(); 23 | }); 24 | }, 25 | 26 | close: () => { 27 | mongoose.connection.close(); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /solutions/07-Details/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { ApolloServer } = require('apollo-server-express'); 3 | const jwt = require('jsonwebtoken'); 4 | const helmet = require('helmet'); 5 | const cors = require('cors'); 6 | const depthLimit = require('graphql-depth-limit'); 7 | const { createComplexityLimitRule } = require('graphql-validation-complexity'); 8 | require('dotenv').config(); 9 | 10 | const db = require('./db'); 11 | const models = require('./models'); 12 | const typeDefs = require('./schema'); 13 | const resolvers = require('./resolvers'); 14 | 15 | // Run our server on a port specified in our .env file or port 4000 16 | const port = process.env.PORT || 4000; 17 | const DB_HOST = process.env.DB_HOST; 18 | 19 | const app = express(); 20 | 21 | db.connect(DB_HOST); 22 | 23 | // Security middleware 24 | app.use(helmet()); 25 | // CORS middleware 26 | app.use(cors()); 27 | 28 | // get the user info from a JWT 29 | const getUser = token => { 30 | if (token) { 31 | try { 32 | // return the user information from the token 33 | return jwt.verify(token, process.env.JWT_SECRET); 34 | } catch (err) { 35 | // if there's a problem with the token, throw an error 36 | throw new Error('Session invalid'); 37 | } 38 | } 39 | }; 40 | 41 | // Apollo Server setup 42 | // updated to include `validationRules` 43 | const server = new ApolloServer({ 44 | typeDefs, 45 | resolvers, 46 | validationRules: [depthLimit(5), createComplexityLimitRule(1000)], 47 | context: ({ req }) => { 48 | // get the user token from the headers 49 | const token = req.headers.authorization; 50 | // try to retrieve a user with the token 51 | const user = getUser(token); 52 | // add the db models and the user to the context 53 | return { models, user }; 54 | } 55 | }); 56 | 57 | // Apply the Apollo GraphQL middleware and set the path to /api 58 | server.applyMiddleware({ app, path: '/api' }); 59 | 60 | app.listen({ port }, () => 61 | console.log( 62 | `GraphQL Server running at http://localhost:${port}${server.graphqlPath}` 63 | ) 64 | ); 65 | -------------------------------------------------------------------------------- /solutions/07-Details/models/index.js: -------------------------------------------------------------------------------- 1 | const Note = require('./note'); 2 | const User = require('./user'); 3 | 4 | const models = { 5 | Note, 6 | User 7 | }; 8 | 9 | module.exports = models; 10 | -------------------------------------------------------------------------------- /solutions/07-Details/models/note.js: -------------------------------------------------------------------------------- 1 | // Require the mongose library 2 | const mongoose = require('mongoose'); 3 | 4 | // Define the note's database schema 5 | const noteSchema = new mongoose.Schema( 6 | { 7 | content: { 8 | type: String, 9 | required: true 10 | }, 11 | // reference the author's object ID 12 | author: { 13 | type: mongoose.Schema.Types.ObjectId, 14 | ref: 'User', 15 | required: true 16 | }, 17 | favoriteCount: { 18 | type: Number, 19 | default: 0 20 | }, 21 | favoritedBy: [ 22 | { 23 | type: mongoose.Schema.Types.ObjectId, 24 | ref: 'User' 25 | } 26 | ] 27 | }, 28 | { 29 | // Assigns createdAt and updatedAt fields with a Date type 30 | timestamps: true 31 | } 32 | ); 33 | 34 | // Define the 'Note' model with the schema 35 | const Note = mongoose.model('Note', noteSchema); 36 | // Export the module 37 | module.exports = Note; 38 | -------------------------------------------------------------------------------- /solutions/07-Details/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const UserSchema = new mongoose.Schema( 4 | { 5 | username: { 6 | type: String, 7 | required: true, 8 | index: { unique: true } 9 | }, 10 | email: { 11 | type: String, 12 | required: true, 13 | index: { unique: true } 14 | }, 15 | password: { 16 | type: String, 17 | required: true 18 | }, 19 | avatar: { 20 | type: String 21 | } 22 | }, 23 | { 24 | // Assigns createdAt and updatedAt fields with a Date type 25 | timestamps: true 26 | } 27 | ); 28 | 29 | const User = mongoose.model('User', UserSchema); 30 | module.exports = User; 31 | -------------------------------------------------------------------------------- /solutions/07-Details/resolvers/index.js: -------------------------------------------------------------------------------- 1 | const Query = require('./query'); 2 | const Mutation = require('./mutation'); 3 | const Note = require('./note'); 4 | const User = require('./user'); 5 | const { GraphQLDateTime } = require('graphql-iso-date'); 6 | 7 | module.exports = { 8 | Query, 9 | Mutation, 10 | Note, 11 | User, 12 | DateTime: GraphQLDateTime 13 | }; 14 | -------------------------------------------------------------------------------- /solutions/07-Details/resolvers/mutation.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt'); 2 | const jwt = require('jsonwebtoken'); 3 | const { 4 | AuthenticationError, 5 | ForbiddenError 6 | } = require('apollo-server-express'); 7 | const mongoose = require('mongoose'); 8 | require('dotenv').config(); 9 | 10 | const gravatar = require('../util/gravatar'); 11 | 12 | module.exports = { 13 | newNote: async (parent, args, { models, user }) => { 14 | if (!user) { 15 | throw new AuthenticationError('You must be signed in to create a note'); 16 | } 17 | 18 | return await models.Note.create({ 19 | content: args.content, 20 | author: mongoose.Types.ObjectId(user.id), 21 | favoriteCount: 0 22 | }); 23 | }, 24 | deleteNote: async (parent, { id }, { models, user }) => { 25 | // if not a user, throw an Authentication Error 26 | if (!user) { 27 | throw new AuthenticationError('You must be signed in to delete a note'); 28 | } 29 | 30 | // find the note 31 | const note = await models.Note.findById(id); 32 | // if the note owner and current user don't match, throw a forbidden error 33 | if (note && String(note.author) !== user.id) { 34 | throw new ForbiddenError("You don't have permissions to delete the note"); 35 | } 36 | 37 | try { 38 | // if everything checks out, remove the note 39 | await note.remove(); 40 | return true; 41 | } catch (err) { 42 | // if there's an error along the way, return false 43 | return false; 44 | } 45 | }, 46 | updateNote: async (parent, { content, id }, { models, user }) => { 47 | // if not a user, throw an Authentication Error 48 | if (!user) { 49 | throw new AuthenticationError('You must be signed in to update a note'); 50 | } 51 | 52 | // find the note 53 | const note = await models.Note.findById(id); 54 | // if the note owner and current user don't match, throw a forbidden error 55 | if (note && String(note.author) !== user.id) { 56 | throw new ForbiddenError("You don't have permissions to update the note"); 57 | } 58 | 59 | // Update the note in the db and return the updated note 60 | return await models.Note.findOneAndUpdate( 61 | { 62 | _id: id 63 | }, 64 | { 65 | $set: { 66 | content 67 | } 68 | }, 69 | { 70 | new: true 71 | } 72 | ); 73 | }, 74 | toggleFavorite: async (parent, { id }, { models, user }) => { 75 | // if no user context is passed, throw auth error 76 | if (!user) { 77 | throw new AuthenticationError(); 78 | } 79 | 80 | // check to see if the user has already favorited the note 81 | let noteCheck = await models.Note.findById(id); 82 | const hasUser = noteCheck.favoritedBy.indexOf(user.id); 83 | 84 | // if the user exists in the list 85 | // pull them from the list and reduce the favoriteCount by 1 86 | if (hasUser >= 0) { 87 | return await models.Note.findByIdAndUpdate( 88 | id, 89 | { 90 | $pull: { 91 | favoritedBy: mongoose.Types.ObjectId(user.id) 92 | }, 93 | $inc: { 94 | favoriteCount: -1 95 | } 96 | }, 97 | { 98 | // Set new to true to return the updated doc 99 | new: true 100 | } 101 | ); 102 | } else { 103 | // if the user doesn't exists in the list 104 | // add them to the list and increment the favoriteCount by 1 105 | return await models.Note.findByIdAndUpdate( 106 | id, 107 | { 108 | $push: { 109 | favoritedBy: mongoose.Types.ObjectId(user.id) 110 | }, 111 | $inc: { 112 | favoriteCount: 1 113 | } 114 | }, 115 | { 116 | new: true 117 | } 118 | ); 119 | } 120 | }, 121 | signUp: async (parent, { username, email, password }, { models }) => { 122 | // normalize email address 123 | email = email.trim().toLowerCase(); 124 | // hash the password 125 | const hashed = await bcrypt.hash(password, 10); 126 | // create the gravatar url 127 | const avatar = gravatar(email); 128 | try { 129 | const user = await models.User.create({ 130 | username, 131 | email, 132 | avatar, 133 | password: hashed 134 | }); 135 | 136 | // create and return the json web token 137 | return jwt.sign({ id: user._id }, process.env.JWT_SECRET); 138 | } catch (err) { 139 | // if there's a problem creating the account, throw an error 140 | throw new Error('Error creating account'); 141 | } 142 | }, 143 | 144 | signIn: async (parent, { username, email, password }, { models }) => { 145 | if (email) { 146 | // normalize email address 147 | email = email.trim().toLowerCase(); 148 | } 149 | const user = await models.User.findOne({ 150 | $or: [{ email }, { username }] 151 | }); 152 | 153 | // if no user is found, throw an authentication error 154 | if (!user) { 155 | throw new AuthenticationError('Error signing in'); 156 | } 157 | 158 | // if the passwords don't match, throw an authentication error 159 | const valid = await bcrypt.compare(password, user.password); 160 | if (!valid) { 161 | throw new AuthenticationError('Error signing in'); 162 | } 163 | 164 | // create and return the json web token 165 | return jwt.sign({ id: user._id }, process.env.JWT_SECRET); 166 | } 167 | }; 168 | -------------------------------------------------------------------------------- /solutions/07-Details/resolvers/note.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Resolve the author info for a note when requested 3 | author: async (note, args, { models }) => { 4 | return await models.User.findById(note.author); 5 | }, 6 | // Resolved the favoritedBy info for a note when requested 7 | favoritedBy: async (note, args, { models }) => { 8 | return await models.User.find({ _id: { $in: note.favoritedBy } }); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /solutions/07-Details/resolvers/query.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | notes: async (parent, args, { models }) => { 3 | return await models.Note.find().limit(100); 4 | }, 5 | note: async (parent, args, { models }) => { 6 | return await models.Note.findById(args.id); 7 | }, 8 | user: async (parent, args, { models }) => { 9 | return await models.User.findOne({ username: args.username }); 10 | }, 11 | users: async (parent, args, { models }) => { 12 | return await models.User.find({}).limit(100); 13 | }, 14 | me: async (parent, args, { models, user }) => { 15 | return await models.User.findById(user.id); 16 | }, 17 | noteFeed: async (parent, { cursor }, { models }) => { 18 | // hard code the limit to 10 items 19 | const limit = 10; 20 | // set the default hasNextPage value to false 21 | let hasNextPage = false; 22 | // if no cursor is passed the default query will be empty 23 | // this will pull the newest notes from the db 24 | let cursorQuery = {}; 25 | 26 | // if there is a cursor 27 | // our query will look for notes with an ObjectId less than that of the cursor 28 | if (cursor) { 29 | cursorQuery = { _id: { $lt: cursor } }; 30 | } 31 | 32 | // find the limit + 1 of notes in our db, sorted newest to oldest 33 | let notes = await models.Note.find(cursorQuery) 34 | .sort({ _id: -1 }) 35 | .limit(limit + 1); 36 | 37 | // if the number of notes we find exceeds our limit 38 | // set hasNextPage to true & trim the notes to the limit 39 | if (notes.length > limit) { 40 | hasNextPage = true; 41 | notes = notes.slice(0, -1); 42 | } 43 | 44 | // the new cursor will be the Mongo ObjectID of the last item in the feed array 45 | const newCursor = notes[notes.length - 1]._id; 46 | 47 | return { 48 | notes, 49 | cursor: newCursor, 50 | hasNextPage 51 | }; 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /solutions/07-Details/resolvers/user.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Resolve the list of notes for a user when requested 3 | notes: async (user, args, { models }) => { 4 | return await models.Note.find({ author: user._id }).sort({ _id: -1 }); 5 | }, 6 | // Resolve the list of favorites for a user when requested 7 | favorites: async (user, args, { models }) => { 8 | return await models.Note.find({ favoritedBy: user._id }).sort({ _id: -1 }); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /solutions/07-Details/schema.js: -------------------------------------------------------------------------------- 1 | const { gql } = require('apollo-server-express'); 2 | 3 | module.exports = gql` 4 | scalar DateTime 5 | 6 | type Note { 7 | id: ID! 8 | content: String! 9 | author: User! 10 | favoriteCount: Int! 11 | favoritedBy: [User] 12 | createdAt: DateTime! 13 | updatedAt: DateTime! 14 | } 15 | 16 | type User { 17 | id: ID! 18 | username: String! 19 | email: String! 20 | avatar: String 21 | notes: [Note!]! 22 | favorites: [Note!]! 23 | } 24 | 25 | type NoteFeed { 26 | notes: [Note]! 27 | cursor: String! 28 | hasNextPage: Boolean! 29 | } 30 | 31 | type Query { 32 | notes: [Note!]! 33 | note(id: ID): Note! 34 | user(username: String!): User 35 | users: [User!]! 36 | me: User! 37 | noteFeed(cursor: String): NoteFeed 38 | } 39 | 40 | type Mutation { 41 | newNote(content: String!): Note 42 | updateNote(id: ID!, content: String!): Note! 43 | deleteNote(id: ID!): Boolean! 44 | toggleFavorite(id: ID!): Note! 45 | signUp(username: String!, email: String!, password: String!): String! 46 | signIn(username: String, email: String, password: String!): String! 47 | } 48 | `; 49 | -------------------------------------------------------------------------------- /solutions/07-Details/util/gravatar.js: -------------------------------------------------------------------------------- 1 | /* Take in an email and generate a Gravatar url */ 2 | /* https://gravatar.com/site/implement/ */ 3 | const md5 = require('md5'); 4 | 5 | const gravatar = email => { 6 | const hash = md5(email); 7 | return `https://www.gravatar.com/avatar/${hash}.jpg?d=identicon`; 8 | }; 9 | 10 | module.exports = gravatar; 11 | -------------------------------------------------------------------------------- /solutions/README.md: -------------------------------------------------------------------------------- 1 | # JavaScript Everywhere API Solutions 2 | 3 | > API code example solutions for JavaScript Everywhere by Adam Scott, published by O'Reilly Media 4 | 5 | This directory contains chapter by chapter solutions for the code examples found in the book. For the starter project, visit the `/src` directory and for the final project, visit `/final`. 6 | 7 | ## Getting Help 8 | 9 | The best place to get help is our Spectrum channel, [spectrum.chat/jseverywhere](https://spectrum.chat/jseverywhere). 10 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // index.js 2 | // This is the main entry point of our application 3 | -------------------------------------------------------------------------------- /src/util/gravatar.js: -------------------------------------------------------------------------------- 1 | /* Take in an email and generate a Gravatar url */ 2 | /* https://gravatar.com/site/implement/ */ 3 | const md5 = require('md5'); 4 | 5 | const gravatar = email => { 6 | const hash = md5(email); 7 | return `https://www.gravatar.com/avatar/${hash}.jpg?d=identicon`; 8 | }; 9 | 10 | module.exports = gravatar; 11 | --------------------------------------------------------------------------------