Skip to content

Commit

Permalink
Merge pull request #11892 from Automattic/netlify-functions-example
Browse files Browse the repository at this point in the history
Netlify functions example
  • Loading branch information
vkarpov15 authored Jul 20, 2022
2 parents 92cb6fb + 2751883 commit b8c99cf
Show file tree
Hide file tree
Showing 26 changed files with 728 additions and 1 deletion.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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',
cancel_url: 'localhost:3000/cancel'
});
13 changes: 13 additions & 0 deletions examples/ecommerce-netlify-functions/.config/index.js
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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'

});
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
'use strict';
const mongoose = require('mongoose');

const productSchema = new mongoose.Schema({
name: String,
price: Number,
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;

Original file line number Diff line number Diff line change
@@ -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) {
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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 };
Original file line number Diff line number Diff line change
@@ -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 };
Loading

0 comments on commit b8c99cf

Please sign in to comment.