├── .env.sample ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── db └── client.js ├── fql ├── EqualsCurrentIdentityArity1.fql ├── EqualsCurrentIdentityArity2.fql ├── Owner.fql └── SubmitOrder.fql ├── functions ├── customer_orders.js ├── list_products.js ├── login.js ├── register.js ├── seed.js └── submit_order.js ├── jsonSchemas ├── login.json ├── register.json └── submit_order.json ├── package-lock.json ├── package.json ├── seed ├── customers.js ├── orders.js ├── products.js └── stores.js └── serverless.yml /.env.sample: -------------------------------------------------------------------------------- 1 | # Copyright Fauna, Inc. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | FAUNA_SECRET= 5 | FAUNA_DOMAIN= 6 | FAUNA_SCHEME= 7 | FAUNA_PORT= 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Copyright Fauna, Inc. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | # package directories 5 | node_modules 6 | jspm_packages 7 | 8 | # Serverless directories 9 | .serverless 10 | 11 | # dotenv files (natively supported by Serverless Framework) 12 | .env 13 | 14 | # IDE settings 15 | .idea/ 16 | .vscode/ 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Fauna Labs 2 | 3 | First, thank you for your interest in contributing to Fauna Labs! Contributions like yours help the Fauna community build more effectively, ship faster, and delight customers. We appreciate it! 4 | 5 | Reading and following these guidelines helps to make the contribution process easy and effective for everyone involved. It also communicates that you agree to respect the time of the developers managing and developing these open source projects. We reciprocate that respect by addressing your issue, assessing changes, and helping you finalize your pull requests. 6 | 7 | ## Quicklinks 8 | 9 | * [Code of Conduct](#code-of-conduct) 10 | * [Getting Started](#getting-started) 11 | * [Issues](#issues) 12 | * [Pull Requests](#pull-requests) 13 | * [Getting Help](#getting-help) 14 | 15 | ## Code of Conduct 16 | 17 | Fauna is dedicated to providing a positive experience for everyone in the Fauna community, and we need your help to ensure that everyone feels welcomed. By participating and contributing to Fauna Labs, you agree to uphold our [Code of Conduct](https://forums.fauna.com/guidelines). 18 | 19 | ## Getting Started 20 | 21 | Contributions are made to this repository via Issues and Pull Requests (PRs). 22 | 23 | - To report security vulnerabilities, please follow the guidance in [SECURITY.md](SECURITY.md). 24 | 25 | ### Issues 26 | 27 | Issues should be used to report problems, request new features, or discuss potential changes before a PR is created. Please search for existing Issues and PRs before creating your own. 28 | 29 | If you find an Issue that addresses the problem you're having, please add your own reproduction information to the existing issue rather than creating a new issue. Adding a [reaction](https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) also indicates that a particular problem affects more than just the reporter. 30 | 31 | ### Pull Requests 32 | 33 | PRs to Fauna Labs repositories are always welcome and can be a quick way to get your fix or improvement slated for the next release. In general, PRs should: 34 | 35 | - Only fix/add the functionality in question **OR** address wide-spread whitespace/style issues, not both. 36 | - Add unit or integration tests for fixed or changed functionality (if a test suite already exists). 37 | - Address a single concern in the fewest number of changed lines possible. 38 | - Include documentation in the repository. 39 | - Be accompanied by a complete Pull Request template (loaded automatically when a PR is created). 40 | 41 | Breaking changes require an Issue to discuss your proposal first. 42 | 43 | Fauna Labs follows the ["fork-and-pull" Git workflow](https://github.com/susam/gitpr) 44 | 45 | 1. Fork the repository to your own Github account 46 | 2. Clone the project to your machine 47 | 3. Create a branch locally with a succinct but descriptive name 48 | 4. Commit changes to the branch 49 | 5. Following any formatting and testing guidelines specific to this repository 50 | 6. Push changes to your fork 51 | 7. Open a PR in our repository and follow the PR template so that we can efficiently review the changes. 52 | 53 | ## Getting Help 54 | 55 | Join us in the [Fauna forums](https://forums.fauna.com/). For best reults, post your question in the relevant category with appropriate tags. 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Fauna, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repository contains unofficial patterns, sample code, or tools to help developers build more effectively with [Fauna][fauna]. All [Fauna Labs][fauna-labs] repositories are provided “as-is” and without support. By using this repository or its contents, you agree that this repository may never be officially supported and moved to the [Fauna organization][fauna-organization]. 2 | 3 | --- 4 | 5 | # Fauna Serverless Framework plugin usage example 6 | 7 | [Serverless quick start][serverless-quick-start] 8 | [Serverless Fauna plugin][serverless-fauna-plugin] 9 | [Fauna get started][fauna-get-started] 10 | 11 | ## Structure of this repository 12 | 13 | - `fql` - [Fauna Query Language (FQL)][fql] queries used in [serverless.yml](serverless.yml). 14 | - `functions` - AWS Lambda functions for accessing your database. 15 | - `jsonSchemas` - Used to specify the request shape for Amazon API Gateway. 16 | - `seed` - A Lambda function to populate your database with demo data. 17 | 18 | ## Setup 19 | 20 | - Create an empty database in the [Fauna dashboard][fauna-dashboard]. 21 | - Create an Admin key for your new database and copy the generated secret. 22 | - Copy `.env.sample` to `.env` and paste the generated secret as the `FAUNA_SECRET` value. 23 | - Run `npm install` to install dependencies. 24 | - Run `serverless fauna deploy` to create the Fauna collections, indexes, roles, and functions in your database. 25 | - Run `serverless invoke local --function seed` to seed your database with demo data. 26 | 27 | ## Deploy to Amazon Web Services (AWS) 28 | 29 | ``` 30 | serverless deploy 31 | ``` 32 | 33 | After you deploy, you have the following database schema populated with the demo data from the [seed](seed) directory. 34 | ``` 35 | [ 36 | { 37 | data: [ 38 | Collection("customers"), 39 | Collection("orders"), 40 | Collection("products"), 41 | Collection("stores") 42 | ] 43 | }, 44 | { 45 | data: [ 46 | Index("products_by_customer"), 47 | Index("all_orders"), 48 | Index("all_customers"), 49 | Index("all_stores"), 50 | Index("all_products"), 51 | Index("products_by_store"), 52 | Index("products_by_price_high_to_low"), 53 | Index("products_by_price_low_to_high"), 54 | Index("customer_by_email"), 55 | Index("orders_by_customer") 56 | ] 57 | }, 58 | { 59 | data: [Role("customer")] 60 | }, 61 | { 62 | data: [Ref(Ref("functions"), "submit_order")] 63 | } 64 | ] 65 | 66 | ``` 67 | 68 | The Serverless Framework also creates an Amazon API Gateway REST API and Lambda functions and connects them together. 69 | 70 | | Endpoint | Function | Description | 71 | | --------------------- | --------------- | --------------------------------------- | 72 | | GET /products | list_products | Return list of all products | 73 | | POST /customers | register | Customer registration | 74 | | POST /customers/login | login | Exchange customer credentials to secret | 75 | | POST /orders | submit_order | Customer purchase product(s) | 76 | | GET /orders | customer_orders | Return list of customer orders | 77 | 78 | 79 | ## Invoke functions locally 80 | 81 | ### Customer registration 82 | 83 | ``` 84 | serverless invoke local \ 85 | --function register \ 86 | --data '{"body": "{\"email\":\"test@fauna.com\",\"password\":\"111111\",\"address\":{\"street\":\"72 Waxwing Terrace\",\"city\":\"Washington\",\"state\":\"DC\",\"zipCode\":\"20002\"},\"creditCard\":{\"network\":\"Visa\",\"number\":\"4916112310613672\"}}"}' 87 | ``` 88 | 89 | ### Exchange customer credentials for an application secret 90 | 91 | ``` 92 | serverless invoke local \ 93 | --function login \ 94 | --data '{"body": "{\"email\":\"test@fauna.com\",\"password\":\"111111\"}"}' 95 | ``` 96 | 97 | Copy the `secret/id` values to use as replacements for the `CUSTOMER_SECRET/CUSTOMER_ID` placeholders in the following requests. 98 | 99 | ### Return a list of all products 100 | 101 | #### All products 102 | 103 | ``` 104 | serverless invoke local \ 105 | --function list_products \ 106 | --data '{"headers": {"secret": "CUSTOMER_SECRET"}}' 107 | ``` 108 | 109 | #### All products, sorted by price (low to high) 110 | 111 | ``` 112 | serverless invoke local \ 113 | --function list_products \ 114 | --data '{"queryStringParameters": {"priceSort": "low-to-high"}, "headers": {"secret": "CUSTOMER_SECRET"}}' 115 | ``` 116 | 117 | #### Products available in a specified store 118 | 119 | ``` 120 | serverless invoke local \ 121 | --function list_products \ 122 | --data '{"queryStringParameters": {"storeId": "303"}, "headers": {"secret": "CUSTOMER_SECRET"}}' 123 | ``` 124 | 125 | ### Submit one customer order 126 | 127 | ``` 128 | serverless invoke local \ 129 | --function submit_order \ 130 | --data '{"headers": {"secret": "CUSTOMER_SECRET"},"body": "{\"products\":[{\"productId\":\"209\",\"quantity\":1}]}"}' 131 | ``` 132 | 133 | ### List all customer orders 134 | 135 | ``` 136 | serverless invoke local \ 137 | --function customer_orders \ 138 | --data '{"headers": {"secret": "CUSTOMER_SECRET"}}' 139 | ``` 140 | 141 | --- 142 | 143 | Copyright Fauna, Inc. or its affiliates. All rights reserved. SPDX-License-Identifier: MIT-0 144 | 145 | [fauna]: https://www.fauna.com/ 146 | [fauna-dashboard]: https://dashboard.fauna.com/ 147 | [fauna-get-started]: https://docs.fauna.com/fauna/current/start/ 148 | [fauna-labs]: https://github.com/fauna-labs 149 | [fauna-organization]: https://github.com/fauna 150 | [fql]: https://docs.fauna.com/fauna/current/api/fql/ 151 | [serverless-fauna-plugin]: https://github.com/fauna/serverless-fauna/ 152 | [serverless-quick-start]: https://www.serverless.com/framework/docs/providers/aws/guide/quick-start/ -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | The following versions of this software currently receive support for 6 | security vulnerabilities: 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | | :white_check_mark: | 11 | | | :x: | 12 | 13 | End of life (EOL) dates for security support will be posted here. 14 | 15 | ## Reporting a Vulnerability 16 | 17 | If you believe you have found a vulnerability in this repository, please 18 | do not submit a GitHub issue. Instead, please email security@fauna.com and 19 | devrel@fauna.com with the subject line _Suspected security vulnerability 20 | in fauna-labs/_. Please include the steps 21 | to reproduce and any exploit code if available. 22 | -------------------------------------------------------------------------------- /db/client.js: -------------------------------------------------------------------------------- 1 | // Copyright Fauna, Inc. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | const { Client } = require('faunadb') 5 | 6 | module.exports.client = new Client({ 7 | secret: process.env.FAUNA_SECRET, 8 | domain: process.env.FAUNA_DOMAIN, 9 | scheme: process.env.FAUNA_SCHEME, 10 | port: process.env.FAUNA_PORT, 11 | }) 12 | -------------------------------------------------------------------------------- /fql/EqualsCurrentIdentityArity1.fql: -------------------------------------------------------------------------------- 1 | Lambda( 2 | ['ref'], 3 | Equals(Var('ref'), CurrentIdentity()) 4 | ) 5 | -------------------------------------------------------------------------------- /fql/EqualsCurrentIdentityArity2.fql: -------------------------------------------------------------------------------- 1 | Lambda( 2 | ['ref', '_'], 3 | Equals(Var('ref'), CurrentIdentity()) 4 | ) 5 | -------------------------------------------------------------------------------- /fql/Owner.fql: -------------------------------------------------------------------------------- 1 | Lambda("ref", Equals( 2 | CurrentIdentity(), 3 | Select(["data", "customer"], Get(Var("ref"))) 4 | )) -------------------------------------------------------------------------------- /fql/SubmitOrder.fql: -------------------------------------------------------------------------------- 1 | Lambda( 2 | ["customerId", "products"], 3 | Let( 4 | { 5 | customer: Get(Var("customerId")), 6 | products: Map( 7 | Var("products"), 8 | Lambda( 9 | "requestedProduct", 10 | Let( 11 | { 12 | product: Get( 13 | Ref( 14 | Collection("products"), 15 | Select("productId", Var("requestedProduct")) 16 | ) 17 | ) 18 | }, 19 | { 20 | ref: Select("ref", Var("product")), 21 | price: Select(["data", "price"], Var("product")), 22 | currentQuantity: Select(["data", "quantity"], Var("product")), 23 | requestedQuantity: Select( 24 | ["quantity"], 25 | Var("requestedProduct") 26 | ), 27 | backorderLimit: Select( 28 | ["data", "backorderLimit"], 29 | Var("product") 30 | ) 31 | } 32 | ) 33 | ) 34 | ) 35 | }, 36 | Do( 37 | Foreach( 38 | Var("products"), 39 | Lambda( 40 | "product", 41 | If( 42 | LTE( 43 | Select("requestedQuantity", Var("product")), 44 | Select("currentQuantity", Var("product")) 45 | ), 46 | Var("product"), 47 | Abort( 48 | Concat([ 49 | "Stock quantity for Product [", 50 | Select(["ref", "id"], Var("product")), 51 | "] not enough – requested at [", 52 | ToString(Time("now")), 53 | "]" 54 | ]) 55 | ) 56 | ) 57 | ) 58 | ), 59 | Foreach( 60 | Var("products"), 61 | Lambda( 62 | "product", 63 | Update(Select("ref", Var("product")), { 64 | data: { 65 | quantity: Subtract( 66 | Select("currentQuantity", Var("product")), 67 | Select("requestedQuantity", Var("product")) 68 | ) 69 | } 70 | }) 71 | ) 72 | ), 73 | Foreach( 74 | Var("products"), 75 | Lambda( 76 | "product", 77 | If( 78 | LTE( 79 | Subtract( 80 | Select("currentQuantity", Var("product")), 81 | Select("requestedQuantity", Var("product")) 82 | ), 83 | Select("backorderLimit", Var("product")) 84 | ), 85 | Update(Select("ref", Var("product")), { 86 | data: { backordered: true } 87 | }), 88 | Var("product") 89 | ) 90 | ) 91 | ), 92 | Let( 93 | { 94 | shoppingCart: Map( 95 | Var("products"), 96 | Lambda("product", { 97 | product: Select("ref", Var("product")), 98 | quantity: Select("requestedQuantity", Var("product")), 99 | price: Select("price", Var("product")) 100 | }) 101 | ) 102 | }, 103 | Create(Collection("orders"), { 104 | data: { 105 | customer: Select("ref", Var("customer")), 106 | cart: Var("shoppingCart"), 107 | status: "processing", 108 | creationDate: Time("now"), 109 | shipDate: null, 110 | deliveryAddress: Select(["data", "address"], Var("customer")), 111 | creditCard: Select(["data", "creditCard"], Var("customer")) 112 | } 113 | }) 114 | ) 115 | ) 116 | ) 117 | ) -------------------------------------------------------------------------------- /functions/customer_orders.js: -------------------------------------------------------------------------------- 1 | // Copyright Fauna, Inc. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | const { query: q } = require('faunadb') 5 | const { client } = require('../db/client') 6 | 7 | module.exports.list = (event) => { 8 | return client 9 | .query( 10 | q.Map( 11 | q.Paginate( 12 | q.Match(q.Index(process.env.orders_by_customer), q.CurrentIdentity()) 13 | ), 14 | (ref) => q.Get(ref) 15 | ), 16 | { secret: event.headers.secret } 17 | ) 18 | .then((body) => ({ 19 | statusCode: 200, 20 | body: JSON.stringify(body), 21 | })) 22 | .catch((error) => ({ 23 | statusCode: error.requestResult.statusCode, 24 | body: error.requestResult.responseRaw, 25 | })) 26 | } 27 | -------------------------------------------------------------------------------- /functions/list_products.js: -------------------------------------------------------------------------------- 1 | // Copyright Fauna, Inc. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | const { query: q } = require('faunadb') 5 | const { client } = require('../db/client') 6 | 7 | const MatchByStore = (storeId) => 8 | q.Match( 9 | q.Index(process.env.products_by_store), 10 | q.Ref(q.Collection(process.env.stores), storeId) 11 | ) 12 | 13 | const MatchByCustomer = (customerId) => 14 | q.Match( 15 | q.Index(process.env.products_by_customer), 16 | q.Ref(q.Collection(process.env.customers), customerId) 17 | ) 18 | 19 | const GetMatch = ({ storeId, customerId }) => { 20 | const conditions = [ 21 | storeId && MatchByStore(storeId), 22 | customerId && MatchByCustomer(customerId), 23 | ].filter((c) => c) 24 | 25 | switch (conditions.length) { 26 | case 0: 27 | return q.Match(q.Index(process.env.all_products)) 28 | case 1: 29 | return conditions[0] 30 | default: 31 | return q.Intersection(conditions) 32 | } 33 | } 34 | 35 | const SortByPrice = (order) => 36 | q.Index( 37 | order === 'low-to-high' 38 | ? process.env.products_by_price_low_to_high 39 | : process.env.products_by_price_high_to_low 40 | ) 41 | 42 | module.exports.list = async (event) => { 43 | const { storeId, customerId, priceSort } = event.queryStringParameters || {} 44 | 45 | const Match = GetMatch({ storeId, customerId }) 46 | const MatchAndSort = priceSort ? q.Join(Match, SortByPrice(priceSort)) : Match 47 | const MapLambda = priceSort ? (_, ref) => q.Get(ref) : (ref) => q.Get(ref) 48 | 49 | return client 50 | .query(q.Map(q.Paginate(MatchAndSort), MapLambda), { 51 | secret: event.headers.secret, 52 | }) 53 | .then((body) => ({ 54 | statusCode: 200, 55 | body: JSON.stringify(body), 56 | })) 57 | .catch((error) => ({ 58 | statusCode: error.requestResult.statusCode, 59 | body: error.requestResult.responseRaw, 60 | })) 61 | } 62 | -------------------------------------------------------------------------------- /functions/login.js: -------------------------------------------------------------------------------- 1 | // Copyright Fauna, Inc. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | const { query: q } = require('faunadb') 5 | const { client } = require('../db/client') 6 | 7 | module.exports.login = async (event) => { 8 | const { email, password } = JSON.parse(event.body) 9 | 10 | return client 11 | .query( 12 | q.Login(q.Match(q.Index(process.env.customer_by_email), email), { 13 | password, 14 | }) 15 | ) 16 | .then(({ secret, instance }) => ({ 17 | statusCode: 200, 18 | body: JSON.stringify({ 19 | secret, 20 | id: instance.id, 21 | }), 22 | })) 23 | .catch((error) => ({ 24 | statusCode: error.requestResult.statusCode, 25 | body: error.requestResult.responseRaw, 26 | })) 27 | } 28 | -------------------------------------------------------------------------------- /functions/register.js: -------------------------------------------------------------------------------- 1 | // Copyright Fauna, Inc. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | const { query: q } = require('faunadb') 5 | const { client } = require('../db/client') 6 | 7 | module.exports.create = async (event) => { 8 | const { password, ...data } = JSON.parse(event.body) 9 | 10 | return client 11 | .query( 12 | q.If( 13 | q.Exists(q.Match(q.Index(process.env.customer_by_email), data.email)), 14 | q.Abort('Email occupied'), 15 | q.Create(q.Collection(process.env.customers), { 16 | credentials: { password }, 17 | data, 18 | }) 19 | ) 20 | ) 21 | .then((body) => ({ 22 | statusCode: 200, 23 | body: JSON.stringify(body), 24 | })) 25 | .catch((error) => ({ 26 | statusCode: error.requestResult.statusCode, 27 | body: error.requestResult.responseRaw, 28 | })) 29 | } 30 | -------------------------------------------------------------------------------- /functions/seed.js: -------------------------------------------------------------------------------- 1 | // Copyright Fauna, Inc. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | const { query: q } = require('faunadb') 5 | const { client } = require('../db/client') 6 | 7 | const customers = require('../seed/customers') 8 | const orders = require('../seed/orders') 9 | const products = require('../seed/products') 10 | const stores = require('../seed/stores') 11 | 12 | module.exports.seed = async () => { 13 | try { 14 | await client.query( 15 | q.Map([...customers, ...orders, ...products, ...stores], (doc) => 16 | q.Create(q.Select(['ref'], doc), { data: q.Select(['data'], doc) }) 17 | ) 18 | ) 19 | 20 | return { msg: 'success' } 21 | } catch (error) { 22 | return { error } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /functions/submit_order.js: -------------------------------------------------------------------------------- 1 | // Copyright Fauna, Inc. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | const { query: q } = require('faunadb') 5 | const { client } = require('../db/client') 6 | 7 | module.exports.submit = async (event) => { 8 | const { products } = JSON.parse(event.body) 9 | 10 | return client 11 | .query(q.Call(process.env.submit_order, [q.CurrentIdentity(), products]), { 12 | secret: event.headers.secret, 13 | }) 14 | .then((body) => ({ 15 | statusCode: 200, 16 | body: JSON.stringify(body), 17 | })) 18 | .catch((error) => ({ 19 | statusCode: error.requestResult.statusCode, 20 | body: error.requestResult.responseRaw, 21 | })) 22 | } 23 | -------------------------------------------------------------------------------- /jsonSchemas/login.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "email": { 5 | "type": "string" 6 | }, 7 | "password": { 8 | "type": "string" 9 | } 10 | }, 11 | "required": [ 12 | "email", 13 | "password" 14 | ] 15 | } -------------------------------------------------------------------------------- /jsonSchemas/register.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "email": { 5 | "type": "string" 6 | }, 7 | "password": { 8 | "type": "string" 9 | }, 10 | "creditCard": { 11 | "type": "object", 12 | "properties": { 13 | "network": { 14 | "type": "string" 15 | }, 16 | "number": { 17 | "type": "string" 18 | } 19 | }, 20 | "required": [ 21 | "network", 22 | "number" 23 | ] 24 | }, 25 | "address": { 26 | "type": "object", 27 | "properties": { 28 | "street": { 29 | "type": "string" 30 | }, 31 | "city": { 32 | "type": "string" 33 | }, 34 | "state": { 35 | "type": "string" 36 | }, 37 | "zipCode": { 38 | "type": "string" 39 | } 40 | }, 41 | "required": [ 42 | "street", 43 | "city", 44 | "state", 45 | "zipCode" 46 | ] 47 | } 48 | }, 49 | "required": [ 50 | "email", 51 | "password", 52 | "address" 53 | ] 54 | } -------------------------------------------------------------------------------- /jsonSchemas/submit_order.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "products": { 5 | "type": "array", 6 | "items": { 7 | "type": "object", 8 | "properties": { 9 | "productId": { 10 | "type": "string" 11 | }, 12 | "quantity": { 13 | "type": "number" 14 | } 15 | }, 16 | "required": [ 17 | "productId", 18 | "quantity" 19 | ] 20 | } 21 | } 22 | }, 23 | "required": [ 24 | "products" 25 | ] 26 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fauna-labs/serverless-fauna-example", 3 | "version": "0.1.1", 4 | "description": "A sample application demonstrating how to use the Fauna plugin for the Serverless Framework.", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/fauna-labs/serverless-fauna-example.git" 12 | }, 13 | "keywords": [ 14 | "Fauna", 15 | "Serverless Framework", 16 | "serverless", 17 | "plugin", 18 | "database" 19 | ], 20 | "author": "Fauna Inc.", 21 | "engines": { 22 | "node": ">=12.0" 23 | }, 24 | "license": "MIT-0", 25 | "bugs": { 26 | "url": "https://github.com/fauna-labs/serverless-fauna-example/issues" 27 | }, 28 | "homepage": "https://github.com/fauna-labs/serverless-fauna-example#readme", 29 | "dependencies": { 30 | "faunadb": "^4.4.1", 31 | "serverless": "^2.46.0" 32 | }, 33 | "devDependencies": { 34 | "@fauna-labs/serverless-fauna": "^0.1.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /seed/customers.js: -------------------------------------------------------------------------------- 1 | // Copyright Fauna, Inc. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | const fauna = require('faunadb') 5 | const { Ref, Collection } = fauna.query 6 | 7 | module.exports = [ 8 | { 9 | ref: Ref(Collection('customers'), '101'), 10 | data: { 11 | email: 'alice.appleseed@unknown.com', 12 | firstName: 'Alice', 13 | lastName: 'Appleseed', 14 | address: { 15 | street: '87856 Mendota Court', 16 | city: 'Washington', 17 | state: 'DC', 18 | zipCode: '20220', 19 | }, 20 | telephone: '208-346-0715', 21 | creditCard: { 22 | network: 'Visa', 23 | number: '4556781272473393', 24 | }, 25 | }, 26 | }, 27 | { 28 | ref: Ref(Collection('customers'), '102'), 29 | data: { 30 | email: 'bob.brown@unknown.com', 31 | firstName: 'Bob', 32 | lastName: 'Brown', 33 | address: { 34 | street: '72 Waxwing Terrace', 35 | city: 'Washington', 36 | state: 'DC', 37 | zipCode: '20002', 38 | }, 39 | telephone: '719-872-8799', 40 | creditCard: { 41 | network: 'Visa', 42 | number: '4916112310613672', 43 | }, 44 | }, 45 | }, 46 | { 47 | ref: Ref(Collection('customers'), '103'), 48 | data: { 49 | email: 'carol.clark@unknown.com', 50 | firstName: 'Carol', 51 | lastName: 'Clark', 52 | address: { 53 | street: '5 Troy Trail', 54 | city: 'Washington', 55 | state: 'DC', 56 | zipCode: '20220', 57 | }, 58 | telephone: '907-949-4470', 59 | creditCard: { 60 | network: 'Visa', 61 | number: '4532636730015542', 62 | }, 63 | }, 64 | }, 65 | ] 66 | -------------------------------------------------------------------------------- /seed/orders.js: -------------------------------------------------------------------------------- 1 | // Copyright Fauna, Inc. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | const fauna = require('faunadb') 5 | const { Ref, Collection, Time } = fauna.query 6 | 7 | module.exports = [ 8 | { 9 | ref: Ref(Collection('orders'), '299005128666513927'), 10 | data: { 11 | customer: Ref(Collection('customers'), '103'), 12 | cart: [ 13 | { 14 | product: Ref(Collection('products'), '201'), 15 | quantity: 25, 16 | price: 6.98, 17 | }, 18 | { 19 | product: Ref(Collection('products'), '203'), 20 | quantity: 10, 21 | price: 4.99, 22 | }, 23 | ], 24 | status: 'processing', 25 | creationDate: Time('2021-05-19T08:19:21.303607Z'), 26 | deliveryAddress: { 27 | street: '5 Troy Trail', 28 | city: 'Washington', 29 | state: 'DC', 30 | zipCode: '20220', 31 | }, 32 | creditCard: { 33 | network: 'Visa', 34 | number: '4532636730015542', 35 | }, 36 | }, 37 | }, 38 | { 39 | ref: Ref(Collection('orders'), '299005128666514951'), 40 | data: { 41 | customer: Ref(Collection('customers'), '102'), 42 | cart: [ 43 | { 44 | product: Ref(Collection('products'), '203'), 45 | quantity: 15, 46 | price: 4.99, 47 | }, 48 | { 49 | product: Ref(Collection('products'), '202'), 50 | quantity: 45, 51 | price: 24.99, 52 | }, 53 | ], 54 | status: 'processing', 55 | creationDate: Time('2021-05-19T08:19:21.303607Z'), 56 | deliveryAddress: { 57 | street: '72 Waxwing Terrace', 58 | city: 'Washington', 59 | state: 'DC', 60 | zipCode: '20002', 61 | }, 62 | creditCard: { 63 | network: 'Visa', 64 | number: '4916112310613672', 65 | }, 66 | }, 67 | }, 68 | { 69 | ref: Ref(Collection('orders'), '299005128666515975'), 70 | data: { 71 | customer: Ref(Collection('customers'), '101'), 72 | cart: [ 73 | { 74 | product: Ref(Collection('products'), '204'), 75 | quantity: 10, 76 | price: 3.99, 77 | }, 78 | { 79 | product: Ref(Collection('products'), '206'), 80 | quantity: 5, 81 | price: 3.49, 82 | }, 83 | { 84 | product: Ref(Collection('products'), '208'), 85 | quantity: 20, 86 | price: 1.49, 87 | }, 88 | ], 89 | status: 'processing', 90 | creationDate: Time('2021-05-19T08:19:21.303607Z'), 91 | deliveryAddress: { 92 | street: '87856 Mendota Court', 93 | city: 'Washington', 94 | state: 'DC', 95 | zipCode: '20220', 96 | }, 97 | creditCard: { 98 | network: 'Visa', 99 | number: '4556781272473393', 100 | }, 101 | }, 102 | }, 103 | ] 104 | -------------------------------------------------------------------------------- /seed/products.js: -------------------------------------------------------------------------------- 1 | // Copyright Fauna, Inc. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | const fauna = require('faunadb') 5 | const { Ref, Collection } = fauna.query 6 | 7 | module.exports = [ 8 | { 9 | ref: Ref(Collection('products'), '201'), 10 | ts: 1621412361760000, 11 | data: { 12 | name: 'cups', 13 | description: 'Translucent 9 Oz, 100 ct', 14 | price: 6.98, 15 | quantity: 100, 16 | store: Ref(Collection('stores'), '302'), 17 | backorderLimit: 5, 18 | backordered: false, 19 | }, 20 | }, 21 | { 22 | ref: Ref(Collection('products'), '202'), 23 | ts: 1621412361760000, 24 | data: { 25 | name: 'pinata', 26 | description: 'Original Classic Donkey Pinata', 27 | price: 24.99, 28 | quantity: 20, 29 | store: Ref(Collection('stores'), '302'), 30 | backorderLimit: 10, 31 | backordered: false, 32 | }, 33 | }, 34 | { 35 | ref: Ref(Collection('products'), '203'), 36 | ts: 1621412361760000, 37 | data: { 38 | name: 'pizza', 39 | description: 'Frozen Cheese', 40 | price: 4.99, 41 | quantity: 100, 42 | store: Ref(Collection('stores'), '303'), 43 | backorderLimit: 15, 44 | backordered: false, 45 | }, 46 | }, 47 | { 48 | ref: Ref(Collection('products'), '204'), 49 | ts: 1621412361760000, 50 | data: { 51 | name: 'avocados', 52 | description: 'Conventional Hass, 4ct bag', 53 | price: 3.99, 54 | quantity: 1000, 55 | store: Ref(Collection('stores'), '301'), 56 | backorderLimit: 15, 57 | backordered: false, 58 | }, 59 | }, 60 | { 61 | ref: Ref(Collection('products'), '205'), 62 | ts: 1621412361760000, 63 | data: { 64 | name: 'limes', 65 | description: 'Conventional, 1 ct', 66 | price: 0.35, 67 | quantity: 1000, 68 | store: Ref(Collection('stores'), '301'), 69 | backorderLimit: 15, 70 | backordered: false, 71 | }, 72 | }, 73 | { 74 | ref: Ref(Collection('products'), '206'), 75 | ts: 1621412361760000, 76 | data: { 77 | name: 'limes', 78 | description: 'Organic, 16 oz bag', 79 | price: 3.49, 80 | quantity: 50, 81 | store: Ref(Collection('stores'), '302'), 82 | backorderLimit: 15, 83 | backordered: false, 84 | }, 85 | }, 86 | { 87 | ref: Ref(Collection('products'), '207'), 88 | ts: 1621412361760000, 89 | data: { 90 | name: 'limes', 91 | description: 'Conventional, 16 oz bag', 92 | price: 2.99, 93 | quantity: 30, 94 | store: Ref(Collection('stores'), '303'), 95 | backorderLimit: 15, 96 | backordered: false, 97 | }, 98 | }, 99 | { 100 | ref: Ref(Collection('products'), '208'), 101 | ts: 1621412361760000, 102 | data: { 103 | name: 'cilantro', 104 | description: 'Organic, 1 bunch', 105 | price: 1.49, 106 | quantity: 100, 107 | store: Ref(Collection('stores'), '301'), 108 | backorderLimit: 15, 109 | backordered: false, 110 | }, 111 | }, 112 | { 113 | ref: Ref(Collection('products'), '209'), 114 | ts: 1621412361760000, 115 | data: { 116 | name: 'pinata', 117 | description: 'Giant Taco Pinata', 118 | price: 23.99, 119 | quantity: 10, 120 | store: Ref(Collection('stores'), '302'), 121 | backorderLimit: 10, 122 | backordered: false, 123 | }, 124 | }, 125 | ] 126 | -------------------------------------------------------------------------------- /seed/stores.js: -------------------------------------------------------------------------------- 1 | // Copyright Fauna, Inc. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | const fauna = require('faunadb') 5 | const { Ref, Collection } = fauna.query 6 | 7 | module.exports = [ 8 | { 9 | ref: Ref(Collection('stores'), '301'), 10 | ts: 1621412361760000, 11 | data: { 12 | name: 'DC Fruits', 13 | address: { 14 | street: '13 Pierstorff Drive', 15 | city: 'Washington', 16 | state: 'DC', 17 | zipCode: '20220', 18 | }, 19 | }, 20 | }, 21 | { 22 | ref: Ref(Collection('stores'), '302'), 23 | ts: 1621412361760000, 24 | data: { 25 | name: 'Party Supplies', 26 | address: { 27 | street: '7529 Capitalsaurus Court', 28 | city: 'Washington', 29 | state: 'DC', 30 | zipCode: '20002', 31 | }, 32 | }, 33 | }, 34 | { 35 | ref: Ref(Collection('stores'), '303'), 36 | ts: 1621412361760000, 37 | data: { 38 | name: 'Foggy Bottom Market', 39 | address: { 40 | street: '4 Florida Ave', 41 | city: 'Washington', 42 | state: 'DC', 43 | zipCode: '20037', 44 | }, 45 | }, 46 | }, 47 | ] 48 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | # Copyright Fauna, Inc. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | service: serverless-fauna-example 5 | configValidationMode: error 6 | useDotenv: true 7 | 8 | ## If you use Serverless Framework Pro, uncomment the following two lines and replace with your app name and org. 9 | # app: 10 | # org: 11 | 12 | provider: 13 | name: aws 14 | runtime: nodejs12.x 15 | lambdaHashingVersion: 20201221 16 | environment: 17 | FAUNA_SECRET: ${self:fauna.client.secret} 18 | FAUNA_DOMAIN: ${self:fauna.client.domain} 19 | FAUNA_SCHEME: ${self:fauna.client.scheme} 20 | FAUNA_PORT: ${self:fauna.client.port} 21 | 22 | plugins: 23 | - "@fauna-labs/serverless-fauna" 24 | 25 | functions: 26 | seed: 27 | handler: functions/seed.seed 28 | 29 | list_products: 30 | handler: functions/list_products.list 31 | events: 32 | - http: 33 | method: get 34 | path: /products 35 | cors: true 36 | request: 37 | parameters: 38 | headers: 39 | secret: true 40 | querystrings: 41 | priceSort: false 42 | customerId: false 43 | storeId: false 44 | environment: 45 | products_by_store: ${self:fauna.indexes.products_by_store.name} 46 | products_by_customer: ${self:fauna.indexes.products_by_customer.name} 47 | all_products: ${self:fauna.indexes.all_products.name} 48 | products_by_price_low_to_high: ${self:fauna.indexes.products_by_price_low_to_high.name} 49 | products_by_price_high_to_low: ${self:fauna.indexes.products_by_price_high_to_low.name} 50 | stores: ${self:fauna.collections.stores.name} 51 | customers: ${self:fauna.collections.customers.name} 52 | 53 | register: 54 | handler: functions/register.create 55 | events: 56 | - http: 57 | method: post 58 | path: /customers 59 | cors: true 60 | request: 61 | schemas: 62 | application/json: ${file(./jsonSchemas/register.json)} 63 | environment: 64 | customer_by_email: ${self:fauna.indexes.customer_by_email.name} 65 | customers: ${self:fauna.collections.customers.name} 66 | 67 | login: 68 | handler: functions/login.login 69 | events: 70 | - http: 71 | method: post 72 | path: /customers/login 73 | cors: true 74 | request: 75 | schemas: 76 | application/json: ${file(./jsonSchemas/login.json)} 77 | environment: 78 | customer_by_email: ${self:fauna.indexes.customer_by_email.name} 79 | 80 | submit_order: 81 | handler: functions/submit_order.submit 82 | events: 83 | - http: 84 | method: post 85 | path: /orders 86 | cors: true 87 | request: 88 | parameters: 89 | headers: 90 | secret: true 91 | schemas: 92 | application/json: ${file(./jsonSchemas/submit_order.json)} 93 | environment: 94 | submit_order: ${self:fauna.functions.submit_order.name} 95 | 96 | customer_orders: 97 | handler: functions/customer_orders.list 98 | events: 99 | - http: 100 | method: get 101 | path: /orders 102 | cors: true 103 | request: 104 | parameters: 105 | headers: 106 | secret: true 107 | environment: 108 | orders_by_customer: ${self:fauna.indexes.orders_by_customer.name} 109 | 110 | 111 | fauna: 112 | # The following declarations create the same collections, indexes, roles, and functions that 113 | # are created if you select "Pre-populate with demo data" when creating a new database in the 114 | # Fauna dashboard (https://dashboard.fauna.com). 115 | # 116 | # To populate the collections with the same demo data, deploy this application, 117 | # then invoke the `seed` function: 118 | # serverless deploy 119 | # serverless invoke --function seed 120 | client: 121 | secret: ${env:FAUNA_SECRET} 122 | domain: ${env:FAUNA_DOMAIN, "db.fauna.com"} 123 | scheme: ${env:FAUNA_SCHEME, "https"} 124 | port: ${env:FAUNA_PORT, "443"} 125 | collections: 126 | # Collections accept any parameter that CreateCollection() accepts. 127 | # For more details on CreateCollection(), read https://docs.fauna.com/fauna/current/api/fql/functions/createcollection?lang=javascript#param_object 128 | customers: 129 | name: customers 130 | data: 131 | # Setting 'deletion_policy' to 'retain' leaves a resource intact if the entire application is removed with `serverless remove`. 132 | # Typically you would set this to 'retain' for a production environment and leave it unset for environments that are 133 | # frequently created and destroyed. 134 | deletion_policy: retain 135 | 136 | orders: 137 | name: orders 138 | 139 | products: 140 | name: products 141 | 142 | stores: 143 | name: stores 144 | 145 | indexes: 146 | # Indexes accept any parameter that CreateIndex() accepts 147 | # For more details on CreateIndex(), read https://docs.fauna.com/fauna/current/api/fql/functions/createindex?lang=javascript#param_object 148 | orders_by_customer: 149 | name: orders_by_customer 150 | source: ${self:fauna.collections.orders.name} 151 | terms: 152 | fields: 153 | - data.customer 154 | 155 | products_by_customer: 156 | name: products_by_customer 157 | source: ${self:fauna.collections.orders.name} 158 | terms: 159 | fields: 160 | - data.customer 161 | values: 162 | fields: 163 | - data.cart.product 164 | 165 | products_by_store: 166 | name: products_by_store 167 | source: ${self:fauna.collections.products.name} 168 | terms: 169 | fields: 170 | - data.store 171 | values: 172 | fields: 173 | - ref 174 | 175 | products_by_price_high_to_low: 176 | name: products_by_price_high_to_low 177 | source: ${self:fauna.collections.products.name} 178 | terms: 179 | fields: 180 | - ref 181 | values: 182 | fields: 183 | - path: data.price 184 | reverse: true 185 | - ref 186 | 187 | products_by_price_low_to_high: 188 | name: products_by_price_low_to_high 189 | source: ${self:fauna.collections.products.name} 190 | terms: 191 | fields: 192 | - ref 193 | values: 194 | fields: 195 | - data.price 196 | - ref 197 | 198 | customer_by_email: 199 | name: customer_by_email 200 | source: ${self:fauna.collections.customers.name} 201 | data: 202 | deletion_policy: retain 203 | terms: 204 | fields: 205 | - data.email 206 | 207 | all_orders: 208 | name: all_orders 209 | source: ${self:fauna.collections.orders.name} 210 | 211 | all_customers: 212 | name: all_customers 213 | source: ${self:fauna.collections.customers.name} 214 | 215 | all_stores: 216 | name: all_stores 217 | source: ${self:fauna.collections.stores.name} 218 | 219 | all_products: 220 | name: all_products 221 | source: ${self:fauna.collections.products.name} 222 | 223 | functions: 224 | submit_order: 225 | name: submit_order 226 | body: ${file(./fql/SubmitOrder.fql)} 227 | role: admin 228 | 229 | 230 | roles: 231 | # Roles accept any parameter that CreateRole() accepts 232 | # For more details on CreateRole(), read https://docs.fauna.com/fauna/current/api/fql/functions/createrole?lang=javascript 233 | customer: 234 | name: customer 235 | membership: ${self:fauna.collections.customers.name} 236 | privileges: 237 | - collection: ${self:fauna.collections.products.name} 238 | actions: 239 | read: true 240 | - collection: ${self:fauna.collections.stores.name} 241 | actions: 242 | read: true 243 | - collection: ${self:fauna.collections.orders.name} 244 | actions: 245 | read: ${file(./fql/Owner.fql)} 246 | - index: ${self:fauna.indexes.all_products.name} 247 | actions: 248 | read: true 249 | - index: ${self:fauna.indexes.products_by_store.name} 250 | actions: 251 | read: true 252 | - index: ${self:fauna.indexes.products_by_price_low_to_high.name} 253 | actions: 254 | read: true 255 | - index: ${self:fauna.indexes.products_by_price_high_to_low.name} 256 | actions: 257 | read: true 258 | - index: ${self:fauna.indexes.orders_by_customer.name} 259 | actions: 260 | read: ${file(./fql/EqualsCurrentIdentityArity1.fql)} 261 | - index: ${self:fauna.indexes.products_by_customer.name} 262 | actions: 263 | read: ${file(./fql/EqualsCurrentIdentityArity1.fql)} 264 | - function: ${self:fauna.functions.submit_order.name} 265 | actions: 266 | call: ${file(./fql/EqualsCurrentIdentityArity2.fql)} 267 | --------------------------------------------------------------------------------