├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── __tests__ ├── addressResolvers.test.js └── customerResolvers.test.js ├── assets └── graphquill-logo.png ├── database ├── mongo │ ├── dbConnection.js │ ├── models │ │ └── cartModel.js │ ├── resetMongoDB.js │ └── seedMongoDB.js └── psql │ ├── dbConnection.js │ ├── resetPSQL.js │ ├── seedCustomerOrders.js │ ├── seedCustomers.js │ ├── seedOrderProducts.js │ ├── seedProducts.js │ ├── seedWarehouseInventory.js │ ├── seedWarehouses.js │ └── setupTablesPSQL.js ├── graphquill.config.js ├── package-lock.json ├── package.json └── server ├── address ├── addressResolver.js └── addressSchema.js ├── baseSchema.js ├── cart ├── cartResolver.js └── cartSchema.js ├── customer ├── customerResolver.js └── customerSchema.js ├── index.js ├── order ├── orderResolver.js └── orderSchema.js ├── product ├── productResolver.js └── productSchema.js ├── resolvers.js ├── schema.js └── warehouse ├── warehouseResolver.js └── warehouseSchema.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "rules": { 4 | // "consistent-return": "off", 5 | "no-console": "off", 6 | "react/prop-types": "off", 7 | // "linebreak-style": 0 // windows CRLF LF thing? 8 | "func-names": "off", 9 | "no-plusplus": ["error", { "allowForLoopAfterthoughts": true }] // allows for i++ or i-- in third arg of for loop 10 | }, 11 | "env": { 12 | "node": true, 13 | "es6": true, 14 | "jest": true 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 | notes -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 HolyTriumvirate 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 |

4 | 5 | # GraphQuill's Mock GraphQL API 6 | This GraphQL API was created and used for testing during the development of [GraphQuill](https://github.com/oslabs-beta/GraphQuill). GraphQuill is a real-time GraphQL API exploration extension for VSCode. 7 | 8 | In an effort to allow other engineers to test out GraphQuill on a mock GraphQL API, we've open-sourced this repo. This project uses local installations of **PostgreSQL's Core Distribution** and **MongoDB Community Edition**. If you need to install them, refer to [How to Install PostgresQL and MongoDb](#to-install-and-setup-postgresql-and-mongodb) 9 | 10 | There is also a dockerized version here for users who prefer to use Docker, [repo here](https://github.com/GraphQuill/Mock-GraphQL-API-Docker) 11 | 12 | --- 13 | 14 | ## Using this GraphQL API 15 | Clone this repo: 16 | 17 | ```javascript 18 | git clone https://github.com/GraphQuill/Mock-GraphQL-API.git 19 | ``` 20 | 21 | Within the cloned folder, install all npm packages: 22 | 23 | ```javascript 24 | npm install 25 | ``` 26 | 27 | After installing npm packages, run the resetDbs script to seed the database (this script can also be used to reset the database to a "default"/predictable starting point, which is exactly what it was used for during GraphQuill's development): 28 | 29 | ```javascript 30 | npm run resetDbs 31 | ``` 32 | 33 | Your GraphQL API has been setup now and seeded with data from [faker](https://www.npmjs.com/package/faker). Now you can use the [GraphQuill](https://github.com/oslabs-beta/GraphQuill) extension to view the API's schema, send queries or mutations and receive responses, all within VS Code 😀 34 | 35 | --- 36 | ## The Schema of this API 37 | 38 | This API was built as the backend of an eCommerce store. Below is the GraphQL schema that describes GraphQL types, available queries and mutations. 39 | 40 | ```graphql 41 | AVAILABLE QUERIES: 42 | address (id: Int!): Address! 43 | addresses: [Address]! 44 | customer (id: Int!): Customer! 45 | customers: [Customer]! 46 | cart (customerId: Int!): Cart! 47 | order (orderId: Int!): Order! 48 | customerOrders (customerId: Int!): [Order]! 49 | product (productId: Int!): Product! 50 | products: [Product]! 51 | warehouse (warehouseId: Int!): Warehouse! 52 | warehouses: [Warehouse]! 53 | 54 | AVAILABLE MUTATIONS: 55 | createOrUpdateAddress ( 56 | customerId: Int!, 57 | address: String!, 58 | address2: String, 59 | city: String!, 60 | state: String!, 61 | zipCode: String! 62 | ): Int! 63 | addCustomer ( 64 | firstName: String!, 65 | lastName: String!, 66 | email: String!, 67 | phoneNumber: String! 68 | ): Customer! 69 | updateCustomer ( 70 | id: Int!, 71 | firstName: String, 72 | lastName: String, 73 | email: String, 74 | phoneNumber: String 75 | ): Customer! 76 | deleteCustomer (id: Int!): Int! 77 | createOrUpdateCart ( 78 | customerId: Int!, 79 | newItem: String! 80 | ): Cart! 81 | removeItemsFromCart ( 82 | customerId: Int!, 83 | itemsToRemove: String! 84 | ): Cart! 85 | deleteCart (customerId: Int!): Cart! 86 | addOrder ( 87 | customerId: Int!, 88 | products: OrderProduct! 89 | ): Int! 90 | addProduct ( 91 | name: String!, 92 | description: String!, 93 | price: Float!, 94 | weight: Float! 95 | ): Product! 96 | updateProduct ( 97 | productId: Int!, 98 | name: String, 99 | description: String, 100 | price: Float, 101 | weight: Float 102 | ): Product! 103 | deleteProduct (productId: Int!): Product! 104 | addWarehouse ( 105 | name: String!, 106 | addressId: Int! 107 | ): Warehouse! 108 | updateWarehouse ( 109 | warehouseId: Int!, 110 | name: String, 111 | id: Int 112 | ): Warehouse! 113 | deleteWarehouse (warehouseId: Int!): Warehouse! 114 | 115 | 116 | TYPES: 117 | Address 118 | id: Int!, 119 | address: String!, 120 | address2: String, 121 | city: String!, 122 | state: String!, 123 | zipCode: String! 124 | 125 | Customer 126 | id: Int!, 127 | firstName: String!, 128 | lastName: String!, 129 | email: String!, 130 | phoneNumber: String!, 131 | address: Address, 132 | cart: Cart 133 | 134 | Cart 135 | customerId: Int!, 136 | products: [String!], 137 | wishlist: [String!] 138 | 139 | Order 140 | orderId: Int!, 141 | customer: Customer, 142 | products: [Product!] 143 | 144 | Product 145 | productId: Int!, 146 | name: String!, 147 | description: String!, 148 | price: Float!, 149 | weight: Float!, 150 | productQty: Int 151 | 152 | Warehouse 153 | warehouseId: Int!, 154 | name: String!, 155 | address: Address 156 | ``` 157 | 158 | --- 159 | 160 | ## To Install And Setup PostgreSQL and MongoDB: 161 | We recommend using homebrew for both installs if you are on macOs. Follows the links below to installation instructions. 162 | Install Postgres (we used v11.4 on macOs): https://www.postgresql.org/download/ 163 | Install MongoDB (we used v4.2.1 on macOs): https://docs.mongodb.com/manual/administration/install-community/ 164 | 165 | **1. After installing Postgres**, you'll need to create a user 'graphquill' with the password 'graphquill' which has access to the graphquillpsql database. 166 | You may need to start the Postgres database on your computer. For macOS: 167 | * first check if homebrew has it running with: 168 | 169 | ```javascript 170 | brew services list 171 | ``` 172 | 173 | * if it is not running, start it using: 174 | 175 | ```javascript 176 | brew services start postgresql 177 | ``` 178 | 179 | * Open the postgres terminal: 180 | 181 | ```javascript 182 | psql postgres 183 | ``` 184 | 185 | * Then create a user: 186 | 187 | ```javascript 188 | CREATE USER graphquill WITH PASSWORD 'graphquill'; 189 | ``` 190 | 191 | * Create the database: 192 | 193 | ```javascript 194 | CREATE DATABASE graphquillpsql; 195 | ``` 196 | 197 | * Give the graphquill user access to modify the database: 198 | 199 | ```javascript 200 | GRANT ALL PRIVILEGES ON DATABASE graphquillpsql TO graphquill; 201 | ``` 202 | **2. After installing MongoDB** you will need to start the server. 203 | 204 | * Again if you used homebrew you can check that using: 205 | 206 | ```javascript 207 | brew services list 208 | ``` 209 | 210 | * and start MongoDB using: 211 | 212 | ```javascript 213 | brew services start mongodb-community 214 | ``` 215 | 216 | After completing these database setups, you can continue along with using this repo. ([Instructions](#using-this-graphql-api)) 217 | -------------------------------------------------------------------------------- /__tests__/addressResolvers.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | Query: { address, addresses }, 3 | Mutation: { createOrUpdateAddress, updateAddress }, 4 | } = require('../server/address/addressResolver'); 5 | 6 | describe('testing address resolver', () => { 7 | it('should be a function', () => { 8 | expect(typeof address).toBe('function'); 9 | }); 10 | it('returns an address object', () => { 11 | // a mock promise generator that "mimics" expected database activity 12 | const querySubstitute = (q) => { 13 | if (q === 'SELECT * FROM addresses WHERE id = $1 LIMIT 1') { 14 | return Promise.resolve({ 15 | rows: [{ 16 | address: '68 White St', 17 | city: 'New York', 18 | state: 'NY', 19 | zipCode: '10013', 20 | }], 21 | }); 22 | } 23 | return 'Error!!'; 24 | }; 25 | 26 | // create mock parent, args and context arguments as required by the resolver 27 | const args = { id: 1 }; 28 | const context = { psqlPool: { query: querySubstitute } }; 29 | 30 | // run the thenable promise and confirm the resolver is working as expected with these inputs 31 | // masking their actual functionality... 32 | address(null, args, context).then((results) => { 33 | expect(results).toEqual({ 34 | address: '68 White St', 35 | city: 'New York', 36 | state: 'NY', 37 | zipCode: '10013', 38 | }); 39 | }); 40 | }); 41 | }); 42 | 43 | describe('testing addresses resolver', () => { 44 | it('should be a function', () => { 45 | expect(typeof addresses).toBe('function'); 46 | }); 47 | it('should return an array of addresses', () => { 48 | const querySubstitute = (q) => { 49 | if (q !== 'SELECT * FROM addresses') return 'Error!!'; 50 | return Promise.resolve({ 51 | rows: [ 52 | { 53 | address: '68 White St', city: 'NY', state: 'NY', zipCode: '10013', 54 | }, { 55 | address: 'Not White St', city: 'Somewhere', state: 'IDK', zipCode: '88388', 56 | }, 57 | ], 58 | }); 59 | }; 60 | const context = { psqlPool: { query: querySubstitute } }; 61 | return addresses(null, null, context).then((result) => { 62 | expect(result).toEqual([ 63 | { 64 | address: '68 White St', city: 'NY', state: 'NY', zipCode: '10013', 65 | }, { 66 | address: 'Not White St', city: 'Somewhere', state: 'IDK', zipCode: '88388', 67 | }, 68 | ]); 69 | }); 70 | }); 71 | }); 72 | 73 | describe('testing the createOrUpdateAddress mutation', () => { 74 | it('should be a function', () => { 75 | expect(typeof createOrUpdateAddress).toBe('function'); 76 | }); 77 | it('should return a value of 1 to signal a successful change if the addressId is found', () => { 78 | const querySubstitute = (qu) => { 79 | // console.log(qu); 80 | if (qu.includes('SELECT "addressId" FROM customers')) { 81 | // console.log('in if stateemnt'); 82 | return Promise.resolve({ 83 | rowCount: 1, 84 | rows: [{ addressId: 12 }], 85 | }); 86 | } 87 | if (qu.includes('WITH foundAddress AS (')) { 88 | // console.log('in second if'); 89 | return Promise.resolve({ 90 | rowCount: 1, 91 | }); 92 | } 93 | return Promise.resolve({ 94 | rowCount: 'error with rowCount', 95 | addressId: 'error with addressId', 96 | }); 97 | }; 98 | const context = { psqlPool: { query: querySubstitute } }; 99 | const args = { 100 | customerId: 'Rich dude', 101 | address: '1 Park Ave', 102 | address2: 'apt2', 103 | city: 'NYC', 104 | state: 'NY', 105 | zipCode: '10017', 106 | }; 107 | return createOrUpdateAddress(null, args, context).then((result) => { 108 | expect(result).toEqual(1); 109 | }); 110 | }); 111 | 112 | it('should return a value of 1 to signal a successful change if the addressId is not found and needs to be created', () => { 113 | const querySubstitute = (qu) => { 114 | // console.log(qu); 115 | if (qu.includes('SELECT "addressId" FROM customers')) { 116 | // console.log('in if stateemnt'); 117 | return Promise.resolve({ 118 | rowCount: 0, 119 | rows: [], 120 | }); 121 | } 122 | if (qu.includes('WITH newAddress AS (')) { 123 | // console.log('in second if'); 124 | return Promise.resolve({ 125 | rowCount: 1, 126 | }); 127 | } 128 | return Promise.resolve({ 129 | rowCount: 'error with rowCount', 130 | addressId: 'error with addressId', 131 | }); 132 | }; 133 | const context = { psqlPool: { query: querySubstitute } }; 134 | const args = { 135 | customerId: 'Rich dude', 136 | address: '1 Park Ave', 137 | address2: 'apt2', 138 | city: 'NYC', 139 | state: 'NY', 140 | zipCode: '10017', 141 | }; 142 | return createOrUpdateAddress(null, args, context).then((result) => { 143 | expect(result).toEqual(1); 144 | }); 145 | }); 146 | }); 147 | 148 | describe('tests for deprecated updateAddress mutation', () => { 149 | it('should be a function', () => { 150 | expect(typeof updateAddress).toBe('function'); 151 | }); 152 | 153 | it('should return an address type', () => { 154 | const querySubstitute = (q) => { 155 | if (q.includes('WITH foundAddress AS (')) { 156 | return Promise.resolve({ 157 | rows: [{ 158 | address: '68White St', 159 | address2: 'Fl5', 160 | city: 'NYC', 161 | state: 'NY', 162 | zipCode: '10013', 163 | }], 164 | }); 165 | } 166 | }; 167 | const args = { 168 | customerId: 10, 169 | }; 170 | const context = { psqlPool: { query: querySubstitute } }; 171 | return updateAddress(null, args, context).then((result) => { 172 | expect(result).toEqual({ 173 | address: '68White St', 174 | address2: 'Fl5', 175 | city: 'NYC', 176 | state: 'NY', 177 | zipCode: '10013', 178 | }); 179 | }); 180 | }); 181 | }); 182 | -------------------------------------------------------------------------------- /__tests__/customerResolvers.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | Query: { customer, customers }, 3 | Mutation: { 4 | addCustomer, addCustomerAndAddress, updateCustomer, deleteCustomer, 5 | }, 6 | Customer: { address, cart }, 7 | } = require('../server/customer/customerResolver'); 8 | 9 | describe('testing customer Query', () => { 10 | // it('should be equal', () => expect(1).toEqual(1)); 11 | it('should be a function', () => { 12 | expect(typeof customer).toBe('function'); 13 | }); 14 | it('should return a customer when queried with an id in the argument', () => { 15 | const querySub = (q) => { 16 | if (q === 'SELECT * FROM customers WHERE id = $1 LIMIT 1') { 17 | return Promise.resolve({ 18 | rows: [{ 19 | firstName: 'bob', 20 | lastName: 'roberts', 21 | email: 'bob@bob.com', 22 | phoneNumber: '212-332-3094', 23 | }], 24 | }); 25 | } 26 | }; 27 | const args = { id: 3 }; 28 | const context = { psqlPool: { query: querySub } }; 29 | return customer(null, args, context).then((result) => { 30 | expect(result).toEqual({ 31 | firstName: 'bob', 32 | lastName: 'roberts', 33 | email: 'bob@bob.com', 34 | phoneNumber: '212-332-3094', 35 | }); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /assets/graphquill-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GraphQuill/Mock-GraphQL-API-Local/15d04f2eecfe3c39b83c16aa4ea7796767bef661/assets/graphquill-logo.png -------------------------------------------------------------------------------- /database/mongo/dbConnection.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | // require in models 4 | const CartModel = require('./models/cartModel'); 5 | 6 | // export an async function that awaits the connection to the database, 7 | // and returns all defined models 8 | module.exports = async () => { 9 | // internal pool handling set to 5 connections? 10 | await mongoose.connect( 11 | 'mongodb://localhost:27017/graphquillMock', 12 | { 13 | // these options are just to get rid of some deprecation warnings from mongo 14 | useNewUrlParser: true, 15 | useUnifiedTopology: true, 16 | // poolSize: 5, 17 | socketTimeoutMS: 500, 18 | connectTimeoutMS: 3000, 19 | }, 20 | ) 21 | .then(() => console.log('Connected to MongoDB')) 22 | .catch((err) => console.log('\n\nERROR ON FIRST CONNECTION ATTEMPT TO MONGO:\n\n', err)); 23 | // return an object with all the models on it 24 | return { CartModel }; 25 | }; 26 | -------------------------------------------------------------------------------- /database/mongo/models/cartModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const { Schema } = mongoose; 4 | 5 | // create the cart schema 6 | const cartSchema = new Schema({ 7 | customerId: { type: Number, required: true }, 8 | products: { type: [String] }, 9 | wishlist: { type: [String] }, 10 | }); 11 | 12 | // export out the generated collection, it will be used in the seeding file and 13 | // as context in the GraphQL API 14 | module.exports = mongoose.model('Cart', cartSchema); 15 | -------------------------------------------------------------------------------- /database/mongo/resetMongoDB.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const mongoConnection = require('./dbConnection'); 3 | 4 | // function def that is async to control the thread of execution 5 | const reset = async () => { 6 | await mongoConnection(); 7 | 8 | // drop the carts collection to wipe all data 9 | await mongoose.connection.dropCollection('carts', (err, result) => { 10 | if (err) return console.log(err); 11 | return console.log('collection deleted:', result); 12 | }); 13 | 14 | await mongoose.disconnect(); 15 | }; 16 | 17 | reset(); 18 | -------------------------------------------------------------------------------- /database/mongo/seedMongoDB.js: -------------------------------------------------------------------------------- 1 | const faker = require('faker'); // npm package for making fake data 2 | const mongoose = require('mongoose'); // disconnecting isn't working b/c of async shit 3 | const mongoConnection = require('./dbConnection'); 4 | 5 | console.log('Seeding MongoDB with random carts for 25 customers'); 6 | 7 | // async function that will seed the mongo Cart collection 8 | const seed = async (count) => { 9 | const { CartModel } = await mongoConnection(); 10 | 11 | for (let i = 0; i < count; i++) { 12 | // create a products array of 1 - 5 items 13 | const productsArray = [ 14 | faker.commerce.productName(), 15 | Math.random() > 0.5 ? faker.commerce.productName() : '', 16 | Math.random() > 0.5 ? faker.commerce.productName() : '', 17 | Math.random() > 0.5 ? faker.commerce.productName() : '', 18 | Math.random() > 0.5 ? faker.commerce.productName() : '', 19 | ]; 20 | 21 | // eslint-disable-next-line no-await-in-loop 22 | await CartModel.create({ 23 | customerId: i, 24 | // three random products 25 | products: productsArray, 26 | }, (err, data) => { 27 | if (err) console.log('ERROR CREATING CARTS: ', err); 28 | console.log('CART ADDED FOR CUSTOMER: ', data.customerId); 29 | if (i === count - 1) mongoose.disconnect(); // conditionally disconnect from database 30 | }); 31 | } 32 | return 'resolved'; 33 | }; 34 | 35 | seed(25); 36 | -------------------------------------------------------------------------------- /database/psql/dbConnection.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | 3 | const yourUsername = 'graphquill'; 4 | const yourPassword = 'graphquill'; 5 | 6 | // start a new pool of connections 7 | const pool = new Pool({ 8 | connectionString: `postgres://${yourUsername}:${yourPassword}!!@localhost/graphquillpsql`, 9 | }); 10 | 11 | // export the pool, it can be queried directly, or clients/connections can be "checked out", 12 | // the connection/client can be queried, and then "released" 13 | module.exports = pool; 14 | -------------------------------------------------------------------------------- /database/psql/resetPSQL.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is used as the first step of resetting the psql database. 3 | * It drops the two tables in the psql database. 4 | */ 5 | 6 | const Pool = require('./dbConnection'); 7 | 8 | // using an async function to ensure that these two drops occur before the next script is run. 9 | async function resetDb() { 10 | console.log('resetting psql database...'); 11 | await Pool.connect() 12 | .then(async (client) => { 13 | await client.query('DROP TABLE IF EXISTS customers CASCADE').then(() => { 14 | console.log('customers table dropped'); 15 | client.release(); 16 | }); 17 | }); 18 | await Pool.connect() 19 | .then(async (client) => { 20 | await client.query('DROP TABLE IF EXISTS addresses CASCADE').then(() => { 21 | console.log('addresses table dropped'); 22 | client.release(); 23 | }); 24 | }); 25 | await Pool.connect() 26 | .then(async (client) => { 27 | await client.query('DROP TABLE IF EXISTS "customerOrders" CASCADE').then(() => { 28 | console.log('customerOrders table dropped'); 29 | client.release(); 30 | }); 31 | }); 32 | 33 | await Pool.connect() 34 | .then(async (client) => { 35 | await client.query('DROP TABLE IF EXISTS "orderProducts" CASCADE').then(() => { 36 | console.log('orderProducts table dropped'); 37 | client.release(); 38 | }); 39 | }); 40 | 41 | await Pool.connect() 42 | .then(async (client) => { 43 | await client.query('DROP TABLE IF EXISTS "products" CASCADE').then(() => { 44 | console.log('products table dropped'); 45 | client.release(); 46 | }); 47 | }); 48 | 49 | await Pool.connect() 50 | .then(async (client) => { 51 | await client.query('DROP TABLE IF EXISTS "warehouses" CASCADE').then(() => { 52 | console.log('warehouses table dropped'); 53 | client.release(); 54 | }); 55 | }); 56 | 57 | await Pool.connect() 58 | .then(async (client) => { 59 | await client.query('DROP TABLE IF EXISTS "warehouseInventory" CASCADE').then(() => { 60 | console.log('warehouseInventory table dropped'); 61 | client.release(); 62 | }); 63 | }); 64 | 65 | // line break in console 66 | console.log(''); 67 | } 68 | 69 | resetDb(); 70 | -------------------------------------------------------------------------------- /database/psql/seedCustomerOrders.js: -------------------------------------------------------------------------------- 1 | const Pool = require('./dbConnection'); 2 | 3 | async function seedCustomerOrders() { 4 | // create an array of variables to be inserted into the database 5 | const values = [ 6 | Math.ceil(Math.random() * 25), 7 | ]; 8 | 9 | // console.log('full input array is', values); 10 | 11 | await Pool.query(` 12 | INSERT INTO "customerOrders"("customerId") 13 | VALUES ($1) 14 | RETURNING * 15 | `, values) 16 | .then((newRow) => console.log(`NEW ORDER FOR CUSTOMER: ${newRow.rows[0].customerId}`)) 17 | .catch((err) => console.log('ERROR ADDING ORDER', err)); 18 | } 19 | 20 | // seed with a random number of inputs 21 | // const random = Math.random() * 25; 22 | // console.log(`Seeding ${Math.floor(random) + 1} values`); 23 | console.log('Seeding customerOrders'); 24 | 25 | // // seems to be fixed: 26 | // // EXPECT ERRORS HERE as js will create a lot of pool connections faster than they can be handled 27 | // create the 25 customers in the database 28 | for (let i = 0; i < 25; i++) { 29 | seedCustomerOrders(); 30 | } 31 | -------------------------------------------------------------------------------- /database/psql/seedCustomers.js: -------------------------------------------------------------------------------- 1 | const faker = require('faker'); 2 | const Pool = require('./dbConnection'); 3 | 4 | // console.log(typeof Number(faker.address.zipCode())); 5 | async function seedCustomers() { 6 | // create an array of variables to be inserted into the database 7 | const values = [ 8 | faker.address.streetAddress(), 9 | faker.address.secondaryAddress(), 10 | faker.address.city(), 11 | faker.address.state(), 12 | faker.address.zipCode(), // this seems to sometimes throw an error when the column type is INT 13 | faker.name.firstName(), 14 | faker.name.lastName(), 15 | faker.internet.email(), 16 | faker.phone.phoneNumber(), 17 | ]; 18 | 19 | // use pool connections to reduce connection timeout errors due to a full pool 20 | await Pool.connect() 21 | .then((client) => { 22 | client.query(` 23 | WITH newAddress AS ( 24 | INSERT INTO addresses("address", "address2", "city", "state", "zipCode") 25 | VALUES ($1, $2, $3, $4, $5) 26 | RETURNING * 27 | ) 28 | INSERT INTO customers("firstName", "lastName", "email", "addressId", "phoneNumber") 29 | VALUES ($6, $7, $8, (SELECT id FROM newAddress), $9) 30 | RETURNING * 31 | `, values) 32 | .then((newRow) => console.log(`NEW CUSTOMER ADDED: ${newRow.rows[0].firstName} ${newRow.rows[0].lastName}`)) 33 | .catch((err) => console.log('ERROR ADDING CUSTOMER AND/OR ADDRESS (THIS IS SOMEWHAT EXPECTED FOR SEEDING SCRIPT)', err)) 34 | .finally(() => { 35 | client.release(); 36 | }); 37 | }); 38 | } 39 | 40 | console.log('Seeding customers and warehouses'); 41 | 42 | // create the 25 customers in the database 43 | for (let i = 0; i < 25; i++) { 44 | seedCustomers(); 45 | } 46 | -------------------------------------------------------------------------------- /database/psql/seedOrderProducts.js: -------------------------------------------------------------------------------- 1 | const Pool = require('./dbConnection'); 2 | 3 | async function seedCustomerOrders() { 4 | // create an array of variables to be inserted into the database 5 | const values = [ 6 | Math.ceil(Math.random() * 250), 7 | Math.ceil(Math.random() * 15), 8 | Math.ceil(Math.random() * 25), 9 | ]; 10 | 11 | // use pool connection clients to reduce connection timeout errors from a full pool 12 | await Pool.connect() 13 | .then(async (client) => { 14 | await client.query(` 15 | INSERT INTO "orderProducts"("productId", "productQty", "orderId") 16 | VALUES ($1, $2, $3) 17 | RETURNING * 18 | `, values) 19 | .then((newRow) => console.log(`NEW PRODUCT FOR ORDER: ${newRow.rows[0].orderId}`)) 20 | .finally(client.release()); 21 | }) 22 | .catch((err) => console.log('ERROR ADDING PRODUCT TO ORDER', err, '\n\n VALUES ARE \n', values)); 23 | } 24 | 25 | console.log('Seeding customerOrders'); 26 | 27 | // create the 100 products in random orders in the database 28 | for (let i = 0; i < 100; i++) { 29 | seedCustomerOrders(); 30 | } 31 | -------------------------------------------------------------------------------- /database/psql/seedProducts.js: -------------------------------------------------------------------------------- 1 | const faker = require('faker'); 2 | const Pool = require('./dbConnection'); 3 | 4 | async function seedProducts() { 5 | // create an array of variables to be inserted into the database 6 | const values = [ 7 | faker.commerce.productName(), 8 | faker.commerce.productAdjective() + faker.commerce.productMaterial(), 9 | Math.ceil(Math.random() * 100000), 10 | Math.ceil(Math.random() * 50), 11 | ]; 12 | 13 | // use pool connect clients to reduce instances of connection timeout errors from a full pool 14 | await Pool.connect() 15 | .then(async (client) => { 16 | await client.query(` 17 | INSERT INTO products("name", "description", "price", "weight") 18 | VALUES ($1, $2, $3, $4) 19 | RETURNING * 20 | `, values) 21 | .then((newRow) => console.log(`NEW PRODUCT ADDED: ${newRow.rows[0].name}`)) 22 | .finally(() => client.release()); 23 | }) 24 | .catch((err) => console.log('ERROR ADDING PRODUCT', err)); 25 | } 26 | 27 | console.log('Seeding products'); 28 | 29 | // create the 250 products in the database 30 | for (let i = 0; i < 250; i++) { 31 | seedProducts(); 32 | } 33 | -------------------------------------------------------------------------------- /database/psql/seedWarehouseInventory.js: -------------------------------------------------------------------------------- 1 | const Pool = require('./dbConnection'); 2 | 3 | async function seedWarehouseInventory() { 4 | // create an array of variables to be inserted into the database 5 | const values = [ 6 | Math.ceil(Math.random() * 250), 7 | Math.ceil(Math.random() * 25), 8 | Math.ceil(Math.random() * 2500), 9 | ]; 10 | 11 | // console.log('full input array is', values); 12 | 13 | // use a connection client to prevent errors from a full pool & connection timeouts 14 | await Pool.connect() 15 | .then((client) => { 16 | client.query(` 17 | INSERT INTO "warehouseInventory"("productId", "warehouseId", quantity) 18 | VALUES ($1, $2, $3) 19 | RETURNING * 20 | `, values) 21 | .then((newRow) => console.log(`NEW INVENTORY ADDED FOR WAREHOUSE: ${newRow.rows[0].warehouseId}`)) 22 | .catch((err) => console.log('ERROR ADDING WAREHOUSE INVENTORY', err, values)) 23 | .finally(() => client.release()); 24 | }); 25 | } 26 | 27 | console.log('Seeding warehouse inventory'); 28 | 29 | // create the warehouse inventories for the 25 warehouses 30 | for (let i = 0; i < 25; i++) { 31 | seedWarehouseInventory(); 32 | } 33 | -------------------------------------------------------------------------------- /database/psql/seedWarehouses.js: -------------------------------------------------------------------------------- 1 | const faker = require('faker'); 2 | const Pool = require('./dbConnection'); 3 | 4 | async function seedWarehouses() { 5 | // create an array of variables to be inserted into the database 6 | const values = [ 7 | faker.address.streetAddress(), 8 | faker.address.secondaryAddress(), 9 | faker.address.city(), 10 | faker.address.state(), 11 | faker.address.zipCode(), 12 | faker.commerce.department(), 13 | ]; 14 | 15 | // connect to a pool client to reduce chances of a full pool & connection errors 16 | await Pool.connect() 17 | .then(async (client) => { 18 | // send query to client to insert and address and warehouse 19 | await client.query(` 20 | WITH newAddress AS ( 21 | INSERT INTO addresses("address", "address2", "city", "state", "zipCode") 22 | VALUES ($1, $2, $3, $4, $5) 23 | RETURNING * 24 | ) 25 | INSERT INTO warehouses("name", "addressId") 26 | VALUES ($6, (SELECT id FROM newAddress)) 27 | RETURNING * 28 | `, values) 29 | .then((newRow) => console.log(`NEW WAREHOUSE ADDED: ${newRow.rows[0].name}`)) 30 | .finally(() => client.release()); 31 | }) 32 | .catch((err) => console.log('ERROR ADDING WAREHOUSE', err)); 33 | } 34 | 35 | console.log('Seeding warehouses'); 36 | 37 | // create the 25 warehouses in the database 38 | for (let i = 0; i < 25; i++) { 39 | seedWarehouses(); 40 | } 41 | -------------------------------------------------------------------------------- /database/psql/setupTablesPSQL.js: -------------------------------------------------------------------------------- 1 | const Pool = require('./dbConnection'); 2 | 3 | // function that will create each table and then add necessary constraints 4 | async function setup() { 5 | // setup the customer table 6 | console.log('setting up customers table...'); 7 | await Pool.connect() 8 | .then(async (client) => { 9 | await client.query(` 10 | CREATE TABLE IF NOT EXISTS customers ( 11 | id SERIAL PRIMARY KEY, 12 | "firstName" VARCHAR(255) NOT NULL, 13 | "lastName" VARCHAR(255) NOT NULL, 14 | "email" VARCHAR(255) NOT NULL, 15 | "addressId" INT, 16 | "phoneNumber" VARCHAR(40) 17 | ) 18 | `) 19 | .then(() => console.log('customers table made')) // notify the console on completion 20 | .finally(() => client.release()); 21 | }); 22 | 23 | // setup the addresses table 24 | console.log('setting up addresses table...'); 25 | await Pool.connect() 26 | .then(async (client) => { 27 | await client.query(` 28 | CREATE TABLE IF NOT EXISTS addresses ( 29 | id SERIAL PRIMARY KEY, 30 | "address" VARCHAR(255) NOT NULL, 31 | "address2" VARCHAR(255), 32 | "city" VARCHAR(255) NOT NULL, 33 | "state" VARCHAR(255) NOT NULL, 34 | "zipCode" VARCHAR(10) NOT NULL 35 | ) 36 | `) 37 | .then(() => console.log('addresses table made')) // notify the console on completion 38 | .finally(() => client.release()); 39 | }); 40 | 41 | // setup customers to orders join table 42 | await Pool.connect() 43 | .then(async (client) => { 44 | client.query(` 45 | CREATE TABLE IF NOT EXISTS "customerOrders" ( 46 | "orderId" SERIAL PRIMARY KEY, 47 | "customerId" INT 48 | )`) 49 | .then(() => console.log('customerOrders table made')) // notify the console on completion 50 | .finally(() => client.release()); 51 | }); 52 | 53 | // create orrders to products join table (also stores quantities) 54 | await Pool.connect() 55 | .then(async (client) => { 56 | await client.query(` 57 | CREATE TABLE IF NOT EXISTS "orderProducts" ( 58 | "orderProductId" SERIAL PRIMARY KEY, 59 | "productId" INT, 60 | "productQty" INT, 61 | "orderId" INT 62 | )`) 63 | .then(() => console.log('orderProducts table created')) 64 | .finally(() => client.release()); 65 | }); 66 | 67 | // create table for all products 68 | await Pool.connect() 69 | .then(async (client) => { 70 | await client.query(` 71 | CREATE TABLE IF NOT EXISTS "products" ( 72 | "productId" SERIAL PRIMARY KEY, 73 | "name" VARCHAR(255), 74 | "description" VARCHAR(255), 75 | "price" INT, 76 | "weight" INT 77 | )`) 78 | .then(() => console.log('products table created')) 79 | .finally(() => client.release()); 80 | }); 81 | 82 | // create table for all warehouses 83 | await Pool.connect() 84 | .then(async (client) => { 85 | await client.query(` 86 | CREATE TABLE IF NOT EXISTS "warehouses" ( 87 | "warehouseId" SERIAL PRIMARY KEY, 88 | "name" VARCHAR(255), 89 | "addressId" INT 90 | )`) 91 | .then(() => console.log('warehouses table created')) 92 | .finally(() => client.release()); 93 | }); 94 | 95 | // create table for warehouseInventories 96 | await Pool.connect() 97 | .then(async (client) => { 98 | await client.query(` 99 | CREATE TABLE IF NOT EXISTS "warehouseInventory" ( 100 | "id" SERIAL PRIMARY KEY, 101 | "productId" INT, 102 | "warehouseId" INT, 103 | "quantity" INT 104 | )`) 105 | .then(() => console.log('warehouseInventory table created')) 106 | .finally(() => client.release()); 107 | }); 108 | 109 | 110 | // ! Add foregin key constraints 111 | // add foregin key constraint, this will throw an error if the previous queries are not 112 | await Pool.connect() 113 | .then(async (client) => { 114 | await client.query(` 115 | ALTER TABLE customers 116 | ADD CONSTRAINT "addressConstraint" 117 | FOREIGN KEY ("addressId") 118 | REFERENCES addresses (id) 119 | ON DELETE CASCADE 120 | `) 121 | .then(() => console.log('constraint added for customers and addresses')) // shoot a message to the console 122 | .finally(() => client.release()); 123 | }); 124 | 125 | await Pool.connect() 126 | .then(async (client) => { 127 | await client.query(` 128 | ALTER TABLE "customerOrders" 129 | ADD CONSTRAINT "customerOrderConstraint" 130 | FOREIGN KEY ("customerId") 131 | REFERENCES customers (id) 132 | ON DELETE CASCADE 133 | `) 134 | .then(() => console.log('constraint added for customerOrders and customers')) // shoot a message to the console 135 | .finally(() => client.release()); 136 | }); 137 | 138 | await Pool.connect() 139 | .then(async (client) => { 140 | await client.query(` 141 | ALTER TABLE "orderProducts" 142 | ADD CONSTRAINT "customerOrderProductsConstraint" 143 | FOREIGN KEY ("orderId") 144 | REFERENCES "customerOrders" ("orderId") 145 | ON DELETE CASCADE 146 | `) 147 | .then(() => console.log('constraint added for customerOrders and orderProducts')) // shoot a message to the console 148 | .finally(() => client.release()); 149 | }); 150 | 151 | await Pool.connect() 152 | .then(async (client) => { 153 | await client.query(` 154 | ALTER TABLE "orderProducts" 155 | ADD CONSTRAINT "orderProductsToProductsConstraint" 156 | FOREIGN KEY ("productId") 157 | REFERENCES "products" ("productId") 158 | ON DELETE CASCADE 159 | `) 160 | .then(() => console.log('constraint added for products and orderProducts')) // shoot a message to the console 161 | .finally(() => client.release()); 162 | }); 163 | 164 | await Pool.connect() 165 | .then(async (client) => { 166 | await client.query(` 167 | ALTER TABLE "warehouseInventory" 168 | ADD CONSTRAINT "warehouseInventoryToProductsConstraint" 169 | FOREIGN KEY ("productId") 170 | REFERENCES "products" ("productId") 171 | ON DELETE CASCADE 172 | `) 173 | .then(() => console.log('constraint added for products and warehouseInventory')) // shoot a message to the console 174 | .finally(() => client.release()); 175 | }); 176 | 177 | await Pool.connect() 178 | .then(async (client) => { 179 | await client.query(` 180 | ALTER TABLE "warehouseInventory" 181 | ADD CONSTRAINT "warehouseInventoryToWarehouseConstraint" 182 | FOREIGN KEY ("warehouseId") 183 | REFERENCES "warehouses" ("warehouseId") 184 | ON DELETE CASCADE 185 | `) 186 | .then(() => console.log('constraint added for warehouse and warehouseInventory')) // shoot a message to the console 187 | .finally(() => client.release()); 188 | }); 189 | 190 | await Pool.connect() 191 | .then(async (client) => { 192 | await client.query(` 193 | ALTER TABLE "warehouses" 194 | ADD CONSTRAINT "warehousesToAddressesConstraint" 195 | FOREIGN KEY ("addressId") 196 | REFERENCES "addresses" ("id") 197 | ON DELETE CASCADE 198 | `) 199 | .then(() => console.log('constraint added for warehouse and warehouseInventory')) // shoot a message to the console 200 | .finally(() => client.release()); 201 | }); 202 | 203 | // line break in console 204 | console.log(''); 205 | } // setup functon def closing curly 206 | 207 | // invoke setup function 208 | setup(); 209 | -------------------------------------------------------------------------------- /graphquill.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // we've left this config file here, it you want to test run that VS Code command, 3 | // then delete this file 4 | 5 | // change "./server/index.js" to the relative path from the root directory to 6 | // the file that starts your server 7 | entry: './server/index.js', 8 | 9 | // change 3000 to the port number that your server runs on 10 | portNumber: 3000, 11 | 12 | // to increase the amount of time allowed for the server to startup, add a time 13 | // in milliseconds (integer) to the "serverStartupTimeAllowed" 14 | // serverStartupTimeAllowed: 5000, 15 | }; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "final-mockAPI", 3 | "version": "1.0.0", 4 | "description": "Mock/Test API for testing during the development of GraphQuill", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node server/index.js", 8 | "test": "jest --verbose", 9 | "dev": "nodemon server/index.js", 10 | "resetPsql": "node database/psql/resetPSQL.js && node database/psql/setupTablesPSQL.js && node database/psql/seedCustomers.js && node database/psql/seedProducts.js && node database/psql/seedCustomerOrders.js && node database/psql/seedOrderProducts.js && node database/psql/seedWarehouses.js && node database/psql/seedWarehouseInventory.js", 11 | "resetMongo": "node database/mongo/resetMongoDB.js && node database/mongo/seedMongoDB.js", 12 | "resetDbs": "npm run resetPsql && npm run resetMongo" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/GraphQuill/Mock-GraphQL-API.git" 17 | }, 18 | "keywords": [], 19 | "author": "GraphQuill.1.0 Team", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/GraphQuill/Mock-GraphQL-API/issues" 23 | }, 24 | "homepage": "https://github.com/GraphQuill/Mock-GraphQL-API#readme", 25 | "devDependencies": { 26 | "eslint": "^6.1.0", 27 | "eslint-config-airbnb": "^18.0.1", 28 | "eslint-plugin-import": "^2.18.2", 29 | "eslint-plugin-jsx-a11y": "^6.2.3", 30 | "eslint-plugin-react": "^7.16.0", 31 | "eslint-plugin-react-hooks": "^1.7.0", 32 | "jest": "^24.9.0", 33 | "nodemon": "^1.19.4" 34 | }, 35 | "dependencies": { 36 | "dotenv": "^8.2.0", 37 | "express": "^4.17.1", 38 | "express-graphql": "^0.9.0", 39 | "faker": "^4.1.0", 40 | "graphql": "^14.5.8", 41 | "graphql-tools": "^4.0.6", 42 | "mongoose": "^5.7.7", 43 | "pg": "^7.12.1", 44 | "pg-hstore": "^2.3.3" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /server/address/addressResolver.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file exports a single object with keys of Query and Mutation 3 | * The values for those keys are objects themselves, where each key is the name of a 4 | * resolver function and the value is the corresponding resolver function. 5 | */ 6 | 7 | module.exports = { 8 | // the Query key sets all allowable queries that the user can make 9 | // queries are basically "Read" functionality 10 | Query: { 11 | // select a single address via id in the PSQL table 12 | address: (parent, args, context) => { 13 | // console.log('args: ', args); 14 | // set query text and values (id from arguments obj) 15 | const query = 'SELECT * FROM addresses WHERE id = $1 LIMIT 1'; 16 | const values = [args.id]; 17 | // return the async query to the d-base, parse out the only row that is returned 18 | return context.psqlPool.query(query, values) 19 | // the data.rows[0] is what actually gets returned because graphql plays nice with promises 20 | .then((data) => data.rows[0]) 21 | .catch((err) => console.log('ERROR GETTING AN ADDRESS', err)); 22 | }, 23 | 24 | // returns all addresses, it's kind of useless but it was easy to make 25 | addresses: (parent, args, { psqlPool }) => { 26 | // alternatively, context can be destructured, but I find it less readable 27 | // console.log('addresses queried'); 28 | const query = 'SELECT * FROM addresses'; 29 | // return the promise of a query that resolves to the data.rows 30 | return psqlPool.query(query) 31 | .then((data) => data.rows) 32 | .catch((err) => console.log('ERROR LOOKING UP ADDRESSES', err)); 33 | }, 34 | }, 35 | 36 | Mutation: { 37 | // mutation added that handles creating an address if the user doesn't have one, OR 38 | // updating their existing address 39 | createOrUpdateAddress: async (parent, args, { psqlPool }) => { 40 | // destrucutre the args 41 | const { 42 | customerId, address, address2, city, state, zipCode, 43 | } = args; 44 | 45 | // query for the addressId in the customers table based on args.customerId 46 | const query1 = `SELECT "addressId" FROM customers 47 | WHERE id = $1`; 48 | // the only value is the customerId (destructured from the args) 49 | const values1 = [customerId]; 50 | // console.log(query1, values1); 51 | 52 | // querying the database is asyncronous, so use await to set the addressId to the resolved 53 | // value of the query 54 | const addressId = await psqlPool.query(query1, values1) 55 | .then((res) => { 56 | if (!res.rowCount) return null; 57 | return res.rows[0].addressId; 58 | }); 59 | 60 | // if the returned addressId is null, insert a new address 61 | if (!addressId) { 62 | // the query inserts a new address into the addresses table, then places that 63 | // new id into the addressId column of the customers table 64 | const queryInsert = `WITH newAddress AS ( 65 | INSERT INTO addresses ("address", "address2", "city", "state", "zipCode") 66 | VALUES ($1, $2, $3, $4, $5) 67 | RETURNING * 68 | ) 69 | UPDATE customers SET "addressId" = (SELECT id FROM newAddress) 70 | WHERE id = $6; 71 | `; 72 | // values are all of the things required for making an address, and the 73 | // customerId from the previous query 74 | const valuesInsert = [ 75 | address, address2, city, state, zipCode, customerId, 76 | ]; 77 | // console.log(queryInsert, valuesInsert); 78 | 79 | // return the result of the query promise (inserting the address) 80 | // becuase of how the above queries are setup, the return value is the count of 81 | // updated rows which the front end can use to determine if the address was added 82 | return psqlPool.query(queryInsert, valuesInsert) 83 | .then((res) => res.rowCount) 84 | .catch((err) => { 85 | console.log('ERROR INSERTING A NEW ADDRESS', err); 86 | return 0; // if there's an error, return a zero that the query failed 87 | }); 88 | } 89 | // else there is an existing address that needs to be updated 90 | // this query will update the found address (and not change anything in the customers table) 91 | const queryUpdate = `WITH foundAddress AS ( 92 | SELECT "addressId" AS id 93 | FROM customers 94 | WHERE id = $1 95 | ) 96 | UPDATE addresses SET 97 | "address" = $2, 98 | "address2" = $3, 99 | "city" = $4, 100 | "state" = $5, 101 | "zipCode" = $6 102 | WHERE id = (SELECT id FROM foundAddress) 103 | RETURNING *; 104 | `; 105 | const valuesUpdate = [ 106 | customerId, address, address2, city, state, zipCode, 107 | ]; 108 | // console.log(queryUpdate, valuesUpdate); 109 | 110 | // return the result of the query promise 111 | return psqlPool.query(queryUpdate, valuesUpdate) 112 | .then((res) => res.rowCount) 113 | .catch((err) => console.log('ERROR updating customer address', err)); 114 | }, 115 | 116 | // ! DEPRECATED, but kept for reference of a simpler mutation 117 | updateAddress: (parent, args, { psqlPool }) => { 118 | // DEPRECATED FOR createOrUpdateAddress 119 | // initialize the query statement 120 | let query = `WITH foundAddress AS ( 121 | SELECT "addressId" as id 122 | FROM customers 123 | WHERE id = $1 124 | ) 125 | UPDATE addresses SET 126 | `; 127 | 128 | // initialize the values array which will have the id as the first argument 129 | const values = [args.customerId]; 130 | let count = 2; // count will track which item in the array needs to be referenced in SQL 131 | 132 | // iterate through each element in the arguments and add to the query and values variables 133 | Object.keys(args).forEach((e) => { 134 | if (e !== 'customerId' && args[e]) { // omit id and null arguments 135 | query += `"${e}"= $${count}, `; 136 | count += 1; 137 | values.push(args[e]); 138 | } 139 | }); 140 | 141 | // remove the last comma off the query 142 | query = query.slice(0, query.length - 2); 143 | 144 | // add in selector w/ first item in array being the id from args 145 | query += ' WHERE id = (SELECT id FROM foundAddress) RETURNING *'; 146 | 147 | // console.log(query, values); 148 | 149 | return psqlPool.query(query, values) 150 | .then((res) => res.rows[0]) 151 | .catch((err) => console.log('ERROR WHILE UPDATING CUSTOMER\'S ADDRESS: updateAddress Mutation', err)); 152 | }, // ! DEPRECATED ------------ 153 | }, 154 | }; 155 | -------------------------------------------------------------------------------- /server/address/addressSchema.js: -------------------------------------------------------------------------------- 1 | // the Base schema is required in so that we can add types (Address), and to 2 | // extend the Query and Mutation schemas 3 | const Base = require('../baseSchema'); 4 | 5 | // this file defines the address schema 6 | const Address = ` 7 | # Address describes an instance of a user's address 8 | type Address { 9 | # id corresponds to the id in PSQL 10 | id: Int! 11 | 12 | # Line 1 of an address 13 | address: String! 14 | 15 | # Optional line 2 of an address (no exclaimation point) 16 | address2: String 17 | 18 | # The rest of these are fairly self explainatory 19 | city: String! 20 | state: String! 21 | # zipCode was made a string due to how faker (npm package) produces zipcodes 22 | zipCode: String! 23 | } 24 | 25 | # Queries define what endpoints the user can query from 26 | # the keys inside of the parameters are arguments (which are used in the resolver functions) 27 | 28 | extend type Query { 29 | # The address query grabs an Address from the PSQL database with a corresponding id (NOT the addressId in the customers table) 30 | address(id: Int!): Address! 31 | 32 | # The addresses query returns all addresses, it's not that useful... 33 | addresses: [Address!]! # iterable of addresses where no element can be null 34 | } 35 | 36 | extend type Mutation { 37 | # Deprecated mutation, functionality is replaced by createOrUpdateAddress 38 | updateAddress( 39 | customerId: Int!, 40 | address: String, 41 | address2: String, 42 | city: String, 43 | state: String, 44 | zipCode: String): Address! @deprecated (reason: "Use \`createOrUpdateAddress\`") 45 | 46 | # returns the number of created/updated addresses in the sql database (0 or 1) 47 | createOrUpdateAddress( 48 | # looks up an addressed by finding the addressId in the customers table (using the customerId) 49 | customerId: Int!, 50 | 51 | # first line of address 52 | address: String!, 53 | 54 | # second, optional, line of address 55 | address2: String, 56 | city: String!, 57 | state: String!, 58 | zipCode: String!): Int! 59 | 60 | } 61 | `; 62 | 63 | // return a thunk so hoisting can handle unordered schema imports in the schema.js file 64 | 65 | // The address and base schemas are exported together because the address extends the base schema 66 | module.exports = () => [Address, Base]; 67 | -------------------------------------------------------------------------------- /server/baseSchema.js: -------------------------------------------------------------------------------- 1 | // setup a base schema that will be combined with the others 2 | 3 | // by deprecating the dummy Query and mutation, they're basically hidden in 4 | // the GraphiQL documentation which is nice 5 | const Base = ` 6 | directive @deprecated( 7 | reason: String = "No longer supported" 8 | ) on FIELD_DEFINITION | ENUM_VALUE 9 | 10 | type Query { 11 | dummy: Boolean @deprecated (reason: "dummy query does nothing") 12 | } 13 | 14 | type Mutation { 15 | dummy: Boolean @deprecated (reason: "does nothing") 16 | } 17 | `; 18 | 19 | // the export is a function that returns an array, it has something to do with 20 | // circularly dependant schemas. It's more clear in schemas that are not the 21 | // base. 22 | module.exports = () => [Base]; 23 | -------------------------------------------------------------------------------- /server/cart/cartResolver.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Query: { 3 | // query to lookup a cart by customerId 4 | cart: async (parent, args, 5 | // destructrue the CartModel out of the context parameter 6 | { mongo: { CartModel } }) => { 7 | // use findOne to return a single mongo document. Because the mongo document has the same 8 | // keys as the graphql Cart type, the default resolver will "coerce" this into a graphql type 9 | const queryResult = await CartModel.findOne({ customerId: args.customerId }, 10 | // await is used to block further execution and set the queryResult variable to the data 11 | // that is returned from the findOne query 12 | (err, data) => { 13 | if (err) return console.log('ERROR IN MONOG QUERY cart Query: ', err); 14 | if (!data) { 15 | return console.log('cart does not exist'); 16 | } 17 | // return console.log('the found cart is', data); 18 | return data; // I'm pretty sure this is doing nothing... 19 | }); 20 | // console.log('findOne data is', queryResult); 21 | 22 | // defaultCart object made to be returned if the query returns null 23 | // not all users need to have carts so this is possible and avoids graphql type error 24 | const defaultCart = { customerId: args.customerId, products: [], wishlist: [] }; 25 | 26 | // return the queryResult if it's truthy (i.e. not null), otherwise return the defaultCart 27 | return queryResult || defaultCart; 28 | }, 29 | }, 30 | 31 | Mutation: { 32 | // Mutation that updates a cart with a given item, or creates the cart if it doesn't exist 33 | createOrUpdateCart: async (parent, args, 34 | // destrucutre out the Cart Model 35 | { mongo: { CartModel } }) => CartModel.findOneAndUpdate({ 36 | // first parameter of findOneAndUpdate: "lookup conditions" 37 | customerId: args.customerId, 38 | }, { 39 | // second parameter: "what to update" 40 | // used $addToSet instead which treats the products value as a set and doesn't allow 41 | // duplicates when "pushing" to the array. 42 | $addToSet: { products: args.newItem }, 43 | }, { 44 | // third parameter: options 45 | lean: true, // reduces overhead of returning document (I think) 46 | new: true, // returns the modified document, instead of the document before changes 47 | upsert: true, // adds the docuemnt if one is not found (update/insert) 48 | useFindAndModify: false, // this has to do with some deprecation thing 49 | }, (err, data) => { 50 | if (err) return console.log('ERROR in addOrUpdateCart mutation', err); 51 | // return console.log('data is', data); 52 | return data; // I'm pretty sure this doesn't actually do anything... 53 | }), 54 | 55 | // mutation to remove items from a cart 56 | // inputted arguments are the customerId and itemsToRemove (an array of strings) 57 | removeItemsFromCart: (parent, args, { mongo: { CartModel } }) => CartModel.findOneAndUpdate({ 58 | // conditions 59 | customerId: args.customerId, 60 | }, { 61 | // updateObject: pullAll removes all instances that match the argument 62 | $pullAll: { products: args.itemsToRemove }, 63 | }, { 64 | // options 65 | new: true, // option to return the updated mongoose document (instead of before it updates) 66 | useFindAndModify: false, 67 | }, (err, data) => { // callback to error handle 68 | if (err) return console.log(err); 69 | // return console.log('data found is ', data); 70 | return data; // this really doesn't do anything... 71 | }), 72 | 73 | deleteCart: async (parent, args, { mongo: { CartModel } }) => { 74 | // initialize a variable that will hold the found document 75 | let document; 76 | 77 | // make this blocking, to set the value of document 78 | await CartModel.findOneAndRemove({ 79 | // conditions 80 | customerId: args.customerId, 81 | }, { 82 | // options 83 | useFindAndModify: false, 84 | select: 'customerId', // select to only return the customerId field 85 | }, (err, data) => { 86 | // callback 87 | if (err) return console.log(err); 88 | // console.log(data); 89 | 90 | // set the value of document to the returned data from mongoose 91 | document = data; 92 | return data; // this also doesn't really do anything... 93 | }); 94 | // console.log('document', document); 95 | 96 | // return the document to satisfy the 'Cart' output of this mutation in the schema 97 | return document; 98 | }, 99 | }, 100 | 101 | 102 | }; 103 | -------------------------------------------------------------------------------- /server/cart/cartSchema.js: -------------------------------------------------------------------------------- 1 | const Base = require('../baseSchema'); 2 | 3 | const Cart = ` 4 | # A Cart must have a customerId, the products and wishlist can both be empty Arrays or null 5 | type Cart { 6 | # The id associated with the customer in the customers PSQL table 7 | customerId: Int! 8 | 9 | # A null-able array containing strings of product names 10 | products: [String!] 11 | 12 | # Same as products, currently the mutations for this are not setup 13 | wishlist: [String!] 14 | } 15 | 16 | # Queries define what endpoints the user can query from 17 | # the keys inside of the parameters are arguments (which are used in the resolver functions) 18 | 19 | extend type Query { 20 | # cart returns a Cart type based on an argument of customerId 21 | cart(customerId: Int!): Cart! 22 | } 23 | 24 | extend type Mutation { 25 | # Creates a cart in the mongodb if it doesn't exist, otherwise updates the existing cart 26 | # for the given customer 27 | createOrUpdateCart(customerId: Int!, newItem: String!): Cart! 28 | 29 | # Removes items (by name) from the cart of the given customer 30 | removeItemsFromCart(customerId: Int!, itemsToRemove: [String!]): Cart! 31 | 32 | # deletes an entire cart of a customer 33 | deleteCart(customerId: Int!): Cart! 34 | } 35 | `; 36 | 37 | // return a thunk so hoisting can handle unordered schema imports in the schema.js file 38 | module.exports = () => [Cart, Base]; 39 | -------------------------------------------------------------------------------- /server/customer/customerResolver.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Query: { 3 | // returns a single customer's information based on their id 4 | customer: (parent, args, context) => { 5 | const query = 'SELECT * FROM customers WHERE id = $1 LIMIT 1'; 6 | const values = [args.id]; 7 | // graphql plays nice with promises, so the result of the returned promise is what is sent 8 | // as the response in graphql 9 | return context.psqlPool.query(query, values) 10 | // setting the address is handled by the Customer type resolver below (built into GQL) 11 | .then((data) => data.rows[0]) 12 | .catch((err) => console.log('ERROR LOOKING UP CUSTOMER', err)); 13 | }, 14 | 15 | // returns all customers' information 16 | customers: (parent, args, context) => { 17 | const query = 'SELECT * FROM customers'; 18 | // using pool.connect to create an individual connection to the database, because 19 | // there are some nested queries in the type resolver. This limits the number of 20 | // connections to the pool to avoid "too many connections" errors 21 | return context.psqlPool.connect() 22 | .then((client) => client.query(query) 23 | .then((data) => { 24 | client.release(); 25 | return data.rows; 26 | }) 27 | .catch((err) => { 28 | client.release(); 29 | console.log('ERROR LOOKING UP CUSTOMERS', err); 30 | })) 31 | .catch((err) => console.log('ERROR IN customers Query', err)); 32 | }, 33 | }, 34 | 35 | // Mutations are what modifies data in the databases 36 | Mutation: { 37 | // replacement for addCustomerAndAddress to only add the user to the psql database 38 | addCustomer: (parent, args, { psqlPool }) => { 39 | // destructure the args input 40 | const { 41 | firstName, lastName, email, phoneNumber, 42 | } = args; 43 | 44 | // initalize the query message to add a row in the customers table 45 | const query = `INSERT INTO customers ("firstName", "lastName", "email", "phoneNumber") 46 | VALUES ($1, $2, $3, $4) 47 | RETURNING *;`; 48 | 49 | // values are the input arguments 50 | const values = [firstName, lastName, email, phoneNumber]; 51 | 52 | // return the result of the query to the psql database 53 | return psqlPool.query(query, values) 54 | // return the newly created row that comes from the query's response 55 | .then((res) => res.rows[0]) 56 | .catch((err) => console.log('ERROR ADDING CUSTOMER TO DB addCustomer mutaiton', err)); 57 | }, 58 | 59 | // new key-value pair = new mutation type/exposes endpoint 60 | // ! DEPRECATED addCustomerAndAddress because in a real app the customer info is added, 61 | // and address is added upon their first order 62 | addCustomerAndAddress: (parent, args, { psqlPool }) => { 63 | // destructuring the arguments 64 | const { 65 | firstName, lastName, email, phoneNumber, 66 | address, address2, city, state, zipCode, 67 | } = args; 68 | 69 | // big ass query to add an address and user at the same time 70 | const query = `WITH newAddress AS ( 71 | INSERT INTO addresses("address", "address2", "city", "state", "zipCode") 72 | VALUES ($1, $2, $3, $4, $5) 73 | RETURNING * 74 | ) 75 | INSERT INTO customers("firstName", "lastName", "email", "addressId", "phoneNumber") 76 | VALUES ($6, $7, $8, (SELECT id FROM newAddress), $9) 77 | RETURNING *`; 78 | // initializing values array 79 | const values = [ 80 | address, address2, city, state, zipCode, 81 | firstName, lastName, email, phoneNumber, 82 | ]; 83 | 84 | // return the async query to the database 85 | return psqlPool.query(query, values) 86 | .then((res) => res.rows[0]) 87 | .catch((err) => console.log('ERROR ADDING CUSTOMER AND ADDRESS TO DATABASE: addCustomerAndAddress Mutation', err)); 88 | }, // ! DEPRECATED 89 | 90 | // mutation that updates the information of a customer 91 | updateCustomer: (parent, args, { psqlPool }) => { 92 | // initialize the query statement 93 | let query = 'UPDATE customers SET '; 94 | // initialize the values array which will have the id as the first argument 95 | const values = [args.id]; 96 | let count = 2; // count will track which item in the array needs to be referenced in SQL 97 | 98 | // iterate through each element in the arguments and add to the query and values variables 99 | Object.keys(args).forEach((e) => { 100 | if (e !== 'id' && args[e]) { // omit id and null arguments 101 | query += `"${e}"= $${count}, `; 102 | count += 1; 103 | values.push(args[e]); 104 | } 105 | }); 106 | 107 | // remove the last comma off the query 108 | query = query.slice(0, query.length - 2); 109 | 110 | // add in selector w/ first item in array being the id from args 111 | query += ' WHERE id = $1 RETURNING *'; 112 | // console.log(query, values); 113 | 114 | // return the async call to the psql database 115 | return psqlPool.query(query, values) 116 | .then((res) => res.rows[0]) 117 | .catch((err) => console.log('ERROR UPDATING CUSTOMER INFORMATION: updateCustomer Mutation', err)); 118 | }, 119 | 120 | deleteCustomer: (parent, args, { psqlPool }) => { 121 | // only need to delete the address row because cascading delete is TRUE 122 | const query = `DELETE FROM addresses 123 | WHERE addresses.id = ( 124 | SELECT id FROM customers 125 | WHERE id = $1 126 | )`; 127 | 128 | // setup the values array 129 | const values = [args.id]; 130 | 131 | // console.log(query, value); 132 | return psqlPool.query(query, values) 133 | .then((res) => res.rowCount) 134 | .catch((err) => console.log('ERROR DELETING USER & ADDRESS: deleteCustomer Mutation', err)); 135 | }, 136 | }, 137 | 138 | //* This is a type resolver which is useful for defining the nested GQL definitions in a schema 139 | //* this one sets the address key/parameter of a Customer (to the info of an Address TYPE) 140 | Customer: { 141 | /** 142 | * the parameters for *`ALL RESOLVER FUNCTIONS`* are: 143 | * 1. the `parent`/previous value (the Customer is the parent of the address property) 144 | * 2. `arguments`, I'm not sure where these get passed, but it may be the same 145 | * arguemnts that were passed into the Customer query 146 | * 3. `context` (the databse connection in this case) 147 | * 4. There is a fourth argument (called `info`) but it's not used for basic cases 148 | */ 149 | 150 | address: (parent, args, context) => { 151 | const query = 'SELECT * FROM addresses WHERE id = $1 LIMIT 1'; 152 | const values = [parent.addressId]; 153 | 154 | // return the async query to find the address with the appropriate addressId 155 | // also using the connect method here to control the number of connections to the pool 156 | return context.psqlPool.connect() 157 | .then((client) => client.query(query, values) 158 | .then((data) => { 159 | client.release(); // release the pool client/connection 160 | return data.rows[0]; // actual return value 161 | }) 162 | .catch((err) => { 163 | // release the client regardless of error or not, or else the pool will "drain" 164 | client.release(); 165 | console.log('ERROR GETTING CUSTOMER\'S ADDRESS', err); 166 | })) 167 | .catch((err) => console.log('ERROR IN Customer Type Resolver', err)); 168 | }, 169 | 170 | cart: async (parent, args, { mongo: { CartModel } }) => { 171 | const customerId = parent.id; 172 | 173 | const queryResult = await CartModel.findOne({ customerId }, 174 | // await is used to block further execution and set the queryResult variable to the data 175 | // that is returned from the findOne query 176 | (err, data) => { 177 | if (err) return console.log('ERROR IN MONOG QUERY Customer Type Resolver: ', err); 178 | if (!data) { 179 | return console.log('cart does not exist'); 180 | } 181 | // return console.log('the found cart is', data); 182 | return data; // I'm pretty sure this is doing nothing... 183 | }); 184 | 185 | const defaultCart = { customerId: args.customerId, products: [], wishlist: [] }; 186 | 187 | return queryResult || defaultCart; 188 | }, 189 | }, 190 | }; 191 | -------------------------------------------------------------------------------- /server/customer/customerSchema.js: -------------------------------------------------------------------------------- 1 | const Base = require('../baseSchema'); 2 | const Address = require('../address/addressSchema'); 3 | const Cart = require('../cart/cartSchema'); 4 | 5 | // NOTE: # (hashtags) are used as comments within the graphql schema 6 | 7 | const Customer = ` 8 | # types are similar to classes or schemas, they tell GQL what types to expect at each variable 9 | # they should reflect the database schema/setup VERY closely (if not identically) 10 | 11 | # The Customer type reflects a row in the PSQL database except there is a special resolver that 12 | # grabs the address from the addresses table for that nested information 13 | type Customer { 14 | id: Int! 15 | firstName: String! 16 | lastName: String! 17 | email: String! 18 | phoneNumber: String! 19 | address: Address # a customer doesn't NEED to have an address, i.e. you can sign up for amazon before giving them your address... 20 | cart: Cart 21 | } 22 | 23 | extend type Query { 24 | # Find a single customer via their id (same as id in PSQL table) 25 | customer(id: Int!): Customer! 26 | 27 | # Return information of all customers (this isn't very practical or useful, but it was easy to make and test) 28 | customers: [Customer!]! 29 | } 30 | 31 | extend type Mutation { 32 | # mutation to add a new customer to the PSQL database 33 | addCustomer( 34 | firstName: String!, 35 | lastName: String!, 36 | email: String!, 37 | phoneNumber: String! 38 | ): Customer! 39 | 40 | # Deprecated mutation that adds a customer and address simultaneously 41 | addCustomerAndAddress( 42 | firstName: String!, 43 | lastName: String!, 44 | email: String!, 45 | phoneNumber: String!, 46 | 47 | # address details 48 | address: String!, 49 | address2: String, 50 | city: String, 51 | state: String!, 52 | zipCode: String!): Customer! @deprecated (reason: "Use \`addCustomer\` A user should be created without an address, and the address created separately.") 53 | 54 | # updates user information (name, email, phone no.) 55 | updateCustomer( 56 | id: Int!, 57 | firstName: String, 58 | lastName: String, 59 | email: String, 60 | phoneNumber: String 61 | ): Customer! 62 | 63 | # deletes customer AND associated address 64 | deleteCustomer( 65 | id: Int! # customer id 66 | ): Int! 67 | } 68 | `; 69 | 70 | // export a function that returns the array of all the schemas that are necessary to build 71 | // this one (and the base schema) 72 | module.exports = () => [Customer, Address, Cart, Base]; 73 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const graphQLHTTP = require('express-graphql'); 3 | 4 | const app = express(); 5 | const PORT = 3000; 6 | 7 | // import in the combined schema from schema.js 8 | const schema = require('./schema'); 9 | 10 | // import the pool connection to pass into context 11 | const psqlPool = require('../database/psql/dbConnection'); 12 | 13 | // import the mongo Models (they are on the export of dbConnection) 14 | const mongoConnectionAndModels = require('../database/mongo/dbConnection'); 15 | 16 | // async function to start the server (to await the mongo connection) 17 | const startServer = async () => { 18 | // this is asyncronous, so use await to avoid sending an unresolved promise to context in app.use 19 | 20 | // await the mongo connection to be passed into context only when it is connected 21 | const mongo = await mongoConnectionAndModels(); 22 | 23 | console.log('--before graphql endpoint code'); 24 | // setup the single graphql endpoint 25 | app.use('/graphql', 26 | graphQLHTTP({ 27 | schema, 28 | graphiql: true, 29 | // best practice is to pass & control d-base connections and current user sessions via context 30 | context: { 31 | psqlPool, 32 | mongo, 33 | }, 34 | })); 35 | 36 | console.log('-- before app.listen'); 37 | // changed from PORT to 3000 38 | app.listen(PORT, () => console.log(`Listening on PORT ${PORT}`)); 39 | }; 40 | 41 | // example that is used to slow down the startup of the server for demonstration purposes only 42 | // async function that delays how long until the server runs, for testing 43 | // async function slowDownStuff() { 44 | // return new Promise((resolve) => { 45 | // setTimeout(() => { resolve(); }, 5000); 46 | // }); 47 | // } 48 | // slowDownStuff().then(() => startServer()); 49 | 50 | 51 | // run the async function defined above to connect to mongo and run the server 52 | startServer(); 53 | -------------------------------------------------------------------------------- /server/order/orderResolver.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module: orderResolver.js 3 | * @author: Austin Ruby 4 | * @description: define resolvers for order data 5 | */ 6 | 7 | module.exports = { 8 | Query: { 9 | order: (parent, args, context) => { 10 | const query = 'SELECT * FROM "customerOrders" WHERE "orderId" = $1'; 11 | const values = [args.orderId]; 12 | 13 | return context.psqlPool.query(query, values) 14 | .then((data) => data.rows[0]) 15 | .catch((err) => console.log('ERROR LOOKING UP ORDER', err)); 16 | }, 17 | customerOrders: (parent, args, context) => { 18 | const query = 'SELECT * FROM "customerOrders" WHERE "customerId" = $1'; 19 | const values = [args.customerId]; 20 | 21 | return context.psqlPool.query(query, values) 22 | .then((data) => data.rows) 23 | .catch((err) => console.log('ERROR LOOKING UP ORDERS FOR ', err)); 24 | }, 25 | }, 26 | Mutation: { 27 | addOrder: async (parent, args, context) => { 28 | console.log('addOrder args are: ', args); 29 | // query to insert row in customerOrders table with customerId provided 30 | const customerOrderQuery = 'INSERT INTO "customerOrders" ("customerId") VALUES ($1) RETURNING *'; 31 | // query to add row/s to orderProducts table based on productIds, productQtys provided 32 | // and orderId created by customerOrderQuery 33 | const orderProductQuery = 'INSERT INTO "orderProducts" ("productId", "productQty", "orderId") VALUES ($1, $2, $3) RETURNING *'; 34 | // destructure customerId and products array from args 35 | const { customerId, products } = args; 36 | 37 | // declare client outside of try block to enable calling release() in catch block 38 | let client; 39 | // wrapping all async in one try block 40 | try { 41 | // assign client to awaited returned value of invoking connect on psqlPool 42 | client = await context.psqlPool.connect(); 43 | // await returned value of querying client to add customerOrder 44 | const customerOrderData = await client.query(customerOrderQuery, [customerId]); 45 | // destructure returned orderId off of customerOrderData 46 | const { orderId } = customerOrderData.rows[0]; 47 | // await resolution of all promises before continuing thread of execution 48 | await Promise.all( 49 | // using map to return array of promises 50 | products.map((product) => { 51 | // destructure productId and productQty off of product 52 | const { productId, productQty } = product; 53 | const values = [productId, productQty, orderId]; 54 | // insert into orderProducts for each product 55 | return client.query(orderProductQuery, values) 56 | // still using then here because it allows for more specific error messaging and isn't too nested 57 | .then((orderProductData) => { 58 | console.log(orderProductData.rows); 59 | }) 60 | .catch((err) => console.log(`ERROR CREATING ORDERPRODUCT FOR PRODUCT ID ${productId}: `, err)); 61 | }), 62 | ); 63 | // release client after all orderProduct queries resolve and return orderId 64 | client.release(); 65 | return orderId; 66 | } catch (err) { // handle errors in try block 67 | // if client is truthy, release it 68 | if (client) client.release(); 69 | // log error 70 | console.log('ERROR WITH ASYNC IN ADD ORDER TWO', err); 71 | } 72 | // return null to keep eslint happy 73 | return null; 74 | }, 75 | }, 76 | 77 | // type resolver for nested types: Customer and Products 78 | Order: { 79 | customer: (parent, args, context) => { 80 | const query = 'SELECT * FROM customers WHERE id = $1 LIMIT 1'; 81 | const values = [parent.customerId]; 82 | 83 | return context.psqlPool.query(query, values) 84 | .then((data) => data.rows[0]) 85 | .catch((err) => console.log('ERROR LOOKING UP CUSTOMER ON ORDER', err)); 86 | }, 87 | products: (parent, args, context) => { 88 | const query = ` 89 | SELECT products."productId", products.name, products.description, products.price, products.weight, "orderProducts"."productQty" FROM "customerOrders" 90 | JOIN "orderProducts" ON "customerOrders"."orderId" = "orderProducts"."orderId" 91 | JOIN products ON "orderProducts"."productId" = products."productId" 92 | WHERE "customerOrders"."orderId" = $1; 93 | `; 94 | const values = [parent.orderId]; 95 | 96 | return context.psqlPool.query(query, values) 97 | .then((data) => data.rows) 98 | .catch((err) => console.log('ERROR LOOKING UP PRODUCTS FOR ORDER', err)); 99 | }, 100 | }, 101 | }; 102 | -------------------------------------------------------------------------------- /server/order/orderSchema.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module : orderSchema.js 3 | * @author : Austin Ruby 4 | * @function : Define structure of orders and how the API will query for and mutate order data 5 | */ 6 | 7 | const Base = require('../baseSchema'); 8 | const Customer = require('../customer/customerSchema'); 9 | const Product = require('../product/productSchema'); 10 | 11 | const Order = ` 12 | # The Order type reflects a row in the PSQL database except there is a special resolver that grabs the customer from the customers table for that nested information 13 | type Order { 14 | orderId: Int! 15 | customer: Customer 16 | products: [Product!]! 17 | } 18 | 19 | extend type Query { 20 | # Find a single order via its id (corresponds to orderId in PSQL) 21 | order(orderId: Int!): Order! 22 | 23 | # Find all orders for a given customer by the customer's id 24 | customerOrders(customerId: Int!): [Order!]! 25 | } 26 | 27 | input OrderProduct { 28 | productId: Int! 29 | productQty: Int! 30 | } 31 | 32 | extend type Mutation { 33 | # Create an order with a customer id and product ids 34 | addOrder( 35 | customerId: Int! 36 | products: [OrderProduct!]! 37 | ): Int! 38 | } 39 | `; 40 | 41 | // return a thunk so hoisting can handle unordered schema imports in the schema.js file 42 | module.exports = () => [Order, Customer, Product, Base]; 43 | -------------------------------------------------------------------------------- /server/product/productResolver.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module: productResolver.js 3 | * @author: Austin Ruby 4 | * @description: define resolvers for product data 5 | */ 6 | 7 | module.exports = { 8 | Query: { 9 | // query for a single product from the database by productId 10 | product: (parent, args, context) => { 11 | // create the query and values arguments that are needed for the psql query 12 | const query = 'SELECT * FROM products WHERE "productId"=$1 LIMIT 1'; 13 | const values = [args.productId]; 14 | 15 | // query the database and return a promise (graphql handles promises by resolvnig them and 16 | // using the default resolver to match up key:value pairs as the final response) 17 | return context.psqlPool.query(query, values) 18 | .then((data) => data.rows[0]) 19 | .catch((err) => console.log('ERROR LOOKING UP PRODUCT', err)); 20 | }, 21 | // query for all products in the database 22 | products: (parent, args, context) => { 23 | const query = 'SELECT * FROM products'; 24 | 25 | return context.psqlPool.query(query) 26 | .then((data) => data.rows) 27 | .catch((err) => console.log('ERROR LOOKING UP PRODUCTS', err)); 28 | }, 29 | }, 30 | Mutation: { 31 | // mutation to add a product to the database 32 | addProduct: (parent, args, context) => { 33 | const query = 'INSERT INTO products (name, description, price, weight) VALUES ($1, $2, $3, $4) RETURNING *'; 34 | const values = [args.name, args.description, args.price, args.weight]; 35 | 36 | return context.psqlPool.connect() 37 | .then((client) => client.query(query, values) 38 | .then((data) => { 39 | client.release(); 40 | return data.rows[0]; 41 | }) 42 | .catch((err) => console.log('ERROR INSERTING PRODUCT', err))) 43 | .catch((err) => console.log('ERROR CONNECTING WHILE ADDING PRODUCT', err)); 44 | }, 45 | // mutation to update a product 46 | updateProduct: (parent, args, context) => { 47 | let query = 'UPDATE products SET '; 48 | const values = [args.productId]; 49 | let count = 2; 50 | 51 | Object.keys(args).forEach((el) => { 52 | if (el !== 'productId' && args[el]) { 53 | query += `"${el}"= $${count}, `; 54 | count += 1; 55 | values.push(args[el]); 56 | } 57 | }); 58 | 59 | query = query.slice(0, query.length - 2); 60 | query += ' WHERE "productId"=$1 RETURNING *'; 61 | 62 | return context.psqlPool.connect() 63 | .then((client) => client.query(query, values) 64 | .then((data) => { 65 | client.release(); 66 | return data.rows[0]; 67 | }) 68 | .catch((err) => console.log('ERROR UPDATING PRODUCT', err))) 69 | .catch((err) => console.log('ERROR CONNECTING WHILE UPDATING PRODUCT', err)); 70 | }, 71 | // mutation to delete a product 72 | deleteProduct: (parent, args, context) => { 73 | const query = 'DELETE FROM products WHERE "productId"=$1 RETURNING *'; 74 | const values = [args.productId]; 75 | 76 | return context.psqlPool.connect() 77 | .then((client) => client.query(query, values) 78 | .then((data) => { 79 | client.release(); 80 | return data.rows[0]; 81 | }) 82 | .catch((err) => console.log('ERROR DELETING PRODUCT', err))) 83 | .catch((err) => console.log('ERROR CONNECTING WHILE DELETING PRODUCT', err)); 84 | }, 85 | }, 86 | }; 87 | -------------------------------------------------------------------------------- /server/product/productSchema.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module: productSchema.js 3 | * @author: Austin Ruby 4 | * @description: define schema for product data 5 | */ 6 | 7 | const Base = require('../baseSchema'); 8 | 9 | const Product = ` 10 | # Product describes details about a product 11 | type Product { 12 | productId: Int! 13 | name: String! 14 | description: String! 15 | price: Float! 16 | weight: Float! 17 | productQty: Int 18 | } 19 | 20 | extend type Query { 21 | # Find a single product via its id (same as in PSQL table) 22 | product(productId: Int!): Product! 23 | 24 | # Return information on all products 25 | products: [Product!]! 26 | } 27 | 28 | extend type Mutation { 29 | # mutation to add a new product to the PSQL database 30 | addProduct( 31 | name: String! 32 | description: String! 33 | price: Float! 34 | weight: Float! 35 | ): Product! 36 | 37 | # mutation to update product information 38 | updateProduct( 39 | productId: Int! 40 | name: String 41 | description: String 42 | price: Float 43 | weight: Float 44 | ): Product! 45 | 46 | # mutation to delete a product 47 | deleteProduct( 48 | productId: Int! 49 | ): Product! 50 | } 51 | `; 52 | 53 | // return a thunk so hoisting can handle unordered schema imports in the schema.js file 54 | module.exports = () => [Product, Base]; 55 | -------------------------------------------------------------------------------- /server/resolvers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file's purpose is to combine all of the resolvers, allowing for modular development 3 | * The result will be exported to the schema.js file to ultimately feed makeExecutableSchema 4 | */ 5 | 6 | // resolvers for the Customer type 7 | const { 8 | // this syntax (I think) is destructuring the Query key of the exports from customerResolver, 9 | // as CustomerQuery 10 | Query: CustomerQuery, 11 | Mutation: CustomerMutation, 12 | // Customer is a type resolver is one of the "key" GraphQL benefits, creating a link from 13 | // customer to address (and in more complex scenarios, linking it to its graph connections) 14 | Customer, 15 | } = require('./customer/customerResolver'); 16 | 17 | // resolvers for the Address type 18 | const { 19 | Query: AddressQuery, 20 | Mutation: AddressMutation, 21 | } = require('./address/addressResolver'); 22 | 23 | // resolvers for the Warehouse type 24 | const { 25 | Query: WarehouseQuery, 26 | Mutation: WarehouseMutation, 27 | Warehouse, 28 | } = require('./warehouse/warehouseResolver'); 29 | 30 | // resolvers for the Order type 31 | const { 32 | Query: OrderQuery, 33 | Mutation: OrderMutation, 34 | Order, 35 | } = require('./order/orderResolver'); 36 | 37 | // resolvers for the Product type 38 | const { 39 | Query: ProductQuery, 40 | Mutation: ProductMutation, 41 | } = require('./product/productResolver'); 42 | 43 | // resolvers for the Cart type 44 | const { 45 | Query: CartQuery, 46 | Mutation: CartMutation, 47 | } = require('./cart/cartResolver'); 48 | 49 | // export all of those combined (Query and Mutation are standard & built into GraphQL, 50 | // Customer, Order and Warehouse are type resolvers) 51 | module.exports = { 52 | Query: { 53 | ...CustomerQuery, 54 | ...AddressQuery, 55 | ...WarehouseQuery, 56 | ...OrderQuery, 57 | ...ProductQuery, 58 | ...CartQuery, 59 | }, 60 | Mutation: { 61 | ...CustomerMutation, 62 | ...AddressMutation, 63 | ...WarehouseMutation, 64 | ...OrderMutation, 65 | ...ProductMutation, 66 | ...CartMutation, 67 | }, 68 | Customer, 69 | Order, 70 | Warehouse, 71 | }; 72 | -------------------------------------------------------------------------------- /server/schema.js: -------------------------------------------------------------------------------- 1 | const { makeExecutableSchema } = require('graphql-tools'); 2 | 3 | // Base Schema 4 | const Base = require('./baseSchema'); 5 | 6 | // Schemas with information in Postgres 7 | const Address = require('./address/addressSchema'); 8 | const Customer = require('./customer/customerSchema'); 9 | const Order = require('./order/orderSchema'); 10 | const Product = require('./product/productSchema'); 11 | const Warehouse = require('./warehouse/warehouseSchema'); 12 | 13 | // Schema with information in Mongo 14 | const Cart = require('./cart/cartSchema'); 15 | 16 | // combined resolvers 17 | const resolvers = require('./resolvers'); 18 | 19 | // The makeExecutableSchema in schema.js can handle duplicate schemas and will combine them 20 | // I invoke the functions here to get an iterable (array) to spread into the typeDefs array 21 | // There is a way to do this with just functions (which is what is exported). 22 | module.exports = makeExecutableSchema({ 23 | typeDefs: [ 24 | ...Base(), ...Address(), ...Customer(), ...Order(), 25 | ...Product(), ...Warehouse(), ...Cart(), 26 | ], 27 | resolvers, 28 | }); 29 | -------------------------------------------------------------------------------- /server/warehouse/warehouseResolver.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module: warehouseResolver.js 3 | * @author: Austin Ruby 4 | * @description: define resolvers for warehouse data 5 | */ 6 | 7 | module.exports = { 8 | Query: { 9 | // query to look up data on an individual warehouse 10 | warehouse: (parent, args, context) => { 11 | const query = 'SELECT * FROM warehouses WHERE "warehouseId" = $1 LIMIT 1'; 12 | const values = [args.warehouseId]; 13 | return context.psqlPool.query(query, values) 14 | .then((data) => data.rows[0]) 15 | .catch((err) => console.log('ERROR LOOKING UP WAREHOUSE', err)); 16 | }, 17 | // query to return data on all warehouses 18 | warehouses: (parent, args, context) => { 19 | const query = 'SELECT * FROM warehouses'; 20 | return context.psqlPool.query(query) 21 | .then((data) => data.rows) 22 | .catch((err) => console.log('ERROR LOOKING UP WAREHOUSES', err)); 23 | }, 24 | }, 25 | Mutation: { 26 | // mutation to add a warehouse to the database 27 | addWarehouse: (parent, args, context) => { 28 | const query = 'INSERT INTO warehouses (name, "addressId") VALUES ($1, $2) RETURNING *'; 29 | const values = [args.name, args.addressId]; 30 | 31 | return context.psqlPool.connect() 32 | .then((client) => client.query(query, values) 33 | .then((data) => { 34 | client.release(); 35 | return data.rows[0]; 36 | }) 37 | .catch((err) => console.log('ERROR INSERTING WAREHOUSE', err))) 38 | .catch((err) => console.log('ERROR CONNECTING WHILE ADDING WAREHOUSE', err)); 39 | }, 40 | // mutation to update details of a warehouse 41 | updateWarehouse: (parent, args, context) => { 42 | let query = 'UPDATE warehouses SET '; 43 | const values = [args.warehouseId]; 44 | let count = 2; 45 | 46 | Object.keys(args).forEach((el) => { 47 | if (el !== 'warehouseId' && args[el]) { 48 | query += `"${el}"= $${count}, `; 49 | count += 1; 50 | values.push(args[el]); 51 | } 52 | }); 53 | 54 | query = query.slice(0, query.length - 2); 55 | query += ' WHERE "warehouseId"=$1 RETURNING *'; 56 | 57 | return context.psqlPool.connect() 58 | .then((client) => client.query(query, values) 59 | .then((data) => { 60 | client.release(); 61 | return data.rows[0]; 62 | }) 63 | .catch((err) => console.log('ERROR UPDATING WAREHOUSE', err))) 64 | .catch((err) => console.log('ERROR CONNECTING WHILE UPDATING WAREHOUSE', err)); 65 | }, 66 | // mutation to delete a warehouse from the database 67 | deleteWarehouse: (parent, args, context) => { 68 | const query = 'DELETE FROM warehouses WHERE "warehouseId"=$1 RETURNING *'; 69 | const values = [args.warehouseId]; 70 | 71 | return context.psqlPool.connect() 72 | .then((client) => client.query(query, values) 73 | .then((data) => { 74 | client.release(); 75 | return data.rows[0]; 76 | }) 77 | .catch((err) => console.log('ERROR DELETING WAREHOUSE', err))) 78 | .catch((err) => console.log('ERROR CONNECTING WHILE DELETING WAREHOUSE', err)); 79 | }, 80 | }, 81 | 82 | // type resolver for nested type: Address 83 | Warehouse: { 84 | address: (parent, args, context) => { 85 | const query = 'SELECT * FROM addresses WHERE id=$1 LIMIT 1'; 86 | const values = [parent.addressId]; 87 | 88 | return context.psqlPool.connect() 89 | .then((client) => client.query(query, values) 90 | .then((data) => { 91 | client.release(); 92 | return data.rows[0]; 93 | }) 94 | .catch((err) => { 95 | client.release(); 96 | console.log('ERROR GETTING CUSTOMER\'S ADDRESS', err); 97 | })) 98 | .catch((err) => console.log('ERROR IN WAREHOUSE TYPE RESOLVER', err)); 99 | }, 100 | }, 101 | }; 102 | -------------------------------------------------------------------------------- /server/warehouse/warehouseSchema.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module: warehouseSchema.js 3 | * @author: Austin Ruby 4 | * @description: define schema for warehouse data 5 | */ 6 | 7 | // require in Base schema so we can add types, and 8 | // so we can extend the Query and Mutation schemas 9 | const Base = require('../baseSchema'); 10 | const Address = require('../address/addressSchema'); 11 | 12 | // actually defining warehouse schema 13 | const Warehouse = ` 14 | # Warehouse describes details about a warehouse 15 | type Warehouse { 16 | warehouseId: Int! 17 | name: String! 18 | address: Address # warehouses shouldn't exist without addresses. TODO: FIX THIS 19 | } 20 | 21 | extend type Query { 22 | # Find a single warehouse via its id (same id as in PSQL table) 23 | warehouse(warehouseId: Int!): Warehouse! 24 | 25 | # Return information on all warehouses 26 | warehouses: [Warehouse!]! 27 | } 28 | 29 | extend type Mutation { 30 | # mutation to add a new warehouse to the PSQL database 31 | addWarehouse( 32 | name: String! 33 | addressId: Int! 34 | ): Warehouse! 35 | 36 | # updates warehouse information (name or address_id) 37 | updateWarehouse( 38 | warehouseId: Int!, 39 | name: String, 40 | id: Int 41 | ): Warehouse! 42 | 43 | # deletes warehouse (and address???) 44 | deleteWarehouse( 45 | warehouseId: Int! 46 | ): Warehouse! 47 | } 48 | `; 49 | 50 | // export function that returns array of all schemas necessary to build this one, including base 51 | module.exports = () => [Warehouse, Address, Base]; 52 | --------------------------------------------------------------------------------