├── README.md
├── auth_providers
├── anon-user.json
├── api-key.json
└── local-userpass.json
├── fts.json
├── functions
├── addReview
│ ├── config.json
│ └── source.js
├── backInStock
│ ├── config.json
│ └── source.js
├── cancelOrder
│ ├── config.json
│ └── source.js
├── emailStockNotification
│ ├── config.json
│ └── source.js
├── getCountriesList
│ ├── config.json
│ └── source.js
├── outOfStock
│ ├── config.json
│ └── source.js
├── placeOrder
│ ├── config.json
│ └── source.js
├── productInStock
│ ├── config.json
│ └── source.js
├── stripeCreateCheckoutSession
│ ├── config.json
│ └── source.js
├── textStockNotification
│ ├── config.json
│ └── source.js
└── updateStockLevel
│ ├── config.json
│ └── source.js
├── services
├── AWS-personal
│ ├── config.json
│ └── rules
│ │ └── SES.json
├── AWS2
│ ├── config.json
│ └── rules
│ │ ├── S3.json
│ │ └── SES.json
├── Stripe
│ ├── config.json
│ ├── incoming_webhooks
│ │ └── stripePaymentSuccess
│ │ │ ├── config.json
│ │ │ └── source.js
│ └── rules
│ │ ├── get.json
│ │ └── post.json
├── TwilioText
│ ├── config.json
│ └── rules
│ │ └── sendText.json
├── http
│ ├── config.json
│ └── rules
│ │ └── countries.json
└── mongodb-atlas
│ ├── config.json
│ └── rules
│ ├── ecommerce.customers.json
│ ├── ecommerce.deliveries.json
│ ├── ecommerce.discounts.json
│ ├── ecommerce.meta.json
│ ├── ecommerce.metaData.json
│ ├── ecommerce.orderOverflow.json
│ ├── ecommerce.products.json
│ ├── ecommerce.reviewOverflow.json
│ └── ecommerce.salesTax.json
├── stitch.json
├── triggers
├── backInStock.json
├── outOfStock.json
└── productInStock.json
└── values
├── AWS_private_key.json
├── MongoDBProductImage.json
├── productImagePrefix.json
├── productImageSuffix.json
├── productURLPrefix.json
├── s3MugBucket.json
├── sourceEmail.json
├── stripePublicKey.json
├── stripePurchaseCancelURL.json
├── stripePurchaseSuccessURL.json
├── stripeSecretKey.json
└── twilioNumber.json
/README.md:
--------------------------------------------------------------------------------
1 | # MongoDB eCommerce reference app – backend
2 |
3 | This is an example of how to build an eCommerce app using MongoDB Atlas and MongoDB Stitch/Realm. This repo contains the code and data for the backend part of the application; the [am-MongoDB-eCommerce](https://github.com/am-MongoDB/eCommerce) contains the reference frontend web app.
4 |
5 | [Try out the app](https://ecommerce-iukkg.mongodbstitch.com/#/) before recreating it for yourself.
6 |
7 | ## Stack
8 |
9 | This application backend doesn't require an application or web server.
10 |
11 | The database is MongoDB Atlas, a fully managed cloud database.
12 |
13 | Access to Atlas and other services is through MongoDB Stitch/Realm – the serverless platform from MongoDB.
14 |
15 | The application frontend uses these technologies:
16 |
17 | - Vue.js
18 | - Bulmer
19 | - Buefy
20 | - SaaS
21 |
22 | The frontend code can be hosted on Stich/Realm static hosting.
23 |
24 | ## Application setup
25 |
26 | This backend application stores data in MongoDB Atlas and uses MongoDB Stitch as a serverless platform for all of the backend functionality.
27 |
28 | ### Configure database and load sample data
29 | If you don't already have a [MongoDB Atlas](https://www.mongodb.com/cloud/atlas) cluster, you'll need to [create one](https://www.mongodb.com/cloud/atlas/register) – a free M0 cluster will work.
30 |
31 | To setup the collection indexes and sample product catalog (13K+ products):
32 |
33 | 1. Add your IP address to your [Atlas whitelist](https://docs.atlas.mongodb.com/security-whitelist/)
34 | 2. [Create Atlas database user](https://docs.atlas.mongodb.com/security-add-mongodb-users/)
35 | 3. Download the [`dump`](https://github.com/am-MongoDB/eCommerce/tree/master/dump) folder
36 | 4. Restore the data:
37 | ```
38 | mongorestore --uri="mongodb+srv://your-username:your-password@your-cluster-name.mongodb.net/ecommerce" dump/
39 | ```
40 | 5. Enable full-text-search using the configuration from [fts.json](https://github.com/am-MongoDB/eCommerce-Realm/blob/master/fts.json) and name the FTS index "Titles and descriptions".
41 |
42 | ### Create the Stitch app
43 | 1. Download this repo
44 | ```
45 | git clone https://github.com/am-MongoDB/eCommerce-Realm.git
46 | ```
47 | 2. Use the [Stitch CLI](https://docs.mongodb.com/stitch/deploy/stitch-cli-reference/) to install the Stitch app – **the first attempt will fail**:
48 |
49 | ```
50 | cd eCommerce-Realm-master
51 | stitch-cli import --strategy=replace
52 | ```
53 | 3. Add Stitch secrets (you get these from your other cloud service providers):
54 | ```
55 | stitch-cli secrets add --name=AWS_private_key --value="my-secret-key"
56 | stitch-cli secrets add --name=stripeSecretKey --value="my-secret-key"
57 | stitch-cli secrets add --name=TwilioAuthToken --value="my-secret-key"
58 | stitch-cli secrets add --name=AWS-personal-private-key --value="my-secret-key"
59 |
60 | stitch-cli import --strategy=replace
61 | ```
62 | 4. Configure the Stitch App (through the Stitch UI)
63 | - Link Stitch app to your Atlas cluster. In the Atlas UI, select `clusters` from the left-side, then click on `mongodb-atlas` and then select your Atlas cluster from the dropdown and save.
64 | - Set your own values for each of the `values` (and `secrets` if you didn't use the real values when importing the app) through the Stitch UI.
65 |
66 | 5. Add frontend app to Stitch static hosting
67 | Build the [frontend app](https://github.com/am-MongoDB/eCommerce) and drag the files onto the `Hosting` panel in the Stitch UI.
68 | 6. Copy the link from the Hosting panel and browse to that page
69 |
--------------------------------------------------------------------------------
/auth_providers/anon-user.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "anon-user",
3 | "type": "anon-user",
4 | "disabled": false
5 | }
6 |
--------------------------------------------------------------------------------
/auth_providers/api-key.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "api-key",
3 | "type": "api-key",
4 | "disabled": true
5 | }
6 |
--------------------------------------------------------------------------------
/auth_providers/local-userpass.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "local-userpass",
3 | "type": "local-userpass",
4 | "config": {
5 | "autoConfirm": true,
6 | "confirmEmailSubject": "eCommerce account created",
7 | "emailConfirmationUrl": "https://ecommerce-iukkg.mongodbstitch.com/#/confirm",
8 | "resetPasswordSubject": "eCommerce password reset",
9 | "resetPasswordUrl": "https://ecommerce-iukkg.mongodbstitch.com/#/reset"
10 | },
11 | "disabled": false
12 | }
13 |
--------------------------------------------------------------------------------
/fts.json:
--------------------------------------------------------------------------------
1 | {
2 | "mappings": {
3 | "dynamic": false,
4 | "fields": {
5 | "category": {
6 | "analyzer": "lucene.standard",
7 | "type": "string"
8 | },
9 | "description": {
10 | "analyzer": "lucene.standard",
11 | "type": "string"
12 | },
13 | "productName": {
14 | "analyzer": "lucene.standard",
15 | "type": "string"
16 | }
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/functions/addReview/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "addReview",
3 | "private": false,
4 | "run_as_system": true,
5 | "can_evaluate": {}
6 | }
7 |
--------------------------------------------------------------------------------
/functions/addReview/source.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {Object} ReviewStats
3 | * @property {number} averageReviewScore
4 | * @property {number} numberOfReviews
5 | */
6 |
7 | /**
8 | * Adds the new review to the `products` document. If there are more than 10 reviews in the `products`
9 | * document then archive the oldest review to its own document in the `reviewOverflow` document.
10 | * @param {string} productID – Uniquely identifies which document the review is for
11 | * @param {string} comment – The text of the comment
12 | * @param {number} stars - How the user rates the product (out of 5)
13 | * @returns {ReviewStats} - The average review score and total number of reviews submitted (including this one)
14 | */
15 | exports = function(productID, comment, stars) {
16 | const productsCollection = context.services.get("mongodb-atlas").db("ecommerce").collection("products");
17 | const reviewsCollection = context.services.get("mongodb-atlas").db("ecommerce").collection("reviewOverflow");
18 | let reviewToArchive = null;
19 | let averageReviewScore = 0;
20 | let numberOfReviews = 0;
21 | return productsCollection.findOne({productID: productID})
22 | .then ((product) => {
23 | product.reviews.numberOfReviews++;
24 | numberOfReviews = product.reviews.numberOfReviews;
25 | product.reviews.totalReviewScore += stars;
26 | product.reviews.averageReviewScore = product.reviews.totalReviewScore / product.reviews.numberOfReviews;
27 | averageReviewScore = product.reviews.averageReviewScore;
28 | const newReview = {
29 | 'review': comment,
30 | 'score': stars
31 | };
32 | let newReviewArray = [newReview];
33 | product.reviews.recentReviews = newReviewArray.concat(product.reviews.recentReviews);
34 | if (product.reviews.recentReviews.length > 10) {
35 | reviewToArchive = product.reviews.recentReviews.pop();
36 | reviewToArchive.productID = productID;
37 | product.reviews.overflow = true;
38 | }
39 | return productsCollection.updateOne(
40 | {productID: productID},
41 | {$set: {reviews: product.reviews}}
42 | );
43 | })
44 | .then (() => {
45 | if (reviewToArchive) {
46 | return reviewsCollection.insertOne(reviewToArchive);
47 | } else {
48 | return {
49 | averageReviewScore: averageReviewScore,
50 | numberOfReviews: numberOfReviews
51 | };
52 | }
53 | })
54 | .then (() => {
55 | return {
56 | averageReviewScore: averageReviewScore,
57 | numberOfReviews: numberOfReviews
58 | };
59 | },
60 | (error) => {
61 | console.error(`Failed to store the new review for ${productID}: ${error.message}`);
62 | });
63 | };
--------------------------------------------------------------------------------
/functions/backInStock/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "backInStock",
3 | "private": true,
4 | "can_evaluate": {}
5 | }
6 |
--------------------------------------------------------------------------------
/functions/backInStock/source.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Updates the product to indicate that it's no longer out of stock. Delegates sending
3 | * notifications to registered users to `productInStock`
4 | * @param {object} changeEvent - https://docs.mongodb.com/stitch/triggers/database-triggers/#database-change-events
5 | */
6 | exports = function(changeEvent) {
7 | const products = context.services.get('mongodb-atlas').db("ecommerce").collection("products");
8 | products.updateOne(
9 | {productID: changeEvent.fullDocument.productID},
10 | {$set: {"internal.outOfStock": false}}
11 | )
12 | .then (() => {
13 | context.functions.execute("productInStock", changeEvent);
14 | })
15 | .catch ((error) => {
16 | console.log(error);
17 | });
18 | };
--------------------------------------------------------------------------------
/functions/cancelOrder/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cancelOrder",
3 | "private": false
4 | }
5 |
--------------------------------------------------------------------------------
/functions/cancelOrder/source.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {Object} Result
3 | * @property {boolean} result – Indicates success/failure
4 | * @property {string} error - If failure, contains the error message
5 | */
6 | /**
7 | * Removes the order from the customer's document, and adjusts up the stock levels
8 | * for all products that were part of the order.
9 | * @param {string} orderID - Uniquely identifies the order
10 | * @returns {Result}
11 | */
12 | exports = function(orderID) {
13 | const customerCollection = context.services.get("mongodb-atlas").db("ecommerce").collection("customers");
14 | let query = {owner_id: context.user.id};
15 | let orderToDelete = null;
16 |
17 | return customerCollection.findOne(query)
18 | .then ((customer) => {
19 | let existingIndex = -1;
20 | if (customer.orders.some(function(item, index) {
21 | existingIndex = index;
22 | return item.orderID === orderID;
23 | })) {
24 | orderToDelete = customer.orders[existingIndex];
25 | // Create a by-value copy and remove the order from the derived position
26 | let newOrders = customer.orders.slice();
27 | newOrders.splice(existingIndex, 1);
28 | customer.orders = newOrders;
29 | return customerCollection.updateOne(
30 | {"contact.email": customer.contact.email},
31 | {$set: {orders: newOrders}}
32 | );
33 | } else {
34 | console.log(`Unable to find orderID: ${orderID}`);
35 | }
36 | })
37 | .then (() => {
38 | // Update stock levels for every product in the order
39 | return Promise.all(orderToDelete.items.map((item) => {
40 | item.quantity = 0 - item.quantity;
41 | return context.functions.execute('updateStockLevel', item);
42 | }));
43 | })
44 | .then(() => {
45 | return {result: true};
46 | },
47 | (error) => {
48 | const errorSring = `Failed to delete the order: ${error.message}`;
49 | console.error(errorSring);
50 | return {result: false, error: errorSring};
51 | });
52 | };
--------------------------------------------------------------------------------
/functions/emailStockNotification/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "emailStockNotification",
3 | "private": true,
4 | "run_as_system": true
5 | }
6 |
--------------------------------------------------------------------------------
/functions/emailStockNotification/source.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Uses the Amazon Simple Email Service (SES) to send an email to a single user to
3 | * indicate that the product they were watching is now back in stock.
4 | * @param {string} destinationEmailAddress - Address to send the email to
5 | * @param {string} productName - Short product name
6 | * @param {string} productID - Uniquely identfies the product
7 | * @param {boolean} emailAllowed – Whether the customer has consented to receive email notifications
8 | * @returns {Promise}
9 | */
10 | exports = function(destinationEmailAddress, productName, productID, emailAllowed){
11 | console.log('email function');
12 | const urlPrefix = context.values.get("productURLPrefix");
13 | const imagePrefix = context.values.get("productImagePrefix");
14 | const imageSuffix = context.values.get("productImageSuffix");
15 | const sourceAddress = context.values.get("sourceEmail");
16 |
17 | if(!emailAllowed) {return}
18 |
19 | const body = `
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | Product in stock
29 |
34 |
35 |
80 |
81 |
82 |
83 |
88 |
89 |
90 |
91 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |  |
146 |
147 | |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
The "${productName}" you are watching is now back in stock – click here to order. |
157 |
158 | |
159 |
160 |
161 | |
162 |
163 | |
164 |
165 | |
166 |
167 |
168 |
169 |
170 |
171 |
172 | `;
173 | const ses = context.services.get('AWS-personal').ses("us-east-1");
174 | return ses.SendEmail({
175 | Source: sourceAddress,
176 | Destination: { ToAddresses: [destinationEmailAddress]},
177 | Message: {
178 | Body: {
179 | Html: {
180 | Charset: "UTF-8",
181 | Data: body
182 | }
183 | },
184 | Subject: {
185 | Charset: "UTF-8",
186 | Data: "Product back in stock"
187 | }
188 | }
189 | });
190 | };
--------------------------------------------------------------------------------
/functions/getCountriesList/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "getCountriesList",
3 | "private": false
4 | }
5 |
--------------------------------------------------------------------------------
/functions/getCountriesList/source.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Use the Stitch http service to retrieve the latest list of countries from
3 | * a public REST API.
4 | * TODO - Could cache this list in the `meta` collection and have a scheduled
5 | * trigger refresh it daily.
6 | */
7 | exports = function(){
8 | const http = context.services.get("http");
9 | const countries =[
10 | {name: 'United Kingdom'},
11 | {name: 'United States'}
12 | ];
13 |
14 | return http.get({url: "https://restcountries.eu/rest/v2/all?fields=name"})
15 | .then (response => {
16 | try {
17 | let countryList = (EJSON.parse(response.body.text()));
18 | console.log (`countryList: ${countryList}`);
19 | console.log (`First country: ${countryList[0].name}`);
20 |
21 | return countryList;
22 | }
23 | catch (error) {
24 | console.error(`Error parsing country list: ${error.message}`);
25 | return countries;
26 | }
27 | },
28 | (error) => {
29 | console.error(`Failed to fetch country list ${error.message}`);
30 | return countries;
31 | });
32 | };
--------------------------------------------------------------------------------
/functions/outOfStock/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "outOfStock",
3 | "private": true,
4 | "can_evaluate": {}
5 | }
6 |
--------------------------------------------------------------------------------
/functions/outOfStock/source.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Marks product as being out of stock (using an internal field not known to the
3 | * legacy stock management system)
4 | * @param {object} changeEvent - https://docs.mongodb.com/stitch/triggers/database-triggers/#database-change-events
5 | */
6 | exports = function(changeEvent) {
7 | const products = context.services.get('mongodb-atlas').db("ecommerce").collection("products");
8 | products.updateOne(
9 | {productID: changeEvent.fullDocument.productID},
10 | {$set: {"internal.outOfStock": true}}
11 | )
12 | .catch ((error) => {
13 | console.log(error);
14 | });
15 | };
--------------------------------------------------------------------------------
/functions/placeOrder/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "placeOrder",
3 | "private": false
4 | }
5 |
--------------------------------------------------------------------------------
/functions/placeOrder/source.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {Object} Result
3 | * @property {boolean} result – Indicates success/failure
4 | * @property {string} error - If failure, contains the error message
5 | */
6 | /**
7 | * Creates an order using the contents of the customer's shopping basket (as stored in the
8 | * `customer` document.) Stores the new order in the `customer` document and (if there are
9 | * now more than 10 orders) archives the oldest to its own document in the `orderOverflow`
10 | * collection.
11 | * @param {string} paymentMethod - Stored as part of the order
12 | * @param {email} - Uniquely identifies the customer
13 | * @returns {Result}
14 | */
15 | exports = function(paymentMethod, email) {
16 | const customerCollection = context.services.get("mongodb-atlas").db("ecommerce").collection("customers");
17 | const orderCollection = context.services.get("mongodb-atlas").db("ecommerce").collection("orderOverflow");
18 | let orderToArchive = null;
19 | let thisOrderItems = null;
20 | let query = {};
21 | if (email) {
22 | query = {"contact.email": email};
23 | } else {
24 | query = {owner_id: context.user.id};
25 | }
26 | return customerCollection.findOne(query)
27 | .then ((customer) => {
28 | const totalPrice = customer.shoppingBasket.reduce((total, item) =>
29 | {
30 | return total + (item.quantity * item.price);
31 | }, 0);
32 | let newOrder = {
33 | orderID: `${customer.contact.email}-${Date.now()}`,
34 | status: 'pending',
35 | orderDate: new Date(),
36 | deliveryDate: null,
37 | deliveryAddress: customer.contact.deliveryAddress,
38 | totalPrice: totalPrice,
39 | paymentMethod: paymentMethod,
40 | items: customer.shoppingBasket
41 | };
42 | thisOrderItems = customer.shoppingBasket;
43 | customer.orders = [newOrder].concat(customer.orders);
44 | if (customer.orders.length > 10) {
45 | console.log('Need to archive older order');
46 | orderToArchive = customer.orders.pop();
47 | orderToArchive.customerEmail = customer.contact.email;
48 | orderToArchive.owner_id = context.user.id;
49 | }
50 | return customerCollection.updateOne(
51 | query,
52 | {$set: {
53 | orders: customer.orders,
54 | orderOverflow: true
55 | }}
56 | );
57 | })
58 | .then (() => {
59 | if (orderToArchive) {
60 | return orderCollection.insertOne(orderToArchive);
61 | } else {
62 | return {result: true};
63 | }
64 | })
65 | .then (() => {
66 | // Update stock levels for every product in the order
67 | return Promise.all(thisOrderItems.map((item) => {
68 | return context.functions.execute('updateStockLevel', item);
69 | }));
70 | })
71 | .then(() => {
72 | return {result: true};
73 | },
74 | (error) => {
75 | const errorSring = `Failed to store the new order: ${error.message}`;
76 | console.error(errorSring);
77 | return {result: false, error: errorSring};
78 | });
79 | };
--------------------------------------------------------------------------------
/functions/productInStock/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "productInStock",
3 | "private": true,
4 | "run_as_system": true
5 | }
6 |
--------------------------------------------------------------------------------
/functions/productInStock/source.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Called once a product is back in stock. For each customer that's waiting on
3 | * the product send them a text and/or email notification that it's back in
4 | * stock and remove it from the customer's waiting list
5 | * @param {object} event - Change event: https://docs.mongodb.com/stitch/triggers/database-triggers/#database-change-events
6 | * @returns {Promise}
7 | */
8 | exports = function(event) {
9 | const db = context.services.get("mongodb-atlas").db("ecommerce");
10 | const customers = db.collection("customers");
11 | const product = event.fullDocument;
12 | const notifyCustomer = (customer) => {
13 | return context.functions.execute(
14 | "emailStockNotification",
15 | customer.contact.email,
16 | product.productName,
17 | product.productID,
18 | customer.marketingPreferences.email
19 | )
20 | .then(() => {
21 | return context.functions.execute(
22 | "textStockNotification",
23 | customer.contact.phone,
24 | product.productName,
25 | product.productID,
26 | customer.marketingPreferences.sms
27 | );
28 | })
29 | .then(() => {
30 | let waitingOnProducts = [...customer.waitingOnProducts];
31 | let index = waitingOnProducts.indexOf(product.productID);
32 | waitingOnProducts.splice(index, 1);
33 | return customers.updateOne(
34 | {'contact.email': customer.contact.email},
35 | {$set: {waitingOnProducts: waitingOnProducts}}
36 | );
37 | })
38 | .catch((error) => {
39 | console.log(error);
40 | });
41 | };
42 | return customers.find({waitingOnProducts: product.productID}).toArray().then((customers) => {
43 | return Promise.all(customers.map((customer) => {
44 | return notifyCustomer(customer);
45 | }));
46 | });
47 | };
--------------------------------------------------------------------------------
/functions/stripeCreateCheckoutSession/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stripeCreateCheckoutSession",
3 | "private": false
4 | }
5 |
--------------------------------------------------------------------------------
/functions/stripeCreateCheckoutSession/source.js:
--------------------------------------------------------------------------------
1 | /**
2 | * To process a payment through Stripe Checkout, this function creates a context for the payment;
3 | * the resulting `id` can then be returned to the frontend app when redirecting to stripe.
4 | * @param {number} amount - The amount to charge to the customer's card, in $USD
5 | * @returns {string} - a unique ID associated with this Stripe session that the frontend can use
6 | * to redirect to the Stripe Checkout page to take the payment
7 | */
8 |
9 | exports = function(amount) {
10 | const amountInCents = Math.round(amount * 100);
11 | const http = context.services.get("Stripe");
12 | const secretKey = context.values.get("stripeSecretKey");
13 | const productImage = context.values.get("MongoDBProductImage");
14 | const successURL = context.values.get("stripePurchaseSuccessURL");
15 | const cancelURL = context.values.get("stripePurchaseCancelURL");
16 | const customers = context.services.get("mongodb-atlas").db("ecommerce").collection("customers");
17 |
18 | return customers.findOne({owner_id: context.user.id}, {"contact.email": 1})
19 | .then ((customer) => {
20 | const formData = {
21 | "payment_method_types[]": ["card"],
22 | "line_items[][name]": "MongoDB eCommerce order",
23 | "line_items[][description]": "Your order from MongoDB",
24 | "line_items[][images][]": productImage,
25 | "line_items[][amount]": amountInCents,
26 | "line_items[][currency]": "usd",
27 | "line_items[][quantity]": 1,
28 | "customer_email": customer.contact.email,
29 | success_url: successURL,
30 | cancel_url: cancelURL
31 | };
32 |
33 | // Need to url-encode the form object (as that's the format required by the
34 | // Stripe API)
35 | let formBody = [];
36 | for (let property in formData) {
37 | const encodedKey = encodeURIComponent(property);
38 | const encodedValue = encodeURIComponent(formData[property]);
39 | formBody.push(encodedKey + "=" + encodedValue);
40 | }
41 | formBody = formBody.join("&");
42 | return http.post({
43 | url: "https://api.stripe.com/v1/checkout/sessions",
44 | body: formBody,
45 | headers: {
46 | "Authorization": [ `Bearer ${secretKey}` ],
47 | "Content-Type": [ "application/x-www-form-urlencoded" ]
48 | }
49 | });
50 | })
51 | .then(response => {
52 | // The response body is encoded as raw BSON.Binary. Parse it to JSON.
53 | const ejson_body = EJSON.parse(response.body.text());
54 | return ejson_body.id;
55 | },
56 | (error) => {
57 | console.log(`Failed to create session: ${error}`);
58 | });
59 | };
--------------------------------------------------------------------------------
/functions/textStockNotification/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "textStockNotification",
3 | "private": true,
4 | "run_as_system": true
5 | }
6 |
--------------------------------------------------------------------------------
/functions/textStockNotification/source.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Sends a text message (using the Twilio service) to the user to tell them
3 | * that their watched product is now back in stock
4 | * @param {string} destinationPhone - Phone number to send the text to
5 | * @param {string} productName - Product name to include in the text message
6 | * @param {string} productID - Uniquely identifies the product
7 | * @param {boolean} textAllowed - Whether the customer has agreed to receive text notifiactions
8 | * @returns {Promise}
9 | */
10 | exports = function(destinationPhone, productName, productID, textAllowed){
11 | if (!textAllowed || !destinationPhone.mobile) {return}
12 | const twilio = context.services.get("TwilioText");
13 | const twilioNumber = context.values.get('twilioNumber');
14 | const productURL = `${context.values.get('productURLPrefix')}${productID}`;
15 | const body = `"${productName}" is now back in stock: ${productURL}`;
16 | return twilio.send({
17 | to: destinationPhone.mobile,
18 | from: twilioNumber,
19 | body: body
20 | });
21 | };
--------------------------------------------------------------------------------
/functions/updateStockLevel/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "updateStockLevel",
3 | "private": true,
4 | "run_as_system": true
5 | }
6 |
--------------------------------------------------------------------------------
/functions/updateStockLevel/source.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {Object} LineItem
3 | * @property {string} productID - Uniquely identifies the product
4 | * @property {number} quantity - Number of this product included in the order
5 | */
6 | /**
7 | * Reduce the stock level of the product to reflect this line item from an order.
8 | * @param {LineItem} lineItem: Line item from an order
9 | * @returns {Promise}
10 | */
11 | exports = function (lineItem) {
12 | // This is implemented as a seperate, private Stitch Function so that it can run as
13 | // System to update the product documents.
14 | const productsCollection = context.services.get("mongodb-atlas").db("ecommerce").collection("products");
15 | return productsCollection.findOne({productID: lineItem.productID})
16 | .then ((productDoc) => {
17 | const newStockLevel = Math.max (0, productDoc.stockLevel - lineItem.quantity);
18 | return productsCollection.updateOne(
19 | {productID: lineItem.productID},
20 | {$set: {stockLevel: newStockLevel}}
21 | );
22 | },
23 | (error) => {
24 | const errorSring = `Failed to update existing product document: ${error.message}`;
25 | console.error(errorSring);
26 | });
27 | };
--------------------------------------------------------------------------------
/services/AWS-personal/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "AWS-personal",
3 | "type": "aws",
4 | "config": {
5 | "accessKeyId": "AKIAIF2IIZ5B37QIDHZA"
6 | },
7 | "secret_config": {
8 | "secretAccessKey": "AWS-personal-private-key"
9 | },
10 | "version": 1
11 | }
12 |
--------------------------------------------------------------------------------
/services/AWS-personal/rules/SES.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "SES",
3 | "actions": [
4 | "ses:SendEmail"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/services/AWS2/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "AWS2",
3 | "type": "aws",
4 | "config": {
5 | "accessKeyId": "AKIA2QKJSCXBY426BJM3"
6 | },
7 | "secret_config": {
8 | "secretAccessKey": "AWS_private_key"
9 | },
10 | "version": 1
11 | }
12 |
--------------------------------------------------------------------------------
/services/AWS2/rules/S3.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "S3",
3 | "actions": [
4 | "s3:PutObject"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/services/AWS2/rules/SES.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "SES",
3 | "actions": [
4 | "ses:SendEmail"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/services/Stripe/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Stripe",
3 | "type": "http",
4 | "config": {},
5 | "version": 1
6 | }
7 |
--------------------------------------------------------------------------------
/services/Stripe/incoming_webhooks/stripePaymentSuccess/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stripePaymentSuccess",
3 | "run_as_authed_user": false,
4 | "run_as_user_id": "",
5 | "run_as_user_id_script_source": "",
6 | "options": {
7 | "httpMethod": "POST",
8 | "validationMethod": "NO_VALIDATION"
9 | },
10 | "respond_result": true
11 | }
12 |
--------------------------------------------------------------------------------
/services/Stripe/incoming_webhooks/stripePaymentSuccess/source.js:
--------------------------------------------------------------------------------
1 | // This function is the webhook's request handler.
2 | exports = function(payload, response) {
3 | // This is a binary object that can be accessed as a string using .text()
4 | const body = EJSON.parse(payload.body.text());
5 | console.log(`Customer email: ${body.data.object.customer_email}`);
6 | const customers = context.services.get("mongodb-atlas").db("ecommerce").collection("customers");
7 | return context.functions.execute('placeOrder', 'Credit or debit card', body.data.object.customer_email)
8 | .then (() => {
9 | customers.updateOne({"contact.email": body.data.object.customer_email}, {$set: {shoppingBasket: []}});
10 | return "Order processed successfully";
11 | },
12 | () => {
13 | return "Problem with the order";
14 | });
15 | };
--------------------------------------------------------------------------------
/services/Stripe/rules/get.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "get",
3 | "actions": [
4 | "get"
5 | ],
6 | "when": {
7 | "%%args.url.host": {
8 | "%in": [
9 | "api.stripe.com"
10 | ]
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/services/Stripe/rules/post.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "post",
3 | "actions": [
4 | "post"
5 | ],
6 | "when": {
7 | "%%args.url.host": {
8 | "%in": [
9 | "api.stripe.com"
10 | ]
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/services/TwilioText/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "TwilioText",
3 | "type": "twilio",
4 | "config": {
5 | "sid": "AC417992716e63e127f80fb78bb4bbc0b4"
6 | },
7 | "secret_config": {
8 | "auth_token": "TwilioAuthToken"
9 | },
10 | "version": 1
11 | }
12 |
--------------------------------------------------------------------------------
/services/TwilioText/rules/sendText.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sendText",
3 | "actions": [
4 | "send"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/services/http/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "http",
3 | "type": "http",
4 | "config": {},
5 | "version": 1
6 | }
7 |
--------------------------------------------------------------------------------
/services/http/rules/countries.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "countries",
3 | "actions": [
4 | "get"
5 | ],
6 | "when": {
7 | "%%args.url.host": {
8 | "%in": [
9 | "restcountries.eu"
10 | ]
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/services/mongodb-atlas/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mongodb-atlas",
3 | "type": "mongodb-atlas",
4 | "config": {
5 | "readPreference": "primary",
6 | "wireProtocolEnabled": false
7 | },
8 | "version": 1
9 | }
10 |
--------------------------------------------------------------------------------
/services/mongodb-atlas/rules/ecommerce.customers.json:
--------------------------------------------------------------------------------
1 | {
2 | "database": "ecommerce",
3 | "collection": "customers",
4 | "roles": [
5 | {
6 | "name": "owner",
7 | "apply_when": {
8 | "owner_id": "%%user.id"
9 | },
10 | "fields": {
11 | "wishLists": {}
12 | },
13 | "read": true,
14 | "write": true,
15 | "insert": true,
16 | "delete": true,
17 | "additional_fields": {}
18 | },
19 | {
20 | "name": "shared",
21 | "apply_when": {
22 | "friends": "%%user.id"
23 | },
24 | "fields": {
25 | "wishLists": {
26 | "fields": {
27 | "quantity": {
28 | "write": true,
29 | "read": true
30 | }
31 | },
32 | "write": false,
33 | "read": true
34 | }
35 | },
36 | "write": false,
37 | "insert": true,
38 | "delete": false,
39 | "additional_fields": {}
40 | },
41 | {
42 | "name": "anyone",
43 | "apply_when": {},
44 | "write": {
45 | "%%prevRoot": {
46 | "$exists": false
47 | }
48 | },
49 | "insert": true,
50 | "delete": false,
51 | "additional_fields": {}
52 | }
53 | ],
54 | "schema": {
55 | "properties": {
56 | "_id": {
57 | "bsonType": "objectId"
58 | },
59 | "friends": {
60 | "bsonType": "array",
61 | "items": {
62 | "bsonType": "string"
63 | }
64 | },
65 | "owner_id": {
66 | "bsonType": "string"
67 | }
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/services/mongodb-atlas/rules/ecommerce.deliveries.json:
--------------------------------------------------------------------------------
1 | {
2 | "database": "ecommerce",
3 | "collection": "deliveries",
4 | "roles": [
5 | {
6 | "name": "owner",
7 | "apply_when": {
8 | "owner_id": "%%user.id"
9 | },
10 | "read": true,
11 | "insert": true,
12 | "delete": true,
13 | "additional_fields": {}
14 | }
15 | ],
16 | "schema": {
17 | "properties": {
18 | "_id": {
19 | "bsonType": "objectId"
20 | },
21 | "owner_id": {
22 | "bsonType": "string"
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/services/mongodb-atlas/rules/ecommerce.discounts.json:
--------------------------------------------------------------------------------
1 | {
2 | "database": "ecommerce",
3 | "collection": "discounts",
4 | "roles": [
5 | {
6 | "name": "default",
7 | "apply_when": {},
8 | "read": true,
9 | "write": false,
10 | "insert": false,
11 | "delete": false,
12 | "additional_fields": {}
13 | }
14 | ],
15 | "schema": {
16 | "properties": {
17 | "_id": {
18 | "bsonType": "objectId"
19 | }
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/services/mongodb-atlas/rules/ecommerce.meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "database": "ecommerce",
3 | "collection": "meta",
4 | "roles": [
5 | {
6 | "name": "default",
7 | "apply_when": {},
8 | "read": true,
9 | "write": false,
10 | "insert": false,
11 | "delete": false,
12 | "additional_fields": {}
13 | }
14 | ],
15 | "schema": {
16 | "properties": {
17 | "_id": {
18 | "bsonType": "objectId"
19 | }
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/services/mongodb-atlas/rules/ecommerce.metaData.json:
--------------------------------------------------------------------------------
1 | {
2 | "database": "ecommerce",
3 | "collection": "metaData",
4 | "roles": [
5 | {
6 | "name": "default",
7 | "apply_when": {},
8 | "read": true,
9 | "write": false,
10 | "insert": false,
11 | "delete": false,
12 | "additional_fields": {}
13 | }
14 | ],
15 | "schema": {
16 | "properties": {
17 | "_id": {
18 | "bsonType": "objectId"
19 | }
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/services/mongodb-atlas/rules/ecommerce.orderOverflow.json:
--------------------------------------------------------------------------------
1 | {
2 | "database": "ecommerce",
3 | "collection": "orderOverflow",
4 | "roles": [
5 | {
6 | "name": "owner",
7 | "apply_when": {
8 | "owner_id": "%%user.id"
9 | },
10 | "read": true,
11 | "write": true,
12 | "insert": true,
13 | "delete": true,
14 | "additional_fields": {}
15 | }
16 | ],
17 | "schema": {
18 | "properties": {
19 | "_id": {
20 | "bsonType": "objectId"
21 | },
22 | "owner_id": {
23 | "bsonType": "string"
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/services/mongodb-atlas/rules/ecommerce.products.json:
--------------------------------------------------------------------------------
1 | {
2 | "database": "ecommerce",
3 | "collection": "products",
4 | "roles": [
5 | {
6 | "name": "default",
7 | "apply_when": {},
8 | "fields": {
9 | "internal": {},
10 | "reviews": {
11 | "write": true
12 | }
13 | },
14 | "read": true,
15 | "insert": false,
16 | "delete": false,
17 | "additional_fields": {}
18 | }
19 | ],
20 | "schema": {
21 | "properties": {
22 | "_id": {
23 | "bsonType": "objectId"
24 | },
25 | "owner_id": {
26 | "bsonType": "string"
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/services/mongodb-atlas/rules/ecommerce.reviewOverflow.json:
--------------------------------------------------------------------------------
1 | {
2 | "database": "ecommerce",
3 | "collection": "reviewOverflow",
4 | "roles": [
5 | {
6 | "name": "owner",
7 | "apply_when": {
8 | "owner_id": "%%user.id"
9 | },
10 | "read": true,
11 | "write": true,
12 | "insert": true,
13 | "delete": true,
14 | "additional_fields": {}
15 | },
16 | {
17 | "name": "non-owner",
18 | "apply_when": {},
19 | "read": true,
20 | "write": false,
21 | "insert": false,
22 | "delete": false,
23 | "additional_fields": {}
24 | }
25 | ],
26 | "schema": {
27 | "properties": {
28 | "_id": {
29 | "bsonType": "objectId"
30 | },
31 | "owner_id": {
32 | "bsonType": "string"
33 | }
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/services/mongodb-atlas/rules/ecommerce.salesTax.json:
--------------------------------------------------------------------------------
1 | {
2 | "database": "ecommerce",
3 | "collection": "salesTax",
4 | "roles": [
5 | {
6 | "name": "default",
7 | "apply_when": {},
8 | "read": true,
9 | "write": false,
10 | "insert": false,
11 | "delete": false,
12 | "additional_fields": {}
13 | }
14 | ],
15 | "schema": {
16 | "properties": {
17 | "_id": {
18 | "bsonType": "objectId"
19 | }
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/stitch.json:
--------------------------------------------------------------------------------
1 | {
2 | "config_version": 20180301,
3 | "security": {}
4 | }
5 |
--------------------------------------------------------------------------------
/triggers/backInStock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "backInStock",
3 | "type": "DATABASE",
4 | "config": {
5 | "operation_types": [
6 | "UPDATE",
7 | "REPLACE"
8 | ],
9 | "database": "ecommerce",
10 | "collection": "products",
11 | "service_name": "mongodb-atlas",
12 | "match": {
13 | "fullDocument.internal.outOfStock": true,
14 | "fullDocument.stockLevel": {
15 | "$gt": {
16 | "$numberInt": "0"
17 | }
18 | }
19 | },
20 | "full_document": true,
21 | "unordered": true
22 | },
23 | "function_name": "backInStock",
24 | "disabled": false
25 | }
26 |
--------------------------------------------------------------------------------
/triggers/outOfStock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "outOfStock",
3 | "type": "DATABASE",
4 | "config": {
5 | "operation_types": [
6 | "UPDATE",
7 | "REPLACE"
8 | ],
9 | "database": "ecommerce",
10 | "collection": "products",
11 | "service_name": "mongodb-atlas",
12 | "match": {
13 | "fullDocument.internal.outOfStock": false,
14 | "fullDocument.stockLevel": {
15 | "$eq": {
16 | "$numberInt": "0"
17 | }
18 | }
19 | },
20 | "full_document": true,
21 | "unordered": true
22 | },
23 | "function_name": "outOfStock",
24 | "disabled": false
25 | }
26 |
--------------------------------------------------------------------------------
/triggers/productInStock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "productInStock",
3 | "type": "DATABASE",
4 | "config": {
5 | "operation_types": [
6 | "UPDATE"
7 | ],
8 | "database": "ecommerce",
9 | "collection": "products",
10 | "service_name": "mongodb-atlas",
11 | "match": {
12 | "updateDescription.updatedFields.stockLevel": {
13 | "$gt": {
14 | "$numberInt": "0"
15 | }
16 | }
17 | },
18 | "full_document": true,
19 | "unordered": true
20 | },
21 | "function_name": "productInStock",
22 | "disabled": true
23 | }
24 |
--------------------------------------------------------------------------------
/values/AWS_private_key.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "AWS_private_key",
3 | "value": "AWS_private_key",
4 | "from_secret": true
5 | }
6 |
--------------------------------------------------------------------------------
/values/MongoDBProductImage.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "MongoDBProductImage",
3 | "value": "https://webassets.mongodb.com/_com_assets/cms/MongoDB_Logo_FullColorBlack_RGB-4td3yuxzjs.png",
4 | "from_secret": false
5 | }
6 |
--------------------------------------------------------------------------------
/values/productImagePrefix.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "productImagePrefix",
3 | "value": "https://ecommerce-mongodb-products.s3.amazonaws.com/",
4 | "from_secret": false
5 | }
6 |
--------------------------------------------------------------------------------
/values/productImageSuffix.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "productImageSuffix",
3 | "value": "-0.jpg",
4 | "from_secret": false
5 | }
6 |
--------------------------------------------------------------------------------
/values/productURLPrefix.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "productURLPrefix",
3 | "value": "https://.mongodbstitch.com/#/product?productID=",
4 | "from_secret": false
5 | }
6 |
--------------------------------------------------------------------------------
/values/s3MugBucket.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "s3MugBucket",
3 | "value": "https://.s3.amazonaws.com/",
4 | "from_secret": false
5 | }
6 |
--------------------------------------------------------------------------------
/values/sourceEmail.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sourceEmail",
3 | "value": "",
4 | "from_secret": false
5 | }
6 |
--------------------------------------------------------------------------------
/values/stripePurchaseCancelURL.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stripePurchaseCancelURL",
3 | "value": "https://.mongodbstitch.com/#/basket",
4 | "from_secret": false
5 | }
6 |
--------------------------------------------------------------------------------
/values/stripePurchaseSuccessURL.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stripePurchaseSuccessURL",
3 | "value": "https://.mongodbstitch.com/#/checkedout?session_id={CHECKOUT_SESSION_ID}",
4 | "from_secret": false
5 | }
6 |
--------------------------------------------------------------------------------
/values/stripeSecretKey.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stripeSecretKey",
3 | "value": "stripeSecretKey",
4 | "from_secret": true
5 | }
6 |
--------------------------------------------------------------------------------
/values/twilioNumber.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "twilioNumber",
3 | "value": "",
4 | "from_secret": false
5 | }
6 |
--------------------------------------------------------------------------------