├── .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 |
--------------------------------------------------------------------------------