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 ) => async (event: APIGatewayEvent): Promise => { 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 { 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 => { 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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFuYWdlSXRlbXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvbWFuYWdlSXRlbXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7O0FBRUEsK0NBQWdDO0FBQ2hDLGlEQUE0QjtBQUU1QixTQUFTLFFBQVEsQ0FBQyxVQUFrQixFQUFFLElBQVM7SUFDN0MsT0FBTztRQUNMLFVBQVU7UUFDVixJQUFJLEVBQUUsSUFBSSxDQUFDLFNBQVMsQ0FBQyxJQUFJLENBQUM7S0FDM0IsQ0FBQTtBQUNILENBQUM7QUFFRCx1QkFBdUI7QUFDVixRQUFBLE9BQU8sR0FBRyxLQUFLLEVBQUUsS0FBc0IsRUFBd0IsRUFBRTtJQUM1RSxNQUFNLE1BQU0sR0FBRyxLQUFLLENBQUMsY0FBYyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsY0FBYyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFBO0lBRXhFLElBQUksQ0FBQyxNQUFNLEVBQUU7UUFDWCxPQUFPLFFBQVEsQ0FBQyxHQUFHLEVBQUU7WUFDbkIsTUFBTSxFQUFFLE9BQU87WUFDZixLQUFLLEVBQUUsZ0JBQWdCO1NBQ3hCLENBQUMsQ0FBQTtLQUNIO0lBRUQsTUFBTSxJQUFJLEdBQUcsTUFBTSxFQUFFLENBQUMsT0FBTyxDQUFDO1FBQzVCLFNBQVMsRUFBRSxPQUFPLENBQUMsR0FBRyxDQUFDLFVBQVc7UUFDbEMsR0FBRyxFQUFFLEVBQUUsTUFBTSxFQUFFO0tBQ2hCLENBQUMsQ0FBQTtJQUVGLElBQUksSUFBSSxDQUFDLElBQUksRUFBRTtRQUNiLE9BQU8sUUFBUSxDQUFDLEdBQUcsRUFBRTtZQUNuQixNQUFNLEVBQUUsU0FBUztZQUNqQixJQUFJLEVBQUUsSUFBSSxDQUFDLElBQUk7U0FDaEIsQ0FBQyxDQUFBO0tBQ0g7U0FBTTtRQUNMLE9BQU8sUUFBUSxDQUFDLEdBQUcsRUFBRTtZQUNuQixNQUFNLEVBQUUsT0FBTztZQUNmLEtBQUssRUFBRSxnQkFBZ0I7U0FD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 ================================================ 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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFuYWdlSXRlbXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvbWFuYWdlSXRlbXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7O0FBRUEsK0NBQWdDO0FBQ2hDLGlEQUE0QjtBQUU1QixTQUFTLFFBQVEsQ0FBQyxVQUFrQixFQUFFLElBQVM7SUFDN0MsT0FBTztRQUNMLFVBQVU7UUFDVixJQUFJLEVBQUUsSUFBSSxDQUFDLFNBQVMsQ0FBQyxJQUFJLENBQUM7S0FDM0IsQ0FBQTtBQUNILENBQUM7QUFFRCx1QkFBdUI7QUFDVixRQUFBLE9BQU8sR0FBRyxLQUFLLEVBQUUsS0FBc0IsRUFBd0IsRUFBRTtJQUM1RSxNQUFNLE1BQU0sR0FBRyxLQUFLLENBQUMsY0FBYyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsY0FBYyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFBO0lBRXRFLE1BQU0sSUFBSSxHQUFHLE1BQU0sRUFBRSxDQUFDLE9BQU8sQ0FBQztRQUM1QixTQUFTLEVBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxVQUFXO1FBQ2xDLEdBQUcsRUFBRSxFQUFFLE1BQU0sRUFBRTtLQUNoQixDQUFDLENBQUE7SUFFRixJQUFJLElBQUksQ0FBQyxJQUFJLEVBQUU7UUFDYixPQUFPLFFBQVEsQ0FBQyxHQUFHLEVBQUU7WUFDbkIsTUFBTSxFQUFFLFNBQVM7WUFDakIsSUFBSSxFQUFFLElBQUksQ0FBQyxJQUFJO1NBQ2hCLENBQUMsQ0FBQTtLQUNIO1NBQU07UUFDTCxPQUFPLFFBQVEsQ0FBQyxHQUFHLEVBQUU7WUFDbkIsTUFBTSxFQUFFLE9BQU87WUFDZixLQUFLLEVBQUUsZ0JBQWdCO1NBQ3hCLENBQUMsQ0FBQTtLQUNIO0FBQ0gsQ0FBQyxDQUFBO0FBRUQsaUJBQWlCO0FBQ2pCLG9CQUFvQjtBQUNQLFFBQUEsVUFBVSxHQUFHLEtBQUssRUFDN0IsS0FBc0IsRUFDQSxFQUFFO0lBQ3hCLElBQUksTUFB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 ================================================ 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 (

{siteTitle}

) } 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 ( { 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%", }} >