Full Code of Swizec/serverless-handbook for AI

master 1a5f6e68365e cached
97 files
366.5 KB
100.3k tokens
43 symbols
1 requests
Download .txt
Showing preview only (396K chars total). Download the full file or copy to clipboard to get everything.
Repository: Swizec/serverless-handbook
Branch: master
Commit: 1a5f6e68365e
Files: 97
Total size: 366.5 KB

Directory structure:
gitextract_jy4hurtu/

├── .gitignore
├── .prettierignore
├── .prettierrc
├── .vscode/
│   └── settings.json
├── .yarnrc
├── LICENSE
├── README.md
├── examples/
│   ├── hello-world/
│   │   ├── handler.js
│   │   └── serverless.yml
│   ├── serverless-auth-example/
│   │   ├── .gitignore
│   │   ├── package.json
│   │   ├── serverless.yml
│   │   ├── src/
│   │   │   ├── auth.ts
│   │   │   ├── private.ts
│   │   │   └── util.ts
│   │   └── tsconfig.json
│   ├── serverless-chrome-example/
│   │   ├── dist/
│   │   │   ├── scraper.js
│   │   │   ├── screenshot.js
│   │   │   └── util.js
│   │   ├── package.json
│   │   ├── serverless.yml
│   │   ├── src/
│   │   │   ├── scraper.ts
│   │   │   ├── screenshot.ts
│   │   │   └── util.ts
│   │   └── tsconfig.json
│   ├── serverless-data-pipeline-example/
│   │   ├── .gitignore
│   │   ├── package.json
│   │   ├── serverless.yml
│   │   ├── src/
│   │   │   ├── reduce.ts
│   │   │   ├── sumArray.ts
│   │   │   ├── timesTwo.ts
│   │   │   └── utils.ts
│   │   └── tsconfig.json
│   ├── serverless-graphql-example/
│   │   ├── dist/
│   │   │   ├── dynamodb.js
│   │   │   ├── graphql.js
│   │   │   ├── manageItems.js
│   │   │   ├── mutations.js
│   │   │   └── queries.js
│   │   ├── package.json
│   │   ├── serverless.yml
│   │   ├── src/
│   │   │   ├── graphql.ts
│   │   │   ├── mutations.ts
│   │   │   ├── queries.ts
│   │   │   └── types.d.ts
│   │   └── tsconfig.json
│   └── serverless-rest-example/
│       ├── dist/
│       │   ├── dynamodb.js
│       │   └── manageItems.js
│       ├── package.json
│       ├── serverless.yml
│       ├── src/
│       │   ├── dynamodb.ts
│       │   ├── manageItems.ts
│       │   └── types.d.ts
│       └── tsconfig.json
├── gatsby-browser.js
├── gatsby-config.js
├── now.json
├── package.json
├── src/
│   ├── @swizec/
│   │   └── gatsby-theme-course-platform/
│   │       ├── components/
│   │       │   ├── FormCK/
│   │       │   │   ├── formsQuery.js
│   │       │   │   └── useFormsQuery.js
│   │       │   ├── headerLogo.js
│   │       │   ├── layout.js
│   │       │   └── nav.mdx
│   │       └── constants/
│   │           └── footerLinks.js
│   ├── components/
│   │   ├── ClaimForm.js
│   │   ├── Paywall.js
│   │   ├── TestCloudFunctions.js
│   │   ├── homepage.js
│   │   ├── logo.js
│   │   ├── paywall-copy.mdx
│   │   ├── quickthanks.mdx
│   │   └── useLocalStorage.js
│   ├── gatsby-plugin-theme-ui/
│   │   └── index.js
│   └── pages/
│       ├── 404.mdx
│       ├── appendix-more-databases/
│       │   └── index.mdx
│       ├── claim.mdx
│       ├── databases/
│       │   └── index.mdx
│       ├── dev-qa-prod/
│       │   └── index.mdx
│       ├── downloads/
│       │   └── index.mdx
│       ├── getting-started/
│       │   └── index.mdx
│       ├── glossary.mdx
│       ├── handling-secrets/
│       │   └── index.mdx
│       ├── index.mdx
│       ├── lambda-pipelines/
│       │   └── index.mdx
│       ├── robust-backend-design/
│       │   └── index.mdx
│       ├── serverless-architecture-principles/
│       │   └── index.mdx
│       ├── serverless-authentication/
│       │   └── index.mdx
│       ├── serverless-chrome-puppeteer/
│       │   └── index.mdx
│       ├── serverless-dx/
│       │   └── index.mdx
│       ├── serverless-elements/
│       │   └── index.mdx
│       ├── serverless-flavors/
│       │   └── index.mdx
│       ├── serverless-graphql/
│       │   └── index.mdx
│       ├── serverless-monitoring/
│       │   └── index.mdx
│       ├── serverless-performance/
│       │   └── index.mdx
│       ├── serverless-pros-cons/
│       │   └── index.mdx
│       ├── serverless-rest-api/
│       │   └── index.mdx
│       └── thanks.mdx
└── static/
    └── _redirects

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
package-lock.json

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Typescript v1 declaration files
typings/

# Optional npm cache directory
.npm
.npmrc

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# dotenv environment variables file
.env
.env.development

# gatsby files
.cache/
public

# Mac files
.DS_Store

# Yarn
yarn-error.log
.pnp/
.pnp.js
# Yarn Integrity file
.yarn-integrity

.now
.serverless


================================================
FILE: .prettierignore
================================================
.cache
package.json
package-lock.json
public


================================================
FILE: .prettierrc
================================================
{
  "endOfLine": "lf",
  "semi": false,
  "singleQuote": false,
  "tabWidth": 2,
  "trailingComma": "es5"
}


================================================
FILE: .vscode/settings.json
================================================
{
    "python.pythonPath": "/usr/local/bin/python2.7"
}

================================================
FILE: .yarnrc
================================================
"@swizec:registry" "https://registry.npmjs.org/"


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2015 gatsbyjs

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, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

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.



================================================
FILE: README.md
================================================

# Serverless Handbook for frontend engineers

Curious about serverless? Wanna get into backend but not sure how? This handbook is for you. 

Rather than a book you read start to finish, it's meant to work as a reference. A handbook that stands by your side while you work. Need a recipe? Look it up. Got a question on a thorny issue? The handbook hopes to help.

Open source, contributions and fixes welcome :)

================================================
FILE: examples/hello-world/handler.js
================================================
exports.hello = async (event) => {
  const { name = '' } = event.queryStringParameters || {}

  return {
    statusCode: 200,
    body: `Hello ${name} 👋`,
  }
}


================================================
FILE: examples/hello-world/serverless.yml
================================================
service: hello-world
provider:
    name: aws
    runtime: nodejs12.x
    stage: dev

functions:
    hello:
        handler: ./handler.hello
        events:
            - http:
                  path: hello
                  method: GET
                  cors: true


================================================
FILE: examples/serverless-auth-example/.gitignore
================================================
dist
node_modules
.serverless


================================================
FILE: examples/serverless-auth-example/package.json
================================================
{
  "name": "serverless-auth-example",
  "version": "1.0.0",
  "description": "AWS Lambda example of building a simple auth",
  "main": "index.js",
  "scripts": {
    "build": "tsc",
    "deploy": "npm run build && serverless deploy"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/Swizec/serverless-handbook.git"
  },
  "author": "Swizec",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/Swizec/serverless-handbook/issues"
  },
  "homepage": "https://serverlesshandbook.dev",
  "dependencies": {
    "@types/aws-lambda": "^8.10.72",
    "@types/crypto-js": "^4.0.1",
    "@types/jsonwebtoken": "^8.5.0",
    "@types/lodash.omit": "^4.5.6",
    "aws-lambda": "^1.0.6",
    "crypto-js": "^4.0.0",
    "jsonwebtoken": "^8.5.1",
    "lodash.omit": "^4.5.0",
    "simple-dynamodb": "^1.0.1",
    "typescript": "^4.1.5",
    "uuid": "^8.3.2"
  },
  "engines": {
    "node": "12.x || 14.x"
  }
}


================================================
FILE: examples/serverless-auth-example/serverless.yml
================================================
service: serverless-auth-example

provider:
  name: aws
  runtime: nodejs12.x
  stage: dev
  environment:
    USER_TABLE: ${self:service}-users-${self:provider.stage}
    SALT: someRandomSecretString_pleaseUseProperSecrets:)
    JWT_SECRET: useRealSecretsManagementPlease
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.USER_TABLE}"

functions:
  login:
    handler: dist/auth.login
    events:
      - http:
          path: login
          method: POST
          cors: true
  verify:
    handler: dist/auth.verify
    events:
      - http:
          path: verify
          method: POST
          cors: true
  privateHello:
    handler: dist/private.hello
    events:
      - http:
          path: private
          method: GET
          cors: true

resources:
  Resources:
    UsersTable:
      Type: "AWS::DynamoDB::Table"
      Properties:
        AttributeDefinitions:
          - AttributeName: username
            AttributeType: S
        KeySchema:
          - AttributeName: username
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        TableName: ${self:provider.environment.USER_TABLE}

package:
  exclude:
    - node_modules/typescript/**
    - node_modules/@types/**


================================================
FILE: examples/serverless-auth-example/src/auth.ts
================================================
import { APIGatewayEvent } from "aws-lambda"
import * as db from "simple-dynamodb"
import omit from "lodash.omit"
import * as jwt from "jsonwebtoken"
import { response, hashPassword } from "./util"

async function createUser(username: string, password: string) {
  const result = await db.updateItem({
    TableName: process.env.USER_TABLE!,
    Key: {
      username,
    },
    UpdateExpression: `SET password = :password, createdAt = :createdAt`,
    ExpressionAttributeValues: {
      ":password": hashPassword(username, password),
      ":createdAt": new Date().toISOString(),
    },
  })
  return result.Attributes
}

async function findUser(username: string) {
  const result = await db.getItem({
    TableName: process.env.USER_TABLE!,
    Key: {
      // username is the key, which means it must be unique
      username,
    },
  })

  return result.Item
}

// Logs you in based on username/password combo
// Creates user on first login
export const login = async (event: APIGatewayEvent) => {
  const { username, password } = JSON.parse(event.body || "{}")

  if (!username || !password) {
    return response(400, {
      status: "error",
      error: "Please provide a username and password",
    })
  }

  // find user in database
  let user = await findUser(username)

  if (!user) {
    // user was not found, create
    user = await createUser(username, password)
  } else {
    // check credentials
    if (hashPassword(username, password) !== user.password) {
      // 🚨
      return response(401, {
        status: "error",
        error: "Bad username/password combination",
      })
    }
  }

  // user was created or has valid credentials
  const token = jwt.sign(omit(user, "password"), process.env.SUPER_SECRET!)

  return response(200, {
    user: omit(user, "password"),
    token,
  })
}

// Verifies you have a valid JWT token
export const verify = async (event: APIGatewayEvent) => {
  const { token } = JSON.parse(event.body || "{}")

  if (!token) {
    return response(400, {
      status: "error",
      error: "Please provide a token to verify",
    })
  }

  try {
    jwt.verify(token, process.env.JWT_SECRET!)
    return response(200, { status: "valid" })
  } catch (err) {
    return response(401, err)
  }
}


================================================
FILE: examples/serverless-auth-example/src/private.ts
================================================
import { APIGatewayEvent } from "aws-lambda"
import { response, checkAuth, User } from "./util"

export async function hello(event: APIGatewayEvent) {
  // returns JWT token payload
  const user = checkAuth(event) as User

  if (user) {
    return response(200, {
      message: `Hello ${user.username}`,
    })
  } else {
    return response(401, {
      status: "error",
      error: "This is a private resource",
    })
  }
}


================================================
FILE: examples/serverless-auth-example/src/util.ts
================================================
import { APIGatewayEvent } from "aws-lambda"
import sha256 from "crypto-js/sha256"
import * as jwt from "jsonwebtoken"

export function response(statusCode: number, body: any) {
  return {
    statusCode,
    // permissive CORS headers
    headers: {
      "Access-Control-Allow-Headers": "Content-Type",
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "OPTIONS,POST,GET",
    },
    body: JSON.stringify(body),
  }
}

// Hashing your password before saving is critical
// Hashing is one-way meaning you can never guess the password
// Adding a salt and the username guards against common passwords
export function hashPassword(username: string, password: string) {
  return sha256(
    `${password}${process.env.SALT}${username}${password}`
  ).toString()
}

export type User = { username: string; createdAt: string }

// Used to verify a request is authenticated
export function checkAuth(event: APIGatewayEvent): boolean | User {
  const bearer = event.headers["Authorization"]

  if (bearer) {
    try {
      const decoded = jwt.verify(
        // Bearer prefix from Authorization header
        bearer.replace(/^Bearer /, ""),
        process.env.JWT_SECRET!
      )

      // We saved user info in the token
      return decoded as User
    } catch (err) {
      return false
    }
  } else {
    return false
  }
}


================================================
FILE: examples/serverless-auth-example/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ES2019",
    "module": "commonjs",
    "outDir": "./dist",
    "strict": true,
    "baseUrl": "./",
    "typeRoots": ["node_modules/@types"],
    "types": ["node"],
    "esModuleInterop": true,
    "inlineSourceMap": true,
    "lib": ["ES2019", "dom"]
  }
}


================================================
FILE: examples/serverless-chrome-example/dist/scraper.js
================================================
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const util_1 = require("./util");
async function scrapeGoogle(browser, search) {
    const page = await browser.newPage();
    await page.goto("https://google.com", {
        waitUntil: ["domcontentloaded", "networkidle2"],
    });
    // this part is specific to the page you're scraping
    await page.type("input[type=text]", search);
    const [response] = await Promise.all([
        page.waitForNavigation(),
        page.click("input[type=submit]"),
    ]);
    if (!response.ok()) {
        throw "Couldn't get response";
    }
    await page.goto(response.url());
    // this part is very specific to the page you're scraping
    const searchResults = await page.$$(".rc");
    let links = await Promise.all(searchResults.map(async (result) => {
        return {
            url: await result.$eval("a", (node) => node.getAttribute("href")),
            title: await result.$eval("h3", (node) => node.innerHTML),
            description: await result.$eval("span.st", (node) => node.innerHTML),
        };
    }));
    return {
        statusCode: 200,
        body: JSON.stringify(links),
    };
}
exports.handler = util_1.createHandler(scrapeGoogle);
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2NyYXBlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9zY3JhcGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBRUEsaUNBQXNDO0FBRXRDLEtBQUssVUFBVSxZQUFZLENBQUMsT0FBZ0IsRUFBRSxNQUFjO0lBQzFELE1BQU0sSUFBSSxHQUFHLE1BQU0sT0FBTyxDQUFDLE9BQU8sRUFBRSxDQUFBO0lBQ3BDLE1BQU0sSUFBSSxDQUFDLElBQUksQ0FBQyxvQkFBb0IsRUFBRTtRQUNwQyxTQUFTLEVBQUUsQ0FBQyxrQkFBa0IsRUFBRSxjQUFjLENBQUM7S0FDaEQsQ0FBQyxDQUFBO0lBRUYsb0RBQW9EO0lBQ3BELE1BQU0sSUFBSSxDQUFDLElBQUksQ0FBQyxrQkFBa0IsRUFBRSxNQUFNLENBQUMsQ0FBQTtJQUUzQyxNQUFNLENBQUMsUUFBUSxDQUFDLEdBQUcsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDO1FBQ25DLElBQUksQ0FBQyxpQkFBaUIsRUFBRTtRQUN4QixJQUFJLENBQUMsS0FBSyxDQUFDLG9CQUFvQixDQUFDO0tBQ2pDLENBQUMsQ0FBQTtJQUVGLElBQUksQ0FBQyxRQUFRLENBQUMsRUFBRSxFQUFFLEVBQUU7UUFDbEIsTUFBTSx1QkFBdUIsQ0FBQTtLQUM5QjtJQUVELE1BQU0sSUFBSSxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsR0FBRyxFQUFFLENBQUMsQ0FBQTtJQUUvQix5REFBeUQ7SUFDekQsTUFBTSxhQUFhLEdBQUcsTUFBTSxJQUFJLENBQUMsRUFBRSxDQUFDLEtBQUssQ0FBQyxDQUFBO0lBRTFDLElBQUksS0FBSyxHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FDM0IsYUFBYSxDQUFDLEdBQUcsQ0FBQyxLQUFLLEVBQUUsTUFBTSxFQUFFLEVBQUU7UUFDakMsT0FBTztZQUNMLEdBQUcsRUFBRSxNQUFNLE1BQU0sQ0FBQyxLQUFLLENBQUMsR0FBRyxFQUFFLENBQUMsSUFBSSxFQUFFLEVBQUUsQ0FBQyxJQUFJLENBQUMsWUFBWSxDQUFDLE1BQU0sQ0FBQyxDQUFDO1lBQ2pFLEtBQUssRUFBRSxNQUFNLE1BQU0sQ0FBQyxLQUFLLENBQUMsSUFBSSxFQUFFLENBQUMsSUFBSSxFQUFFLEVBQUUsQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDO1lBQ3pELFdBQVcsRUFBRSxNQUFNLE1BQU0sQ0FBQyxLQUFLLENBQUMsU0FBUyxFQUFFLENBQUMsSUFBSSxFQUFFLEVBQUUsQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDO1NBQ3JFLENBQUE7SUFDSCxDQUFDLENBQUMsQ0FDSCxDQUFBO0lBRUQsT0FBTztRQUNMLFVBQVUsRUFBRSxHQUFHO1FBQ2YsSUFBSSxFQUFFLElBQUksQ0FBQyxTQUFTLENBQUMsS0FBSyxDQUFDO0tBQzVCLENBQUE7QUFDSCxDQUFDO0FBRVksUUFBQSxPQUFPLEdBQUcsb0JBQWEsQ0FBQyxZQUFZLENBQUMsQ0FBQSJ9

================================================
FILE: examples/serverless-chrome-example/dist/screenshot.js
================================================
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = __importDefault(require("fs"));
const util_1 = require("./util");
async function screenshotGoogle(browser, search) {
    const page = await browser.newPage();
    await page.goto("https://google.com", {
        waitUntil: ["domcontentloaded", "networkidle2"],
    });
    // this part is specific to the page you're screenshotting
    await page.type("input[type=text]", search);
    const [response] = await Promise.all([
        page.waitForNavigation(),
        page.click("input[type=submit]"),
    ]);
    if (!response.ok()) {
        throw "Couldn't get response";
    }
    await page.goto(response.url());
    // this part is specific to the page you're screenshotting
    const element = await page.$("#main");
    if (!element) {
        throw "Couldn't find results div";
    }
    const boundingBox = await element.boundingBox();
    const imagePath = `/tmp/screenshot-${new Date().getTime()}.png`;
    if (!boundingBox) {
        throw "Couldn't measure size of results div";
    }
    await page.screenshot({
        path: imagePath,
        clip: boundingBox,
    });
    const data = fs_1.default.readFileSync(imagePath).toString("base64");
    return {
        statusCode: 200,
        headers: {
            "Content-Type": "image/png",
        },
        body: data,
        isBase64Encoded: true,
    };
}
exports.handler = util_1.createHandler(screenshotGoogle);
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2NyZWVuc2hvdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9zY3JlZW5zaG90LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7O0FBQ0EsNENBQW1CO0FBRW5CLGlDQUFzQztBQUV0QyxLQUFLLFVBQVUsZ0JBQWdCLENBQUMsT0FBZ0IsRUFBRSxNQUFjO0lBQzlELE1BQU0sSUFBSSxHQUFHLE1BQU0sT0FBTyxDQUFDLE9BQU8sRUFBRSxDQUFBO0lBQ3BDLE1BQU0sSUFBSSxDQUFDLElBQUksQ0FBQyxvQkFBb0IsRUFBRTtRQUNwQyxTQUFTLEVBQUUsQ0FBQyxrQkFBa0IsRUFBRSxjQUFjLENBQUM7S0FDaEQsQ0FBQyxDQUFBO0lBRUYsMERBQTBEO0lBQzFELE1BQU0sSUFBSSxDQUFDLElBQUksQ0FBQyxrQkFBa0IsRUFBRSxNQUFNLENBQUMsQ0FBQTtJQUUzQyxNQUFNLENBQUMsUUFBUSxDQUFDLEdBQUcsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDO1FBQ25DLElBQUksQ0FBQyxpQkFBaUIsRUFBRTtRQUN4QixJQUFJLENBQUMsS0FBSyxDQUFDLG9CQUFvQixDQUFDO0tBQ2pDLENBQUMsQ0FBQTtJQUVGLElBQUksQ0FBQyxRQUFRLENBQUMsRUFBRSxFQUFFLEVBQUU7UUFDbEIsTUFBTSx1QkFBdUIsQ0FBQTtLQUM5QjtJQUVELE1BQU0sSUFBSSxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsR0FBRyxFQUFFLENBQUMsQ0FBQTtJQUUvQiwwREFBMEQ7SUFDMUQsTUFBTSxPQUFPLEdBQUcsTUFBTSxJQUFJLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFBO0lBRXJDLElBQUksQ0FBQyxPQUFPLEVBQUU7UUFDWixNQUFNLDJCQUEyQixDQUFBO0tBQ2xDO0lBRUQsTUFBTSxXQUFXLEdBQUcsTUFBTSxPQUFPLENBQUMsV0FBVyxFQUFFLENBQUE7SUFDL0MsTUFBTSxTQUFTLEdBQUcsbUJBQW1CLElBQUksSUFBSSxFQUFFLENBQUMsT0FBTyxFQUFFLE1BQU0sQ0FBQTtJQUUvRCxJQUFJLENBQUMsV0FBVyxFQUFFO1FBQ2hCLE1BQU0sc0NBQXNDLENBQUE7S0FDN0M7SUFFRCxNQUFNLElBQUksQ0FBQyxVQUFVLENBQUM7UUFDcEIsSUFBSSxFQUFFLFNBQVM7UUFDZixJQUFJLEVBQUUsV0FBVztLQUNsQixDQUFDLENBQUE7SUFFRixNQUFNLElBQUksR0FBRyxZQUFFLENBQUMsWUFBWSxDQUFDLFNBQVMsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxRQUFRLENBQUMsQ0FBQTtJQUUxRCxPQUFPO1FBQ0wsVUFBVSxFQUFFLEdBQUc7UUFDZixPQUFPLEVBQUU7WUFDUCxjQUFjLEVBQUUsV0FBVztTQUM1QjtRQUNELElBQUksRUFBRSxJQUFJO1FBQ1YsZUFBZSxFQUFFLElBQUk7S0FDdEIsQ0FBQTtBQUNILENBQUM7QUFFWSxRQUFBLE9BQU8sR0FBRyxvQkFBYSxDQUFDLGdCQUFnQixDQUFDLENBQUEifQ==

================================================
FILE: examples/serverless-chrome-example/dist/util.js
================================================
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const chrome_aws_lambda_1 = __importDefault(require("chrome-aws-lambda"));
async function getChrome() {
    let browser = null;
    try {
        browser = await chrome_aws_lambda_1.default.puppeteer.launch({
            args: chrome_aws_lambda_1.default.args,
            defaultViewport: {
                width: 1920,
                height: 1080,
                isMobile: true,
                deviceScaleFactor: 2,
            },
            executablePath: await chrome_aws_lambda_1.default.executablePath,
            headless: chrome_aws_lambda_1.default.headless,
            ignoreHTTPSErrors: true,
        });
    }
    catch (err) {
        console.error("Error launching chrome");
        console.error(err);
    }
    return browser;
}
exports.getChrome = getChrome;
// both scraper and screenshot have the same basic handler
// they just call a different method to do things
exports.createHandler = (workFunction) => async (event) => {
    const search = event.queryStringParameters && event.queryStringParameters.search;
    if (!search) {
        return {
            statusCode: 400,
            body: "Please provide a ?search= parameter",
        };
    }
    const browser = await getChrome();
    if (!browser) {
        return {
            statusCode: 500,
            body: "Error launching Chrome",
        };
    }
    try {
        // call the function that does the real work
        const response = await workFunction(browser, search);
        return response;
    }
    catch (err) {
        console.log(err);
        return {
            statusCode: 500,
            body: "Error scraping Google",
        };
    }
};
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXRpbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy91dGlsLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7O0FBRUEsMEVBQXNDO0FBUy9CLEtBQUssVUFBVSxTQUFTO0lBQzdCLElBQUksT0FBTyxHQUFHLElBQUksQ0FBQTtJQUVsQixJQUFJO1FBQ0YsT0FBTyxHQUFHLE1BQU0sMkJBQU0sQ0FBQyxTQUFTLENBQUMsTUFBTSxDQUFDO1lBQ3RDLElBQUksRUFBRSwyQkFBTSxDQUFDLElBQUk7WUFDakIsZUFBZSxFQUFFO2dCQUNmLEtBQUssRUFBRSxJQUFJO2dCQUNYLE1BQU0sRUFBRSxJQUFJO2dCQUNaLFFBQVEsRUFBRSxJQUFJO2dCQUNkLGlCQUFpQixFQUFFLENBQUM7YUFDckI7WUFDRCxjQUFjLEVBQUUsTUFBTSwyQkFBTSxDQUFDLGNBQWM7WUFDM0MsUUFBUSxFQUFFLDJCQUFNLENBQUMsUUFBUTtZQUN6QixpQkFBaUIsRUFBRSxJQUFJO1NBQ3hCLENBQUMsQ0FBQTtLQUNIO0lBQUMsT0FBTyxHQUFHLEVBQUU7UUFDWixPQUFPLENBQUMsS0FBSyxDQUFDLHdCQUF3QixDQUFDLENBQUE7UUFDdkMsT0FBTyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQTtLQUNuQjtJQUVELE9BQU8sT0FBTyxDQUFBO0FBQ2hCLENBQUM7QUF0QkQsOEJBc0JDO0FBRUQsMERBQTBEO0FBQzFELGlEQUFpRDtBQUNwQyxRQUFBLGFBQWEsR0FBRyxDQUMzQixZQUF3RSxFQUN4RSxFQUFFLENBQUMsS0FBSyxFQUFFLEtBQXNCLEVBQXdCLEVBQUU7SUFDMUQsTUFBTSxNQUFNLEdBQ1YsS0FBSyxDQUFDLHFCQUFxQixJQUFJLEtBQUssQ0FBQyxxQkFBcUIsQ0FBQyxNQUFNLENBQUE7SUFFbkUsSUFBSSxDQUFDLE1BQU0sRUFBRTtRQUNYLE9BQU87WUFDTCxVQUFVLEVBQUUsR0FBRztZQUNmLElBQUksRUFBRSxxQ0FBcUM7U0FDNUMsQ0FBQTtLQUNGO0lBRUQsTUFBTSxPQUFPLEdBQUcsTUFBTSxTQUFTLEVBQUUsQ0FBQTtJQUVqQyxJQUFJLENBQUMsT0FBTyxFQUFFO1FBQ1osT0FBTztZQUNMLFVBQVUsRUFBRSxHQUFHO1lBQ2YsSUFBSSxFQUFFLHdCQUF3QjtTQUMvQixDQUFBO0tBQ0Y7SUFFRCxJQUFJO1FBQ0YsNENBQTRDO1FBQzVDLE1BQU0sUUFBUSxHQUFHLE1BQU0sWUFBWSxDQUFDLE9BQU8sRUFBRSxNQUFNLENBQUMsQ0FBQTtRQUVwRCxPQUFPLFFBQVEsQ0FBQTtLQUNoQjtJQUFDLE9BQU8sR0FBRyxFQUFFO1FBQ1osT0FBTyxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsQ0FBQTtRQUNoQixPQUFPO1lBQ0wsVUFBVSxFQUFFLEdBQUc7WUFDZixJQUFJLEVBQUUsdUJBQXVCO1NBQzlCLENBQUE7S0FDRjtBQUNILENBQUMsQ0FBQSJ9

================================================
FILE: examples/serverless-chrome-example/package.json
================================================
{
  "name": "serverless-chrome-example",
  "version": "1.0.0",
  "description": "AWS Lambda example of using Chrome Puppeteer",
  "main": "index.js",
  "scripts": {
    "build": "tsc",
    "deploy": "npm run build && serverless deploy"
  },
  "engines": {
    "node": "12.x"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/Swizec/serverless-handbook.git"
  },
  "author": "Swizec",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/Swizec/serverless-handbook/issues"
  },
  "homepage": "https://github.com/Swizec/serverless-handbook#readme",
  "dependencies": {
    "@types/aws-lambda": "^8.10.58",
    "@types/puppeteer": "^3.0.1",
    "aws-lambda": "^1.0.6",
    "chrome-aws-lambda": "^3.1.1",
    "puppeteer": "3.1.0",
    "puppeteer-core": "3.1.0"
  }
}


================================================
FILE: examples/serverless-chrome-example/serverless.yml
================================================
service: serverless-chrome-example

provider:
  name: aws
  runtime: nodejs12.x
  stage: dev
  apiGateway:
    binaryMediaTypes:
      - "*/*"

