Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Netlify functions example #11892

Merged
merged 26 commits into from Jul 20, 2022
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
fa7f196
without stripe
IslandRhythms Jun 3, 2022
200668b
not entirely sure how to redirect
IslandRhythms Jun 3, 2022
92787e8
Update checkout.js
IslandRhythms Jun 3, 2022
c9baa82
All thats left is checkout
IslandRhythms Jun 6, 2022
e865463
not done yet
IslandRhythms Jun 7, 2022
062ca30
added checkout test
IslandRhythms Jun 13, 2022
d983d33
created fixtures
IslandRhythms Jul 1, 2022
ad97863
fix addToCart test
IslandRhythms Jul 1, 2022
e852249
fix getCart test
IslandRhythms Jul 1, 2022
a85e5a5
fix getProducts test and remove test data on test completion
IslandRhythms Jul 1, 2022
9a8fad2
fix removeFromCart and removeFromCart test
IslandRhythms Jul 1, 2022
e89b2f9
almost done
IslandRhythms Jul 5, 2022
0d2d299
sinon not doing sinon things
IslandRhythms Jul 5, 2022
3c87f27
not getting sinon error messages
IslandRhythms Jul 5, 2022
6dec0c5
thanks val
IslandRhythms Jul 5, 2022
b693176
Update index.js
IslandRhythms Jul 5, 2022
dfb75b4
add
IslandRhythms Jul 6, 2022
115f9fa
need to fix tests
IslandRhythms Jul 6, 2022
41712f1
tests fixed
IslandRhythms Jul 6, 2022
28b5557
added dummy product script
IslandRhythms Jul 6, 2022
42f40d5
made tests work with live build
IslandRhythms Jul 7, 2022
118c97a
Merge branch 'master' into netlify-functions-example
vkarpov15 Jul 16, 2022
5301deb
fix: cleanup and various updates
vkarpov15 Jul 17, 2022
2e6b064
made requested changes
IslandRhythms Jul 19, 2022
eced2c7
Merge branch 'master' into netlify-functions-example
vkarpov15 Jul 20, 2022
2751883
fix tests
vkarpov15 Jul 20, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Expand Up @@ -41,7 +41,7 @@ test/files/main.js

package-lock.json

.config*
.config.js

