Skip to content

Commit b8c99cf

Browse files
authored
Merge pull request #11892 from Automattic/netlify-functions-example
Netlify functions example
2 parents 92cb6fb + 2751883 commit b8c99cf

26 files changed

+728
-1
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ test/files/main.js
4141

4242
package-lock.json
4343

44-
.config*
44+
.config.js
4545

4646
# Compiled docs
4747
docs/*.html
@@ -50,6 +50,9 @@ docs/typescript/*.html
5050
docs/api/*.html
5151
index.html
5252

53+
# Local Netlify folder
54+
.netlify
55+
5356
# yarn package-lock
5457
yarn.lock
5558

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
'use strict';
2+
3+
module.exports = Object.freeze({
4+
mongodbUri: 'mongodb://localhost:27017/ecommerce',
5+
stripeSecretKey: 'YOUR STRIPE KEY HERE',
6+
success_url: 'localhost:3000/success',
7+
cancel_url: 'localhost:3000/cancel'
8+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use strict';
2+
3+
if (process.env.NODE_ENV) {
4+
try {
5+
module.exports = require('./' + process.env.NODE_ENV);
6+
console.log('Using ' + process.env.NODE_ENV);
7+
} catch (err) {
8+
module.exports = require('./development');
9+
}
10+
} else {
11+
console.log('using production');
12+
module.exports = require('./production');
13+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
'use strict';
2+
3+
module.exports = Object.freeze({
4+
mongodbUri: 'mongodb://localhost:27017/ecommerce_test',
5+
stripeSecretKey: 'test',
6+
success_url: 'localhost:3000/success',
7+
cancel_url: 'localhost:3000/cancel'
8+
9+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"imports":{"netlify:edge":"https://edge-bootstrap.netlify.app/v1/index.ts"}}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# ecommerce-netlify-functions
2+
3+
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).
4+
5+
Other tools include:
6+
7+
1. Stripe for payment processing
8+
2. [Mocha](https://masteringjs.io/mocha) and [Sinon](https://masteringjs.io/sinon) for testing
9+
10+
## Running This Example
11+
12+
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.
13+
2. Run `npm install`
14+
3. Run `npm run seed`
15+
4. Run `npm start`
16+
5. Visit `http://localhost:8888/.netlify/functions/getProducts` to list all available products
17+
6. Run other endpoints using curl or postman
18+
19+
## Testing
20+
21+
Make sure you have a MongoDB instance running on `localhost:27017`, or update `mongodbUri` in `.config/test.js` to your MongoDB server's address.
22+
Then run `npm test`.
23+
24+
```
25+
$ npm test
26+
27+
> test
28+
> env NODE_ENV=test mocha ./test/*.test.js
29+
30+
Using test
31+
32+
33+
Add to Cart
34+
✔ Should create a cart and add a product to the cart
35+
✔ Should find the cart and add to the cart
36+
✔ Should find the cart and increase the quantity of the item(s) in the cart
37+
38+
Checkout
39+
✔ Should do a successful checkout run
40+
41+
Get the cart given an id
42+
✔ Should create a cart and then find the cart.
43+
44+
Products
45+
✔ Should get all products.
46+
47+
Remove From Cart
48+
✔ Should create a cart and then it should remove the entire item from it.
49+
✔ Should create a cart and then it should reduce the quantity of an item from it.
50+
51+
52+
8 passing (112ms)
53+
54+
```
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict';
2+
3+
const config = require('./.config');
4+
const mongoose = require('mongoose');
5+
6+
let conn = null;
7+
8+
module.exports = async function connect() {
9+
if (conn != null) {
10+
return conn;
11+
}
12+
conn = mongoose.connection;
13+
await mongoose.connect(config.mongodbUri);
14+
return conn;
15+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'use strict';
2+
3+
const config = require('../.config')
4+
5+
module.exports = require('stripe')(config.stripeSecretKey);
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
'use strict';
2+
const mongoose = require('mongoose');
3+
4+
const productSchema = new mongoose.Schema({
5+
name: String,
6+
price: Number,
7+
image: String
8+
});
9+
10+
const Product = mongoose.model('Product', productSchema);
11+
12+
module.exports.Product = Product;
13+
14+
const orderSchema = new mongoose.Schema({
15+
items: [
16+
{ productId: { type: mongoose.ObjectId, required: true, ref: 'Product' },
17+
quantity: { type: Number, required: true, validate: v => v > 0 }
18+
}
19+
],
20+
total: {
21+
type: Number,
22+
default: 0
23+
},
24+
status: {
25+
type: String,
26+
enum: ['PAID', 'IN_PROGRESS', 'SHIPPED', 'DELIVERED'],
27+
default: 'PAID'
28+
},
29+
orderNumber: {
30+
type: Number,
31+
required: true
32+
},
33+
name: {
34+
type: String,
35+
required: true
36+
},
37+
email: {
38+
type: String,
39+
required: true
40+
},
41+
address1: {
42+
type: String,
43+
required: true
44+
},
45+
address2: {
46+
type: String
47+
},
48+
city: {
49+
type: String,
50+
required: true
51+
},
52+
state: {
53+
type: String,
54+
required: true
55+
},
56+
zip: {
57+
type: String,
58+
required: true
59+
},
60+
shipping: {
61+
type: String,
62+
required: true,
63+
enum: ['standard', '2day']
64+
},
65+
paymentMethod: {
66+
id: String,
67+
brand: String,
68+
last4: String
69+
}
70+
}, { optimisticConcurrency: true });
71+
72+
const Order = mongoose.model('Order', orderSchema);
73+
74+
module.exports.Order = Order;
75+
76+
const cartSchema = new mongoose.Schema({
77+
items: [{ productId: { type: mongoose.ObjectId, required: true, ref: 'Product' }, quantity: { type: Number, required: true } }],
78+
orderId: { type: mongoose.ObjectId, ref: 'Order' }
79+
}, { timestamps: true });
80+
81+
const Cart = mongoose.model('Cart', cartSchema);
82+
83+
module.exports.Cart = Cart;
84+
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use strict';
2+
3+
const { Cart, Product } = require('../../models');
4+
const connect = require('../../connect');
5+
6+
const handler = async(event) => {
7+
try {
8+
event.body = JSON.parse(event.body || {});
9+
await connect();
10+
const products = await Product.find();
11+
if (event.body.cartId) {
12+
// get the document containing the specified cartId
13+
const cart = await Cart.findOne({ _id: event.body.cartId }).setOptions({ sanitizeFilter: true });
14+
15+
if (cart == null) {
16+
return { statusCode: 404, body: JSON.stringify({ message: 'Cart not found' }) };
17+
}
18+
if(!Array.isArray(event.body.items)) {
19+
return { statusCode: 500, body: JSON.stringify({ error: 'items is not an array' }) };
20+
}
21+
for (const product of event.body.items) {
22+
const exists = cart.items.find(item => item?.productId?.toString() === product?.productId?.toString());
23+
if (!exists && products.find(p => product?.productId?.toString() === p?._id?.toString())) {
24+
cart.items.push(product);
25+
await cart.save();
26+
} else {
27+
exists.quantity += product.quantity;
28+
await cart.save();
29+
}
30+
}
31+
32+
if (!cart.items.length) {
33+
return { statusCode: 200, body: JSON.stringify({ cart: null }) };
34+
}
35+
36+
await cart.save();
37+
return { statusCode: 200, body: JSON.stringify(cart) };
38+
} else {
39+
// If no cartId, create a new cart
40+
const cart = await Cart.create({ items: event.body.items });
41+
return { statusCode: 200, body: JSON.stringify(cart) };
42+
}
43+
} catch (error) {
44+
return { statusCode: 500, body: error.toString() };
45+
}
46+
};
47+
48+
module.exports = { handler };
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
'use strict';
2+
3+
const stripe = require('../../integrations/stripe')
4+
const config = require('../../.config');
5+
const { Cart, Order, Product } = require('../../models');
6+
const connect = require('../../connect');
7+
8+
const handler = async(event) => {
9+
try {
10+
event.body = JSON.parse(event.body || {});
11+
await connect();
12+
const cart = await Cart.findOne({ _id: event.body.cartId });
13+
14+
const stripeProducts = { line_items: [] };
15+
let total = 0;
16+
for (let i = 0; i < cart.items.length; i++) {
17+
const product = await Product.findOne({ _id: cart.items[i].productId });
18+
stripeProducts.line_items.push({
19+
price_data: {
20+
currency: 'usd',
21+
product_data: {
22+
name: product.name
23+
},
24+
unit_amount: product.price
25+
},
26+
quantity: cart.items[i].quantity
27+
});
28+
total = total + (product.price * cart.items[i].quantity);
29+
}
30+
const session = await stripe.checkout.sessions.create({
31+
line_items: stripeProducts.line_items,
32+
mode: 'payment',
33+
success_url: config.success_url,
34+
cancel_url: config.cancel_url
35+
});
36+
const intent = await stripe.paymentIntents.retrieve(session.payment_intent);
37+
if (intent.status !== 'succeeded') {
38+
throw new Error(`Checkout failed because intent has status "${intent.status}"`);
39+
}
40+
const paymentMethod = await stripe.paymentMethods.retrieve(intent['payment_method']);
41+
const orders = await Order.find();
42+
const orderNumber = orders.length ? orders.length + 1 : 1;
43+
const order = await Order.create({
44+
items: event.body.product,
45+
total: total,
46+
orderNumber: orderNumber,
47+
name: event.body.name,
48+
email: event.body.email,
49+
address1: event.body.address1,
50+
city: event.body.city,
51+
state: event.body.state,
52+
zip: event.body.zip,
53+
shipping: event.body.shipping,
54+
paymentMethod: paymentMethod ? { id: paymentMethod.id, brand: paymentMethod.brand, last4: paymentMethod.last4 } : null
55+
});
56+
57+
cart.orderId = order._id;
58+
await cart.save();
59+
return {
60+
statusCode: 200,
61+
body: JSON.stringify({ order: order, cart: cart }),
62+
headers: { Location: session.url }
63+
};
64+
} catch (error) {
65+
return { statusCode: 500, body: error.toString() };
66+
}
67+
};
68+
69+
module.exports = { handler };
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use strict';
2+
3+
const { Cart } = require('../../models');
4+
const connect = require('../../connect');
5+
6+
const handler = async(event) => {
7+
try {
8+
await connect();
9+
// get the document containing the specified cartId
10+
const cart = await Cart.
11+
findOne({ _id: event.queryStringParameters.cartId }).
12+
setOptions({ sanitizeFilter: true });
13+
return { statusCode: 200, body: JSON.stringify({ cart }) };
14+
} catch (error) {
15+
return { statusCode: 500, body: error.toString() };
16+
}
17+
};
18+
19+
module.exports = { handler };
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use strict';
2+
3+
const { Product } = require('../../models');
4+
const connect = require('../../connect');
5+
6+
const handler = async(event) => {
7+
try {
8+
await connect();
9+
const products = await Product.find();
10+
return { statusCode: 200, body: JSON.stringify(products) };
11+
} catch (error) {
12+
return { statusCode: 500, body: error.toString() };
13+
}
14+
};
15+
16+
module.exports = { handler };

0 commit comments

Comments
 (0)