functions:
  scraper:
    handler: dist/scraper.handler
    memorysize: 2536
    timeout: 30
    events:
      - http:
          path: scraper
          method: GET
          cors: true

  screenshot:
    handler: dist/screenshot.handler
    memorysize: 2536
    timeout: 30
    events:
      - http:
          path: screenshot
          method: GET
          cors: true

package:
  exclude:
    - node_modules/puppeteer/.local-chromium/**


================================================
FILE: examples/serverless-chrome-example/src/scraper.ts
================================================
import { Browser } from "puppeteer"

import { createHandler } from "./util"

async function scrapeGoogle(browser: Browser, search: string) {
  const page = await browser.newPage()
  await page.goto("https://google.com", {
    waitUntil: ["domcontentloaded", "networkidle2"],
  })

  // this part is specific to the page you're scraping
  await page.type("input[type=text]", search)

  const [response] = await Promise.all([
    page.waitForNavigation(),
    page.click("input[type=submit]"),
  ])

  if (!response.ok()) {
    throw "Couldn't get response"
  }

  await page.goto(response.url())

  // this part is very specific to the page you're scraping
  const searchResults = await page.$$(".rc")

  let links = await Promise.all(
    searchResults.map(async (result) => {
      return {
        url: await result.$eval("a", (node) => node.getAttribute("href")),
        title: await result.$eval("h3", (node) => node.innerHTML),
        description: await result.$eval("span.st", (node) => node.innerHTML),
      }
    })
  )

  return {
    statusCode: 200,
    body: JSON.stringify(links),
  }
}

export const handler = createHandler(scrapeGoogle)


================================================
FILE: examples/serverless-chrome-example/src/screenshot.ts
================================================
import { Browser } from "puppeteer"
import fs from "fs"

import { createHandler } from "./util"

async function screenshotGoogle(browser: Browser, search: string) {
  const page = await browser.newPage()
  await page.goto("https://google.com", {
    waitUntil: ["domcontentloaded", "networkidle2"],
  })

  // this part is specific to the page you're screenshotting
  await page.type("input[type=text]", search)

  const [response] = await Promise.all([
    page.waitForNavigation(),
    page.click("input[type=submit]"),
  ])

  if (!response.ok()) {
    throw "Couldn't get response"
  }

  await page.goto(response.url())

  // this part is specific to the page you're screenshotting
  const element = await page.$("#main")

  if (!element) {
    throw "Couldn't find results div"
  }

  const boundingBox = await element.boundingBox()
  const imagePath = `/tmp/screenshot-${new Date().getTime()}.png`

  if (!boundingBox) {
    throw "Couldn't measure size of results div"
  }

  await page.screenshot({
    path: imagePath,
    clip: boundingBox,
  })

  const data = fs.readFileSync(imagePath).toString("base64")

  return {
    statusCode: 200,
    headers: {
      "Content-Type": "image/png",
    },
    body: data,
    isBase64Encoded: true,
  }
}

export const handler = createHandler(screenshotGoogle)


================================================
FILE: examples/serverless-chrome-example/src/util.ts
================================================
import { APIGatewayEvent } from "aws-lambda"
import { Browser } from "puppeteer"
import chrome from "chrome-aws-lambda"

export type APIResponse = {
  statusCode: number
  headers?: { [key: string]: string }
  body: string | Buffer
  isBase64Encoded?: boolean
}

export async function getChrome() {
  let browser = null

  try {
    browser = await chrome.puppeteer.launch({
      args: chrome.args,
      defaultViewport: {
        width: 1920,
        height: 1080,
        isMobile: true,
        deviceScaleFactor: 2,
      },
      executablePath: await chrome.executablePath,
      headless: chrome.headless,
      ignoreHTTPSErrors: true,
    })
  } catch (err) {
    console.error("Error launching chrome")
    console.error(err)
  }

  return browser
}

// both scraper and screenshot have the same basic handler
// they just call a different method to do things
export const createHandler = (
  workFunction: (browser: Browser, search: string) => Promise<APIResponse>
) => async (event: APIGatewayEvent): Promise<APIResponse> => {
  const search =
    event.queryStringParameters && event.queryStringParameters.search

  if (!search) {
    return {
      statusCode: 400,
      body: "Please provide a ?search= parameter",
    }
  }

  const browser = await getChrome()

  if (!browser) {
    return {
      statusCode: 500,
      body: "Error launching Chrome",
    }
  }

  try {
    // call the function that does the real work
    const response = await workFunction(browser, search)

    return response
  } catch (err) {
    console.log(err)
    return {
      statusCode: 500,
      body: "Error scraping Google",
    }
  }
}


================================================
FILE: examples/serverless-chrome-example/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ES2019",
    "module": "commonjs",
    "outDir": "./dist",
    "strict": true,
    "baseUrl": "./",
    "typeRoots": ["node_modules/@types"],
    "types": ["node"],
    "esModuleInterop": true,
    "inlineSourceMap": true,
    "lib": ["ES2019", "dom"]
  }
}


================================================
FILE: examples/serverless-data-pipeline-example/.gitignore
================================================
.serverless
dist


================================================
FILE: examples/serverless-data-pipeline-example/package.json
================================================
{
  "name": "serverless-data-pipeline-example",
  "version": "1.0.0",
  "description": "A simple massively distributed adder",
  "main": "index.js",
  "repository": "https://github.com/Swizec/serverless-handbook",
  "author": "Swizec",
  "license": "MIT",
  "scripts": {
    "build": "tsc",
    "deploy": "npm run build && serverless deploy"
  },
  "engines": {
    "node": "12.x"
  },
  "dependencies": {
    "@types/aws-lambda": "^8.10.39",
    "@types/aws-sdk": "^2.7.0",
    "@types/node-fetch": "^2.5.4",
    "@types/uuid": "^3.4.6",
    "aws-lambda": "^1.0.4",
    "aws-sdk": "^2.597.0",
    "querystring": "^0.2.0",
    "simple-dynamodb": "^1.0.1",
    "uuid": "^3.3.3"
  }
}


================================================
FILE: examples/serverless-data-pipeline-example/serverless.yml
================================================
service: serverless-data-pipeline-example

provider:
  name: aws
  runtime: nodejs12.x
  stage: dev
  environment:
    SUMS_TABLE: ${self:service}-summs-${self:provider.stage}
    SCRATCHPAD_TABLE: ${self:service}-scratchpad-${self:provider.stage}
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.SUMS_TABLE}"

    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.SCRATCHPAD_TABLE}"

    # all permissions on all SQS queues, what could possibly go wrong
    - Effect: "Allow"
      Action:
        - "sqs:*"
      Resource: "*"

functions:
  sumArray:
    handler: dist/sumArray.handler
    events:
      - http:
          path: sumArray
          method: POST
          cors: true
    environment:
      timesTwoQueueURL:
        Ref: TimesTwoQueue

  timesTwo:
    handler: dist/timesTwo.handler
    events:
      - sqs:
          arn:
            Fn::GetAtt:
              - TimesTwoQueue
              - Arn
          batchSize: 1
    environment:
      reduceQueueURL:
        Ref: ReduceQueue

  reduce:
    handler: dist/reduce.handler
    events:
      - sqs:
          arn:
            Fn::GetAtt:
              - ReduceQueue
              - Arn
          # this means we get up to 2 messages per invocation
          batchSize: 2
    environment:
      reduceQueueURL:
        Ref: ReduceQueue

resources:
  Resources:
    SumsTable:
      Type: "AWS::DynamoDB::Table"
      Properties:
        AttributeDefinitions:
          - AttributeName: arrayId
            AttributeType: S
        KeySchema:
          - AttributeName: arrayId
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        TableName: ${self:provider.environment.SUMS_TABLE}

    ScratchpadTable:
      Type: "AWS::DynamoDB::Table"
      Properties:
        AttributeDefinitions:
          - AttributeName: packetId
            AttributeType: S
          - AttributeName: arrayId
            AttributeType: S
        KeySchema:
          - AttributeName: packetId
            KeyType: HASH
          - AttributeName: arrayId
            KeyType: RANGE
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        TableName: ${self:provider.environment.SCRATCHPAD_TABLE}

    TimesTwoQueue:
      Type: "AWS::SQS::Queue"
      Properties:
        QueueName: "TimesTwoQueue-${self:provider.stage}"
        VisibilityTimeout: 60

    ReduceQueue:
      Type: "AWS::SQS::Queue"
      Properties:
        QueueName: "ReduceQueue-${self:provider.stage}"
        VisibilityTimeout: 60
        # RedrivePolicy:
        #   deadLetterTargetArn:
        #     Fn::GetAtt:
        #       - UnpackLogDLQueue
        #       - Arn
        #   maxReceiveCount: 10


================================================
FILE: examples/serverless-data-pipeline-example/src/reduce.ts
================================================
import { SQSEvent, SQSRecord } from "aws-lambda"
import * as db from "simple-dynamodb"
import uuidv4 from "uuid/v4"

import { sendSQSMessage, Packet } from "./utils"

export const handler = async (event: SQSEvent) => {
  // grab messages from queue
  // depending on batchSize there could be multiple
  let arrayIds: string[] = event.Records.map((record: SQSRecord) =>
    JSON.parse(record.body)
  )

  // process each ID from batch
  await Promise.all(arrayIds.map(reduceArray))
}

async function reduceArray(arrayId: string) {
  // grab 2 entries from scratchpad table
  // IRL you'd grab as many as you can cost-effectively process in execution
  // depends what you're doing
  const packets = await readPackets(arrayId)

  if (packets.length > 0) {
    // sum packets together
    const sum = packets.reduce(
      (sum: number, packet: Packet) => sum + packet.packetValue,
      0
    )

    // add the new item sum to scratchpad table
    // we do this first so we don't delete rows if it fails
    const newPacket = {
      arrayId,
      packetId: uuidv4(),
      arrayLength: packets[0].arrayLength,
      packetValue: sum,
      packetContains: packets.reduce(
        (count: number, packet: Packet) => count + packet.packetContains,
        0
      ),
    }
    await db.updateItem({
      TableName: process.env.SCRATCHPAD_TABLE!,
      Key: {
        arrayId,
        packetId: uuidv4(),
      },
      UpdateExpression:
        "SET packetValue = :packetValue, arrayLength = :arrayLength, packetContains = :packetContains",
      ExpressionAttributeValues: {
        ":packetValue": newPacket.packetValue,
        ":arrayLength": newPacket.arrayLength,
        ":packetContains": newPacket.packetContains,
      },
    })

    // delete the 2 rows we just summed
    await cleanup(packets)

    // are we done?
    if (newPacket.packetContains >= newPacket.arrayLength) {
      // done, save sum to final table
      await db.updateItem({
        TableName: process.env.SUMS_TABLE!,
        Key: {
          arrayId,
        },
        UpdateExpression: "SET resultSum = :resultSum",
        ExpressionAttributeValues: {
          ":resultSum": sum,
        },
      })
    } else {
      // not done, trigger another reduce step
      await sendSQSMessage(process.env.reduceQueueURL!, arrayId)
    }
  }
}

async function readPackets(arrayId: string): Promise<Packet[]> {
  const result = await db.scanItems({
    TableName: process.env.SCRATCHPAD_TABLE!,
    FilterExpression: "#arrayId = :arrayId",
    ExpressionAttributeNames: { "#arrayId": "arrayId" },
    ExpressionAttributeValues: { ":arrayId": arrayId },
    Limit: 2,
  })

  if (result.Items) {
    return result.Items as Packet[]
  } else {
    return []
  }
}

async function cleanup(packets: Packet[]) {
  await Promise.all(
    packets.map((packet) =>
      db.deleteItem({
        TableName: process.env.SCRATCHPAD_TABLE!,
        Key: {
          arrayId: packet.arrayId,
          packetId: packet.packetId,
        },
      })
    )
  )
}


================================================
FILE: examples/serverless-data-pipeline-example/src/sumArray.ts
================================================
import { APIGatewayEvent } from "aws-lambda"
import uuidv4 from "uuid/v4"
import { response, sendSQSMessage } from "./utils"

interface APIResponse {
  statusCode: number
  body: string
}

export const handler = async (event: APIGatewayEvent): Promise<APIResponse> => {
  const arrayId = uuidv4()

  if (!event.body) {
    return response(400, {
      status: "error",
      error: "Provide a JSON body",
    })
  }

  const array: number[] = JSON.parse(event.body)

  // split array into elements
  // trigger timesTwo lambda for each entry
  for (let packetValue of array) {
    await sendSQSMessage(process.env.timesTwoQueueURL!, {
      arrayId,
      packetId: uuidv4(),
      packetValue,
      arrayLength: array.length,
      packetContains: 1,
    })
  }

  return response(200, {
    status: "success",
    array,
    arrayId,
  })
}


================================================
FILE: examples/serverless-data-pipeline-example/src/timesTwo.ts
================================================
import { SQSEvent, SQSRecord } from "aws-lambda"
import { sendSQSMessage, Packet } from "./utils"
import * as db from "simple-dynamodb"

export const handler = async (event: SQSEvent) => {
  // grab messages from queue
  // depending on batchSize there could be multiple
  let packets: Packet[] = event.Records.map((record: SQSRecord) =>
    JSON.parse(record.body)
  )

  // iterate packets and multiply by 2
  // this would be a more expensive operation usually
  packets = packets.map((packet) => ({
    ...packet,
    packetValue: packet.packetValue * 2,
  }))

  // store each result in scratchpad table
  // in theory it's enough to put them on the queue
  // an intermediary table makes the reduce step easier to implement
  await Promise.all(
    packets.map((packet) =>
      db.updateItem({
        TableName: process.env.SCRATCHPAD_TABLE!,
        Key: { arrayId: packet.arrayId, packetId: packet.packetId },
        UpdateExpression:
          "SET packetValue = :packetValue, arrayLength = :arrayLength, packetContains = :packetContains",
        ExpressionAttributeValues: {
          ":packetValue": packet.packetValue,
          ":arrayLength": packet.arrayLength,
          ":packetContains": packet.packetContains,
        },
      })
    )
  )

  // trigger next step in calculation
  const uniqueArrayIds = Array.from(
    new Set(packets.map((packet) => packet.arrayId))
  )

  await Promise.all(
    uniqueArrayIds.map((arrayId) =>
      sendSQSMessage(process.env.reduceQueueURL!, arrayId)
    )
  )

  return true
}


================================================
FILE: examples/serverless-data-pipeline-example/src/utils.ts
================================================
import AWS from "aws-sdk"

export type Packet = {
  arrayId: string
  packetId: string
  packetValue: number
  arrayLength: number
  packetContains: number
}

export const sendSQSMessage = async (QueueURL: string, Message: any) => {
  Message = JSON.stringify(Message)

  console.log(`SQSing ${Message} to ${QueueURL}`)

  return new AWS.SQS()
    .sendMessage({
      MessageBody: Message,
      QueueUrl: QueueURL,
    })
    .promise()
}

export const fetchSQSMessage = async (QueueURL: string) => {
  console.log(`fetchSQSing from ${QueueURL}`)

  return new AWS.SQS()
    .receiveMessage({
      QueueUrl: QueueURL,
      WaitTimeSeconds: 1,
    })
    .promise()
}

export function response(statusCode: number, body: any) {
  return {
    statusCode,
    body: JSON.stringify(body),
  }
}


================================================
FILE: examples/serverless-data-pipeline-example/tsconfig.json
================================================
{
    "compilerOptions": {
        "target": "ES2019",
        "module": "commonjs",
        "outDir": "./dist",
        "strict": true,
        "baseUrl": "./",
        "typeRoots": ["node_modules/@types"],
        "types": ["node"],
        "esModuleInterop": true,
        "inlineSourceMap": true,
        "lib": ["ES2019"]
    }
}

================================================
FILE: examples/serverless-graphql-example/dist/dynamodb.js
================================================
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const aws_sdk_1 = __importDefault(require("aws-sdk"));
const dynamoDB = new aws_sdk_1.default.DynamoDB.DocumentClient();
exports.updateItem = async (params) => {
    const query = {
        ...params,
    };
    return new Promise((resolve, reject) => {
        dynamoDB.update(query, (err, result) => {
            if (err) {
                console.error(err);
                reject(err);
            }
            else {
                resolve(result);
            }
        });
    });
};
// collect all fields in a JSON object into a DynamoDB expression
exports.buildExpression = (body) => {
    return Object.keys(body)
        .map((key) => `${key} = :${key}`)
        .join(", ");
};
exports.buildAttributes = (body) => {
    return Object.fromEntries(Object.entries(body).map(([key, value]) => [
        `:${key}`,
        typeof value === "string" || typeof value === "number"
            ? value
            : JSON.stringify(value),
    ]));
};
exports.getItem = async (params) => {
    const query = {
        ...params,
    };
    return new Promise((resolve, reject) => {
        dynamoDB.get(query, (err, result) => {
            if (err) {
                console.error(err);
                reject(err);
            }
            else {
                resolve(result);
            }
        });
    });
};
exports.scanItems = async (params) => {
    const query = {
        ...params,
    };
    return new Promise((resolve, reject) => {
        dynamoDB.scan(query, (err, result) => {
            if (err) {
                console.error(err);
                reject(err);
            }
            else {
                resolve(result);
            }
        });
    });
};
exports.deleteItem = async (params) => {
    const query = {
        ...params,
    };
    return new Promise((resolve, reject) => {
        dynamoDB.delete(query, (err, result) => {
            if (err) {
                console.error(err);
                reject(err);
            }
            else {
                resolve(result);
            }
        });
    });
};
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZHluYW1vZGIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvZHluYW1vZGIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBQSxzREFBeUI7QUFFekIsTUFBTSxRQUFRLEdBQUcsSUFBSSxpQkFBRyxDQUFDLFFBQVEsQ0FBQyxjQUFjLEVBQUUsQ0FBQTtBQXdDckMsUUFBQSxVQUFVLEdBQUcsS0FBSyxFQUM3QixNQUF3QixFQUMrQixFQUFFO0lBQ3pELE1BQU0sS0FBSyxHQUFHO1FBQ1osR0FBRyxNQUFNO0tBQ1YsQ0FBQTtJQUVELE9BQU8sSUFBSSxPQUFPLENBQUMsQ0FBQyxPQUFPLEVBQUUsTUFBTSxFQUFFLEVBQUU7UUFDckMsUUFBUSxDQUFDLE1BQU0sQ0FBQyxLQUFLLEVBQUUsQ0FBQyxHQUFHLEVBQUUsTUFBTSxFQUFFLEVBQUU7WUFDckMsSUFBSSxHQUFHLEVBQUU7Z0JBQ1AsT0FBTyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQTtnQkFDbEIsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFBO2FBQ1o7aUJBQU07Z0JBQ0wsT0FBTyxDQUFDLE1BQU0sQ0FBQyxDQUFBO2FBQ2hCO1FBQ0gsQ0FBQyxDQUFDLENBQUE7SUFDSixDQUFDLENBQUMsQ0FBQTtBQUNKLENBQUMsQ0FBQTtBQUVELGlFQUFpRTtBQUNwRCxRQUFBLGVBQWUsR0FBRyxDQUFDLElBQVMsRUFBRSxFQUFFO0lBQzNDLE9BQU8sTUFBTSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUM7U0FDckIsR0FBRyxDQUFDLENBQUMsR0FBVyxFQUFFLEVBQUUsQ0FBQyxHQUFHLEdBQUcsT0FBTyxHQUFHLEVBQUUsQ0FBQztTQUN4QyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUE7QUFDZixDQUFDLENBQUE7QUFFWSxRQUFBLGVBQWUsR0FBRyxDQUFDLElBQVMsRUFBRSxFQUFFO0lBQzNDLE9BQU8sTUFBTSxDQUFDLFdBQVcsQ0FDdkIsTUFBTSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEdBQUcsRUFBRSxLQUFLLENBQUMsRUFBRSxFQUFFLENBQUM7UUFDekMsSUFBSSxHQUFHLEVBQUU7UUFDVCxPQUFPLEtBQUssS0FBSyxRQUFRLElBQUksT0FBTyxLQUFLLEtBQUssUUFBUTtZQUNwRCxDQUFDLENBQUMsS0FBSztZQUNQLENBQUMsQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLEtBQUssQ0FBQztLQUMxQixDQUFDLENBQ0gsQ0FBQTtBQUNILENBQUMsQ0FBQTtBQUVZLFFBQUEsT0FBTyxHQUFHLEtBQUssRUFDMUIsTUFBcUIsRUFDK0IsRUFBRTtJQUN0RCxNQUFNLEtBQUssR0FBRztRQUNaLEdBQUcsTUFBTTtLQUNWLENBQUE7SUFFRCxPQUFPLElBQUksT0FBTyxDQUFDLENBQUMsT0FBTyxFQUFFLE1BQU0sRUFBRSxFQUFFO1FBQ3JDLFFBQVEsQ0FBQyxHQUFHLENBQUMsS0FBSyxFQUFFLENBQUMsR0FBRyxFQUFFLE1BQU0sRUFBRSxFQUFFO1lBQ2xDLElBQUksR0FBRyxFQUFFO2dCQUNQLE9BQU8sQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUE7Z0JBQ2xCLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQTthQUNaO2lCQUFNO2dCQUNMLE9BQU8sQ0FBQyxNQUFNLENBQUMsQ0FBQTthQUNoQjtRQUNILENBQUMsQ0FBQyxDQUFBO0lBQ0osQ0FBQyxDQUFDLENBQUE7QUFDSixDQUFDLENBQUE7QUFFWSxRQUFBLFNBQVMsR0FBRyxLQUFLLEVBQzVCLE1BQXVCLEVBQzBCLEVBQUU7SUFDbkQsTUFBTSxLQUFLLEdBQUc7UUFDWixHQUFHLE1BQU07S0FDVixDQUFBO0lBRUQsT0FBTyxJQUFJLE9BQU8sQ0FBQyxDQUFDLE9BQU8sRUFBRSxNQUFNLEVBQUUsRUFBRTtRQUNyQyxRQUFRLENBQUMsSUFBSSxDQUFDLEtBQUssRUFBRSxDQUFDLEdBQUcsRUFBRSxNQUFNLEVBQUUsRUFBRTtZQUNuQyxJQUFJLEdBQUcsRUFBRTtnQkFDUCxPQUFPLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFBO2dCQUNsQixNQUFNLENBQUMsR0FBRyxDQUFDLENBQUE7YUFDWjtpQkFBTTtnQkFDTCxPQUFPLENBQUMsTUFBTSxDQUFDLENBQUE7YUFDaEI7UUFDSCxDQUFDLENBQUMsQ0FBQTtJQUNKLENBQUMsQ0FBQyxDQUFBO0FBQ0osQ0FBQyxDQUFBO0FBRVksUUFBQSxVQUFVLEdBQUcsS0FBSyxFQUM3QixNQUF3QixFQUMrQixFQUFFO0lBQ3pELE1BQU0sS0FBSyxHQUFHO1FBQ1osR0FBRyxNQUFNO0tBQ1YsQ0FBQTtJQUVELE9BQU8sSUFBSSxPQUFPLENBQUMsQ0FBQyxPQUFPLEVBQUUsTUFBTSxFQUFFLEVBQUU7UUFDckMsUUFBUSxDQUFDLE1BQU0sQ0FBQyxLQUFLLEVBQUUsQ0FBQyxHQUFHLEVBQUUsTUFBTSxFQUFFLEVBQUU7WUFDckMsSUFBSSxHQUFHLEVBQUU7Z0JBQ1AsT0FBTyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQTtnQkFDbEIsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFBO2FBQ1o7aUJBQU07Z0JBQ0wsT0FBTyxDQUFDLE1BQU0sQ0FBQyxDQUFBO2FBQ2hCO1FBQ0gsQ0FBQyxDQUFDLENBQUE7SUFDSixDQUFDLENBQUMsQ0FBQTtBQUNKLENBQUMsQ0FBQSJ9

================================================
FILE: examples/serverless-graphql-example/dist/graphql.js
================================================
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const apollo_server_lambda_1 = require("apollo-server-lambda");
const queries_1 = require("./queries");
const mutations_1 = require("./mutations");
// this is where we define the shape of our API
const schema = apollo_server_lambda_1.gql `
  type Item {
    id: String
    name: String
    body: String
    createdAt: String
    updatedAt: String
  }

  type Query {
    item(id: String!): Item
  }

  type Mutation {
    updateItem(id: String, name: String, body: String): Item
    deleteItem(id: String!): Item
  }
`;
// this is where the shape maps to functions
const resolvers = {
    Query: {
        item: queries_1.item,
    },
    Mutation: {
        updateItem: mutations_1.updateItem,
        deleteItem: mutations_1.deleteItem,
    },
};
const server = new apollo_server_lambda_1.ApolloServer({ typeDefs: schema, resolvers });
exports.handler = server.createHandler({
    cors: {
        origin: "*",
        credentials: true,
    },
});
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ3JhcGhxbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9ncmFwaHFsLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBQUEsK0RBQXdEO0FBRXhELHVDQUFnQztBQUNoQywyQ0FBb0Q7QUFFcEQsK0NBQStDO0FBQy9DLE1BQU0sTUFBTSxHQUFHLDBCQUFHLENBQUE7Ozs7Ozs7Ozs7Ozs7Ozs7O0NBaUJqQixDQUFBO0FBRUQsNENBQTRDO0FBQzVDLE1BQU0sU0FBUyxHQUFHO0lBQ2hCLEtBQUssRUFBRTtRQUNMLElBQUksRUFBSixjQUFJO0tBQ0w7SUFDRCxRQUFRLEVBQUU7UUFDUixVQUFVLEVBQVYsc0JBQVU7UUFDVixVQUFVLEVBQVYsc0JBQVU7S0FDWDtDQUNGLENBQUE7QUFFRCxNQUFNLE1BQU0sR0FBRyxJQUFJLG1DQUFZLENBQUMsRUFBRSxRQUFRLEVBQUUsTUFBTSxFQUFFLFNBQVMsRUFBRSxDQUFDLENBQUE7QUFFbkQsUUFBQSxPQUFPLEdBQUcsTUFBTSxDQUFDLGFBQWEsQ0FBQztJQUMxQyxJQUFJLEVBQUU7UUFDSixNQUFNLEVBQUUsR0FBRztRQUNYLFdBQVcsRUFBRSxJQUFJO0tBQ2xCO0NBQ0YsQ0FBQyxDQUFBIn0=

================================================
FILE: examples/serverless-graphql-example/dist/manageItems.js
================================================
"use strict";
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
    result["default"] = mod;
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const db = __importStar(require("./dynamodb"));
const v4_1 = __importDefault(require("uuid/v4"));
function response(statusCode, body) {
    return {
        statusCode,
        body: JSON.stringify(body),
    };
}
// fetch using /item/ID
exports.getItem = async (event) => {
    const itemId = event.pathParameters ? event.pathParameters.itemId : null;
    if (!itemId) {
        return response(404, {
            status: "error",
            error: "Item not found",
        });
    }
    const item = await db.getItem({
        TableName: process.env.ITEM_TABLE,
        Key: { itemId },
    });
    if (item.Item) {
        return response(200, {
            status: "success",
            item: item.Item,
        });
    }
    else {
        return response(404, {
            status: "error",
            error: "Item not found",
        });
    }
};
// upsert an item
// /item or /item/ID
exports.updateItem = async (event) => {
    let itemId = event.pathParameters ? event.pathParameters.itemId : v4_1.default();
    let createdAt = new Date().toISOString();
    // find item if exists
    const find = await db.getItem({
        TableName: process.env.ITEM_TABLE,
        Key: { itemId },
    });
    if (find.Item) {
        // save createdAt so we don't overwrite on update
        createdAt = find.Item.createdAt;
    }
    else {
        return response(404, {
            status: "error",
            error: "Item not found",
        });
    }
    if (!event.body) {
        return response(400, {
            status: "error",
            error: "Provide a JSON body",
        });
    }
    let body = JSON.parse(event.body);
    if (body.itemId) {
        // this will confuse DynamoDB, you can't update the key
        delete body.itemId;
    }
    const item = await db.updateItem({
        TableName: process.env.ITEM_TABLE,
        Key: { itemId },
        UpdateExpression: `SET ${db.buildExpression(body)}, createdAt = :createdAt, lastUpdatedAt = :lastUpdatedAt`,
        ExpressionAttributeValues: {
            ...db.buildAttributes(body),
            ":createdAt": createdAt,
            ":lastUpdatedAt": new Date().toISOString(),
        },
        ReturnValues: "ALL_NEW",
    });
    return response(200, {
        status: "success",
        item: item.Attributes,
    });
};
exports.deleteItem = async (event) => {
    const itemId = event.pathParameters ? event.pathParameters.itemId : null;
    if (!itemId) {
        return response(400, {
            status: "error",
            error: "Provide an itemId",
        });
    }
    // DynamoDB handles deleting already deleted files, no error :)
    const item = await db.deleteItem({
        TableName: process.env.ITEM_TABLE,
        Key: { itemId },
        ReturnValues: "ALL_OLD",
    });
    return response(200, {
        status: "success",
        itemWas: item.Attributes,
    });
};
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFuYWdlSXRlbXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvbWFuYWdlSXRlbXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7O0FBRUEsK0NBQWdDO0FBQ2hDLGlEQUE0QjtBQUU1QixTQUFTLFFBQVEsQ0FBQyxVQUFrQixFQUFFLElBQVM7SUFDN0MsT0FBTztRQUNMLFVBQVU7UUFDVixJQUFJLEVBQUUsSUFBSSxDQUFDLFNBQVMsQ0FBQyxJQUFJLENBQUM7S0FDM0IsQ0FBQTtBQUNILENBQUM7QUFFRCx1QkFBdUI7QUFDVixRQUFBLE9BQU8sR0FBRyxLQUFLLEVBQUUsS0FBc0IsRUFBd0IsRUFBRTtJQUM1RSxNQUFNLE1BQU0sR0FBRyxLQUFLLENBQUMsY0FBYyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsY0FBYyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFBO0lBRXhFLElBQUksQ0FBQyxNQUFNLEVBQUU7UUFDWCxPQUFPLFFBQVEsQ0FBQyxHQUFHLEVBQUU7WUFDbkIsTUFBTSxFQUFFLE9BQU87WUFDZixLQUFLLEVBQUUsZ0JBQWdCO1NBQ3hCLENBQUMsQ0FBQTtLQUNIO0lBRUQsTUFBTSxJQUFJLEdBQUcsTUFBTSxFQUFFLENBQUMsT0FBTyxDQUFDO1FBQzVCLFNBQVMsRUFBRSxPQUFPLENBQUMsR0FBRyxDQUFDLFVBQVc7UUFDbEMsR0FBRyxFQUFFLEVBQUUsTUFBTSxFQUFFO0tBQ2hCLENBQUMsQ0FBQTtJQUVGLElBQUksSUFBSSxDQUFDLElBQUksRUFBRTtRQUNiLE9BQU8sUUFBUSxDQUFDLEdBQUcsRUFBRTtZQUNuQixNQUFNLEVBQUUsU0FBUztZQUNqQixJQUFJLEVBQUUsSUFBSSxDQUFDLElBQUk7U0FDaEIsQ0FBQyxDQUFBO0tBQ0g7U0FBTTtRQUNMLE9BQU8sUUFBUSxDQUFDLEdBQUcsRUFBRTtZQUNuQixNQUFNLEVBQUUsT0FBTztZQUNmLEtBQUssRUFBRSxnQkFBZ0I7U0FDeEIsQ0FBQyxDQUFBO0tBQ0g7QUFDSCxDQUFDLENBQUE7QUFFRCxpQkFBaUI7QUFDakIsb0JBQW9CO0FBQ1AsUUFBQSxVQUFVLEdBQUcsS0FBSyxFQUM3QixLQUFzQixFQUNBLEVBQUU7SUFDeEIsSUFBSSxNQUFNLEdBQUcsS0FBSyxDQUFDLGNBQWMsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLGNBQWMsQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLFlBQU0sRUFBRSxDQUFBO0lBRTFFLElBQUksU0FBUyxHQUFHLElBQUksSUFBSSxFQUFFLENBQUMsV0FBVyxFQUFFLENBQUE7SUFFeEMsc0JBQXNCO0lBQ3RCLE1BQU0sSUFBSSxHQUFHLE1BQU0sRUFBRSxDQUFDLE9BQU8sQ0FBQztRQUM1QixTQUFTLEVBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxVQUFXO1FBQ2xDLEdBQUcsRUFBRSxFQUFFLE1BQU0sRUFBRTtLQUNoQixDQUFDLENBQUE7SUFFRixJQUFJLElBQUksQ0FBQyxJQUFJLEVBQUU7UUFDYixpREFBaUQ7UUFDakQsU0FBUyxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFBO0tBQ2hDO1NBQU07UUFDTCxPQUFPLFFBQVEsQ0FBQyxHQUFHLEVBQUU7WUFDbkIsTUFBTSxFQUFFLE9BQU87WUFDZixLQUFLLEVBQUUsZ0JBQWdCO1NBQ3hCLENBQUMsQ0FBQTtLQUNIO0lBRUQsSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLEVBQUU7UUFDZixPQUFPLFFBQVEsQ0FBQyxHQUFHLEVBQUU7WUFDbkIsTUFBTSxFQUFFLE9BQU87WUFDZixLQUFLLEVBQUUscUJBQXFCO1NBQzdCLENBQUMsQ0FBQTtLQUNIO0lBRUQsSUFBSSxJQUFJLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLENBQUE7SUFFakMsSUFBSSxJQUFJLENBQUMsTUFBTSxFQUFFO1FBQ2YsdURBQXVEO1FBQ3ZELE9BQU8sSUFBSSxDQUFDLE1BQU0sQ0FBQTtLQUNuQjtJQUVELE1BQU0sSUFBSSxHQUFHLE1BQU0sRUFBRSxDQUFDLFVBQVUsQ0FBQztRQUMvQixTQUFTLEVBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxVQUFXO1FBQ2xDLEdBQUcsRUFBRSxFQUFFLE1BQU0sRUFBRTtRQUNmLGdCQUFnQixFQUFFLE9BQU8sRUFBRSxDQUFDLGVBQWUsQ0FDekMsSUFBSSxDQUNMLDBEQUEwRDtRQUMzRCx5QkFBeUIsRUFBRTtZQUN6QixHQUFHLEVBQUUsQ0FBQyxlQUFlLENBQUMsSUFBSSxDQUFDO1lBQzNCLFlBQVksRUFBRSxTQUFTO1lBQ3ZCLGdCQUFnQixFQUFFLElBQUksSUFBSSxFQUFFLENBQUMsV0FBVyxFQUFFO1NBQzNDO1FBQ0QsWUFBWSxFQUFFLFNBQVM7S0FDeEIsQ0FBQyxDQUFBO0lBRUYsT0FBTyxRQUFRLENBQUMsR0FBRyxFQUFFO1FBQ25CLE1BQU0sRUFBRSxTQUFTO1FBQ2pCLElBQUksRUFBRSxJQUFJLENBQUMsVUFBVTtLQUN0QixDQUFDLENBQUE7QUFDSixDQUFDLENBQUE7QUFFWSxRQUFBLFVBQVUsR0FBRyxLQUFLLEVBQzdCLEtBQXNCLEVBQ0EsRUFBRTtJQUN4QixNQUFNLE1BQU0sR0FBRyxLQUFLLENBQUMsY0FBYyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsY0FBYyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFBO0lBRXhFLElBQUksQ0FBQyxNQUFNLEVBQUU7UUFDWCxPQUFPLFFBQVEsQ0FBQyxHQUFHLEVBQUU7WUFDbkIsTUFBTSxFQUFFLE9BQU87WUFDZixLQUFLLEVBQUUsbUJBQW1CO1NBQzNCLENBQUMsQ0FBQTtLQUNIO0lBRUQsK0RBQStEO0lBQy9ELE1BQU0sSUFBSSxHQUFHLE1BQU0sRUFBRSxDQUFDLFVBQVUsQ0FBQztRQUMvQixTQUFTLEVBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxVQUFXO1FBQ2xDLEdBQUcsRUFBRSxFQUFFLE1BQU0sRUFBRTtRQUNmLFlBQVksRUFBRSxTQUFTO0tBQ3hCLENBQUMsQ0FBQTtJQUVGLE9BQU8sUUFBUSxDQUFDLEdBQUcsRUFBRTtRQUNuQixNQUFNLEVBQUUsU0FBUztRQUNqQixPQUFPLEVBQUUsSUFBSSxDQUFDLFVBQVU7S0FDekIsQ0FBQyxDQUFBO0FBQ0osQ0FBQyxDQUFBIn0=

================================================
FILE: examples/serverless-graphql-example/dist/mutations.js
================================================
"use strict";
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
    result["default"] = mod;
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const db = __importStar(require("simple-dynamodb"));
const v4_1 = __importDefault(require("uuid/v4"));
function remapProps(item) {
    return {
        ...item,
        id: item.itemId,
        name: item.itemName,
    };
}
// upsert an item
// item(name, ...) or item(id, name, ...)
exports.updateItem = async (_, args) => {
    let itemId = args.id ? args.id : v4_1.default();
    let createdAt = new Date().toISOString();
    // find item if exists
    if (args.id) {
        const find = await db.getItem({
            TableName: process.env.ITEM_TABLE,
            Key: { itemId },
        });
        if (find.Item) {
            // save createdAt so we don't overwrite on update
            createdAt = find.Item.createdAt;
        }
        else {
            throw "Item not found";
        }
    }
    const updateValues = {
        itemName: args.name,
        body: args.body,
    };
    const item = await db.updateItem({
        TableName: process.env.ITEM_TABLE,
        Key: { itemId },
        UpdateExpression: `SET ${db.buildExpression(updateValues)}, createdAt = :createdAt, updatedAt = :updatedAt`,
        ExpressionAttributeValues: {
            ...db.buildAttributes(updateValues),
            ":createdAt": createdAt,
            ":updatedAt": new Date().toISOString(),
        },
        ReturnValues: "ALL_NEW",
    });
    return remapProps(item.Attributes);
};
exports.deleteItem = async (_, args) => {
    // DynamoDB handles deleting already deleted files, no error :)
    const item = await db.deleteItem({
        TableName: process.env.ITEM_TABLE,
        Key: {
            itemId: args.id,
        },
        ReturnValues: "ALL_OLD",
    });
    return remapProps(item.Attributes);
};
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibXV0YXRpb25zLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL211dGF0aW9ucy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7QUFBQSxvREFBcUM7QUFDckMsaURBQTRCO0FBUTVCLFNBQVMsVUFBVSxDQUFDLElBQVM7SUFDM0IsT0FBTztRQUNMLEdBQUcsSUFBSTtRQUNQLEVBQUUsRUFBRSxJQUFJLENBQUMsTUFBTTtRQUNmLElBQUksRUFBRSxJQUFJLENBQUMsUUFBUTtLQUNwQixDQUFBO0FBQ0gsQ0FBQztBQUVELGlCQUFpQjtBQUNqQix5Q0FBeUM7QUFDNUIsUUFBQSxVQUFVLEdBQUcsS0FBSyxFQUFFLENBQU0sRUFBRSxJQUFjLEVBQUUsRUFBRTtJQUN6RCxJQUFJLE1BQU0sR0FBRyxJQUFJLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxZQUFNLEVBQUUsQ0FBQTtJQUV6QyxJQUFJLFNBQVMsR0FBRyxJQUFJLElBQUksRUFBRSxDQUFDLFdBQVcsRUFBRSxDQUFBO0lBRXhDLHNCQUFzQjtJQUN0QixJQUFJLElBQUksQ0FBQyxFQUFFLEVBQUU7UUFDWCxNQUFNLElBQUksR0FBRyxNQUFNLEVBQUUsQ0FBQyxPQUFPLENBQUM7WUFDNUIsU0FBUyxFQUFFLE9BQU8sQ0FBQyxHQUFHLENBQUMsVUFBVztZQUNsQyxHQUFHLEVBQUUsRUFBRSxNQUFNLEVBQUU7U0FDaEIsQ0FBQyxDQUFBO1FBRUYsSUFBSSxJQUFJLENBQUMsSUFBSSxFQUFFO1lBQ2IsaURBQWlEO1lBQ2pELFNBQVMsR0FBRyxJQUFJLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQTtTQUNoQzthQUFNO1lBQ0wsTUFBTSxnQkFBZ0IsQ0FBQTtTQUN2QjtLQUNGO0lBRUQsTUFBTSxZQUFZLEdBQUc7UUFDbkIsUUFBUSxFQUFFLElBQUksQ0FBQyxJQUFJO1FBQ25CLElBQUksRUFBRSxJQUFJLENBQUMsSUFBSTtLQUNoQixDQUFBO0lBRUQsTUFBTSxJQUFJLEdBQUcsTUFBTSxFQUFFLENBQUMsVUFBVSxDQUFDO1FBQy9CLFNBQVMsRUFBRSxPQUFPLENBQUMsR0FBRyxDQUFDLFVBQVc7UUFDbEMsR0FBRyxFQUFFLEVBQUUsTUFBTSxFQUFFO1FBQ2YsZ0JBQWdCLEVBQUUsT0FBTyxFQUFFLENBQUMsZUFBZSxDQUN6QyxZQUFZLENBQ2Isa0RBQWtEO1FBQ25ELHlCQUF5QixFQUFFO1lBQ3pCLEdBQUcsRUFBRSxDQUFDLGVBQWUsQ0FBQyxZQUFZLENBQUM7WUFDbkMsWUFBWSxFQUFFLFNBQVM7WUFDdkIsWUFBWSxFQUFFLElBQUksSUFBSSxFQUFFLENBQUMsV0FBVyxFQUFFO1NBQ3ZDO1FBQ0QsWUFBWSxFQUFFLFNBQVM7S0FDeEIsQ0FBQyxDQUFBO0lBRUYsT0FBTyxVQUFVLENBQUMsSUFBSSxDQUFDLFVBQVUsQ0FBQyxDQUFBO0FBQ3BDLENBQUMsQ0FBQTtBQUVZLFFBQUEsVUFBVSxHQUFHLEtBQUssRUFBRSxDQUFNLEVBQUUsSUFBb0IsRUFBRSxFQUFFO0lBQy9ELCtEQUErRDtJQUMvRCxNQUFNLElBQUksR0FBRyxNQUFNLEVBQUUsQ0FBQyxVQUFVLENBQUM7UUFDL0IsU0FBUyxFQUFFLE9BQU8sQ0FBQyxHQUFHLENBQUMsVUFBVztRQUNsQyxHQUFHLEVBQUU7WUFDSCxNQUFNLEVBQUUsSUFBSSxDQUFDLEVBQUU7U0FDaEI7UUFDRCxZQUFZLEVBQUUsU0FBUztLQUN4QixDQUFDLENBQUE7SUFFRixPQUFPLFVBQVUsQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLENBQUE7QUFDcEMsQ0FBQyxDQUFBIn0=

================================================
FILE: examples/serverless-graphql-example/dist/queries.js
================================================
"use strict";
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
    result["default"] = mod;
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const db = __importStar(require("simple-dynamodb"));
function remapProps(item) {
    return {
        ...item,
        id: item.itemId,
        name: item.itemName,
    };
}
// fetch using item(id: String)
exports.item = async (_, args) => {
    const item = await db.getItem({
        TableName: process.env.ITEM_TABLE,
        Key: {
            itemId: args.id,
        },
    });
    return remapProps(item.Item);
};
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicXVlcmllcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9xdWVyaWVzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7Ozs7OztBQUFBLG9EQUFxQztBQUVyQyxTQUFTLFVBQVUsQ0FBQyxJQUFTO0lBQzNCLE9BQU87UUFDTCxHQUFHLElBQUk7UUFDUCxFQUFFLEVBQUUsSUFBSSxDQUFDLE1BQU07UUFDZixJQUFJLEVBQUUsSUFBSSxDQUFDLFFBQVE7S0FDcEIsQ0FBQTtBQUNILENBQUM7QUFFRCwrQkFBK0I7QUFDbEIsUUFBQSxJQUFJLEdBQUcsS0FBSyxFQUFFLENBQU0sRUFBRSxJQUFvQixFQUFFLEVBQUU7SUFDekQsTUFBTSxJQUFJLEdBQUcsTUFBTSxFQUFFLENBQUMsT0FBTyxDQUFDO1FBQzVCLFNBQVMsRUFBRSxPQUFPLENBQUMsR0FBRyxDQUFDLFVBQVc7UUFDbEMsR0FBRyxFQUFFO1lBQ0gsTUFBTSxFQUFFLElBQUksQ0FBQyxFQUFFO1NBQ2hCO0tBQ0YsQ0FBQyxDQUFBO0lBRUYsT0FBTyxVQUFVLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFBO0FBQzlCLENBQUMsQ0FBQSJ9

================================================
FILE: examples/serverless-graphql-example/package.json
================================================
{
  "name": "serverless-rest-example",
  "version": "1.0.0",
  "description": "A simple CRUD example using serverless",
  "main": "index.js",
  "repository": "https://github.com/Swizec/serverless-handbook",
  "author": "Swizec",
  "license": "MIT",
  "scripts": {
    "build": "tsc",
    "deploy": "npm run build && serverless deploy"
  },
  "engines": {
    "node": "12.x"
  },
  "dependencies": {
    "@types/aws-lambda": "^8.10.39",
    "@types/aws-sdk": "^2.7.0",
    "@types/node-fetch": "^2.5.4",
    "@types/uuid": "^3.4.6",
    "apollo-server-lambda": "^2.12.0",
    "aws-lambda": "^1.0.4",
    "aws-sdk": "^2.597.0",
    "querystring": "^0.2.0",
    "simple-dynamodb": "^1.0.0",
    "uuid": "^3.3.3"
  }
}


================================================
FILE: examples/serverless-graphql-example/serverless.yml
================================================
service: serverless-graphql-example

provider:
  name: aws
  runtime: nodejs12.x
  stage: dev
  environment:
    ITEM_TABLE: ${self:service}-items-${self:provider.stage}
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.ITEM_TABLE}"

functions:
  graphql:
    handler: dist/graphql.handler
    events:
      - http:
          path: graphql
          method: GET
          cors: true
      - http:
          path: graphql
          method: POST
          cors: true

resources:
  Resources:
    UsersTable:
      Type: "AWS::DynamoDB::Table"
      Properties:
        AttributeDefinitions:
          - AttributeName: itemId
            AttributeType: S
        KeySchema:
          - AttributeName: itemId
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        TableName: ${self:provider.environment.ITEM_TABLE}


================================================
FILE: examples/serverless-graphql-example/src/graphql.ts
================================================
import { ApolloServer, gql } from "apollo-server-lambda"

import { item } from "./queries"
import { updateItem, deleteItem } from "./mutations"

// this is where we define the shape of our API
const schema = gql`
  type Item {
    id: String
    name: String
    body: String
    createdAt: String
    updatedAt: String
  }

  type Query {
    item(id: String!): Item
  }

  type Mutation {
    updateItem(id: String, name: String, body: String): Item
    deleteItem(id: String!): Item
  }
`

// this is where the shape maps to functions
const resolvers = {
  Query: {
    item,
  },
  Mutation: {
    updateItem,
    deleteItem,
  },
}

const server = new ApolloServer({ typeDefs: schema, resolvers })

export const handler = server.createHandler({
  cors: {
    origin: "*", // for security in production, lock this to your real endpoints
    credentials: true,
  },
})


================================================
FILE: examples/serverless-graphql-example/src/mutations.ts
================================================
import * as db from "simple-dynamodb"
import uuidv4 from "uuid/v4"

type ItemArgs = {
  id: string
  name: string
  body: string
}

function remapProps(item: any) {
  return {
    ...item,
    id: item.itemId,
    name: item.itemName,
  }
}

// upsert an item
// item(name, ...) or item(id, name, ...)
export const updateItem = async (_: any, args: ItemArgs) => {
  let itemId = args.id ? args.id : uuidv4()

  let createdAt = new Date().toISOString()

  // find item if exists
  if (args.id) {
    const find = await db.getItem({
      TableName: process.env.ITEM_TABLE!,
      Key: { itemId },
    })

    if (find.Item) {
      // save createdAt so we don't overwrite on update
      createdAt = find.Item.createdAt
    } else {
      throw "Item not found"
    }
  }

  const updateValues = {
    itemName: args.name,
    body: args.body,
  }

  const item = await db.updateItem({
    TableName: process.env.ITEM_TABLE!,
    Key: { itemId },
    UpdateExpression: `SET ${db.buildExpression(
      updateValues
    )}, createdAt = :createdAt, updatedAt = :updatedAt`,
    ExpressionAttributeValues: {
      ...db.buildAttributes(updateValues),
      ":createdAt": createdAt,
      ":updatedAt": new Date().toISOString(),
    },
    ReturnValues: "ALL_NEW",
  })

  return remapProps(item.Attributes)
}

export const deleteItem = async (_: any, args: { id: string }) => {
  // DynamoDB handles deleting already deleted files, no error :)
  const item = await db.deleteItem({
    TableName: process.env.ITEM_TABLE!,
    Key: {
      itemId: args.id,
    },
    ReturnValues: "ALL_OLD",
  })

  return remapProps(item.Attributes)
}


================================================
FILE: examples/serverless-graphql-example/src/queries.ts
================================================
import * as db from "simple-dynamodb"

function remapProps(item: any) {
  return {
    ...item,
    id: item.itemId,
    name: item.itemName,
  }
}

// fetch using item(id: String)
export const item = async (_: any, args: { id: string }) => {
  const item = await db.getItem({
    TableName: process.env.ITEM_TABLE!,
    Key: {
      itemId: args.id,
    },
  })

  return remapProps(item.Item)
}


================================================
FILE: examples/serverless-graphql-example/src/types.d.ts
================================================
export interface APIResponse {
  statusCode: number
  body: string
}


================================================
FILE: examples/serverless-graphql-example/tsconfig.json
================================================
{
    "compilerOptions": {
        "target": "ES2019",
        "module": "commonjs",
        "outDir": "./dist",
        "strict": true,
        "baseUrl": "./",
        "typeRoots": ["node_modules/@types"],
        "types": ["node"],
        "esModuleInterop": true,
        "inlineSourceMap": true,
        "lib": ["ES2019"]
    }
}

================================================
FILE: examples/serverless-rest-example/dist/dynamodb.js
================================================
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const aws_sdk_1 = __importDefault(require("aws-sdk"));
const dynamoDB = new aws_sdk_1.default.DynamoDB.DocumentClient();
exports.updateItem = async (params) => {
    const query = {
        ...params,
    };
    return new Promise((resolve, reject) => {
        dynamoDB.update(query, (err, result) => {
            if (err) {
                console.error(err);
                reject(err);
            }
            else {
                resolve(result);
            }
        });
    });
};
// collect all fields in a JSON object into a DynamoDB expression
exports.buildExpression = (body) => {
    return Object.keys(body)
        .map((key) => `${key} = :${key}`)
        .join(", ");
};
exports.buildAttributes = (body) => {
    return Object.fromEntries(Object.entries(body).map(([key, value]) => [
        `:${key}`,
        typeof value === "string" || typeof value === "number"
            ? value
            : JSON.stringify(value),
    ]));
};
exports.getItem = async (params) => {
    const query = {
        ...params,
    };
    return new Promise((resolve, reject) => {
        dynamoDB.get(query, (err, result) => {
            if (err) {
                console.error(err);
                reject(err);
            }
            else {
                resolve(result);
            }
        });
    });
};
exports.scanItems = async (params) => {
    const query = {
        ...params,
    };
    return new Promise((resolve, reject) => {
        dynamoDB.scan(query, (err, result) => {
            if (err) {
                console.error(err);
                reject(err);
            }
            else {
                resolve(result);
            }
        });
    });
};
exports.deleteItem = async (params) => {
    const query = {
        ...params,
    };
    return new Promise((resolve, reject) => {
        dynamoDB.delete(query, (err, result) => {
            if (err) {
                console.error(err);
                reject(err);
            }
            else {
                resolve(result);
            }
        });
    });
};
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZHluYW1vZGIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvZHluYW1vZGIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBQSxzREFBeUI7QUFFekIsTUFBTSxRQUFRLEdBQUcsSUFBSSxpQkFBRyxDQUFDLFFBQVEsQ0FBQyxjQUFjLEVBQUUsQ0FBQTtBQXdDckMsUUFBQSxVQUFVLEdBQUcsS0FBSyxFQUM3QixNQUF3QixFQUMrQixFQUFFO0lBQ3pELE1BQU0sS0FBSyxHQUFHO1FBQ1osR0FBRyxNQUFNO0tBQ1YsQ0FBQTtJQUVELE9BQU8sSUFBSSxPQUFPLENBQUMsQ0FBQyxPQUFPLEVBQUUsTUFBTSxFQUFFLEVBQUU7UUFDckMsUUFBUSxDQUFDLE1BQU0sQ0FBQyxLQUFLLEVBQUUsQ0FBQyxHQUFHLEVBQUUsTUFBTSxFQUFFLEVBQUU7WUFDckMsSUFBSSxHQUFHLEVBQUU7Z0JBQ1AsT0FBTyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQTtnQkFDbEIsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFBO2FBQ1o7aUJBQU07Z0JBQ0wsT0FBTyxDQUFDLE1BQU0sQ0FBQyxDQUFBO2FBQ2hCO1FBQ0gsQ0FBQyxDQUFDLENBQUE7SUFDSixDQUFDLENBQUMsQ0FBQTtBQUNKLENBQUMsQ0FBQTtBQUVELGlFQUFpRTtBQUNwRCxRQUFBLGVBQWUsR0FBRyxDQUFDLElBQVMsRUFBRSxFQUFFO0lBQzNDLE9BQU8sTUFBTSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUM7U0FDckIsR0FBRyxDQUFDLENBQUMsR0FBVyxFQUFFLEVBQUUsQ0FBQyxHQUFHLEdBQUcsT0FBTyxHQUFHLEVBQUUsQ0FBQztTQUN4QyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUE7QUFDZixDQUFDLENBQUE7QUFFWSxRQUFBLGVBQWUsR0FBRyxDQUFDLElBQVMsRUFBRSxFQUFFO0lBQzNDLE9BQU8sTUFBTSxDQUFDLFdBQVcsQ0FDdkIsTUFBTSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEdBQUcsRUFBRSxLQUFLLENBQUMsRUFBRSxFQUFFLENBQUM7UUFDekMsSUFBSSxHQUFHLEVBQUU7UUFDVCxPQUFPLEtBQUssS0FBSyxRQUFRLElBQUksT0FBTyxLQUFLLEtBQUssUUFBUTtZQUNwRCxDQUFDLENBQUMsS0FBSztZQUNQLENBQUMsQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLEtBQUssQ0FBQztLQUMxQixDQUFDLENBQ0gsQ0FBQTtBQUNILENBQUMsQ0FBQTtBQUVZLFFBQUEsT0FBTyxHQUFHLEtBQUssRUFDMUIsTUFBcUIsRUFDK0IsRUFBRTtJQUN0RCxNQUFNLEtBQUssR0FBRztRQUNaLEdBQUcsTUFBTTtLQUNWLENBQUE7SUFFRCxPQUFPLElBQUksT0FBTyxDQUFDLENBQUMsT0FBTyxFQUFFLE1BQU0sRUFBRSxFQUFFO1FBQ3JDLFFBQVEsQ0FBQyxHQUFHLENBQUMsS0FBSyxFQUFFLENBQUMsR0FBRyxFQUFFLE1BQU0sRUFBRSxFQUFFO1lBQ2xDLElBQUksR0FBRyxFQUFFO2dCQUNQLE9BQU8sQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUE7Z0JBQ2xCLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQTthQUNaO2lCQUFNO2dCQUNMLE9BQU8sQ0FBQyxNQUFNLENBQUMsQ0FBQTthQUNoQjtRQUNILENBQUMsQ0FBQyxDQUFBO0lBQ0osQ0FBQyxDQUFDLENBQUE7QUFDSixDQUFDLENBQUE7QUFFWSxRQUFBLFNBQVMsR0FBRyxLQUFLLEVBQzVCLE1BQXVCLEVBQzBCLEVBQUU7SUFDbkQsTUFBTSxLQUFLLEdBQUc7UUFDWixHQUFHLE1BQU07S0FDVixDQUFBO0lBRUQsT0FBTyxJQUFJLE9BQU8sQ0FBQyxDQUFDLE9BQU8sRUFBRSxNQUFNLEVBQUUsRUFBRTtRQUNyQyxRQUFRLENBQUMsSUFBSSxDQUFDLEtBQUssRUFBRSxDQUFDLEdBQUcsRUFBRSxNQUFNLEVBQUUsRUFBRTtZQUNuQyxJQUFJLEdBQUcsRUFBRTtnQkFDUCxPQUFPLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFBO2dCQUNsQixNQUFNLENBQUMsR0FBRyxDQUFDLENBQUE7YUFDWjtpQkFBTTtnQkFDTCxPQUFPLENBQUMsTUFBTSxDQUFDLENBQUE7YUFDaEI7UUFDSCxDQUFDLENBQUMsQ0FBQTtJQUNKLENBQUMsQ0FBQyxDQUFBO0FBQ0osQ0FBQyxDQUFBO0FBRVksUUFBQSxVQUFVLEdBQUcsS0FBSyxFQUM3QixNQUF3QixFQUMrQixFQUFFO0lBQ3pELE1BQU0sS0FBSyxHQUFHO1FBQ1osR0FBRyxNQUFNO0tBQ1YsQ0FBQTtJQUVELE9BQU8sSUFBSSxPQUFPLENBQUMsQ0FBQyxPQUFPLEVBQUUsTUFBTSxFQUFFLEVBQUU7UUFDckMsUUFBUSxDQUFDLE1BQU0sQ0FBQyxLQUFLLEVBQUUsQ0FBQyxHQUFHLEVBQUUsTUFBTSxFQUFFLEVBQUU7WUFDckMsSUFBSSxHQUFHLEVBQUU7Z0JBQ1AsT0FBTyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQTtnQkFDbEIsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFBO2FBQ1o7aUJBQU07Z0JBQ0wsT0FBTyxDQUFDLE1BQU0sQ0FBQyxDQUFBO2FBQ2hCO1FBQ0gsQ0FBQyxDQUFDLENBQUE7SUFDSixDQUFDLENBQUMsQ0FBQTtBQUNKLENBQUMsQ0FBQSJ9

================================================
FILE: examples/serverless-rest-example/dist/manageItems.js
================================================
"use strict";
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
    result["default"] = mod;
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const db = __importStar(require("./dynamodb"));
const v4_1 = __importDefault(require("uuid/v4"));
function response(statusCode, body) {
    return {
        statusCode,
        body: JSON.stringify(body),
    };
}
// fetch using /item/ID
exports.getItem = async (event) => {
    const itemId = event.pathParameters ? event.pathParameters.itemId : "";
    const item = await db.getItem({
        TableName: process.env.ITEM_TABLE,
        Key: { itemId },
    });
    if (item.Item) {
        return response(200, {
            status: "success",
            item: item.Item,
        });
    }
    else {
        return response(404, {
            status: "error",
            error: "Item not found",
        });
    }
};
// upsert an item
// /item or /item/ID
exports.updateItem = async (event) => {
    let itemId = event.pathParameters ? event.pathParameters.itemId : v4_1.default();
    let createdAt = new Date().toISOString();
    if (event.pathParameters && event.pathParameters.itemId) {
        // find item if exists
        const find = await db.getItem({
            TableName: process.env.ITEM_TABLE,
            Key: { itemId },
        });
        if (find.Item) {
            // save createdAt so we don't overwrite on update
            createdAt = find.Item.createdAt;
        }
        else {
            return response(404, {
                status: "error",
                error: `Item not found ${event.pathParameters.itemId}`,
            });
        }
    }
    if (!event.body) {
        return response(400, {
            status: "error",
            error: "Provide a JSON body",
        });
    }
    let body = JSON.parse(event.body);
    if (body.itemId) {
        // this will confuse DynamoDB, you can't update the key
        delete body.itemId;
    }
    const item = await db.updateItem({
        TableName: process.env.ITEM_TABLE,
        Key: { itemId },
        UpdateExpression: `SET ${db.buildExpression(body)}, createdAt = :createdAt, lastUpdatedAt = :lastUpdatedAt`,
        ExpressionAttributeValues: {
            ...db.buildAttributes(body),
            ":createdAt": createdAt,
            ":lastUpdatedAt": new Date().toISOString(),
        },
        ReturnValues: "ALL_NEW",
    });
    return response(200, {
        status: "success",
        item: item.Attributes,
    });
};
exports.deleteItem = async (event) => {
    const itemId = event.pathParameters ? event.pathParameters.itemId : null;
    if (!itemId) {
        return response(400, {
            status: "error",
            error: "Provide an itemId",
        });
    }
    // DynamoDB handles deleting already deleted files, no error :)
    const item = await db.deleteItem({
        TableName: process.env.ITEM_TABLE,
        Key: { itemId },
        ReturnValues: "ALL_OLD",
    });
    return response(200, {
        status: "success",
        itemWas: item.Attributes,
    });
};
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFuYWdlSXRlbXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvbWFuYWdlSXRlbXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7O0FBRUEsK0NBQWdDO0FBQ2hDLGlEQUE0QjtBQUU1QixTQUFTLFFBQVEsQ0FBQyxVQUFrQixFQUFFLElBQVM7SUFDN0MsT0FBTztRQUNMLFVBQVU7UUFDVixJQUFJLEVBQUUsSUFBSSxDQUFDLFNBQVMsQ0FBQyxJQUFJLENBQUM7S0FDM0IsQ0FBQTtBQUNILENBQUM7QUFFRCx1QkFBdUI7QUFDVixRQUFBLE9BQU8sR0FBRyxLQUFLLEVBQUUsS0FBc0IsRUFBd0IsRUFBRTtJQUM1RSxNQUFNLE1BQU0sR0FBRyxLQUFLLENBQUMsY0FBYyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsY0FBYyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFBO0lBRXRFLE1BQU0sSUFBSSxHQUFHLE1BQU0sRUFBRSxDQUFDLE9BQU8sQ0FBQztRQUM1QixTQUFTLEVBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxVQUFXO1FBQ2xDLEdBQUcsRUFBRSxFQUFFLE1BQU0sRUFBRTtLQUNoQixDQUFDLENBQUE7SUFFRixJQUFJLElBQUksQ0FBQyxJQUFJLEVBQUU7UUFDYixPQUFPLFFBQVEsQ0FBQyxHQUFHLEVBQUU7WUFDbkIsTUFBTSxFQUFFLFNBQVM7WUFDakIsSUFBSSxFQUFFLElBQUksQ0FBQyxJQUFJO1NBQ2hCLENBQUMsQ0FBQTtLQUNIO1NBQU07UUFDTCxPQUFPLFFBQVEsQ0FBQyxHQUFHLEVBQUU7WUFDbkIsTUFBTSxFQUFFLE9BQU87WUFDZixLQUFLLEVBQUUsZ0JBQWdCO1NBQ3hCLENBQUMsQ0FBQTtLQUNIO0FBQ0gsQ0FBQyxDQUFBO0FBRUQsaUJBQWlCO0FBQ2pCLG9CQUFvQjtBQUNQLFFBQUEsVUFBVSxHQUFHLEtBQUssRUFDN0IsS0FBc0IsRUFDQSxFQUFFO0lBQ3hCLElBQUksTUFBTSxHQUFHLEtBQUssQ0FBQyxjQUFjLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxjQUFjLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQyxZQUFNLEVBQUUsQ0FBQTtJQUUxRSxJQUFJLFNBQVMsR0FBRyxJQUFJLElBQUksRUFBRSxDQUFDLFdBQVcsRUFBRSxDQUFBO0lBRXhDLElBQUksS0FBSyxDQUFDLGNBQWMsSUFBSSxLQUFLLENBQUMsY0FBYyxDQUFDLE1BQU0sRUFBRTtRQUN2RCxzQkFBc0I7UUFDdEIsTUFBTSxJQUFJLEdBQUcsTUFBTSxFQUFFLENBQUMsT0FBTyxDQUFDO1lBQzVCLFNBQVMsRUFBRSxPQUFPLENBQUMsR0FBRyxDQUFDLFVBQVc7WUFDbEMsR0FBRyxFQUFFLEVBQUUsTUFBTSxFQUFFO1NBQ2hCLENBQUMsQ0FBQTtRQUNGLElBQUksSUFBSSxDQUFDLElBQUksRUFBRTtZQUNiLGlEQUFpRDtZQUNqRCxTQUFTLEdBQUcsSUFBSSxDQUFDLElBQUksQ0FBQyxTQUFTLENBQUE7U0FDaEM7YUFBTTtZQUNMLE9BQU8sUUFBUSxDQUFDLEdBQUcsRUFBRTtnQkFDbkIsTUFBTSxFQUFFLE9BQU87Z0JBQ2YsS0FBSyxFQUFFLGtCQUFrQixLQUFLLENBQUMsY0FBYyxDQUFDLE1BQU0sRUFBRTthQUN2RCxDQUFDLENBQUE7U0FDSDtLQUNGO0lBRUQsSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLEVBQUU7UUFDZixPQUFPLFFBQVEsQ0FBQyxHQUFHLEVBQUU7WUFDbkIsTUFBTSxFQUFFLE9BQU87WUFDZixLQUFLLEVBQUUscUJBQXFCO1NBQzdCLENBQUMsQ0FBQTtLQUNIO0lBRUQsSUFBSSxJQUFJLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLENBQUE7SUFFakMsSUFBSSxJQUFJLENBQUMsTUFBTSxFQUFFO1FBQ2YsdURBQXVEO1FBQ3ZELE9BQU8sSUFBSSxDQUFDLE1BQU0sQ0FBQTtLQUNuQjtJQUVELE1BQU0sSUFBSSxHQUFHLE1BQU0sRUFBRSxDQUFDLFVBQVUsQ0FBQztRQUMvQixTQUFTLEVBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxVQUFXO1FBQ2xDLEdBQUcsRUFBRSxFQUFFLE1BQU0sRUFBRTtRQUNmLGdCQUFnQixFQUFFLE9BQU8sRUFBRSxDQUFDLGVBQWUsQ0FDekMsSUFBSSxDQUNMLDBEQUEwRDtRQUMzRCx5QkFBeUIsRUFBRTtZQUN6QixHQUFHLEVBQUUsQ0FBQyxlQUFlLENBQUMsSUFBSSxDQUFDO1lBQzNCLFlBQVksRUFBRSxTQUFTO1lBQ3ZCLGdCQUFnQixFQUFFLElBQUksSUFBSSxFQUFFLENBQUMsV0FBVyxFQUFFO1NBQzNDO1FBQ0QsWUFBWSxFQUFFLFNBQVM7S0FDeEIsQ0FBQyxDQUFBO0lBRUYsT0FBTyxRQUFRLENBQUMsR0FBRyxFQUFFO1FBQ25CLE1BQU0sRUFBRSxTQUFTO1FBQ2pCLElBQUksRUFBRSxJQUFJLENBQUMsVUFBVTtLQUN0QixDQUFDLENBQUE7QUFDSixDQUFDLENBQUE7QUFFWSxRQUFBLFVBQVUsR0FBRyxLQUFLLEVBQzdCLEtBQXNCLEVBQ0EsRUFBRTtJQUN4QixNQUFNLE1BQU0sR0FBRyxLQUFLLENBQUMsY0FBYyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsY0FBYyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFBO0lBRXhFLElBQUksQ0FBQyxNQUFNLEVBQUU7UUFDWCxPQUFPLFFBQVEsQ0FBQyxHQUFHLEVBQUU7WUFDbkIsTUFBTSxFQUFFLE9BQU87WUFDZixLQUFLLEVBQUUsbUJBQW1CO1NBQzNCLENBQUMsQ0FBQTtLQUNIO0lBRUQsK0RBQStEO0lBQy9ELE1BQU0sSUFBSSxHQUFHLE1BQU0sRUFBRSxDQUFDLFVBQVUsQ0FBQztRQUMvQixTQUFTLEVBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxVQUFXO1FBQ2xDLEdBQUcsRUFBRSxFQUFFLE1BQU0sRUFBRTtRQUNmLFlBQVksRUFBRSxTQUFTO0tBQ3hCLENBQUMsQ0FBQTtJQUVGLE9BQU8sUUFBUSxDQUFDLEdBQUcsRUFBRTtRQUNuQixNQUFNLEVBQUUsU0FBUztRQUNqQixPQUFPLEVBQUUsSUFBSSxDQUFDLFVBQVU7S0FDekIsQ0FBQyxDQUFBO0FBQ0osQ0FBQyxDQUFBIn0=

================================================
FILE: examples/serverless-rest-example/package.json
================================================
{
  "name": "serverless-rest-example",
  "version": "1.0.0",
  "description": "A simple CRUD example using serverless",
  "main": "index.js",
  "repository": "https://github.com/Swizec/serverless-handbook",
  "author": "Swizec",
  "license": "MIT",
  "scripts": {
    "build": "tsc",
    "deploy": "npm run build && serverless deploy"
  },
  "engines": {
    "node": "12.x || 14.x"
  },
  "dependencies": {
    "@types/aws-lambda": "^8.10.39",
    "@types/aws-sdk": "^2.7.0",
    "@types/node-fetch": "^2.5.4",
    "@types/uuid": "^3.4.6",
    "aws-lambda": "^1.0.4",
    "aws-sdk": "^2.597.0",
    "prettier": "^2.2.1",
    "querystring": "^0.2.0",
    "uuid": "^3.3.3"
  }
}


================================================
FILE: examples/serverless-rest-example/serverless.yml
================================================
service: serverless-rest-example

provider:
  name: aws
  runtime: nodejs12.x
  region: us-east-1
  stage: dev
  environment:
    ITEM_TABLE: ${self:service}-items-${self:provider.stage}
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.ITEM_TABLE}"

functions:
  getItems:
    handler: dist/manageItems.getItem
    events:
      - http:
          path: item/{itemId}
          method: GET
          cors: true
  updateItems:
    handler: dist/manageItems.updateItem
    events:
      - http:
          path: item
          method: POST
          cors: true
      - http:
          path: item/{itemId}
          method: POST
          cors: true
  deleteItems:
    handler: dist/manageItems.deleteItem
    events:
      - http:
          path: item/{itemId}
          method: DELETE
          cors: true

resources:
  Resources:
    UsersTable:
      Type: "AWS::DynamoDB::Table"
      Properties:
        AttributeDefinitions:
          - AttributeName: itemId
            AttributeType: S
        KeySchema:
          - AttributeName: itemId
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        TableName: ${self:provider.environment.ITEM_TABLE}


================================================
FILE: examples/serverless-rest-example/src/dynamodb.ts
================================================
import AWS from "aws-sdk"

const dynamoDB = new AWS.DynamoDB.DocumentClient()

interface UpdateItemParams {
  TableName: string
  Key: {
    [key: string]: string
  }
  UpdateExpression: string
  ExpressionAttributeValues: {
    [key: string]: string | number | undefined | null
  }
  ReturnValues?: string
}

interface GetItemParams {
  TableName: string
  Key: {
    [key: string]: string
  }
}

interface DeleteItemParams {
  TableName: string
  Key: {
    [key: string]: string
  }
  ReturnValues?: string
}

interface ScanItemsParams {
  TableName: string
  FilterExpression?: string
  ExpressionAttributeNames?: {
    [key: string]: string
  }
  ExpressionAttributeValues?: {
    [key: string]: string | number | undefined | null
  }
}

export const updateItem = async (
  params: UpdateItemParams
): Promise<AWS.DynamoDB.DocumentClient.UpdateItemOutput> => {
  const query = {
    ...params,
  }

  return new Promise((resolve, reject) => {
    dynamoDB.update(query, (err, result) => {
      if (err) {
        console.error(err)
        reject(err)
      } else {
        resolve(result)
      }
    })
  })
}

// collect all fields in a JSON object into a DynamoDB expression
export const buildExpression = (body: any) => {
  return Object.keys(body)
    .map((key: string) => `${key} = :${key}`)
    .join(", ")
}

export const buildAttributes = (body: any) => {
  return Object.fromEntries(
    Object.entries(body).map(([key, value]) => [
      `:${key}`,
      typeof value === "string" || typeof value === "number"
        ? value
        : JSON.stringify(value),
    ])
  )
}

export const getItem = async (
  params: GetItemParams
): Promise<AWS.DynamoDB.DocumentClient.GetItemOutput> => {
  const query = {
    ...params,
  }

  return new Promise((resolve, reject) => {
    dynamoDB.get(query, (err, result) => {
      if (err) {
        console.error(err)
        reject(err)
      } else {
        resolve(result)
      }
    })
  })
}

export const scanItems = async (
  params: ScanItemsParams
): Promise<AWS.DynamoDB.DocumentClient.ScanOutput> => {
  const query = {
    ...params,
  }

  return new Promise((resolve, reject) => {
    dynamoDB.scan(query, (err, result) => {
      if (err) {
        console.error(err)
        reject(err)
      } else {
        resolve(result)
      }
    })
  })
}

export const deleteItem = async (
  params: DeleteItemParams
): Promise<AWS.DynamoDB.DocumentClient.DeleteItemOutput> => {
  const query = {
    ...params,
  }

  return new Promise((resolve, reject) => {
    dynamoDB.delete(query, (err, result) => {
      if (err) {
        console.error(err)
        reject(err)
      } else {
        resolve(result)
      }
    })
  })
}


================================================
FILE: examples/serverless-rest-example/src/manageItems.ts
================================================
import { APIGatewayEvent } from "aws-lambda"
import { APIResponse } from "./types"
import * as db from "./dynamodb"
import uuidv4 from "uuid/v4"

function response(statusCode: number, body: any) {
  return {
    statusCode,
    body: JSON.stringify(body),
  }
}

// fetch using /item/ID
export const getItem = async (event: APIGatewayEvent): Promise<APIResponse> => {
  const itemId = event.pathParameters ? event.pathParameters.itemId : ""

  const item = await db.getItem({
    TableName: process.env.ITEM_TABLE!,
    Key: { itemId },
  })

  if (item.Item) {
    return response(200, {
      status: "success",
      item: item.Item,
    })
  } else {
    return response(404, {
      status: "error",
      error: "Item not found",
    })
  }
}

// upsert an item
// /item or /item/ID
export const updateItem = async (
  event: APIGatewayEvent
): Promise<APIResponse> => {
  let itemId = event.pathParameters ? event.pathParameters.itemId : uuidv4()

  let createdAt = new Date().toISOString()

  if (event.pathParameters && event.pathParameters.itemId) {
    // find item if exists
    const find = await db.getItem({
      TableName: process.env.ITEM_TABLE!,
      Key: { itemId },
    })
    if (find.Item) {
      // save createdAt so we don't overwrite on update
      createdAt = find.Item.createdAt
    } else {
      return response(404, {
        status: "error",
        error: `Item not found ${event.pathParameters.itemId}`,
      })
    }
  }

  if (!event.body) {
    return response(400, {
      status: "error",
      error: "Provide a JSON body",
    })
  }

  let body = JSON.parse(event.body)

  if (body.itemId) {
    // this will confuse DynamoDB, you can't update the key
    delete body.itemId
  }

  const item = await db.updateItem({
    TableName: process.env.ITEM_TABLE!,
    Key: { itemId },
    UpdateExpression: `SET ${db.buildExpression(
      body
    )}, createdAt = :createdAt, lastUpdatedAt = :lastUpdatedAt`,
    ExpressionAttributeValues: {
      ...db.buildAttributes(body),
      ":createdAt": createdAt,
      ":lastUpdatedAt": new Date().toISOString(),
    },
    ReturnValues: "ALL_NEW",
  })

  return response(200, {
    status: "success",
    item: item.Attributes,
  })
}

export const deleteItem = async (
  event: APIGatewayEvent
): Promise<APIResponse> => {
  const itemId = event.pathParameters ? event.pathParameters.itemId : null

  if (!itemId) {
    return response(400, {
      status: "error",
      error: "Provide an itemId",
    })
  }

  // DynamoDB handles deleting already deleted files, no error :)
  const item = await db.deleteItem({
    TableName: process.env.ITEM_TABLE!,
    Key: { itemId },
    ReturnValues: "ALL_OLD",
  })

  return response(200, {
    status: "success",
    itemWas: item.Attributes,
  })
}


================================================
FILE: examples/serverless-rest-example/src/types.d.ts
================================================
export interface APIResponse {
  statusCode: number
  body: string
}


================================================
FILE: examples/serverless-rest-example/tsconfig.json
================================================
{
    "compilerOptions": {
        "target": "ES2019",
        "module": "commonjs",
        "outDir": "./dist",
        "strict": true,
        "baseUrl": "./",
        "typeRoots": ["node_modules/@types"],
        "types": ["node"],
        "esModuleInterop": true,
        "inlineSourceMap": true,
        "lib": ["ES2019"]
    }
}

================================================
FILE: gatsby-browser.js
================================================
// https://serverlesshandbook.dev/?product_id=72rJA8s-O_ZK0H7YXUOQug%3D%3D&product_permalink=qdNn&sale_id=KOlpO90OcOkb7-lTy3I-Cg%3D%3D

// This is from SRD
export const onClientEntry = () => {
  const query = new URLSearchParams(window.location.search)

  if (
    ["72Mdpm2jTgsapPjXuWpuXg==", "72rJA8s-O_ZK0H7YXUOQug=="].includes(
      query.get("product_id")
    ) &&
    ["qdNn", "NsUlA"].includes(query.get("product_permalink")) &&
    query.has("sale_id")
  ) {
    window.localStorage.setItem("unlock_handbook", JSON.stringify(true))
    window.localStorage.setItem("sale_id", JSON.stringify(query.get("sale_id")))
  }
}


================================================
FILE: gatsby-config.js
================================================
module.exports = {
  siteMetadata: {
    title: `Serverless Handbook for Frontend Engineers`,
    description: `Dive into modern backend. Understand any backend.`,
    author: `@swizec`,
    siteUrl: `https://serverlesshandbook.dev/`,
    courseFirstLesson: `/getting-started`,
    convertkit: {
      defaultFormId: "2103715",
      claimFormId: "2175932",
    },
    hasAuthentication: true,
  },
  plugins: [
    "@swizec/gatsby-theme-course-platform",
    {
      resolve: "gatsby-plugin-manifest",
      options: {
        name: "Serverless Handbook for Frontend Engineers",
        short_name: "Serverless Handbook for Frontend Engineers",
        description:
          "Learn everything you need to dive into modern backend. Understand any backend.",
        start_url: "/",
        background_color: "#fff",
        theme_color: "#FF002B",
        display: "standalone",
        icon: "./static/icon.png",
      },
    },
    "gatsby-plugin-remove-serviceworker",
  ],
}


================================================
FILE: now.json
================================================
{
    "version": 2,
    "scope": "swizec",
    "name": "serverlesshandbook-staging",
    "alias": "serverlesshandbook.dev",
    "routes": [{
        "src": "^/(.*).html",
        "headers": {
            "cache-control": "public,max-age=0,must-revalidate"
        },
        "dest": "$1.html"
    }],
    "builds": [{
        "src": "package.json",
        "use": "@now/static-build",
        "config": {
            "distDir": "public"
        }
    }]
}

================================================
FILE: package.json
================================================
{
    "private": true,
    "name": "serverlesshandbook.dev",
    "version": "1.0.0",
    "main": "index.js",
    "license": "MIT",
    "scripts": {
        "start": "gatsby develop",
        "clean": "gatsby clean",
        "build": "gatsby build",
        "serve": "gatsby serve",
        "icon": "npx repng src/components/icon.js -d static -f icon.png -w 512 -h 512",
        "card": "npx repng src/components/twitter-card.js -d static -f card.png -w 1024 -h 512"
    },
    "dependencies": {
        "@swizec/gatsby-theme-course-platform": "^2.3.0",
        "@theme-ui/color": "^0.6.1",
        "gatsby": "^3.2.1",
        "react": "^17.0.1",
        "react-dom": "^17.0.1",
        "typeface-lato": "^1.1.13",
        "typeface-neuton": "^1.1.13",
        "typescript": "^4.2.4",
        "typography-theme-stow-lake": "^0.16.19"
    },
    "engines": {
        "node": "12.x || 14.x"
    }
}


================================================
FILE: src/@swizec/gatsby-theme-course-platform/components/FormCK/formsQuery.js
================================================
export const formsQuery = `
  query {
    site {
      siteMetadata {
        convertkit {
          defaultFormId
        }
      }
    }
  }
`


================================================
FILE: src/@swizec/gatsby-theme-course-platform/components/FormCK/useFormsQuery.js
================================================
import { useStaticQuery, graphql } from "gatsby"

export const useFormsQuery = () => {
  // change this query when you add forms
  const { site } = useStaticQuery(graphql`
    query {
      site {
        siteMetadata {
          convertkit {
            defaultFormId
            claimFormId
          }
        }
      }
    }
  `)

  return site.siteMetadata.convertkit
}


================================================
FILE: src/@swizec/gatsby-theme-course-platform/components/headerLogo.js
================================================
import React from "react"
import { NavLink } from "theme-ui"

const HeaderLogo = ({ siteTitle, logo }) => {
  return (
    <NavLink
      variant="nav"
      href="/"
      sx={{
        width: "100%",
        maxWidth: 220,
        alignItems: "center",
        pl: [0, 2, 2],
      }}
    >
      <p>{siteTitle}</p>
    </NavLink>
  )
}

export default HeaderLogo


================================================
FILE: src/@swizec/gatsby-theme-course-platform/components/layout.js
================================================
import React, { useState, useRef } from "react"
import { Box, Flex } from "theme-ui"
import { Sidenav, Pagination } from "@theme-ui/sidenav"
import {
  Footer,
  Head,
  Header,
  Reactions,
} from "@swizec/gatsby-theme-course-platform"
import Nav from "./nav"
import { Paywall, SnipContent, usePaywall } from "../../../components/Paywall"

const Sidebar = (props) => {
  const { unlocked: contentUnlocked } = usePaywall(props.location.pathname)

  return (
    <Flex
      sx={{
        pt: 64,
      }}
    >
      <Box
        as={Sidenav}
        ref={props.nav}
        open={props.open}
        onClick={(e) => {
          props.setMenu(false)
        }}
        onBlur={(e) => {
          props.setMenu(false)
        }}
        onFocus={(e) => {
          props.setMenu(true)
        }}
        sx={{
          width: [256, 256, 320],
          flex: "none",
          px: 3,
          mt: [64, 0],
          ul: {
            p: 0,
            m: 0,
          },
          "ul > li": {
            mb: 0,
          },
          "ul > li > a": {
            p: "8px",
          },
          maxHeight: "100vh !important",
          overflowY: "scroll",
          height: "100%",
        }}
      >
        <Nav />
      </Box>
      <Box
        sx={{
          width: "100%",
          minWidth: 0,
          maxWidth: 768,
          minHeight: "calc(100vh - 64px)",
          mx: "auto",
          px: [3, 4],
          pb: 5,
        }}
      >
        {contentUnlocked ? (
          <main id="content">{props.children}</main>
        ) : (
          <main id="content">
            <SnipContent>{props.children}</SnipContent>
          </main>
        )}
        {contentUnlocked ? <Reactions page={props.href} /> : null}
        <Paywall pagePath={props.location.pathname} />
        <Nav
          pathname={props.location.pathname}
          components={{
            wrapper: Pagination,
          }}
        />
      </Box>
    </Flex>
  )
}

const Layout = (props) => {
  const fullwidth = props.location.pathname === "/"
  const [menu, setMenu] = useState(false)
  const nav = useRef(null)

  if (props.pageContext.frontmatter) {
    props = {
      ...props,
      ...props.pageContext.frontmatter,
    }
  }

  return (
    <Box
      sx={{
        variant: "styles.root",
      }}
    >
      <Header
        siteTitle="Serverless Handbook"
        courseFirstLesson={props.courseFirstLesson}
        showMenu={false}
        fullwidth={fullwidth}
        menu={menu}
        setMenu={setMenu}
        nav={nav}
      />
      {fullwidth ? (
        <>
          <Head {...props} />
          <main id="content">{props.children}</main>
        </>
      ) : (
        <Sidebar {...props} nav={nav} open={menu} setMenu={setMenu}>
          <Head {...props} />
          {props.children}
        </Sidebar>
      )}
      <Footer />
    </Box>
  )
}

export default Layout


================================================
FILE: src/@swizec/gatsby-theme-course-platform/components/nav.mdx
================================================
- [Getting Started](/getting-started)
- [Serverless Pros & Cons](/serverless-pros-cons)
- [AWS, Azure, Vercel, Netlify, or Firebase?](/serverless-flavors)
- [Good serverless DX](/serverless-dx)
- [Architecture principles](/serverless-architecture-principles)
- [Lambdas, queues, etc](/serverless-elements)
- [Robust backend design](/robust-backend-design)
- [Where to store data](/databases)
- [Creating a REST API](/serverless-rest-api)
- [Using GraphQL](/serverless-graphql)
- [Lambda pipelines](/lambda-pipelines)
- [Monitoring serverless apps](/serverless-monitoring)
- [Dev, QA, and prod](/dev-qa-prod)
- [Serverless performance](/serverless-performance)
- [Serverless Chrome puppeteer](/serverless-chrome-puppeteer)
- [Handling secrets](/handling-secrets)
- [Dealing with authentication](/serverless-authentication)
- [Glossary](/glossary)
- [Appendix: More databases](/appendix-more-databases)
- [Download PDF/epub/mobi](/downloads)


================================================
FILE: src/@swizec/gatsby-theme-course-platform/constants/footerLinks.js
================================================
export default [
    {
        Link: 'https://github.com/Swizec/serverlesshandbook.dev',
        Title: 'GitHub',
    },
    {
        Link: 'https://serverlesshandbook.dev/',
        Title: 'Serverless Handbook',
    },
] 

================================================
FILE: src/components/ClaimForm.js
================================================
import React from "react"
import { useEmailForm } from "@swizec/gatsby-theme-course-platform/src/components/FormCK"
import BouncingLoader from "@swizec/gatsby-theme-course-platform/src/components/BouncingLoader"
import { Flex, Label, Input, Box, Button, Text, Heading, Image } from "theme-ui"
import emailRobot from "@swizec/gatsby-theme-course-platform/src/images/email-robot.gif"
import { useLocalStorage } from "./useLocalStorage"

async function createUser({ name, email }) {
  await fetch(
    "https://tq43ps6oh2.execute-api.us-east-1.amazonaws.com/dev/gumroadPing",
    {
      method: "POST",
      mode: "no-cors",
      body: new URLSearchParams({
        product_permalink: "https://gum.co/NsUlA",
        email,
        full_name: name,
      }).toString(),
    }
  )
}

export const ClaimForm = () => {
  const [unlockHandbook, setUnlockHandbook] = useLocalStorage("unlock_handbook")
  const [saleId, setSaleId] = useLocalStorage("sale_id")

  const {
    formSuccess,
    onSubmit,
    uniqueId,
    register,
    formState,
    submitError,
  } = useEmailForm("claim", async (formData) => {
    await createUser(formData)
    setUnlockHandbook(true)
    setSaleId("came-from-a-claim")
    window.location.href = "/thanks"
  })

  if (formSuccess) {
    return (
      <Flex
        sx={{
          justifyContent: "center",
          flexDirection: "column",
        }}
      >
        <Heading>Thank you ❤️</Heading>
        <p>
          My robots have sent email that tells you how to finish setting up your
          account. Redirecting you to the first chapter with a temporary unlock.
        </p>
        <p>Finish setting up your account and you can login from any device.</p>
        <Image src={emailRobot} />
      </Flex>
    )
  }

  console.log(formState)

  return (
    <Flex
      as="form"
      onSubmit={onSubmit}
      sx={{
        justifyContent: "center",
        flexDirection: "column",
        "& .address": { display: "none" },
      }}
    >
      <Label htmlFor={`${uniqueId}-name`}>Your Name</Label>
      <Input
        id={`${uniqueId}-name`}
        type="text"
        name="name"
        {...register("name", {
          required: "⚠️ Name is required",
          message: "⚠️ Name is required",
        })}
        placeholder="Your name"
      />
      {formState.errors.name && <span>{formState.errors.name.message}</span>}

      <Label htmlFor={`${uniqueId}-email`} sx={{ mt: 2 }}>
        Your Email
      </Label>
      <Input
        id={`${uniqueId}-email`}
        type="email"
        name="email"
        {...register("email", {
          required: "⚠️ E-mail is required",
          pattern: {
            value:
              "^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:.[a-zA-Z0-9-]+)*$",
            message: "⚠️ Invalid email address",
          },
        })}
        placeholder="Your email address"
      />
      {formState.errors.email && <span>{formState.errors.email.message}</span>}

      <Label htmlFor={`${uniqueId}-sharelink`} sx={{ mt: 2 }}>
        Share link
      </Label>
      <Input
        id={`${uniqueId}-sharelink`}
        type="text"
        name="share_link"
        {...register("share_link", {
          required: "⚠️ Share link is required",
          message: "⚠️ Share link is required",
        })}
        placeholder="Where did you share? I'd love to see your post"
      />
      {formState.errors.share_link && (
        <span>{formState.errors.share_link.message}</span>
      )}

      <Box className="address">
        <Label htmlFor={`${uniqueId}-address`} className="required">
          Your Address
        </Label>
        <input
          className="required"
          autoComplete="nope"
          type="text"
          id={`${uniqueId}-address`}
          name="address"
          {...register}
          placeholder="Your address here"
        />
      </Box>
      <Button
        type="submit"
        variant="buy-shiny"
        disabled={formState.isSubmitting}
        sx={{ mt: 2 }}
      >
        {formState.isSubmitting ? <BouncingLoader /> : "Claim my copy"}
      </Button>

      {submitError && <p dangerouslySetInnerHTML={{ __html: submitError }}></p>}

      <Text sx={{ fontSize: "0.8em", mb: 2, textAlign: "center" }}>
        I like privacy too. No spam, no selling your data.
      </Text>
    </Flex>
  )
}


================================================
FILE: src/components/Paywall.js
================================================
import React, { useRef, useEffect } from "react"
import ReactDOMServer from "react-dom/server"
import { useAuth } from "react-use-auth"
import { Box } from "theme-ui"
import { wrapRootElement } from "gatsby-plugin-theme-ui/gatsby-browser"

import { default as PaywallCopy } from "../components/paywall-copy"
import QuickThanks from "./quickthanks"
import { useLocalStorage } from "./useLocalStorage"

function toggleLockedContent(show) {
  if (typeof window !== "undefined") {
    let children = document.getElementById("content").children

    let isLocked = false
    for (let child of children) {
      if (child.id === "lock") isLocked = true
      if (isLocked === true) {
        child.style.display = show ? "block" : "none"
      }
    }
  }
}

function hidePaywall(paywallDiv) {
  if (paywallDiv.current) {
    paywallDiv.current.style = `display: none`
  }

  const overlays =
    typeof window !== "undefined" &&
    document.querySelectorAll(".fadeout-overlay")

  if (overlays) {
    for (let overlay of overlays) {
      overlay.style = "display: none"
    }
  }

  // show content
  toggleLockedContent(true)
}

function showPaywall(paywallDiv) {
  // hide content
  toggleLockedContent(false)

  const overlay = typeof window !== "undefined" && document.createElement("div")
  const main =
    typeof window !== "undefined" && document.querySelector("main#content")

  const style = `
            background-image: linear-gradient(rgba(255, 255, 255, 0) 80%,  rgb(255, 255, 255, 1) 100%);
            width: 100%;
            top: 0px;
            bottom: 0px;
            position: absolute;
          `
  overlay.style = style
  overlay.className = "fadeout-overlay"

  main.style = "position: relative;"
  main.appendChild(overlay)

  const dimensions = main.getBoundingClientRect()

  if (paywallDiv.current) {
    paywallDiv.current.style = `
            top: ${Math.round(dimensions.height * 0.1)}px;
            width: ${Math.round(dimensions.width)}px;
            background-color: var(--theme-ui-colors-muted,#f6f6ff);
          `
  }
}

const LOCKED_PAGES = [
  "/getting-started",
  "/serverless-pros-cons",
  "/serverless-flavors",
  "/serverless-dx",
  "/serverless-architecture-principles",
  "/serverless-elements",
  "/robust-backend-design",
  "/databases",
  "/serverless-rest-api",
  "/serverless-graphql",
  "/lambda-pipelines",
  "/serverless-monitoring",
  "/dev-qa-prod",
  "/serverless-performance",
  "/serverless-chrome-puppeteer",
  "/handling-secrets",
  "/serverless-authentication",
  "/glossary",
  "/appendix-more-databases",
  "/downloads",
]

export function usePaywall(pagePath) {
  const paywallDiv = useRef(null)
  const { isAuthorized } = useAuth()
  const [unlockHandbook] = useLocalStorage("unlock_handbook")
  const [saleId] = useLocalStorage("sale_id")
  const [unlockedPages, setUnlockedPages] = useLocalStorage(
    "unlocked_pages",
    []
  )
  const hasLock = LOCKED_PAGES.includes(pagePath.replace(/\/$/, ""))

  const unlocked =
    !hasLock ||
    (pagePath &&
      unlockedPages.filter((p) => p !== "/downloads").includes(pagePath)) ||
    isAuthorized(["ServerlessHandbook"]) ||
    (unlockHandbook && saleId)

  useEffect(() => {
    // doesn't run during on-page navigation for some reason
    if (typeof window !== "undefined") {
      window.requestAnimationFrame(() => {
        if (unlocked) {
          hidePaywall(paywallDiv)
        } else {
          showPaywall(paywallDiv)
        }
      })
    }
  }, [unlocked])

  function unlockCurrentPage() {
    setUnlockedPages((unlockedPages) => [...unlockedPages, pagePath])
  }

  return { unlocked, paywallDiv, unlockCurrentPage, SnipContent }
}

export function SnipContent({ children }) {
  const html = ReactDOMServer.renderToString(children).split(
    '<div id="lock"></div>'
  )[0]

  //   return wrapRootElement({
  //     element: children,
  //   })

  //   return wrapRootElement({
  //     element: <div dangerouslySetInnerHTML={{ __html: html }} />,
  //   })

  return <div dangerouslySetInnerHTML={{ __html: html }} />

  //   return (
  //     <WrapRootElement
  //       element={<div dangerouslySetInnerHTML={{ __html: html }} />}
  //     />
  //   )
}

export const Paywall = ({ pagePath }) => {
  const { unlocked, paywallDiv, unlockCurrentPage } = usePaywall(pagePath)

  if (unlocked) {
    return <QuickThanks />
  } else {
    return (
      <Box id="paywall-copy" ref={paywallDiv}>
        <PaywallCopy unlockCurrentPage={unlockCurrentPage} />
      </Box>
    )
  }
}


================================================
FILE: src/components/TestCloudFunctions.js
================================================
import React, { useState } from "react"
import { Box, Button, Heading, Input, Label } from "theme-ui"
import prettier from "prettier/standalone"
import parserBabel from "prettier/parser-babel"

export const TestCloudFunction = ({
  serviceName,
  urlPlaceholder,
  jsonPlaceholder,
  defaults = false,
}) => {
  const [url, setUrl] = useState(defaults ? urlPlaceholder : "")
  const [payload, setPayload] = useState(defaults ? jsonPlaceholder : "")
  const [result, setResult] = useState("")
  const [error, setError] = useState(null)

  async function runTest() {
    setError(null)
    setResult(null)

    if (!url) {
      setError(`Paste the URL you got from ${serviceName}`)
    } else {
      const corsUrl = `https://cors.bridged.cc/${url}`

      try {
        let res
        if (payload) {
          res = await fetch(corsUrl, {
            method: "POST",
            body: payload,
            headers: {
              "Content-Type": "application/json",
            },
          })
        } else {
          res = await fetch(corsUrl)
        }

        setResult(await res.text())
      } catch (e) {
        setError(e.message)
      }
    }
  }

  return (
    <Box bg="muted" p={[2, 3, 3]}>
      <Heading as="h3" mb={[1, 2, 2]}>
        Try your function
      </Heading>
      <p>Leave payload empty for GET requests, valid JSON for POST.</p>
      <Label>Target URL:</Label>
      <Input
        value={url}
        onChange={(ev) => setUrl(ev.target.value)}
        placeholder={urlPlaceholder}
      />
      <Label>JSON payload:</Label>
      <Input
        value={payload}
        onChange={(ev) => setPayload(ev.target.value)}
        placeholder={jsonPlaceholder}
      />
      <Button
        onClick={runTest}
        sx={{ mt: [1, 2, 2], mb: [2, 3, 3], cursor: "pointer" }}
      >
        Run {payload ? "POST" : "GET"} request
      </Button>
      {result ? (
        <>
          <Heading as="h4">Result:</Heading>
          <pre>
            {prettier.format(result, {
              parser: "json",
              plugins: [parserBabel],
            })}
          </pre>
        </>
      ) : null}
      {error ? (
        <>
          <Heading color="red" as="h4">
            Error:
          </Heading>
          <pre style={{ color: "red" }}>{error}</pre>
        </>
      ) : null}
    </Box>
  )
}


================================================
FILE: src/components/homepage.js
================================================
import React from "react"
import { useAuth } from "react-use-auth"
import { Heading, Flex, Box, Text, Button } from "theme-ui"
import { GumroadButton, TinyFormCK } from "@swizec/gatsby-theme-course-platform"
import { StaticImage } from "gatsby-plugin-image"

export const ChapterHeading = ({ sx }) => {
  const { isAuthorized } = useAuth()

  if (isAuthorized(["ServerlessHandbook"])) {
    return <Heading sx={sx}>Chapters</Heading>
  } else {
    return <Heading sx={sx}>Chapter Previews</Heading>
  }
}

export const NavGrid = (props) => (
  <Box
    {...props}
    sx={{
      fontFamily: "heading",
      ul: {
        listStyle: "none",
        p: 0,
        display: "grid",
        gridGap: 3,
        gridTemplateRows: [`repeat(9, 1fr)`, `repeat(5, 1fr)`],
        gridTemplateColumns: ["repeat(2, 1fr)", "repeat(3, 1fr)"],
        gridAutoFlow: ["dense", "column"],
        counterReset: "nav-grid",
      },
      li: {
        fontWeight: "bold",
        fontSize: [1, 2, 2],
        counterIncrement: "nav-grid",
        mb: 0,
        "::before": {
          content: "counter(nav-grid)",
          display: "inline-block",
          pr: 1,
        },
      },
      a: {
        color: "inherit",
        textDecoration: "none",
        transition: "color .2s ease-out",
        ":hover,:focus": {
          color: "primary",
        },
      },
    }}
  />
)

const CoverImage = () => {
  return (
    <a href="https://geni.us/serverless-handbook">
      <StaticImage
        src="../images/cover.png"
        alt="Serverless Handbook cover"
        loading="eager"
        objectFit="cover"
        objectPosition="50% 50%"
      />
    </a>
  )
}

export const HomeTitle = () => (
  <Flex sx={{ flexWrap: "wrap" }}>
    <Box
      sx={{
        p: 3,
        minWidth: 250,
        flex: 1,
        textAlign: "center",
        margin: "auto auto",
      }}
    >
      <Heading sx={{ fontSize: 6 }}>
        Serverless Handbook
        <Text sx={{ fontSize: 4, display: "block" }}>
          for frontend engineers
        </Text>
      </Heading>
      <Text>Dive into modern backend. Understand any backend</Text>

      <Box sx={{ my: 3 }}>
        <Flex
          sx={{
            alignItems: "center",
            justifyContent: "center",
            mb: 1,
            flexDirection: ["column", "row"],
          }}
        >
          <Button
            variant="buy-shiny"
            as="a"
            sx={{ mr: [0, 2], mb: [1, 0] }}
            href="https://geni.us/serverless-handbook"
          >
            Buy now on Amazon
          </Button>
          <a
            className="gumroad-button"
            href="https://gum.co/NsUlA"
            data-gumroad-single-product="true"
            target="_blank"
            rel="noopener noreferrer"
          >
            Buy Digital for $15
          </a>
        </Flex>
        <Text sx={{ fontSize: 0, display: "block" }}>
          #1 new release in Web Development
        </Text>
      </Box>

      <Box
        sx={{
          p: 3,
          minWidth: 250,
          flex: 1,
          textAlign: "center",
          display: ["block", "none"],
        }}
      >
        <CoverImage />
      </Box>

      <Box sx={{ mt: 3 }}>
        <TinyFormCK copyBefore="" submitText="Send it to me! 💌">
          <Heading sx={{ fontSize: 4, pt: 2 }}>Get your free chapter!</Heading>
          <p>
            Wanna see what’s in Serverless Handbook, but not ready to buy the
            full book? Start with a free chapter.
          </p>
        </TinyFormCK>
      </Box>
    </Box>

    <Box
      sx={{
        p: 3,
        minWidth: 250,
        flex: 1,
        textAlign: "center",
        display: ["none", "block"],
      }}
    >
      <CoverImage />
    </Box>
  </Flex>
)


================================================
FILE: src/components/logo.js
================================================
import React from "react"
import styled from "@emotion/styled"
import { layout } from "styled-system"

const Svg = styled(({ width, height, ...props }) => (
  <svg xmlns="http://www.w3.org/2000/svg" {...props} />
))`
  transform: rotate3d(1, 1, 1, 0deg);
  ${layout}
`


const Logo = props => {

  return (
    <Svg
      viewBox="0 0 64 64"
      style={{
        display: "block",
        maxWidth: "100%",
        margin: 0,
        fill: "none",
        stroke: "cyan",
      }}
      vectorEffect="non-scaling-stroke"
      width={props.size}
      height={props.size}
    ></Svg>
  )
}

Logo.defaultProps = {
  initial: false,
  color: "white",
  bg: "transparent",
  strokeWidth: 2,
  size: 256,
}

export default Logo

================================================
FILE: src/components/paywall-copy.mdx
================================================
import { Box, Flex, Button } from "theme-ui"
import {
  GumroadOverlay,
  GumroadButton,
  TinyFormCK,
} from "@swizec/gatsby-theme-course-platform"

<Box sx={{
          mb: 4,
          border: t => `1px solid ${t.colors.muted}`,
          borderRadius: 2,
          p: 3
        }}>

Hello! 👋

Are you a frontend engineer diving into backend? Do you have just that one bit of code that can't run in the browser? Something that deals with secrets and APIs?

https://youtu.be/udqyBqCgLrU

That's what cloud functions are for my friend. You take a JavaScript function, run it on serverless, get a URL, and voila.

But that's easy mode. Any tutorial can teach you that.

**What happens when you wanna build a real backend?** When you want to understand what's going on? Have opinions on REST vs GraphQL, NoSQL vs. SQL, databases, queues, talk about performance, cost, data processing, deployment strategies, developer experience?

🤯

### Unlock your free chapter!

Access to this chapter immediately, extra free chapter and Serverless crash course in your email ✌️

<TinyFormCK
  copyBefore=""
  submitText="Send crash course! 💌"
  onSuccess={() => setTimeout(props.unlockCurrentPage, 2000)}
/>
<br />

[![](../images/buy-now-amazon.png)](https://geni.us/serverless-handbook)

## Dive into modern backend. Understand any backend

<p style={{ fontSize: "1.1em" }}>
  <strong>Serverless Handbook</strong> shows you how with{" "}
  <strong>360 pages</strong> for people like you getting into backend
  programming.
</p>

With **digital + paperback content** Serverless Handbook has been more than 1 year in development. Lessons learned from 14 years of building production grade websites and webapps.

> With Serverless Handbook, Swiz teaches the truths of distributed systems – things will fail – but he also gives you insight on how to architect projects using reliability and resilience perspectives so you can monitor and recover.

> ~ Thai Wood, author of Resilience Roundup

If you want to understand backends, grok serverless, or just get a feel for modern backend development, this is the book for you.

Serverless Handbook full of **color illustrations**, **code you can try**, and **insights you can learn**. But it's not a cookbook and it's not a tutorial.

[![Serverless Handbook on your bookshelf](../images/serverless-handbook-bookshelf.jpg)](https://geni.us/serverless-handbook)

Yes, there's a couple tutorials to get you started, to show you how it fits together, but the focus is on high-level concepts.

<p style={{ fontSize: "1.1em" }}>
  <strong>Ideas</strong>, <strong>tactics</strong>, and{" "}
  <strong>mindsets</strong> that you need. Because every project is different.
</p>

The Serverless Handbook takes you _from your very first cloud function to modern backend mastery_. In the words of an early reader:

> Serverless Handbook taught me high-leveled topics. I don't like recipe courses and these chapters helped me to feel like I'm not a total noob anymore.

> The hand-drawn diagrams and high-leveled descriptions gave me the feeling that I don't have any critical "knowledge gaps" anymore.

> ~ Marek C, engineer

If you can JavaScript, you can backend.

Plus it looks great on your bookshelf 😉

[![](../images/buy-now-amazon.png)](https://geni.us/serverless-handbook)

Cheers,<br/>
~Swizec

</Box>


================================================
FILE: src/components/quickthanks.mdx
================================================
import { Box } from "theme-ui"

<Box sx={{
          mb: 4,
          border: t => `1px solid ${t.colors.muted}`,
          borderRadius: 2,
          variant: "styles.pre"
        }}>

Thanks for supporting Serverless Handbook!
Consider sharing it with friends ❤️

Add it to your bookshelf if you haven't.

[![](../images/buy-now-amazon.png)](https://geni.us/serverless-handbook)

[![Serverless Handbook on your bookshelf](../images/serverless-handbook-bookshelf.jpg)](https://geni.us/serverless-handbook)

Cheers,<br/>
~Swizec

</Box>


================================================
FILE: src/components/useLocalStorage.js
================================================
import { useState } from "react"

// copypasta dependency borrowed from https://medium.com/javascript-in-plain-english/uselocalstorage-react-hook-2532e922d5b1
export function useLocalStorage(key, initialValue) {
  // State to store our value
  // Pass initial state function to useState so logic is only executed once
  const [storedValue, setStoredValue] = useState(() => {
    try {
      // Get from local storage by key
      const item =
        typeof window !== "undefined" && window.localStorage.getItem(key)
      // Parse stored json or if none return initialValue
      return item ? JSON.parse(item) : initialValue
    } catch (error) {
      // If error also return initialValue
      console.log(error)
      return initialValue
    }
  })

  // Return a wrapped version of useState's setter function that ...
  // ... persists the new value to localStorage.
  const setValue = (value) => {
    try {
      // Allow value to be a function so we have same API as useState
      const valueToStore =
        value instanceof Function ? value(storedValue) : value
      // Save state
      setStoredValue(valueToStore)
      // Save to local storage
      if (typeof window !== "undefined") {
        window.localStorage.setItem(key, JSON.stringify(valueToStore))
      }
    } catch (error) {
      console.log(error)
    }
  }

  return [storedValue, setValue]
}


================================================
FILE: src/gatsby-plugin-theme-ui/index.js
================================================
import { future } from "@theme-ui/presets"
import merge from "lodash.merge"
import { toTheme } from "@theme-ui/typography"
import typography from "typography-theme-stow-lake"
import amazonBuy from "../images/buy-now-amazon.png"
import "typeface-neuton"
import "typeface-lato"

import { courseTheme } from "@swizec/gatsby-theme-course-platform"

const theme = merge(
  future,
  {
    buttons: {
      buy: {
        cursor: "pointer",
        fontWeight: "heading",
        background: `url(${amazonBuy})`,
        backgroundSize: "fit",
        backgroundPosition: "center center",
        backgroundRepeat: "no-repeat",
        textIndent: "-1000%",
        width: 240,
      },
      "buy-shiny": {
        cursor: "pointer",
        fontWeight: "heading",
        bg: "highlight",
        borderRadius: 35,
        "&:hover": { bg: "secondary" },
      },
    },
  },
  toTheme(typography),
  courseTheme
)

export default theme


================================================
FILE: src/pages/404.mdx
================================================
export const title = "404"

# 404

Page not found


================================================
FILE: src/pages/appendix-more-databases/index.mdx
================================================
---
title: "Appendix: Databases in more detail"
description: "Serverless systems don't have a hard drive ... where do you keep your data?"
image: "./img/databases.png"
---

# Appendix: Databases in more detail

Read additional details and insights that didn't fit into the main chapter on databases.

## Flat file database

![](../../images/flat-file.png)

The simplest way to store data is a [flat file database](https://en.wikipedia.org/wiki/Flat-file_database). Even if you call it "just organized files".

Serverless systems don't have drives to store files so these flat file databases aren't a popular choice. You'd have to use S3 or similar, which negates some of the built-in advantages.

We mention them here because they're often a great choice and most engineers forget they exist.

### Advantages of flat files

Compared to other databases, flat files have zero overhead. Your data goes straight to storage without your database adding any logic on top.

This gives flat files amazing write performance. As long as you're adding data to the end of a file.

Optimizing file access for speed comes down to your operating system. Performance mostly has to do with [memory paging](https://en.wikipedia.org/wiki/Paging), [filesystem](https://en.wikipedia.org/wiki/File_system) configuration, [direct memory access](https://en.wikipedia.org/wiki/Direct_memory_access), and hardware-level optimizations.

The end result is:

- **fast append performance**, because you stream data from memory to drive without processor involvement
- **fast read performance for common reads**, because computers use their memory as read-through cache. Once you read a file, subsequent reads come from much faster memory
- **fast search for predicted lookups**, because you can structure your files in a way that makes common lookups instant
- **great scalability**, because you can spread your data over as many servers as you'd like

### Disadvantages of flat files

Where flat files struggle are data updates. 

To add a line at the beginning of a file, you have to move the whole thing. To change a line in the middle, you have to update everything that comes after.

Another problem is lack of a query interface. 

You have to read all your files to compare, analyze, and search. If you didn't think of a use-case beforehand, you're left with a slow search through everything.

- **slow data updates**, because you often have to rewrite more than just the changes
- **no data shape guarantees**, because you can store individual  data items any way you like. If you change your schema, you have to deal with inconsistencies, or rewrite your data
- **slow broad reads**, because you lose benefits of read-through-cache, if you read data across your entire database with random patterns
- **no ACID compliance** unless you build it yourself at the application layer

<div id="lock" />

### How to use flat files

A flat-file database happens any time you write data to files in a structured manner. Photos on your phone are a great example. 

When you take a photo, your phone stores it as a file. Filenames often follow a naming convention – `IMG_0001`, `IMG_0002`, etc. Some cameras add dates.

Now you can sequentially access photos. Start at the beginning, go until the end. You can even keep track of how far you've gotten so you can jump ahead next time.

What about finding all photos from a certain date?

That's why images contain [EXIF metadata](https://en.wikipedia.org/wiki/Exif) – information about date, time, GPS location, even phone orientation. It's all stored in the file in a structured way.

To find images from a certain date, you can search through your files and look at the metadata they contain. 

#### Hierarchical and flat file organization

You can speed up seek operations by adding common search keys to your file names.

A good example is organizing photos by year - month - date. Each year is a directory containing directories for months. Each of those contains directories for days. Those contain photos.

Hierarchical storage slashes search speed by a huge factor. You can jump right into a specific category (date).

But hierarchical storage makes scanning harder. Counting all images now requires a recursive search through directories.

A better approach might be encoding your meta data in the filename. Something like `IMG_${year}-${month}-${day}-001.jpg`.

Gives you quick access to specific dates *and* fast scans across many. 👌

### When you should store data in flat files

Due to its low overhead, flat file storage is a great choice when you're looking for speed and simplicity.

**Use flat files when:**

1. You need fast append-only writes
2. You have simple querying requirements
3. You read data more often than you write data
4. You write data that you rarely read

**Avoid flat files when:**

1. You need to cross-reference data or use complex queries
2. You need fast access across your entire database
3. Your data often changes

No. 3 is the flat file database killer.

The most common use cases for flat files are logs, large datasets, and large binary files (image, video, etc).

You often append logs and rarely read them. You store large datasets as structured files for easy sharing. You save images and rarely update, and they contain orders of magnitude more binary data than structured metadata.

## Relational databases – RDBMS

![](../../images/relational_generic.png)

[Relational databases](https://en.wikipedia.org/wiki/Relational_database) are the most common type of database people think of when you say "database". Data lives in a structured data model and many features exist to optimize performance.

Choosing a relational database for your business data is almost always the right decision. 

### Advantages of relational databases

Relational databases have been around since the 1970's. They're battle tested, reliable, and can adapt to almost any workload.

Some modern solutions incorporate features from the NoSQL universe to become even more versatile. [Postgres](https://en.wikipedia.org/wiki/PostgreSQL) is a great example since it often outperforms NoSQL solutions on performance benchmarks.

The defining feature of relational databases is their relational data model. The relational data model makes it easy to model complex data using small isolated concepts. Everything else stems from there.

**Decades of research have gone into optimizing relational database performance**. Down to features like reserving empty space on a hard drive so data commonly used together sits together to make access faster 🤯

With a relational database, you get:

- **fast write performance**, because most systems write to memory first and save to drive later
- **fast read performance**, because you can build lookup data structures [(indexes)](https://en.wikipedia.org/wiki/Database_index) for common queries
- **flexible querying**, because you can use a query language (usually SQL) to access any data in any configuration
- **strict ACID compliance**, because it's a core design objective
- some **logical data validation**, because you can describe the shape of your data and have the database enforce its consistency

### Disadvantages of relational databases

Benefits of relational databases come with the downside of being harder to use, requiring more expertise to tune performance, and some loss in flexibility.

You can create a database that's fast as lightning, or shoot yourself in the foot.

Main disadvantages are:

- **high level of complexity**, because it's easy to get started and the fine-tuning rabbit hole goes forever deep
- **performance traps at scale**, because a database that's blazing fast at 10,000 entries, might crawl to a halt at 10,000,000 entries
- **tradeoffs between read and write performance**, because you can get more read performance by sacrificing write performance and vice-versa. Mostly to do with index building
- **data shape inflexibility**, because with a typical configuration, you have to redefine the shape of your data every time you add a property
- **lack of horizontal scalability**, because relational databases can't give you most of their benefits when split across many servers

A common solution for flexibility is to add a blobby JSON field to every model. You lose automatic data integrity and performance optimization on that field, but gain the flexibility to store any data in any shape. Perfect use-case for less important metadata.

### How to use a relational DB

We'll focus on the basics here. You should approach the rest with just-in-time learning – run into a problem, learn how to solve it. ✌️

You start with a database server.

#### A database server

Since you don't want to run your own servers (the whole point of serverless), you'll need a provider. 3rd party services are okay, if your serverless provider doesn't offer their own.

Using a "native" service cuts down on network overhead and makes your system faster. With some luck, your provider runs the database and cloud functions in the same datacenter. Perhaps even on the same physical machine. 🤘

In the AWS world, I've found [RDS – Relational Database Service](https://aws.amazon.com/rds/) works great. Gives you a stable provisioned database server.

Despite giving you a reserved database instance, RDS still offers advantages over rolling your own:

- drive space grows as necessary
- regular updates run automatically
- if a server falls over, RDS brings up a new one with the same data
- automatic backups
- easy restore from backup
- optional multi-zone replication for extra reliability (copies your DB over several data centers)

A more serverless version of RDS is [Amazon Aurora DB](https://aws.amazon.com/rds/aurora/). Implemented as a layer on top of RDS, it scales your database based on usage. Even shutting down when nothing's happening. Less expensive for intermittent workloads *and* you don't have to predict how much power you'll need.

When choosing which relational database software to use, **always choose Postgres**. It's open source, crazy fast, and with great support for modern NoSQL features.

https://twitter.com/Swizec/status/1210371195889049600

#### Model your data

The next step is to model your data.

*How* to model your data is as much an art as it is a science. Sort of a mix between [domain modeling](https://en.wikipedia.org/wiki/Domain_model) and [object modeling](https://en.wikipedia.org/wiki/Object_model).

Models also known as tables store each property as a column. Model instances live as rows inside those tables.

Let's say you're building a blog. You have `authors` and `posts`.

Each author has an `id` (automatically assigned), a `created_at` timestamp, and a `name`. Each post also has an `id` (automatically assigned), a `created_at` timestamp, a `title`, and some `content`.

Ids are numbers, timestamps are timestamps, the rest is text. A relational database ensures that's always true so you can expect valid data.

To create a connection between models, you use a [foreign key](https://en.wikipedia.org/wiki/Foreign_key). A column that points to the identifier of a different table.

For `authors` and `posts` you get a schema like this:

![](../../images/relational.png)

Which in SQL – [Standard Query Language](https://en.wikipedia.org/wiki/SQL) – looks something like this:

```sql
CREATE TABLE IF NOT EXISTS "authors" (
	"id" serial,
	"created_at" timestamp,
	"name" text,
	PRIMARY KEY( id )
);

CREATE TABLE IF NOT EXISTS "posts" (
	"id" serial,
	"created_at" timestamp,
	"title" text,
	"content" text,
	"author_id" integer,
	PRIMARY KEY( id, author_id ),
	FOREIGN KEY ( author_id ) REFERENCES authors( id )
);
```

That creates 2 empty tables in your database and connects them via a foreign key. Postgres automatically increments identifiers and ensures uniqueness as you insert new rows.

Having the `posts` table "belong to" (point at) the `authors` table means each author can have multiple posts.

#### Query your data

You interact with a relational database primarily through [SQL](https://en.wikipedia.org/wiki/SQL). Many web server frameworks come with an [ORM](https://en.wikipedia.org/wiki/Object-relational_mapping) – object-relational-mapping – layer on top of SQL that simplifies common operations.

Regardless of using an ORM, you'll have to know SQL for anything complex. At least have an understanding of how it works.

A basic query that fetches all `authors` looks like this:

```sql
SELECT * FROM authors;
```

Writing keywords in all caps is customary but not required. I think it stems from ye olden times before syntax highlighting.

To fetch just names, you'd do this:

```sql
SELECT name FROM authors;
```

Select *what* from *where*. SQL is meant to be readable as natural English. Used to be the main user interface after all.

To fetch authors created after a certain date, something like this:

```sql
SELECT name FROM authors WHERE created_at >= '2019-12-26';
```

You can put almost any condition in a `WHERE` clause.

Where life gets real tricky real fast is selecting data from multiple tables. You have to use [SQL joins](https://en.wikipedia.org/wiki/Join_(SQL)), which are based on set arithmetic. 

If you want a list of post titles and dates with each author:

```sql
SELECT a.name, p.title, p.created_at
FROM authors a, posts po
WHERE p.author_id = a.id;
```

This is called an [inner join](https://en.wikipedia.org/wiki/Join_(SQL)#Inner_join) where you take a cartesian join combining every row in `authors` with every row in `posts` and filter away the non-matches.

Those are some basics that cover most use-cases. It takes some practice to use SQL effectively so practice away :)

#### Speed up your data

Relational databases use a [query planner](https://en.wikipedia.org/wiki/Query_plan) to execute SQL queries as efficiently as possible. You often don't even have to think about performance.

Common ways to speed up your database include:

1. **Adding indexes** – data structures that help your database find data matching specific queries. Depending on the type of index you choose, it can behave like a directory tree, or like a hash table
2. **Denormalization** – storing properties often used together in the same table even if it means duplicating some data. Like having an author name field in each post.
3. **Partitioning** – telling your database how to chunk a large table into files so common lookups read from the same physical file

Tuning a relational database for performance is a whole field of software engineering so I wouldn't worry about it too much. Learn about it when you need to ✌️

### When you should store data in a relational DB

Choosing a relational database is almost always the correct choice.

**Use relational DBs when:**

1. You don't know how you're using your data
2. You benefit from data integrity
3. You need good performance up to hundreds of million entries
4. Your app fits in a single data center (availability zone)
5. You often use different objects together

**Avoid relational DBs when:**

1. You're storing lots of binary data (images, video)
2. You don't care about data integrity
3. You don't want to invest in initial setup
4. You just need a quick way to save some data
5. You have so much data it doesn't fit on 1 server

This makes relational databases the perfect choice for most applications. You wouldn't use them to store files, but should consider it for metadata about those files. They're also not a great choice for fast append-only writes like logs or tweets.

I wouldn't worry about number 5. If you ever reach the scale where your data doesn't fit in a single database, you'll have a team to solve the problem for you :)

## The NoSQL approach

![](../../images/nosql.png)

"NoSQL" is a broad range of technologies built for different purposes with different design goals in mind. A sort of catch-all for any database that doesn't use relational data models.

Yes even flat file storage is a sort of NoSQL database.

Wikipedia offers [a great description of NoSQL databases](https://en.wikipedia.org/wiki/NoSQL)

> The data structures used by NoSQL databases are different from those used by default in relational databases, making some operations faster in NoSQL. The particular suitability of a given NoSQL database depends on the problem it must solve.

This variety is where NoSQL differs most from relational databases. Where relational databases aim to fit most use cases, NoSQL often aims to solve a specific problem.

### Flavors of NoSQL

You can classify NoSQL databases in 4 broad categories:

1. **key:value store** that works like a dictionary. A unique key points to a stored value. Offers fast read/write performance and is often used as a caching layer in front of a relational database. Some solutions offer sortable keys so you can perform data scans.
2. **document store** that maps unique keys to documents. Basically key:value stores that allow complex values. They often support a query language that lets you search based on value contents, not just the key.
3. **graph database** that stores graph data structures efficiently. Useful for data models with a lot of circular references like social graphs or road maps.
4. **wide column database** that is a sort of mix between a document store and a relational database. Keys map to objects that all fit a schema, but that schema isn't prescriptive. There's no guarantee every object has every column, but you always know every column that might exist.

Everything else is a variation on these themes. Most modern databases support multiple models.

You can stringify JSON objects into a key:value store to create a document store without querying support. Or you can store simple values in a document DB to create a key:value store with too much overhead.

There's nothing stopping you from forcing a graph to live inside a table database either. 😉

### Which NoSQL flavor should you pick?

It really depends. What are you trying to do?

I would prioritize a hosted fully managed database solution that my serverless provider offers. This cuts down on networking overhead, just like the RDBMS section, and makes your life easier because there's one less thing to manage.

Then I would pick whatever fits my use case.

**Use key:value stores** when you need blazing fast data with low overhead. Great for implementing caching layers and queue systems. [Redis](https://en.wikipedia.org/wiki/Redis) is a great choice here. [Memcached](https://en.wikipedia.org/wiki/Memcached), if you just need cache.

I often use Redis *and* Memcached in real world projects.

**Use a document DB or wide column store** when you want a generic database for business data that isn't a relational database. You get the benefit of flexibility and horizontal scalability. No schemas to prepare in advance and little thought about optimizing queries. 

You pay for that 3 years down the line with inconsistent data. Learned my lesson ✌️

[MongoDB](https://en.wikipedia.org/wiki/MongoDB) is a good document/object store. [Amazon DynamoDB](https://en.wikipedia.org/wiki/Amazon_DynamoDB) and [Google's Bigtable](https://en.wikipedia.org/wiki/Bigtable) are great examples of wide column stores.

**Use a graph DB** when you're *actually* storing graph data. While you can fit a graph into any database (I've tried), you're going to benefit from graph querying support that comes with a real graph DB. They're optimized for just that use-case.

Haven't had a good excuse to use one yet, but I've heard [Neo4j](https://en.wikipedia.org/wiki/Neo4j) is great.

**My favorite advantage of most NoSQL databases** is their wonderful integration with the JavaScript/TypeScript ecosystem. Most let you store JSON blobs, which means there's no translation between JavaScript objects and database objects.

That makes your life much easier compared to a SQL-based solution.

### Disadvantages of NoSQL databases

Disadvantages of NoSQL mostly come from its advantages. Funny how that works.

The simplicity of key:value stores gives you speed at the cost of not being able to store complex data.

The write speed of document databases comes at the cost of some ACID compliance. Often using the [eventual consistency](https://en.wikipedia.org/wiki/Eventual_consistency) model to write fast and propagate to other instances and objects later.

The ease of schema-less development comes at the cost of inconsistent data. If you want entries to look the same, you often have to take care of it yourself.

NoSQL databases also struggle with relational data. And it turns out most real world data is relational.

You *can* store relational data in a NoSQL database, but it can be cumbersome to query. Often there's no support for joins so you have to search through different parts of your database and assemble objects by hand.

### How to use a NoSQL DB

How to use a NoSQL DB depends on which database you pick. I recommend using an official library.

Serverless Handbook focuses on the AWS ecosystem with the serverless framework, so we're going to look at DynamoDB. 

DynamoDB is a great choice for saving JSON data, scales well, works fast, and is pretty cheap to use. And unlike RDS, you can set it up using `serverless.yml`.

#### Create a DynamoDB table

To define a new DynamoDB table, add this to your config.

```yaml
provider:
    environment:
        DATA_TABLE: ${self:service}-data-${self:provider.stage}
    iamRoleStatements:
        - Effect: Allow
          Action:
              - dynamodb:Query
              - dynamodb:Scan
              - dynamodb:GetItem
              - dynamodb:PutItem
              - dynamodb:UpdateItem
              - dynamodb:DeleteItem
          Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DATA_TABLE}"
```

Using environment variables for table names makes them easier to use. Postfixing with the stage lets you replicate configuration between development and production without messing up your data.

`iamRoleStatements` give your app permission to use this table.

You then need to create the actual table:

```yaml
resources:
    Resources:
        DataTable:
            Type: "AWS::DynamoDB::Table"
            Properties:
                AttributeDefinitions:
                    - AttributeName: dataId
                      AttributeType: S
                KeySchema:
                    - AttributeName: dataId
                      KeyType: HASH
                ProvisionedThroughput:
                    ReadCapacityUnits: 1
                    WriteCapacityUnits: 1
                TableName: ${self:provider.environment.DATA_TABLE}
```

You're defining an AWS resource of type `DynamoDB::Table` with a required `dataId` column that's going to be used as a key. `HASH` keys give you key:value lookups, `RANGE` keys are easier to scan through.

Each table can have 2 keys at most.

Name your table using the environment variable defined earlier for consistency. ✌️

#### Save some data

Saving data to a DynamoDB table can be a little cumbersome with Amazon's default SDK. There doesn't seem to be a clear best library so I've been writing small wrappers myself.

You save data using upserts: If a key exists, the data is updated. If it doesn't, it's created. I recommend using [uuid](https://en.wikipedia.org/wiki/Universally_unique_identifier) for identifiers.

Something like this:

```typescript
export const createData = async (_: any, params: CreateDataParams) => {
    const dataId = uuidv4();

    const result = await updateItem({
        TableName: process.env.DATE_TABLE!,
        Key: {
            dataId
        },
        UpdateExpression: "SET dataName = :dataName, createdAt = :createdAt",
        ExpressionAttributeValues: {
            ":dataName": params.dataName,
            ":createdAt": new Date().toISOString()
        },
        ReturnValues: "ALL_NEW"
    });

    return result.Attributes;
};
```

Create an identifier with the uuid library, run an update expression on the appropriate table. Return the entire new row.

`UpdateExpression` and `ExpressionAttributeValues` is split into two objects to help DynamoDB prevent injection attacks. Also helps with performance.

`updateItem` is a wrapper I built around the official SDK. You can [see the code on GitHub](https://github.com/Swizec/markdownlandingpage.com/blob/master/server/src/dynamodb.ts#L35). I'll turn it into a library once I'm happy with the ergonomics.

You now have data saved in the database.

**To update this data** you have to create a similar method that gets your `dataId` as a parameter and uses it to run an `updateItem` query. Make sure you aren't always creating a new identifier.

#### Read some data

Unlike with a relational database, you have a choice of *scanning* and *getting*. A scan lets you search for entries that match a criteria. Getting lets you fetch an exact entry.

Something like this:

```typescript
export const allDataWithName = async ({ dataName }) => {
    const result = await scanItems({
        TableName: process.env.DATA_TABLE!,
        FilterExpression: "#dataName = :dataName",
        ExpressionAttributeNames: {
          "#dataName": "dataName"
        },
        ExpressionAttributeValues: {
          ":dataName": dataName
        }
    });

    return result.Items;
};
```

`scanItems` is again [a little helper method I wrote](https://github.com/Swizec/markdownlandingpage.com/blob/master/server/src/dynamodb.ts#L73). It needs to do more because this is quite cumbersome.

But it lets you *scan* through a table looking for entries that fit a criteria. 

You can use the `getItem` approach when you know exactly what you're looking for.

```typescript
export const data = async ({ dataId }) => {
    const result = await getItem({ Key: { dataId } });

    if (!result.Item) {
        return {};
    }

    return result.Item
};
```

Notice how we get a JavaScript object that we can return without modification. That's the beauty of NoSQL. ✌️

## Blockchain

![](../../images/blockchain.png)

Blockchain is the new kid on the block. Usually mixed up with cryptocurrencies and financial speculation, it's actually a solid way to share and store data.

You've probably used one before 👉 git.

That's right, [git](https://en.wikipedia.org/wiki/Git) and [The Blockchain](https://en.wikipedia.org/wiki/Blockchain) share the same underlying data structure: a merkle tree.

A [merkle tree](https://en.wikipedia.org/wiki/Merkle_tree) stores data in a cryptographically verified sequence of blocks. Each new block contains a cryptographic hash of the previous block.

That means you can always verify your data. Follow the chain and validate every hash. Once you reach the initial block, you know your chain is valid.

As a result you don't need a central authority to tell you the current state of your data. Clients can independently decide, if the data they have is valid. Often by assuming the longest valid chain is correct.

Adding a consensus algorithm makes the process even more robust. When you add new data, how many servers have to agree that the data is valid? 

The result is a slow, but robustly decentralized database. 

I wouldn't use the blockchain to store real data just yet, but it's an exciting space to watch. [Blockstack](https://blockstack.org/) is a great way to get started.

================================================
FILE: src/pages/claim.mdx
================================================
import { Box } from "theme-ui"
import { ClaimForm } from "../components/ClaimForm"

# Claim your digital copy

Access interactive features, clickable links, copypasta code, and the latest updates.

<lite-youtube videoid="9hhEorVr5Mk" autoload></lite-youtube>

<br />

Hello 👋

You came here from the back cover of Serverless Handbook, or one of the prompts offering interactive features. I hope that means you're enjoying the book :)

As promised, the live digital edition is included with your purchase.

Same content as the Kindle or Paperback, except the gifs move, the emojis all work, the code is copypasta, and links are clickable. I know there's some real big links in the book 😅

My favorite interactive feature right now are the cloud function test widgets embedded in some chapters. Quick way to test what you're building.

## Here's how it works

1. Share a photo of your book on [twitter](https://twitter.com/intent/tweet?text=Learning%20about%20%23serverless%20from%20@swizec's%20new%20Serverless%20Handbook%20and%20...%20https%3A%2F%2Fserverlesshandbook.dev), [facebook](https://facebook.com/sharer.php?u=https%3A%2F%2Fserverlesshandbook.dev), [linkedin](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fserverlesshandbook.dev), or your favorite place to share things
2. Add a few thoughts
3. Submit this form 👇

<ClaimForm />

4. You'll get an account on ServerlessHandbook.dev with lifetime access

If you don't feel like sharing, email me at hi@swizec.com

Cheers,<br/>
~Swizec


================================================
FILE: src/pages/databases/index.mdx
================================================
---
title: "Databases and serverless"
description: "Serverless systems don't have a hard drive ... where do you keep your data?"
image: "./img/databases.png"
---

# Where and how to store data

![](../../images/chapter_headers/databases.svg)

_**Serverless systems don't have a hard drive, where does your data go?** You need a database._

But how do you choose which database and where do you put it? It depends.

Every database has advantages and disadvantages. Always fit your tech to your problem, not your problem to your tech.

**First, what is a database?** It's [a system for storing and organizing data](https://en.wikipedia.org/wiki/Database). 

> A database is an organized collection of data, generally stored and accessed electronically from a computer system.

Every database technology gives you these features:

- keeps your data
- lets you query data
- lets you update data

*Keeping* data is the difference between a [cache](https://en.wikipedia.org/wiki/Cache_(computing)) and a database. You can have an in-memory database for speed, but that doesn't make it a cache.

## How to choose a database

Databases seek to find a balance between different optimization criteria. Your choice depends on how that balance fits the problem you're solving.

The common criteria are:

1. Speed of reading data
2. Speed of writing data
3. Speed of updating data
4. Speed of changing the shape of data
5. Correctness of data
6. Scalability

Notice how the list is about speed? That's because speed of data access is the biggest predictor of app performance.

I've seen API endpoints hit the database 30+ times. Queries that take 10ms instead of 1ms can mean the difference between a great user experience and a broken app.

### ACID – a database correctness model

We'll focus on speed in this chapter. But to gain speed and scalability, databases sacrifice correctness. It's important that you know what correctness means in a database context.

Traditional databases follow the [ACID model](https://en.wikipedia.org/wiki/ACID) of transactional correctness. A transaction being a logical operation on your data.

- **Atomicity** ensures that operations inside a transaction succeed or fail together and aren't visible until they all succeed
- **Consistency** ensures your data is in a valid state and doesn't become corrupted by a half-failed transaction
- **Isolation** ensures transactions executed in parallel behave the same as if they happened one after another
- **Durability** ensures that once a transaction succeeds, it stays succeeded and the data doesn't vanish

Certain databases add additional levels of logical correctness on top of the ACID model. We'll talk about those later.

<div id="lock" />

Goals of the ACID model might remind you of the [Architecture Principles](/serverless-architecture-principles) chapter. That's because it aims to guarantee, at the database level, what your serverless architecture aims to ensure at the ecosystem level.

You're building a glorified database for your web and mobile apps :)

## Types of databases

You can classify databases into 4 categories based on how they prioritize opposing optimization criteria.

1. **Flat file storage** the simplest and fastest solution, great for large data
2. **Relational databases** the most correct and surprisingly fast solution, great for complex data
3. **NoSQL** the class of databases breaking ACID for greater speed/scalability; different types exist
4. **Blockchain** a distributed database without a central authority, the industry is figuring out what it's good for

Regardless of what you choose, you will talk to your database through the network. It runs on a different machine.

The network round-trips are a bottomline performance limit. No matter how fast your database, you can't get data faster than it can fly through the network.

Serverless providers optimize by running your code as close to your database as possible. If that's not enough, you'll have to run your own servers.

## Flat file database

![](../../images/flat-file.png)

The simplest way to store data is a [flat file database](https://en.wikipedia.org/wiki/Flat-file_database). You might call it "organized files".

Flat files are commonly used for blobby binary data like images. You'll want to put them on S3 for a serverless environment. That negates some of the advantages.

### Advantages of flat files

Compared to other databases, flat files have zero overhead. Your data goes straight to storage without database logic.

This creates amazing read and write performance. As long as you're adding data to the end of a file, creating a new file, and reading the file start to finish.

A good naming structure gives you fast access to specific files.

### Disadvantages of flat files

Flat files struggle with updates. 

To add a line at the beginning of a file, you have to move the whole thing. To change a line in the middle, you have to update everything that comes after.

There's no query interface either. You have to read your files to compare, analyze, and search. And without a database, there's no ACID or data shape guarantees.

### When should you store data in flat files

Flat file storage is great when you're looking for speed and simplicity.

**Use flat files when:**

1. You need fast append-only writes
2. You have simple querying requirements
3. You read data more often than you write data
4. You write data that you rarely read

**Avoid flat files when:**

1. You need to cross-reference data or use complex queries
2. You need fast access across your entire database
3. Your data changes a lot

No. 3 is the flat file database killer.

Common use cases for flat files are logs, large datasets, and binary files (image, video, etc).

*Read about how to use flat files [in the appendix](/appendix-more-databases#flat-file-database)*

## Relational databases – RDBMS

![](../../images/relational_generic.png)

[Relational databases](https://en.wikipedia.org/wiki/Relational_database) are the most common type of database. Data lives in a structured data model and many features exist to optimize performance.

Choosing a relational database for your business data is almost always the right decision. 

### Advantages of relational databases

Relational databases have been around since the 1970's. They're battle tested, reliable, and can adapt to almost any workload.

Modern systems incorporate popular features from NoSQL like unstructured JSON data. [Postgres](https://en.wikipedia.org/wiki/PostgreSQL) even outperforms NoSQL solutions on certain benchmarks.

The defining feature of relational databases is the relational data model which lets you model complex data using small isolated concepts. You almost always end up reimplementing this idea with other databases.

And after decades of research, relational databases are *fast*.

### Disadvantages of relational databases

Relational databases are harder to use, require more expertise to tune performance, and you lose flexibility. This can be a good thing.

You can create a database that's fast as lightning, reach a magic number of entries, and performance falls off a cliff. It's hard to horizontally scale a relational database.

But you *can* make it more flexible with a blobby JSON field on every model. Perfect for metadata.

### When to store data in a relational DB

Choosing a relational database is almost always the correct choice.

**Use relational DBs when:**

1. You don't know how you're using your data
2. You benefit from data integrity
3. You need good performance up to hundreds of millions of entries
4. Your app fits in a single data center (availability zone)
5. You often use different objects together

**Avoid relational DBs when:**

1. You're storing binary data (images, video)
2. You don't care about data integrity
3. You don't want to invest in initial setup
4. You just need a quick way to save something
5. You have more data than fits on 1 server

This makes relational databases the perfect choice for typical applications. You wouldn't use an RDBMS for files, but should consider it for metadata about those files.

*Read about how to use relational databases [in the appendix](/appendix-more-databases#relational-databases--rdbms)*

## The NoSQL approach

![](../../images/nosql.png)

"NoSQL" represents a broad range of technologies built for different reasons. A catch-all for any database that isn't relational.

Flat files are a type of NoSQL database.

Wikipedia offers [a great description](https://en.wikipedia.org/wiki/NoSQL):

> The data structures used by NoSQL databases are different from those used by default in relational databases, making some operations faster in NoSQL. The particular suitability of a given NoSQL database depends on the problem it must solve.

This variety is where NoSQL shines. Where relational databases aim to fit many use cases, NoSQL solutions aim to solve a specific problem.

### Flavors of NoSQL

You can classify NoSQL databases in 4 categories:

1. **key:value store** works like a dictionary. A unique key points to a stored value. Fast read/write performance makes this an ideal caching layer in front of a relational database.
2. **document store** maps unique keys to documents. Like key:value stores with complex values. Many come with a great query engine.
3. **graph database** store graph data structures efficiently. Useful for domains with many circular references like social connections and road maps.
4. **wide column database** act as a mix between a document store and a relational database. Keys map to objects that fit a schema, but the schema isn't prescriptive.

Typical modern databases support multiple models.

### Which NoSQL flavor should you pick?

It depends. What are you trying to do?

I would prioritize a managed database solution from my serverless provider. Cuts down on networking overhead and makes your life easier because there's one less thing to manage.

Then I would pick what fits my use case.

**Use key:value stores** when you need blazing fast data with low overhead. 

**Use a document DB or wide column store** when you want a generic database that isn't relational.

**Use a graph DB** when you're storing graph data.

**My favorite advantage of NoSQL databases** is the wonderful integration with the JavaScript/TypeScript ecosystem. Store JSON blobs, read JavaScript objects.

### Disadvantages of NoSQL databases

Disadvantages of NoSQL stem from its advantages. Funny how that works.

The simplicity of key:value stores gives you speed at the cost of not being able to store complex data.

The write speed of document databases comes at the cost of ACID compliance. Often using the [eventual consistency](https://en.wikipedia.org/wiki/Eventual_consistency) model to write fast and distribute later.

The ease of schema-less development comes at the cost of inconsistent data. NoSQL databases tend to struggle with relations between objects. You can do it, but feels clunky.

*Read more about choosing a NoSQL database and how to use it [in the appendix](/appendix-more-databases#the-nosql-approach-to-data)*

## Blockchain

![](../../images/blockchain.png)

Blockchain is the new kid on the block. Mixed up with cryptocurrencies and financial speculation, it's a solid way to share and store data.

You've used one before 👉 git.

That's right, [git](https://en.wikipedia.org/wiki/Git) and [The Blockchain](https://en.wikipedia.org/wiki/Blockchain) share the same underlying data structure: a merkle tree.

A [merkle tree](https://en.wikipedia.org/wiki/Merkle_tree) stores data in a cryptographically verified sequence of blocks. Each block contains a cryptographic hash of the previous block, which means you can verify the whole chain.

As a result you don't need a central authority to tell you the current state of your data. Each client can decide, if their data is valid. 

Adding a consensus algorithm makes the process even more robust. When you add new data, how many servers have to agree that the data is valid? 

The result is a slow, but robustly decentralized database. 

I wouldn't use the blockchain in production just yet, but it's an exciting space to watch. [Blockstack](https://blockstack.org/) is a great way to start.

## What should you choose?

Projects tend to use a combination of database technologies.

Files for large binary blobs, relational database for business data, key:value store for persistent caching, document store for complex data that lives together.

Adding JSON blobs to relational data is a common compromise. ✌️

Next chapter, we look at building a RESTful API for your data.

================================================
FILE: src/pages/dev-qa-prod/index.mdx
================================================
---
title: "Serverless dev, QA, and prod"
description: "How do you test and share code without breaking user experience?"
image: "./img/dev-qa-prod.png"
---

# Serverless dev, QA, and prod

![Dev, QA, and prod](../../images/chapter_headers/dev-qa-prod.svg)

You're building an app and want to show a friend. Do you ship to production?

You're trying a new feature that doesn't do localhost. Do you publish?

You've built a pipeline that edits user data and want to make sure it works. Test in production?

If you're brave enough ... 

## Before there's users

None of this matters until you have users. Build on the main branch, ship to production, test in real life. Enjoy yourself!

Coding at this stage is *fun*. 

You don't have to worry about corrupting user data. No concerns about disrupting a user's workflow. You don't even need to worry about shipping bugs!

If nobody noticed the bug, was the bug even there?

A word of caution: It's easy to fill your database with crappy data. Try to start production clean. 

Thank me later when counting users isn't a 5 step process. You'd be surprised how hard it can be to answer *"How many users do we have?"*.

## Localhost vs. Production

Once you have users, you need a way to distinguish production from development. That's easy on a solo project.

Localhost is for development, production is for production. Run a copy of production on your machine and test.

The bigger your system, the trickier this gets. You need to host a database, run queues, caching layers, etc.

<div id="lock" />

You can get close with the [LocalStack plugin for the Serverless Framework](https://www.serverless.com/plugins/serverless-localstack). But only production is like production. ✌️

Plus you can't show off localhost to a friend or coworker.

## The 3 stage split

A common solution to the production vs. development problem is the 3 stage split:

0. localhost
1. development
2. staging / QA
3. production

You build and test on localhost. Get fast iteration and reasonable certainty that your code works.

With the Serverless Framework, you can [run lambdas locally](https://www.serverless.com/framework/docs/providers/aws/cli-reference/invoke-local/) like this:

```
sls invoke local --function yourFunction
```

You then push to development. A deployed environment that's like production, but changes lots. Data is irrelevant, used by everyone on the team.

The development environment helps you test your code with others' work. You can show off to a friend, coworker, or product manager for early feedback.

When that works, you push to staging. A more stable environment used to test code right before it ships. Features are production ready, early feedback incorporated.

Staging is the playground for QA and final sign-off from product managers.

Then you push to production. 🚀

### How to use the 3 stage split

Infrastructure-as-code makes the 3 stage split easy to set up. Have 3 branches of your codebase, deploy each to its own stage.

With the serverless framework, you configure the deploy stage in `serverless.yml`:

```yaml
# serverless.yml
service: my-service

provider:
	name: aws
	stage: dev
```

Deploy with `sls deploy` and that creates or updates the `dev` stage. 

Stages work via name-spacing. Every resource embeds the stage as part of its name and URL. Keep that in mind when naming resources manually.

Like when naming a queue:

```yaml
# serverless.yml
resources:
  Resources:
    TimesTwoQueue:
      Type: "AWS::SQS::Queue"
      Properties:
        # include the stage variable in your name
        QueueName: "TimesTwoQueue-${self:provider.stage}"
```

Current stage is embedded in the string through the `${self:provider.stage}` variable.

### Dynamic stages

Editing the stage in `serverless.yml` on every deploy is annoying. Pass it in the command line instead.

```yaml
# serverless.yml

provider:
	name: aws
	# use stage option, dev by default
	stage: ${opt:stage, "dev"}
```

Deploy with `sls deploy --stage prod` to deploy to production. Defaults to `dev`.

Use a new stage to set up a new environment, existing stage to update. The framework figures it out for you.

Make sure stage names match your [`.env.X` configuration files](/handling-secrets).

## Deploy previews

The 3 stage split starts breaking down around the 6 to 7 engineers mark. More if your projects are small, less if they're big.

You start stepping on each other's toes.

Alice is working on a big feature and she'll need 3 months. During that time none of her work can go to production. She'd like to test on development.

Bob meanwhile is fixing bugs and keeping the lights on. He needs to merge his work into development, staging, and production every day.

How can Bob and Alice work together?

There's 2 solutions:

1. Deploy previews
2. Feature flags

With infrastructure-as-code, deploy previews are the simple solution. Create a new stage for every large feature, deploy, show off, and test.

You get an isolated environment with all the working bits and pieces. Automate it with GitHub Actions to create a new stage for every pull request.

That's the model Netlify and Vercel promote. Every pull request is automatically deployed on a new copy of production with every update. 👌

## Trunk-based development

A popular approach in large teams is trunk-based development.

Everyone works on the main branch, deploys to production regularly, and uses feature flags to disable features before they're ready. A strong automated testing culture is critical.

[Google uses the Beyonce rule](https://www.oreilly.com/library/view/software-engineering-at/9781492082781/ch01.html):

> If you liked it, you shoulda put a test on it

Anyone can change any code at any time. Tests help you prevent accidents.

Feature flags let you disable new features before they're ready. Your code hits production quickly which ensures it doesn't break. If you refactored something, others can use it. If you created new functionality, it's available.

But you disable user-facing parts of your feature to avoid a broken experience.

Implementing feature flags can be as easy as an environment variable with a bunch of IF statements, or as complex as progressive canary deploys. Those let you reveal a feature to 1% of users, then 5%, then 10, ...

## Which approach should you pick?

The best approach depends on team size, established norms, correctness requirements, and your deployment environment.

If you have a clean infrastructure-as-code approach, creating new stages is great. If you need manual setup, the 3 stage approach is best.

You can even split your project into sub-projects. Isolated areas of concern that can move and deploy independently. Known as microservices.

And remember, the easier your code is to fix and deploy, the less you have to worry about any of this. **Optimize for fast iteration over avoiding mistakes**.

For side projects I like to test in production. Live wild 🤘

Next chapter, we look at how to think about serverless performance.

================================================
FILE: src/pages/downloads/index.mdx
================================================
---
title: "Downloads"
description: ""
image: ""
---

# Download PDF/epub/mobi

<div id="lock" />

Hi,

The beauty of digital books is that you can have moving gifs, copy pastable code, clickable links, interactive features, responsive layouts, blazing fast load times, low battery usage, and offline support.

Websites are good at that.

But I get it. You're not into that. You like e-ink and 30meg PDF files. 🤷‍♀️

Here you go:

- [download PDF](https://course-downloadables.s3.amazonaws.com/serverless-handbook/Serverless+Handbook.pdf)
- [download epub](https://course-downloadables.s3.amazonaws.com/serverless-handbook/Serverless+Handbook.epub)
- [download mobi](https://course-downloadables.s3.amazonaws.com/serverless-handbook/Serverless+Handbook.mobi)

Content should be the same as on this website as of Mar 25th, 2021. If something's different, pelase let me know.

Cheers,<br/>
~Swizec


================================================
FILE: src/pages/getting-started/index.mdx
================================================
---
title: "Getting Started"
description: "Your first introduction to serverless and why you should use it"
image: "./img/getting-started.png"
---

import { TestCloudFunction } from "../../components/TestCloudFunctions"

# Getting Started with Serverless

![](../../images/chapter_headers/getting-started.svg)

Hello friend ❤️

> I'm happy you're giving serverless a try. It's one of the most exciting shifts in web development since React introduced us to components.

Creating your first serverless application can be intimidating. Type "serverless" into Google and you're hit with millions of results all assuming you know what you're doing.

There's Serverless, the open source framework, then there's AWS Serverless, and a "serverless computing" Wikipedia article, your friends mention lambda functions, then there's cloud functions from Netlify and Vercel ... and aren't Heroku, Microsoft Azure, and DigitalOcean droplets a type of serverless? _"CloudFlare edge workers!"_ someone shouts in the background.

It's all one big mess.

![](giphy:overwhelmed)

That's why I created the Serverless Handbook. The resource I wish I had :)

Let's start with a short history lesson to get a better understanding of what serverless is and what it isn't. Then you'll build your first serverless backend – an app that says Hello 👋

Don't want the intro? [Jump straight to your first app](/getting-started#your-first-serverless-backend)

## What is serverless

> Serverless is other people's servers running your code.

The logical next step to platform as a service, which came from The Cloud, which came from virtual private servers, which came from colocation, which came from a computer on your desk running a web server. 🤯

### First we all had servers.

![The world's first web server, a NeXT Computer](../../images/First_Web_Server.jpg)

You installed Linux on a computer, hooked it up to the internet, begged your internet provider for a static IP address, and let it run 24/7. Mine lived in the bedroom and I'll never forget that IP. Good ol' 193.77.212.100.

With a static IP address, you can tell [DNS](https://en.wikipedia.org/wiki/Domain_Name_System) servers how to find your server with a domain. People can type that domain into a URL and find your server.

But a domain doesn't give you a website or a webapp.

For that, you need to configure Apache or Nginx, set up a reverse proxy to talk to your application, run your application, ensure that it's running and ... it gets out of hand fast. Just to put up a simple website.

### Then came colocation

![A colocation server rack](../../images/Rack001.png)

Colocation was a solution for the bedroom problem. What happens if your house catches fire? What if power goes out? Or Mom trips on the power cable and unplugs your computer?

Residential hosting is not reliable.

Your internet is lower tier than a business would get. Less reliable and if the provider needs to do maintenance, they think nothing of shutting off your pipes during non-peak hours. Your server needs strong internet 24/7.

When you go on vacation, nobody's there to care for your server. Site might go down for a week before you notice. 😱

Colocation lets you take that same server and put it in a data center. They supply the rack space, stable power, good internet, and physical security.

You're left to deal with configuration, maintenance, and replacing hard drives when they fail.

_PS: Computers break all the time. A large data center replaces a hard drive every few minutes just because a typical drive lasts 4 years and when you have thousands, the stats are not in your favor._

It's on you to keep everything running.

### Then came virtualization

<div id="lock" />

Colocation solved physical problems, but not the fact that your servers are bored.

A typical server runs at about 30% utilization, which means you're wasting money.

Reasons for low utilization vary.

You have to keep the hardware happy and thermally content, you have to over-provision in case of traffic spikes or developer mistakes. Sometimes your site just isn't as popular as you'd like.

What if we could run multiple servers on the same machine?

![](giphy:great_idea)

The first type of virtualization were basic [virtual hosts](https://en.wikipedia.org/wiki/Virtual_hosting). They let you run multiple websites on the same machine. A domain maps to an application on your computer, web server knows the mapping, and voila: sites can share resources.

Worked great but caused problems.

Websites on the same computer are _very_ close together. You could hack one site and gain access to another. You could starve every website for resources by attacking 1 site with a lot of traffic. You could config yourself into a corner with overlapping configuration.

[Virtual private servers](https://en.wikipedia.org/wiki/Virtual_private_server) and later [containerization](https://en.wikipedia.org/wiki/OS-level_virtualisation) were the solution to that problem. Rather than multiple websites on the same machine, you can host _multiple whole computers_ on the same machine. Like a computer with many brains. 🤯

The VPS – virtual private server – was born. Providers of "ssh access" became popular in the early 2000's. Pay a few bucks a month and you get a real live server on the internet. No hardware required.

You're on the hook for software setup and you share the machine with other users.

What if your site gets popular?

### The cloud is born

![A data center](../../images/photo-1558494949-ef010cbdcc31.jpeg)

Early VPS was a lot like The Cloud. Computers running on the internet without touching hardware.

Where VPS struggled was scale.

Once your traffic started to grow, you'd need more servers to handle the load. There's only so much a single server can do every second.

And while computers are getting stronger and stronger (known as [vertical scaling](https://en.wikipedia.org/wiki/Scalability#VERTICAL-SCALING)), it's cheaper to share the load between a lot of small computers ([horizontal scaling](https://en.wikipedia.org/wiki/Scalability#VERTICAL-SCALING)).

But how do you ensure your servers are all the same? How do you spin them up quickly when traffic spikes on Black Friday?

You deal with it by hand.

Set up a server, make sure it works. Create a new server. Copy configuration. Create scripts for common tasks and spend hours making sure everything's okay.

Repeat for each new server. 🤮

Cloud solves this problem with automation and containers. [Docker](<https://en.wikipedia.org/wiki/Docker_(software)>) for containerization, [kubernetes](https://en.wikipedia.org/wiki/Kubernetes) for orchestration.

You start every new server from an image in your cloud provider's library. Comes with basic setup and common defaults. You add tweaks and create a new image.

The cloud provider gives you easy controls to create as many instances of that server as you'd like. Press a button, get a server. ✨

Sometimes you can make it scale automatically. Scripts notice traffic rising and create new servers. When traffic subsides, the same scripts tear the servers down.

### Platform as a Service

A variation on The Cloud is the [PaaS](https://en.wikipedia.org/wiki/Platform_as_a_service) – platform as a service.

With PaaS you pay somebody else to deal with the cloud while you focus on code. They configure your servers and dockers and kubernetes and make everything play together. You build the app.

`git push` to deploy and voila. 👌

Many PaaS providers let you drop down a few levels and break everything. You get to mess with low level configs, operating system libraries, web servers, databases, etc. Empowering _and_ dangerous. I tend to get it wrong.

While PaaS takes care of your servers, _you_ have to take care of the "frontend". Set up domains and DNS, make your application run right for the platform, configure your own [CDN](https://en.wikipedia.org/wiki/Content_delivery_network), deal with static files, and so on.

The platform does servers.

### Serverless is born

[Serverless](https://en.wikipedia.org/wiki/Serverless_computing) is the logical next step after PaaS.

Once you have a system that uses containers to automatically scale and descale based on demand, use repeatable configuration, and painless deploys ... that's serverless right there.

Serverless's main innovation are **_tiny_** containers and the ecosystem of services and tools around it.

Server containers so tiny you can spin them up and down in milliseconds. They achieve this because the code they run is:

1. Small
2. Standardized
3. Does 1 thing

A serverless "server" is a function responding to an API endpoint. Request comes in, server wakes up, runs for a few milliseconds, and goes back to bed.

The platform takes care of optimization, configuration, and everything else. You get an input and return an output.

Servers never idle because they live as long as the request they're serving.

Biggest benefit of this approach?

**Metered pricing.** No more money wasted on idling servers waiting for requests. Pay for the time you're getting work done.

## Your first serverless backend

![](giphy:hello_world)

In the next few minutes you're going to build your first serverless backend. A service that says Hello 👋

We're using open source technologies and deploying on AWS Lambda. You can learn about other providers in the [Serverless Flavors](/serverless-flavors) chapter.

You'll need a computer configured for JavaScript development: Have nodejs installed, a code editor, and a terminal.

### Setup for serverless work

When working with serverless I like to use the open source [Serverless](https://github.com/serverless/serverless) framework. We'll talk more about why in the [Good serverless dev experience](/serverless-dx) chapter.

With the serverless framework we're going to configure servers using YAML files. You write config, framework figures out the rest.

Install it globally:

```sh
npm install -g serverless
```

You'll need AWS credentials too.

I recommend following [Serverless's guide on AWS setup](https://serverless.com/framework/docs/providers/aws/guide/credentials/). It walks you through the necessary steps on your Amazon account and a couple terminal commands to run.

### Create a tiny project

There are no special initializers for serverless projects. You start with a directory and add a configuration file.

![](../../images/start-serverless.gif)

```sh
mkdir hello-world
cd hello-world
touch serverless.yml
touch handler.js
```

You now have a project with 2 files:

- `serverless.yml` for configuration
- `handler.js` for server code

In future chapters you'll write backends using TypeScript. But one thing at a time :)

### Configure your first server

Configuration for your server goes in `serverless.yml`. We're telling the Serverless framework that we want to use AWS, run nodejs, and that this is a dev project.

Then we'll tell it where to find the code.

```yaml
# serverless.yml

service: hello-world

provider:
  name: aws
  runtime: nodejs12.x
  stage: dev
```

Our service is called `hello-world` and there's a couple details about our provider. The `stage` tells the difference between development, QA, and production deployments. More on that in the [Dev, QA, and prod](/dev-qa-prod) chapter.

#### Let's tell our server how to run code.

```yaml
# serverless.yml

service: hello-world

provider:
    name: aws
    runtime: nodejs12.x
    stage: dev

functions:
    hello:
        handler: ./handler.hello
        events:
            - http:
	              path: hello
	              method: GET
	              cors: true
```

We started a `functions` section.

Each entry becomes its own tiny server – a serverless lambda. Together, they're the `hello-world` service.

The `hello` lambda calls an exported `hello` function inside our `handler.js` file when a GET request hits `/hello`.

All that from these few lines of code 👌

_PS: enabling [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) lets you call this function from other websites. Like your frontend app._

### Write your first backend function

Backend functions in a serverless environment look like the JavaScript functions you're used to. Grab arguments, return a response.

Add a hello function to `handler.js`

```javascript
// handler.js

exports.hello = async (event) => {
  return {
    statusCode: 200,
    body: "Hello 👋",
  }
}
```

It's an async function that accepts a trigger event and returns a response. A success status with a `Hello 👋` body.

That's it. You wrote backend code. 🤘

### Deploy your first serverless backend

To deploy, we run `serverless deploy`.

![](../../images/deploy-serverless.gif)

And your server is up.

You get a URL for your lambda and some debugging output. My URL is `https://z7pc0lqnw9.execute-api.us-east-1.amazonaws.com/dev/hello`, if you open it in your browser, it's going to say `Hello 👋`

<TestCloudFunction
  serviceName="serverless-hello-world"
  urlPlaceholder="https://z7pc0lqnw9.execute-api.us-east-1.amazonaws.com/dev/hello"
  defaults
/>

I'll keep it up because it's free unless somebody clicks. And when they do, current AWS pricing gives me 1,000,000 clicks per month for free 😛

### What you got

The Serverless framework talked to AWS and configured many things.

![](../../images/hello-world-lambda.png)

- **API Gateway** to proxy requests from the internet to your function
- **Lambda** to run your code. This is a tiny container that wakes up when called.
- **CloudWatch logs** to collect logs from your code. Helps with debugging.

All those are configured for you. No UI to click through, no config to forget about next time, nothing your friends have to set up to deploy the same code.

![](../../images/hello-world.png)

Exciting!

Next chapter, we talk about the pros & cons of using serverless in your next project.


================================================
FILE: src/pages/glossary.mdx
================================================
export const title = "Glossary"

# Glossary

The Serverless Handbook uses lots of terms that might be unfamiliar. I introduce them at first use and understand new words are hard.

Refer here any time you encounter an unfamiliar word. If something's missing ping me on twitter @swizec.

Words and phrases in alphabetical order.

- **ACID** short for atomicity-consistency-isolation-durability, a model of database correctness
- **API** short for Application Programming Interface, use to mean everything from the URL structure of a server to the public interface of a class or function
- **API Gateway** the AWS service that routes HTTP requests from public URLs to internal services
- **ARN** short for Amazon Resource Name, the globally unique identifier for AWS items
- **AWS** amazon web services
- **CDN** short for Content Distribution Network, an approach to serving static files that improves latency
- **CLI** short for Command Line Interface, the terminal you type commands into
- **CORS** a security protocol that limits which domains can access certain assets
- **CPU** short for central processing unit, the part of computers that does most of the work
- **CRUD** short for Create Read Update Delete, a typical set of operations apps need to support
- **CloudWatch** the AWS service that collects and displays basic logs and metrics from every other service
- **DB** short for database
- **DLQ** short for Dead Letter Queue, a special queue used to hold bad messages for further debugging
- **DNS** short for Domain Name System/Server, it translates domains to IP addresses
- **DX** short for developer experience, a catch-all for how it feels to work with a technology
- **Go** a popular lightweight language for server programming 
- **I/O** short for input/output, the process of reading and writing to an external medium like a hard drive or database
- **IP address** the globally unique identifier for a computer on the internet
- **JSON** a popular data format based on JavaScript objects
- **JVM** short for Java Virtual Machine, the runtime environment for Java applications
- **QA** short for Quality Assurance, used to mean both the process and the teams doing it
- **RDS** short for Relational Database Service, an AWS service for hosting relational databases like Postgres and MySQL
- **S3** the AWS static file hosting service
- **SNS** short for Simple Notification Service, a messaging pub/sub service on AWS 
- **SQS** short for Simple Queue Service, a messaging queue service on AWS
- **SSL** the encrypted communication layer used on the web
- **UI** short for user interface
- **VPS** short for Virtual Private Server, a type of shared hosting
- **apache** a common web server popular with the open source community
- **azure** Microsoft's cloud and serverless computing platform
- **blockchain** a distributed ledger used for cryptocurrencies, data storage, and smart contracts
- **cache** a fast data storage service or app for temporary data used to improve performance
- **chrome puppeteer** a library used to automate browser tasks with Chrome
- **cloud function** another name for a unit in function-as-a-service serverless computing
- **cloudfront** AWS's CDN service
- **compute** a fuzzy term for unit of computation used to talk about pricing and performance
- **devops** is a set of practices that combines development and IT operations
- **docker** a popular computer virtualization software and toolkit
- **dynamodb** a wide-table NoSQL database service on AWS
- **edge workers** a type of cloud function that works like a CDN and aims to reduce latency
- **exponential backoff** a common approach to reducing load on a 3rd party service that is struggling
- **firebase** Google's suite of backend-as-a-service offerings
- **git** a common version control system
- **graphql** an API protocol based on queries commonly used to  increase client power and flexibility
- **hashing function** a secure method of uniquely encoding a string in a way that cannot be reversed
- **heroku** a popular platform-as-a-service provider
- **http** short for HyperText Transfer Protocol, the underlying protocol of the web
- **https** the secure encrypted version of http that uses SSL
- **jamstack** Javascript And Markdown stack, a stack of technologies used for modern static-first websites
- **kubernetes** a popular toolkit for managing servers and containers
- **lambda function** a unit in function-as-a-service serverless computing, synonym for cloud function
- **netlify** a jamstack and serverless provider, popularized the jamstack approach
- **nginx** a popular web server created in 2004 to address Apache's performance issues
- **nosql** an umbrella term for databases that trade ACID compliance for specific performance or usability gains
- **PaaS/platform as a service** a type of hosting that aims to solve infrastructure complexity
- **poison pill** an unprocessable message or request
- **queue** a data structure that processes messages in order, also used as shorthand for a service or application that acts as a queue
- **rainbow tables** a pre-computed cache of hashes used to reverse hashed passwords
- **relational database** a popular approach to storing business data since the 1970's, often synonymous with the concept of a database
- **rest** a common approach to designing web APIs
- **server** depending on context, a machine running software that serves internet requests, or the software itself
- **serverless** an on-demand hosting environment
- **ssh** short for secure shell, a protocol for remotely controlling services
- **upsert** an operation that inserts a new object or updates an existing object with the same identifier
- **vercel** a jamstack and serverless provider
- **yak shaving** a useless activity that indirectly helps you solve a larger problem



================================================
FILE: src/pages/handling-secrets/index.mdx
================================================
---
title: "Handling Secrets"
description: "Talking to 3rd party APIs is where Serverless shines. Which of the 3 ways to handle secrets should you choose?"
image: "./img/handling-secrets.png"
---

# Handling Secrets

![](../../images/chapter_headers/handling-secrets.svg)

How do you send an SMS when users click a button?

You find a JavaScript library that talks to an SMS provider. Configure your API keys, call the library, user gets an SMS. Yay!

3 months later you wake up to a $5,000 bill. Someone looked at your JavaScript code, took the API keys, and ran a spam campaign.

![](giphy:oops)

Orchestrating 3rd party services is where cloud functions shine. The perfect environment for glue code.

Isolated code that does *one* thing with no cruft. Runs on-demand, consumes no resources when not in use, scales near infinitely. Perfection.

**And it runs on a server where users can't see the code.** There's no right-click inspect, no JavaScript files downloaded, no user environment at all.

😍

## What is a secret

A secret is any piece of information you can't share. Any key with access to a special resource. Passwords and API tokens, for example. 

You can add semi-secret configuration variables. URLs for parts of your system, ports of a database server, kinda-hardcoded data, etc.

How secretive you have to be depends on context.

Configuration variables are okay to leak, if the system is otherwise secure. But they can give an attacker information about your system.

Production passwords for sensitive health information ... you don't even want your engineers to know those. Especially not former engineers.

## 3 ways to handle secrets

There are 3 ways to handle secrets. From least to most secure.

1. Hardcoded values
2. Dotenv files
3. Secrets manager

Each method comes with different pros and cons. Pros in terms of security, cons in how cumbersome to use.

<div id="lock" />

## Hardcoded values in code

```typescript
MY_SECRET_KEY="f3q20-98facv87432q4"
```

Hardcoded secrets are the easiest to use and the least secure.

They're okay for prototyping. Reduce moving pieces and focus on the API integration. Ignore the yak shaving around your goal.

Code runs on the server and users won't be able to steal your secrets.

**But anyone with access to your code can see the secrets.** 

Share on GitHub and that includes the whole world. Bots always scrape GitHub looking for strings that look like keys. Your secret *will* be stolen.

AWS is paranoid enough that their own bot looks for secret keys. If they find yours, your AWS account gets locked. [Ask me how I know](https://swizec.com/blog/what-happens-when-you-push-aws-credentials-to-github/) 😅

Another issue with hardcoded keys is that they're hard to change. You have to re-deploy every time. And you're forced to use the same account for testing and production.

## Dotenv files

A step up from hardcoded keys are dotenv files – `.env`. Configuration files in your codebase that hold secrets.

```bash
# .env
MY_SECRET_KEY=f3q20-98facv87432q4
MY_API_URL=https://example.com
```

A `.env` file holds your secrets and configuration variables in one place. Makes them easier to use and change without searching through code.

**You should *not* store these in version control.** That's where the increased security comes from. 

The common approach is to:

1. Have a blank `.env` file with every variable stored in version control
2. Every engineer makes a copy
3. Fills out values from team members or a shared passwords manager

You'll never leak secrets to GitHub by accident. But they're unencrypted on everyone's laptop, difficult to change across a large team, and packaged into your deploys.

Anyone who breaks into your laptop or steals a deploy package from S3 can read the secrets.

On the bright side, dotenv files are easy to split between environments. You can have `.env.local`, `.env.production`, `.env.development` with different values for every secret. ✌️

### How to use .env files

Many frameworks support `.env` files by default. Populate the file and read values from `process.env`.

When using the Serverless Framework, you'll need a plugin: `serverless-dotenv-plugin`. Here's what you do.

Install the plugin:

```
yarn add serverless-dotenv-plugin
```

Enable it in your serverless.yml config:

```yaml
# serverless.yml
plugins:
  - serverless-dotenv-plugin
```

Run deploy and access values with `process.env` 🤘

You can match environment specific files to deployments using the `stage: X` config. serverless-dotenv-plugin reads the `.env.X` file that matches your stage.

## Secrets manager

The most secure way to handle secrets is using a secrets manager.

A secrets manager works like the password manager in your browser. You have to authenticate to get access, re-authenticate *every time*, and secrets are encrypted when not in use.

You can even make your secrets double blind. *Nobody* needs to know their values.

Engineers can't see secrets in the code, they're not saved on anyone's laptop, you can't steal them from the server, and with the right configuration, secrets change every N days.

![](giphy:secure_vault)

### How to use a secrets manager

If you're on Netlify or Vercel, their secrets system is a secrets manager. They control the run-time and inject those values into `process.env`.

On AWS, you'll have to partially build your own.

**First**, save your secrets in [AWS Secrets Manager](https://console.aws.amazon.com/secretsmanager/home). Follow the wizard, it's great.

![AWS Secrets Manager configuration](../../images/secrets-manager.png)

You can store many secret values in 1 configuration. I recommend grouping by environment – dev, staging, production – or use a logical grouping based on what you're doing. One secret per API.

**Second**, give your code permission to access secrets.

```yaml
# serverless.yml
provider:
  # ...
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "secretsmanager:GetSecretValue"
      Resource: "arn:aws:secretsmanager:${self:provider.region}:*"
```

You're giving permission to `GetSecretValue`, *not* to make changes. This is important. You do not want somebody hacking into your system and locking you out.

Using an asterisk – `*` – for secret name is convenient. For m
Download .txt
gitextract_jy4hurtu/

├── .gitignore
├── .prettierignore
├── .prettierrc
├── .vscode/
│   └── settings.json
├── .yarnrc
├── LICENSE
├── README.md
├── examples/
│   ├── hello-world/
│   │   ├── handler.js
│   │   └── serverless.yml
│   ├── serverless-auth-example/
│   │   ├── .gitignore
│   │   ├── package.json
│   │   ├── serverless.yml
│   │   ├── src/
│   │   │   ├── auth.ts
│   │   │   ├── private.ts
│   │   │   └── util.ts
│   │   └── tsconfig.json
│   ├── serverless-chrome-example/
│   │   ├── dist/
│   │   │   ├── scraper.js
│   │   │   ├── screenshot.js
│   │   │   └── util.js
│   │   ├── package.json
│   │   ├── serverless.yml
│   │   ├── src/
│   │   │   ├── scraper.ts
│   │   │   ├── screenshot.ts
│   │   │   └── util.ts
│   │   └── tsconfig.json
│   ├── serverless-data-pipeline-example/
│   │   ├── .gitignore
│   │   ├── package.json
│   │   ├── serverless.yml
│   │   ├── src/
│   │   │   ├── reduce.ts
│   │   │   ├── sumArray.ts
│   │   │   ├── timesTwo.ts
│   │   │   └── utils.ts
│   │   └── tsconfig.json
│   ├── serverless-graphql-example/
│   │   ├── dist/
│   │   │   ├── dynamodb.js
│   │   │   ├── graphql.js
│   │   │   ├── manageItems.js
│   │   │   ├── mutations.js
│   │   │   └── queries.js
│   │   ├── package.json
│   │   ├── serverless.yml
│   │   ├── src/
│   │   │   ├── graphql.ts
│   │   │   ├── mutations.ts
│   │   │   ├── queries.ts
│   │   │   └── types.d.ts
│   │   └── tsconfig.json
│   └── serverless-rest-example/
│       ├── dist/
│       │   ├── dynamodb.js
│       │   └── manageItems.js
│       ├── package.json
│       ├── serverless.yml
│       ├── src/
│       │   ├── dynamodb.ts
│       │   ├── manageItems.ts
│       │   └── types.d.ts
│       └── tsconfig.json
├── gatsby-browser.js
├── gatsby-config.js
├── now.json
├── package.json
├── src/
│   ├── @swizec/
│   │   └── gatsby-theme-course-platform/
│   │       ├── components/
│   │       │   ├── FormCK/
│   │       │   │   ├── formsQuery.js
│   │       │   │   └── useFormsQuery.js
│   │       │   ├── headerLogo.js
│   │       │   ├── layout.js
│   │       │   └── nav.mdx
│   │       └── constants/
│   │           └── footerLinks.js
│   ├── components/
│   │   ├── ClaimForm.js
│   │   ├── Paywall.js
│   │   ├── TestCloudFunctions.js
│   │   ├── homepage.js
│   │   ├── logo.js
│   │   ├── paywall-copy.mdx
│   │   ├── quickthanks.mdx
│   │   └── useLocalStorage.js
│   ├── gatsby-plugin-theme-ui/
│   │   └── index.js
│   └── pages/
│       ├── 404.mdx
│       ├── appendix-more-databases/
│       │   └── index.mdx
│       ├── claim.mdx
│       ├── databases/
│       │   └── index.mdx
│       ├── dev-qa-prod/
│       │   └── index.mdx
│       ├── downloads/
│       │   └── index.mdx
│       ├── getting-started/
│       │   └── index.mdx
│       ├── glossary.mdx
│       ├── handling-secrets/
│       │   └── index.mdx
│       ├── index.mdx
│       ├── lambda-pipelines/
│       │   └── index.mdx
│       ├── robust-backend-design/
│       │   └── index.mdx
│       ├── serverless-architecture-principles/
│       │   └── index.mdx
│       ├── serverless-authentication/
│       │   └── index.mdx
│       ├── serverless-chrome-puppeteer/
│       │   └── index.mdx
│       ├── serverless-dx/
│       │   └── index.mdx
│       ├── serverless-elements/
│       │   └── index.mdx
│       ├── serverless-flavors/
│       │   └── index.mdx
│       ├── serverless-graphql/
│       │   └── index.mdx
│       ├── serverless-monitoring/
│       │   └── index.mdx
│       ├── serverless-performance/
│       │   └── index.mdx
│       ├── serverless-pros-cons/
│       │   └── index.mdx
│       ├── serverless-rest-api/
│       │   └── index.mdx
│       └── thanks.mdx
└── static/
    └── _redirects
Download .txt
SYMBOL INDEX (43 symbols across 26 files)

FILE: examples/serverless-auth-example/src/auth.ts
  function createUser (line 7) | async function createUser(username: string, password: string) {
  function findUser (line 22) | async function findUser(username: string) {

FILE: examples/serverless-auth-example/src/private.ts
  function hello (line 4) | async function hello(event: APIGatewayEvent) {

FILE: examples/serverless-auth-example/src/util.ts
  function response (line 5) | function response(statusCode: number, body: any) {
  function hashPassword (line 21) | function hashPassword(username: string, password: string) {
  type User (line 27) | type User = { username: string; createdAt: string }
  function checkAuth (line 30) | function checkAuth(event: APIGatewayEvent): boolean | User {

FILE: examples/serverless-chrome-example/dist/scraper.js
  function scrapeGoogle (line 4) | async function scrapeGoogle(browser, search) {

FILE: examples/serverless-chrome-example/dist/screenshot.js
  function screenshotGoogle (line 8) | async function screenshotGoogle(browser, search) {

FILE: examples/serverless-chrome-example/dist/util.js
  function getChrome (line 7) | async function getChrome() {

FILE: examples/serverless-chrome-example/src/scraper.ts
  function scrapeGoogle (line 5) | async function scrapeGoogle(browser: Browser, search: string) {

FILE: examples/serverless-chrome-example/src/screenshot.ts
  function screenshotGoogle (line 6) | async function screenshotGoogle(browser: Browser, search: string) {

FILE: examples/serverless-chrome-example/src/util.ts
  type APIResponse (line 5) | type APIResponse = {
  function getChrome (line 12) | async function getChrome() {

FILE: examples/serverless-data-pipeline-example/src/reduce.ts
  function reduceArray (line 18) | async function reduceArray(arrayId: string) {
  function readPackets (line 81) | async function readPackets(arrayId: string): Promise<Packet[]> {
  function cleanup (line 97) | async function cleanup(packets: Packet[]) {

FILE: examples/serverless-data-pipeline-example/src/sumArray.ts
  type APIResponse (line 5) | interface APIResponse {

FILE: examples/serverless-data-pipeline-example/src/utils.ts
  type Packet (line 3) | type Packet = {
  function response (line 35) | function response(statusCode: number, body: any) {

FILE: examples/serverless-graphql-example/dist/manageItems.js
  function response (line 15) | function response(statusCode, body) {

FILE: examples/serverless-graphql-example/dist/mutations.js
  function remapProps (line 15) | function remapProps(item) {

FILE: examples/serverless-graphql-example/dist/queries.js
  function remapProps (line 11) | function remapProps(item) {

FILE: examples/serverless-graphql-example/src/mutations.ts
  type ItemArgs (line 4) | type ItemArgs = {
  function remapProps (line 10) | function remapProps(item: any) {

FILE: examples/serverless-graphql-example/src/queries.ts
  function remapProps (line 3) | function remapProps(item: any) {

FILE: examples/serverless-graphql-example/src/types.d.ts
  type APIResponse (line 1) | interface APIResponse {

FILE: examples/serverless-rest-example/dist/manageItems.js
  function response (line 15) | function response(statusCode, body) {

FILE: examples/serverless-rest-example/src/dynamodb.ts
  type UpdateItemParams (line 5) | interface UpdateItemParams {
  type GetItemParams (line 17) | interface GetItemParams {
  type DeleteItemParams (line 24) | interface DeleteItemParams {
  type ScanItemsParams (line 32) | interface ScanItemsParams {

FILE: examples/serverless-rest-example/src/manageItems.ts
  function response (line 6) | function response(statusCode: number, body: any) {

FILE: examples/serverless-rest-example/src/types.d.ts
  type APIResponse (line 1) | interface APIResponse {

FILE: src/components/ClaimForm.js
  function createUser (line 8) | async function createUser({ name, email }) {

FILE: src/components/Paywall.js
  function toggleLockedContent (line 11) | function toggleLockedContent(show) {
  function hidePaywall (line 25) | function hidePaywall(paywallDiv) {
  function showPaywall (line 44) | function showPaywall(paywallDiv) {
  constant LOCKED_PAGES (line 76) | const LOCKED_PAGES = [
  function usePaywall (line 99) | function usePaywall(pagePath) {
  function SnipContent (line 137) | function SnipContent({ children }) {

FILE: src/components/TestCloudFunctions.js
  function runTest (line 17) | async function runTest() {

FILE: src/components/useLocalStorage.js
  function useLocalStorage (line 4) | function useLocalStorage(key, initialValue) {
Condensed preview — 97 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (396K chars).
[
  {
    "path": ".gitignore",
    "chars": 1034,
    "preview": "package-lock.json\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n*."
  },
  {
    "path": ".prettierignore",
    "chars": 45,
    "preview": ".cache\npackage.json\npackage-lock.json\npublic\n"
  },
  {
    "path": ".prettierrc",
    "chars": 108,
    "preview": "{\n  \"endOfLine\": \"lf\",\n  \"semi\": false,\n  \"singleQuote\": false,\n  \"tabWidth\": 2,\n  \"trailingComma\": \"es5\"\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 55,
    "preview": "{\n    \"python.pythonPath\": \"/usr/local/bin/python2.7\"\n}"
  },
  {
    "path": ".yarnrc",
    "chars": 49,
    "preview": "\"@swizec:registry\" \"https://registry.npmjs.org/\"\n"
  },
  {
    "path": "LICENSE",
    "chars": 1076,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2015 gatsbyjs\n\nPermission is hereby granted, free of charge, to any person obtainin"
  },
  {
    "path": "README.md",
    "chars": 411,
    "preview": "\n# Serverless Handbook for frontend engineers\n\nCurious about serverless? Wanna get into backend but not sure how? This h"
  },
  {
    "path": "examples/hello-world/handler.js",
    "chars": 161,
    "preview": "exports.hello = async (event) => {\n  const { name = '' } = event.queryStringParameters || {}\n\n  return {\n    statusCode:"
  },
  {
    "path": "examples/hello-world/serverless.yml",
    "chars": 265,
    "preview": "service: hello-world\nprovider:\n    name: aws\n    runtime: nodejs12.x\n    stage: dev\n\nfunctions:\n    hello:\n        handl"
  },
  {
    "path": "examples/serverless-auth-example/.gitignore",
    "chars": 30,
    "preview": "dist\nnode_modules\n.serverless\n"
  },
  {
    "path": "examples/serverless-auth-example/package.json",
    "chars": 934,
    "preview": "{\n  \"name\": \"serverless-auth-example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"AWS Lambda example of building a simple a"
  },
  {
    "path": "examples/serverless-auth-example/serverless.yml",
    "chars": 1531,
    "preview": "service: serverless-auth-example\n\nprovider:\n  name: aws\n  runtime: nodejs12.x\n  stage: dev\n  environment:\n    USER_TABLE"
  },
  {
    "path": "examples/serverless-auth-example/src/auth.ts",
    "chars": 2249,
    "preview": "import { APIGatewayEvent } from \"aws-lambda\"\nimport * as db from \"simple-dynamodb\"\nimport omit from \"lodash.omit\"\nimport"
  },
  {
    "path": "examples/serverless-auth-example/src/private.ts",
    "chars": 429,
    "preview": "import { APIGatewayEvent } from \"aws-lambda\"\nimport { response, checkAuth, User } from \"./util\"\n\nexport async function h"
  },
  {
    "path": "examples/serverless-auth-example/src/util.ts",
    "chars": 1355,
    "preview": "import { APIGatewayEvent } from \"aws-lambda\"\nimport sha256 from \"crypto-js/sha256\"\nimport * as jwt from \"jsonwebtoken\"\n\n"
  },
  {
    "path": "examples/serverless-auth-example/tsconfig.json",
    "chars": 298,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2019\",\n    \"module\": \"commonjs\",\n    \"outDir\": \"./dist\",\n    \"strict\": true,\n "
  },
  {
    "path": "examples/serverless-chrome-example/dist/scraper.js",
    "chars": 3033,
    "preview": "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nconst util_1 = require(\"./util\");\nasync fun"
  },
  {
    "path": "examples/serverless-chrome-example/dist/screenshot.js",
    "chars": 3432,
    "preview": "\"use strict\";\nvar __importDefault = (this && this.__importDefault) || function (mod) {\n    return (mod && mod.__esModule"
  },
  {
    "path": "examples/serverless-chrome-example/dist/util.js",
    "chars": 3643,
    "preview": "\"use strict\";\nvar __importDefault = (this && this.__importDefault) || function (mod) {\n    return (mod && mod.__esModule"
  },
  {
    "path": "examples/serverless-chrome-example/package.json",
    "chars": 800,
    "preview": "{\n  \"name\": \"serverless-chrome-example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"AWS Lambda example of using Chrome Pupp"
  },
  {
    "path": "examples/serverless-chrome-example/serverless.yml",
    "chars": 584,
    "preview": "service: serverless-chrome-example\n\nprovider:\n  name: aws\n  runtime: nodejs12.x\n  stage: dev\n  apiGateway:\n    binaryMed"
  },
  {
    "path": "examples/serverless-chrome-example/src/scraper.ts",
    "chars": 1155,
    "preview": "import { Browser } from \"puppeteer\"\n\nimport { createHandler } from \"./util\"\n\nasync function scrapeGoogle(browser: Browse"
  },
  {
    "path": "examples/serverless-chrome-example/src/screenshot.ts",
    "chars": 1314,
    "preview": "import { Browser } from \"puppeteer\"\nimport fs from \"fs\"\n\nimport { createHandler } from \"./util\"\n\nasync function screensh"
  },
  {
    "path": "examples/serverless-chrome-example/src/util.ts",
    "chars": 1643,
    "preview": "import { APIGatewayEvent } from \"aws-lambda\"\nimport { Browser } from \"puppeteer\"\nimport chrome from \"chrome-aws-lambda\"\n"
  },
  {
    "path": "examples/serverless-chrome-example/tsconfig.json",
    "chars": 298,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2019\",\n    \"module\": \"commonjs\",\n    \"outDir\": \"./dist\",\n    \"strict\": true,\n "
  },
  {
    "path": "examples/serverless-data-pipeline-example/.gitignore",
    "chars": 17,
    "preview": ".serverless\ndist\n"
  },
  {
    "path": "examples/serverless-data-pipeline-example/package.json",
    "chars": 683,
    "preview": "{\n  \"name\": \"serverless-data-pipeline-example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"A simple massively distributed a"
  },
  {
    "path": "examples/serverless-data-pipeline-example/serverless.yml",
    "chars": 3266,
    "preview": "service: serverless-data-pipeline-example\n\nprovider:\n  name: aws\n  runtime: nodejs12.x\n  stage: dev\n  environment:\n    S"
  },
  {
    "path": "examples/serverless-data-pipeline-example/src/reduce.ts",
    "chars": 3025,
    "preview": "import { SQSEvent, SQSRecord } from \"aws-lambda\"\nimport * as db from \"simple-dynamodb\"\nimport uuidv4 from \"uuid/v4\"\n\nimp"
  },
  {
    "path": "examples/serverless-data-pipeline-example/src/sumArray.ts",
    "chars": 844,
    "preview": "import { APIGatewayEvent } from \"aws-lambda\"\nimport uuidv4 from \"uuid/v4\"\nimport { response, sendSQSMessage } from \"./ut"
  },
  {
    "path": "examples/serverless-data-pipeline-example/src/timesTwo.ts",
    "chars": 1540,
    "preview": "import { SQSEvent, SQSRecord } from \"aws-lambda\"\nimport { sendSQSMessage, Packet } from \"./utils\"\nimport * as db from \"s"
  },
  {
    "path": "examples/serverless-data-pipeline-example/src/utils.ts",
    "chars": 795,
    "preview": "import AWS from \"aws-sdk\"\n\nexport type Packet = {\n  arrayId: string\n  packetId: string\n  packetValue: number\n  arrayLeng"
  },
  {
    "path": "examples/serverless-data-pipeline-example/tsconfig.json",
    "chars": 334,
    "preview": "{\n    \"compilerOptions\": {\n        \"target\": \"ES2019\",\n        \"module\": \"commonjs\",\n        \"outDir\": \"./dist\",\n       "
  },
  {
    "path": "examples/serverless-graphql-example/dist/dynamodb.js",
    "chars": 5513,
    "preview": "\"use strict\";\nvar __importDefault = (this && this.__importDefault) || function (mod) {\n    return (mod && mod.__esModule"
  },
  {
    "path": "examples/serverless-graphql-example/dist/graphql.js",
    "chars": 1829,
    "preview": "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nconst apollo_server_lambda_1 = require(\"apo"
  },
  {
    "path": "examples/serverless-graphql-example/dist/manageItems.js",
    "chars": 7144,
    "preview": "\"use strict\";\nvar __importStar = (this && this.__importStar) || function (mod) {\n    if (mod && mod.__esModule) return m"
  },
  {
    "path": "examples/serverless-graphql-example/dist/mutations.js",
    "chars": 4437,
    "preview": "\"use strict\";\nvar __importStar = (this && this.__importStar) || function (mod) {\n    if (mod && mod.__esModule) return m"
  },
  {
    "path": "examples/serverless-graphql-example/dist/queries.js",
    "chars": 1554,
    "preview": "\"use strict\";\nvar __importStar = (this && this.__importStar) || function (mod) {\n    if (mod && mod.__esModule) return m"
  },
  {
    "path": "examples/serverless-graphql-example/package.json",
    "chars": 715,
    "preview": "{\n  \"name\": \"serverless-rest-example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"A simple CRUD example using serverless\",\n"
  },
  {
    "path": "examples/serverless-graphql-example/serverless.yml",
    "chars": 1158,
    "preview": "service: serverless-graphql-example\n\nprovider:\n  name: aws\n  runtime: nodejs12.x\n  stage: dev\n  environment:\n    ITEM_TA"
  },
  {
    "path": "examples/serverless-graphql-example/src/graphql.ts",
    "chars": 872,
    "preview": "import { ApolloServer, gql } from \"apollo-server-lambda\"\n\nimport { item } from \"./queries\"\nimport { updateItem, deleteIt"
  },
  {
    "path": "examples/serverless-graphql-example/src/mutations.ts",
    "chars": 1632,
    "preview": "import * as db from \"simple-dynamodb\"\nimport uuidv4 from \"uuid/v4\"\n\ntype ItemArgs = {\n  id: string\n  name: string\n  body"
  },
  {
    "path": "examples/serverless-graphql-example/src/queries.ts",
    "chars": 397,
    "preview": "import * as db from \"simple-dynamodb\"\n\nfunction remapProps(item: any) {\n  return {\n    ...item,\n    id: item.itemId,\n   "
  },
  {
    "path": "examples/serverless-graphql-example/src/types.d.ts",
    "chars": 69,
    "preview": "export interface APIResponse {\n  statusCode: number\n  body: string\n}\n"
  },
  {
    "path": "examples/serverless-graphql-example/tsconfig.json",
    "chars": 334,
    "preview": "{\n    \"compilerOptions\": {\n        \"target\": \"ES2019\",\n        \"module\": \"commonjs\",\n        \"outDir\": \"./dist\",\n       "
  },
  {
    "path": "examples/serverless-rest-example/dist/dynamodb.js",
    "chars": 5513,
    "preview": "\"use strict\";\nvar __importDefault = (this && this.__importDefault) || function (mod) {\n    return (mod && mod.__esModule"
  },
  {
    "path": "examples/serverless-rest-example/dist/manageItems.js",
    "chars": 7140,
    "preview": "\"use strict\";\nvar __importStar = (this && this.__importStar) || function (mod) {\n    if (mod && mod.__esModule) return m"
  },
  {
    "path": "examples/serverless-rest-example/package.json",
    "chars": 677,
    "preview": "{\n  \"name\": \"serverless-rest-example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"A simple CRUD example using serverless\",\n"
  },
  {
    "path": "examples/serverless-rest-example/serverless.yml",
    "chars": 1497,
    "preview": "service: serverless-rest-example\n\nprovider:\n  name: aws\n  runtime: nodejs12.x\n  region: us-east-1\n  stage: dev\n  environ"
  },
  {
    "path": "examples/serverless-rest-example/src/dynamodb.ts",
    "chars": 2701,
    "preview": "import AWS from \"aws-sdk\"\n\nconst dynamoDB = new AWS.DynamoDB.DocumentClient()\n\ninterface UpdateItemParams {\n  TableName:"
  },
  {
    "path": "examples/serverless-rest-example/src/manageItems.ts",
    "chars": 2784,
    "preview": "import { APIGatewayEvent } from \"aws-lambda\"\nimport { APIResponse } from \"./types\"\nimport * as db from \"./dynamodb\"\nimpo"
  },
  {
    "path": "examples/serverless-rest-example/src/types.d.ts",
    "chars": 69,
    "preview": "export interface APIResponse {\n  statusCode: number\n  body: string\n}\n"
  },
  {
    "path": "examples/serverless-rest-example/tsconfig.json",
    "chars": 334,
    "preview": "{\n    \"compilerOptions\": {\n        \"target\": \"ES2019\",\n        \"module\": \"commonjs\",\n        \"outDir\": \"./dist\",\n       "
  },
  {
    "path": "gatsby-browser.js",
    "chars": 628,
    "preview": "// https://serverlesshandbook.dev/?product_id=72rJA8s-O_ZK0H7YXUOQug%3D%3D&product_permalink=qdNn&sale_id=KOlpO90OcOkb7-"
  },
  {
    "path": "gatsby-config.js",
    "chars": 980,
    "preview": "module.exports = {\n  siteMetadata: {\n    title: `Serverless Handbook for Frontend Engineers`,\n    description: `Dive int"
  },
  {
    "path": "now.json",
    "chars": 455,
    "preview": "{\n    \"version\": 2,\n    \"scope\": \"swizec\",\n    \"name\": \"serverlesshandbook-staging\",\n    \"alias\": \"serverlesshandbook.de"
  },
  {
    "path": "package.json",
    "chars": 896,
    "preview": "{\n    \"private\": true,\n    \"name\": \"serverlesshandbook.dev\",\n    \"version\": \"1.0.0\",\n    \"main\": \"index.js\",\n    \"licens"
  },
  {
    "path": "src/@swizec/gatsby-theme-course-platform/components/FormCK/formsQuery.js",
    "chars": 145,
    "preview": "export const formsQuery = `\n  query {\n    site {\n      siteMetadata {\n        convertkit {\n          defaultFormId\n     "
  },
  {
    "path": "src/@swizec/gatsby-theme-course-platform/components/FormCK/useFormsQuery.js",
    "chars": 375,
    "preview": "import { useStaticQuery, graphql } from \"gatsby\"\n\nexport const useFormsQuery = () => {\n  // change this query when you a"
  },
  {
    "path": "src/@swizec/gatsby-theme-course-platform/components/headerLogo.js",
    "chars": 366,
    "preview": "import React from \"react\"\nimport { NavLink } from \"theme-ui\"\n\nconst HeaderLogo = ({ siteTitle, logo }) => {\n  return (\n "
  },
  {
    "path": "src/@swizec/gatsby-theme-course-platform/components/layout.js",
    "chars": 2893,
    "preview": "import React, { useState, useRef } from \"react\"\nimport { Box, Flex } from \"theme-ui\"\nimport { Sidenav, Pagination } from"
  },
  {
    "path": "src/@swizec/gatsby-theme-course-platform/components/nav.mdx",
    "chars": 940,
    "preview": "- [Getting Started](/getting-started)\n- [Serverless Pros & Cons](/serverless-pros-cons)\n- [AWS, Azure, Vercel, Netlify, "
  },
  {
    "path": "src/@swizec/gatsby-theme-course-platform/constants/footerLinks.js",
    "chars": 223,
    "preview": "export default [\n    {\n        Link: 'https://github.com/Swizec/serverlesshandbook.dev',\n        Title: 'GitHub',\n    },"
  },
  {
    "path": "src/components/ClaimForm.js",
    "chars": 4352,
    "preview": "import React from \"react\"\nimport { useEmailForm } from \"@swizec/gatsby-theme-course-platform/src/components/FormCK\"\nimpo"
  },
  {
    "path": "src/components/Paywall.js",
    "chars": 4522,
    "preview": "import React, { useRef, useEffect } from \"react\"\nimport ReactDOMServer from \"react-dom/server\"\nimport { useAuth } from \""
  },
  {
    "path": "src/components/TestCloudFunctions.js",
    "chars": 2342,
    "preview": "import React, { useState } from \"react\"\nimport { Box, Button, Heading, Input, Label } from \"theme-ui\"\nimport prettier fr"
  },
  {
    "path": "src/components/homepage.js",
    "chars": 3776,
    "preview": "import React from \"react\"\nimport { useAuth } from \"react-use-auth\"\nimport { Heading, Flex, Box, Text, Button } from \"the"
  },
  {
    "path": "src/components/logo.js",
    "chars": 725,
    "preview": "import React from \"react\"\nimport styled from \"@emotion/styled\"\nimport { layout } from \"styled-system\"\n\nconst Svg = style"
  },
  {
    "path": "src/components/paywall-copy.mdx",
    "chars": 3328,
    "preview": "import { Box, Flex, Button } from \"theme-ui\"\nimport {\n  GumroadOverlay,\n  GumroadButton,\n  TinyFormCK,\n} from \"@swizec/g"
  },
  {
    "path": "src/components/quickthanks.mdx",
    "chars": 537,
    "preview": "import { Box } from \"theme-ui\"\n\n<Box sx={{\n          mb: 4,\n          border: t => `1px solid ${t.colors.muted}`,\n      "
  },
  {
    "path": "src/components/useLocalStorage.js",
    "chars": 1376,
    "preview": "import { useState } from \"react\"\n\n// copypasta dependency borrowed from https://medium.com/javascript-in-plain-english/u"
  },
  {
    "path": "src/gatsby-plugin-theme-ui/index.js",
    "chars": 933,
    "preview": "import { future } from \"@theme-ui/presets\"\nimport merge from \"lodash.merge\"\nimport { toTheme } from \"@theme-ui/typograph"
  },
  {
    "path": "src/pages/404.mdx",
    "chars": 50,
    "preview": "export const title = \"404\"\n\n# 404\n\nPage not found\n"
  },
  {
    "path": "src/pages/appendix-more-databases/index.mdx",
    "chars": 27363,
    "preview": "---\ntitle: \"Appendix: Databases in more detail\"\ndescription: \"Serverless systems don't have a hard drive ... where do yo"
  },
  {
    "path": "src/pages/claim.mdx",
    "chars": 1512,
    "preview": "import { Box } from \"theme-ui\"\nimport { ClaimForm } from \"../components/ClaimForm\"\n\n# Claim your digital copy\n\nAccess in"
  },
  {
    "path": "src/pages/databases/index.mdx",
    "chars": 12630,
    "preview": "---\ntitle: \"Databases and serverless\"\ndescription: \"Serverless systems don't have a hard drive ... where do you keep you"
  },
  {
    "path": "src/pages/dev-qa-prod/index.mdx",
    "chars": 7011,
    "preview": "---\ntitle: \"Serverless dev, QA, and prod\"\ndescription: \"How do you test and share code without breaking user experience?"
  },
  {
    "path": "src/pages/downloads/index.mdx",
    "chars": 896,
    "preview": "---\ntitle: \"Downloads\"\ndescription: \"\"\nimage: \"\"\n---\n\n# Download PDF/epub/mobi\n\n<div id=\"lock\" />\n\nHi,\n\nThe beauty of di"
  },
  {
    "path": "src/pages/getting-started/index.mdx",
    "chars": 13865,
    "preview": "---\ntitle: \"Getting Started\"\ndescription: \"Your first introduction to serverless and why you should use it\"\nimage: \"./im"
  },
  {
    "path": "src/pages/glossary.mdx",
    "chars": 5827,
    "preview": "export const title = \"Glossary\"\n\n# Glossary\n\nThe Serverless Handbook uses lots of terms that might be unfamiliar. I intr"
  },
  {
    "path": "src/pages/handling-secrets/index.mdx",
    "chars": 7663,
    "preview": "---\ntitle: \"Handling Secrets\"\ndescription: \"Talking to 3rd party APIs is where Serverless shines. Which of the 3 ways to"
  },
  {
    "path": "src/pages/index.mdx",
    "chars": 15168,
    "preview": "import { graphql } from \"gatsby\"\nimport {\n  Box,\n  Flex,\n  Button,\n  Container,\n  Heading,\n  Grid as ThemeUIGrid,\n} from"
  },
  {
    "path": "src/pages/lambda-pipelines/index.mdx",
    "chars": 19150,
    "preview": "---\ntitle: \"Lambda pipelines for serverless data processing\"\ndescription: \"Build a robust massively distributed data, ev"
  },
  {
    "path": "src/pages/robust-backend-design/index.mdx",
    "chars": 13181,
    "preview": "---\ntitle: \"Robust backend design\"\ndescription: \"A fundamental paradox means your backend can never be perfect. How do y"
  },
  {
    "path": "src/pages/serverless-architecture-principles/index.mdx",
    "chars": 8353,
    "preview": "---\ntitle: \"Architecture principles\"\ndescription: \"Learn how to design a serverless system that keeps working in the fac"
  },
  {
    "path": "src/pages/serverless-authentication/index.mdx",
    "chars": 17212,
    "preview": "---\ntitle: \"Serverless authentication\"\ndescription: \"Learn about user authentication and how it works\"\nimage: \"./img/dea"
  },
  {
    "path": "src/pages/serverless-chrome-puppeteer/index.mdx",
    "chars": 15944,
    "preview": "---\ntitle: \"Serverless Chrome puppeteer\"\ndescription: \"Build browser automations with Chrome Puppeteer and AWS Lambda. T"
  },
  {
    "path": "src/pages/serverless-dx/index.mdx",
    "chars": 10145,
    "preview": "---\ntitle: \"Create a good serverless developer experience\"\ndescription: \"How to setup your project for a pleasant develo"
  },
  {
    "path": "src/pages/serverless-elements/index.mdx",
    "chars": 12944,
    "preview": "---\ntitle: \"Elements of serverless – lambdas, queues, gateways, and more\"\ndescription: \"Learn about the elements of your"
  },
  {
    "path": "src/pages/serverless-flavors/index.mdx",
    "chars": 8503,
    "preview": "---\ntitle: \"AWS, Azure, Vercel, Netlify, or Firebase?\"\ndescription: \"How do you choose between serverless providers?\"\nim"
  },
  {
    "path": "src/pages/serverless-graphql/index.mdx",
    "chars": 19897,
    "preview": "---\ntitle: \"Serverless GraphQL API\"\ndescription: \"Learn why GraphQL, how GraphQL, should it replace REST, how to choose,"
  },
  {
    "path": "src/pages/serverless-monitoring/index.mdx",
    "chars": 7007,
    "preview": "---\ntitle: \"Monitoring serverless apps\"\ndescription: \"Server code is invisible, how do you know when it breaks?\"\nimage: "
  },
  {
    "path": "src/pages/serverless-performance/index.mdx",
    "chars": 12046,
    "preview": "---\ntitle: \"Serverless performance\"\ndescription: \"How does performance impact your serverless setup? What should you thi"
  },
  {
    "path": "src/pages/serverless-pros-cons/index.mdx",
    "chars": 8398,
    "preview": "---\ntitle: \"Serverless Pros & Cons\"\ndescription: \"Serverless isn't for everyone every time. Learn how to make the right "
  },
  {
    "path": "src/pages/serverless-rest-api/index.mdx",
    "chars": 16701,
    "preview": "---\ntitle: \"Creating a serverless REST API\"\ndescription: \"What is REST and how does it work? How do you build a serverle"
  },
  {
    "path": "src/pages/thanks.mdx",
    "chars": 1134,
    "preview": "export const title = \"Thanks ❤️\"\n\nexport const description = \"Thanks for supporting the Serverless Handbook\"\n\nimport { B"
  },
  {
    "path": "static/_redirects",
    "chars": 59,
    "preview": "/serverless-cost    /serverless-performance#optimizing-cost"
  }
]

About this extraction

This page contains the full source code of the Swizec/serverless-handbook GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 97 files (366.5 KB), approximately 100.3k tokens, and a symbol index with 43 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!