# Compiled docs
docs/*.html
Expand All @@ -50,6 +50,9 @@ docs/typescript/*.html
docs/api/*.html
index.html

# Local Netlify folder
.netlify

# yarn package-lock
yarn.lock

Expand Down
8 changes: 8 additions & 0 deletions examples/ecommerce-netlify-functions/.config/development.js
@@ -0,0 +1,8 @@
'use strict';

module.exports = Object.freeze({
mongodbUri: 'mongodb://localhost:27017/ecommerce',
stripeSecretKey: 'YOUR STRIPE KEY HERE',
success_url: 'localhost:3000/success',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👎 camelCase, no snake_case

cancel_url: 'localhost:3000/cancel'
});
13 changes: 13 additions & 0 deletions examples/ecommerce-netlify-functions/.config/index.js
@@ -0,0 +1,13 @@
'use strict';

if (process.env.NODE_ENV) {
try {
module.exports = require('./' + process.env.NODE_ENV);
console.log('Using ' + process.env.NODE_ENV);
} catch (err) {
module.exports = require('./development');
}
} else {
console.log('using production');
module.exports = require('./production');
}
9 changes: 9 additions & 0 deletions examples/ecommerce-netlify-functions/.config/test.js
@@ -0,0 +1,9 @@
'use strict';

module.exports = Object.freeze({
mongodbUri: 'mongodb://localhost:27017/ecommerce_test',
stripeSecretKey: 'test',
success_url: 'localhost:3000/success',
cancel_url: 'localhost:3000/cancel'

});
@@ -0,0 +1 @@
{"imports":{"netlify:edge":"https://edge-bootstrap.netlify.app/v1/index.ts"}}
54 changes: 54 additions & 0 deletions examples/ecommerce-netlify-functions/README.md
@@ -0,0 +1,54 @@
# ecommerce-netlify-functions

This sample demonstrates using Mongoose to build an eCommerce shopping cart using [Netlify Functions](https://www.netlify.com/products/functions/), which runs on [AWS Lambda](https://mongoosejs.com/docs/lambda.html).

Other tools include:

1. Stripe for payment processing
2. [Mocha](https://masteringjs.io/mocha) and [Sinon](https://masteringjs.io/sinon) for testing

## Running This Example

1. Make sure you have a MongoDB instance running on `localhost:27017`, or update `mongodbUri` in `.config/development.js` to your MongoDB server's address.
2. Run `npm install`
3. Run `npm run seed`
4. Run `npm start`
5. Visit `http://localhost:8888/.netlify/functions/getProducts` to list all available products
6. Run other endpoints using curl or postman

## Testing

Make sure you have a MongoDB instance running on `localhost:27017`, or update `mongodbUri` in `.config/test.js` to your MongoDB server's address.
Then run `npm test`.

```
$ npm test

> test
> env NODE_ENV=test mocha ./test/*.test.js

Using test


Add to Cart
✔ Should create a cart and add a product to the cart
✔ Should find the cart and add to the cart
✔ Should find the cart and increase the quantity of the item(s) in the cart

Checkout
✔ Should do a successful checkout run

Get the cart given an id
✔ Should create a cart and then find the cart.

Products
✔ Should get all products.

Remove From Cart
✔ Should create a cart and then it should remove the entire item from it.
✔ Should create a cart and then it should reduce the quantity of an item from it.


8 passing (112ms)

```
15 changes: 15 additions & 0 deletions examples/ecommerce-netlify-functions/connect.js
@@ -0,0 +1,15 @@
'use strict';

const config = require('./.config');
const mongoose = require('mongoose');

let conn = null;

module.exports = async function connect() {
if (conn != null) {
return conn;
}
conn = mongoose.connection;
await mongoose.connect(config.mongodbUri);
return conn;
}
5 changes: 5 additions & 0 deletions examples/ecommerce-netlify-functions/integrations/stripe.js
@@ -0,0 +1,5 @@
'use strict';

const config = require('../.config')

module.exports = require('stripe')(config.stripeSecretKey);
84 changes: 84 additions & 0 deletions examples/ecommerce-netlify-functions/models.js
@@ -0,0 +1,84 @@
'use strict';
const mongoose = require('mongoose');

const productSchema = new mongoose.Schema({
name: String,
price: Number,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't update tests to reflect this change

image: String
});

const Product = mongoose.model('Product', productSchema);

module.exports.Product = Product;

const orderSchema = new mongoose.Schema({
items: [
{ productId: { type: mongoose.ObjectId, required: true, ref: 'Product' },
quantity: { type: Number, required: true, validate: v => v > 0 }
}
],
total: {
type: Number,
default: 0
},
status: {
type: String,
enum: ['PAID', 'IN_PROGRESS', 'SHIPPED', 'DELIVERED'],
default: 'PAID'
},
orderNumber: {
type: Number,
required: true
},
name: {
type: String,
required: true
},
email: {
type: String,
required: true
},
address1: {
type: String,
required: true
},
address2: {
type: String
},
city: {
type: String,
required: true
},
state: {
type: String,
required: true
},
zip: {
type: String,
required: true
},
shipping: {
type: String,
required: true,
enum: ['standard', '2day']
},
paymentMethod: {
id: String,
brand: String,
last4: String
}
}, { optimisticConcurrency: true });

const Order = mongoose.model('Order', orderSchema);

module.exports.Order = Order;

const cartSchema = new mongoose.Schema({
items: [{ productId: { type: mongoose.ObjectId, required: true, ref: 'Product' }, quantity: { type: Number, required: true } }],
orderId: { type: mongoose.ObjectId, ref: 'Order' }
}, { timestamps: true });

const Cart = mongoose.model('Cart', cartSchema);

module.exports.Cart = Cart;

@@ -0,0 +1,48 @@
'use strict';

const { Cart, Product } = require('../../models');
const connect = require('../../connect');

const handler = async(event) => {
try {
event.body = JSON.parse(event.body || {});
await connect();
const products = await Product.find();
if (event.body.cartId) {
// get the document containing the specified cartId
const cart = await Cart.findOne({ _id: event.body.cartId }).setOptions({ sanitizeFilter: true });

if (cart == null) {
return { statusCode: 404, body: JSON.stringify({ message: 'Cart not found' }) };
}
if(!Array.isArray(event.body.items)) {
return { statusCode: 500, body: JSON.stringify({ error: 'items is not an array' }) };
}
for (const product of event.body.items) {
IslandRhythms marked this conversation as resolved.
Show resolved Hide resolved
const exists = cart.items.find(item => item?.productId?.toString() === product?.productId?.toString());
if (!exists && products.find(p => product?.productId?.toString() === p?._id?.toString())) {
cart.items.push(product);
await cart.save();
} else {
exists.quantity += product.quantity;
await cart.save();
}
}

if (!cart.items.length) {
return { statusCode: 200, body: JSON.stringify({ cart: null }) };
}

await cart.save();
return { statusCode: 200, body: JSON.stringify(cart) };
} else {
// If no cartId, create a new cart
const cart = await Cart.create({ items: event.body.items });
return { statusCode: 200, body: JSON.stringify(cart) };
}
} catch (error) {
return { statusCode: 500, body: error.toString() };
}
};

module.exports = { handler };
69 changes: 69 additions & 0 deletions examples/ecommerce-netlify-functions/netlify/functions/checkout.js
@@ -0,0 +1,69 @@
'use strict';

const stripe = require('../../integrations/stripe')
const config = require('../../.config');
const { Cart, Order, Product } = require('../../models');
const connect = require('../../connect');

const handler = async(event) => {
try {
event.body = JSON.parse(event.body || {});
await connect();
const cart = await Cart.findOne({ _id: event.body.cartId });

const stripeProducts = { line_items: [] };
let total = 0;
for (let i = 0; i < cart.items.length; i++) {
const product = await Product.findOne({ _id: cart.items[i].productId });
stripeProducts.line_items.push({
price_data: {
currency: 'usd',
product_data: {
name: product.name
},
unit_amount: product.price
},
quantity: cart.items[i].quantity
});
total = total + (product.price * cart.items[i].quantity);
}
const session = await stripe.checkout.sessions.create({
line_items: stripeProducts.line_items,
mode: 'payment',
success_url: config.success_url,
cancel_url: config.cancel_url
});
const intent = await stripe.paymentIntents.retrieve(session.payment_intent);
if (intent.status !== 'succeeded') {
throw new Error(`Checkout failed because intent has status "${intent.status}"`);
}
const paymentMethod = await stripe.paymentMethods.retrieve(intent['payment_method']);
const orders = await Order.find();
const orderNumber = orders.length ? orders.length + 1 : 1;
const order = await Order.create({
items: event.body.product,
total: total,
orderNumber: orderNumber,
name: event.body.name,
email: event.body.email,
address1: event.body.address1,
city: event.body.city,
state: event.body.state,
zip: event.body.zip,
shipping: event.body.shipping,
paymentMethod: paymentMethod ? { id: paymentMethod.id, brand: paymentMethod.brand, last4: paymentMethod.last4 } : null
});

cart.orderId = order._id;
await cart.save();
return {
statusCode: 200,
body: JSON.stringify({ order: order, cart: cart }),
headers: { Location: session.url }
};
} catch (error) {
return { statusCode: 500, body: error.toString() };
}
};

module.exports = { handler };
19 changes: 19 additions & 0 deletions examples/ecommerce-netlify-functions/netlify/functions/getCart.js
@@ -0,0 +1,19 @@
'use strict';

const { Cart } = require('../../models');
const connect = require('../../connect');

const handler = async(event) => {
try {
await connect();
// get the document containing the specified cartId
const cart = await Cart.
findOne({ _id: event.queryStringParameters.cartId }).
setOptions({ sanitizeFilter: true });
return { statusCode: 200, body: JSON.stringify({ cart }) };
} catch (error) {
return { statusCode: 500, body: error.toString() };
}
};

module.exports = { handler };
@@ -0,0 +1,16 @@
'use strict';

const { Product } = require('../../models');
const connect = require('../../connect');

const handler = async(event) => {
try {
await connect();
const products = await Product.find();
return { statusCode: 200, body: JSON.stringify(products) };
} catch (error) {
return { statusCode: 500, body: error.toString() };
}
};

module.exports = { handler };