Skip to content

Commit

Permalink
task3.1: create lambda function for products list
Browse files Browse the repository at this point in the history
✔️ Create a lambda function called `getProductsList` of Product Service which will be triggered by the HTTP GET method.
✔️ The requested URL should be `/products`.
✔️ The response from the lambda should be a full array of products.
✔️ This endpoint should be integrated with Frontend app for Product List Page representation

Additional tasks:
✔️ Async/await is used in lambda functions
✔️ ES6 modules are used for Product Service implementation
✔️ ESBuild is configured for Product Service.

Extra effort:
* api gateway is defined as separate service to reuse for all future lambdas
* app is build on deploy stage with forked scriptable plugin
* api gateway url is passed to frontend app build
  • Loading branch information
Guria committed Oct 17, 2022
1 parent ac8dff7 commit 7a180ea
Show file tree
Hide file tree
Showing 28 changed files with 1,318 additions and 325 deletions.
1,110 changes: 864 additions & 246 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
},
"devDependencies": {
"@serverless/compose": "^1.3.0",
"@serverless/typescript": "^3.21.0",
"@typescript-eslint/eslint-plugin": "^5.40.0",
"@typescript-eslint/parser": "^5.40.0",
"eslint": "^8.25.0",
Expand All @@ -24,6 +25,8 @@
"npm-run-all": "^4.1.5",
"prettier": "2.7.1",
"serverless": "^3.22.0",
"ts-node": "^10.4.0",
"tsconfig-paths": "^3.9.0",
"typescript": "^4.8.4"
},
"engines": {
Expand Down
138 changes: 138 additions & 0 deletions plugins/scriptable/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"use strict";

const vm = require("vm");
const fs = require("fs");
const Module = require("module");
const path = require("path");
const Bluebird = require("bluebird");
const { execSync } = require("child_process");

// same as https://github.com/weixu365/serverless-scriptable-plugin
// but without legacy config namespace support and with proper interpolation of parameters in scripts
class Scriptable {
constructor(serverless, options) {
this.serverless = serverless;
this.options = options;
this.hooks = {};
this.commands = {};

this.stdin = process.stdin;
this.stdout = process.stdout;
this.stderr = process.stderr;
this.showCommands = true;

const scriptable = this.getScripts("scriptable") || {};

if (this.isFalse(scriptable.showCommands)) {
this.showCommands = false;
}

if (this.isFalse(scriptable.showStdoutOutput)) {
console.log(
"Not showing command output because showStdoutOutput is false"
);
this.stdout = "ignore";
}

if (this.isFalse(scriptable.showStderrOutput)) {
console.log(
"Not showing command error output because showStderrOutput is false"
);
this.stderr = "ignore";
}

this.setupHooks(scriptable.hooks);
this.setupCustomCommands(scriptable.commands);
}

setupHooks(hooks = {}) {
// Hooks are run at serverless lifecycle events.
Object.keys(hooks).forEach((event) => {
this.hooks[event] = this.runScript("hooks", event);
}, this);
}

setupCustomCommands(commands = {}) {
// Custom Serverless commands would run by `npx serverless <command-name>`
Object.keys(commands).forEach((name) => {
this.hooks[`${name}:command`] = this.runScript("commands", name);

this.commands[name] = {
usage: `Run ${commands[name]}`,
lifecycleEvents: ["command"],
};
}, this);
}

isFalse(val) {
return val != null && !val;
}

getScripts(namespace) {
const { custom } = this.serverless.service;
return custom && custom[namespace];
}

runScript(type, event) {
return () => {
const eventScript = this.getScripts("scriptable")[type][event];
const scripts = Array.isArray(eventScript) ? eventScript : [eventScript];

return Bluebird.each(scripts, (script) => {
if (fs.existsSync(script) && path.extname(script) === ".js") {
return this.runJavascriptFile(script);
}

return this.runCommand(script);
});
};
}

runCommand(script) {
if (this.showCommands) {
console.log(`Running command: ${script}`);
}

return execSync(script, { stdio: [this.stdin, this.stdout, this.stderr] });
}

runJavascriptFile(scriptFile) {
if (this.showCommands) {
console.log(`Running javascript file: ${scriptFile}`);
}

const buildModule = () => {
const m = new Module(scriptFile, module.parent);
m.exports = exports;
m.filename = scriptFile;
m.paths = Module._nodeModulePaths(path.dirname(scriptFile)).concat(
module.paths
);

return m;
};

const sandbox = {
module: buildModule(),
require: (id) => sandbox.module.require(id),
console,
process,
serverless: this.serverless,
options: this.options,
__filename: scriptFile,
__dirname: path.dirname(fs.realpathSync(scriptFile)),
};

// See: https://github.com/nodejs/node/blob/7c452845b8d44287f5db96a7f19e7d395e1899ab/lib/internal/modules/cjs/helpers.js#L14
sandbox.require.resolve = (req) =>
Module._resolveFilename(req, sandbox.module);

const scriptCode = fs.readFileSync(scriptFile);
const script = vm.createScript(scriptCode, scriptFile);
const context = vm.createContext(sandbox);

return script.runInContext(context);
}
}

module.exports = Scriptable;
12 changes: 12 additions & 0 deletions plugins/scriptable/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "@guria.dev/serverless-scriptable",
"version": "1.0.0",
"private": true,
"main": "index.js",
"dependencies": {
"bluebird": "^3.7.2"
},
"engines": {
"node": ">=16.0.0"
}
}
11 changes: 11 additions & 0 deletions serverless-compose.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
services:
api-gw:
path: ./services/api-gateway

products-api:
path: ./services/products-api
params:
apiGatewayRestApiId: ${api-gw.apiGatewayRestApiId}
apiGatewayRestApiRootResourceId: ${api-gw.apiGatewayRestApiRootResourceId}

shop-frontend-app:
path: ./services/shop-frontend-app
params:
productsApiServiceEndpoint: ${products-api.ServiceEndpoint}
25 changes: 25 additions & 0 deletions services/api-gateway/serverless.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
service: aws-js-practitioner-api

provider:
name: aws
runtime: nodejs16.x

custom:
vars:
prefix: ${self:service}-${sls:stage}

resources:
Resources:
ApiGW:
Type: AWS::ApiGateway::RestApi
Properties:
Name: ${self:custom.vars.prefix}
Description: API Gateway for the AWS JS Practitioner course

Outputs:
apiGatewayRestApiId:
Value: !Ref ApiGW

apiGatewayRestApiRootResourceId:
Value:
Fn::GetAtt: [ApiGW, RootResourceId]
42 changes: 42 additions & 0 deletions services/products-api/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const path = require("path");
const projectFilesPaths = [
path.resolve(__dirname, "tsconfig.json"),
path.resolve(__dirname, "tsconfig.eslint.json"),
];

module.exports = {
root: true,
parser: "@typescript-eslint/parser",
env: {
node: true,
browser: false,
},
overrides: [
{
files: ["*.ts", "*.tsx"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: projectFilesPaths,
},
extends: [
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"prettier",
],
plugins: ["@typescript-eslint", "react", "prettier"],
settings: {
react: {
version: "detect",
},
},
},
{
files: [".eslintrc.js"],
plugins: ["prettier"],
parserOptions: {
ecmaVersion: 2018,
sourceType: "commonjs",
},
},
],
};
11 changes: 11 additions & 0 deletions services/products-api/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# package directories
node_modules
jspm_packages

# Serverless directories
.serverless

# esbuild directories
.esbuild

lib
21 changes: 21 additions & 0 deletions services/products-api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@guria.dev/aws-js-practitioner-products-api",
"version": "1.0.0",
"scripts": {
"lint": "eslint ."
},
"engines": {
"node": ">=16.0.0"
},
"dependencies": {
"@middy/core": "^3.6.1",
"@middy/http-cors": "^3.6.1",
"@middy/http-json-body-parser": "^3.6.1"
},
"devDependencies": {
"@types/aws-lambda": "^8.10.107",
"@types/node": "^16.11.65",
"esbuild": "^0.15.11",
"serverless-esbuild": "^1.33.0"
}
}
39 changes: 39 additions & 0 deletions services/products-api/serverless.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { AWS } from "@serverless/typescript";

import getProductsList from "@functions/getProductsList";

const serverlessConfiguration: AWS = {
service: "products-api",
frameworkVersion: "3",
plugins: ["serverless-esbuild"],
provider: {
name: "aws",
runtime: "nodejs16.x",
apiGateway: {
minimumCompressionSize: 1024,
shouldStartNameWithService: true,
restApiId: "${param:apiGatewayRestApiId}",
restApiRootResourceId: "${param:apiGatewayRestApiRootResourceId}",
},
environment: {
AWS_NODEJS_CONNECTION_REUSE_ENABLED: "1",
NODE_OPTIONS: "--enable-source-maps --stack-trace-limit=1000",
},
},
functions: { getProductsList },
package: { individually: true },
custom: {
esbuild: {
bundle: true,
minify: false,
sourcemap: true,
exclude: ["aws-sdk"],
target: "node16",
define: { "require.resolve": undefined },
platform: "node",
concurrency: 10,
},
},
};

module.exports = serverlessConfiguration;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { formatJSONResponse, middyfy } from "@libs/api-gateway";
import { products } from "@guria.dev/aws-js-practitioner-commons/mocks";
import type { APIGatewayProxyHandler } from "aws-lambda";

const handler: APIGatewayProxyHandler = async () => {
return formatJSONResponse(await products);
};

export const main = middyfy(handler);
17 changes: 17 additions & 0 deletions services/products-api/src/functions/getProductsList/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { AWS } from "@serverless/typescript";
import { handlerPath } from "@libs/handler-resolver";
import { PRODUCTS_API_PATH } from "@guria.dev/aws-js-practitioner-commons/constants/api-paths";

const handler: AWS["functions"][string] = {
handler: handlerPath(__dirname, "handler.main"),
events: [
{
http: {
method: "get",
path: PRODUCTS_API_PATH,
},
},
],
};

export default handler;
16 changes: 16 additions & 0 deletions services/products-api/src/libs/api-gateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import middy from "@middy/core";
import middyJsonBodyParser from "@middy/http-json-body-parser";
import middyCors from "@middy/http-cors";

export const middyfy = (handler) => {
return middy(handler)
.use(middyJsonBodyParser())
.use(middyCors({ origin: "*" }));
};

export const formatJSONResponse = (response: unknown) => {
return {
statusCode: 200,
body: JSON.stringify(response),
};
};
6 changes: 6 additions & 0 deletions services/products-api/src/libs/handler-resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const handlerPath = (context: string, path: string) => {
return `${context
.split(process.cwd())[1]
.substring(1)
.replace(/\\/g, "/")}/${path}`;
};
12 changes: 12 additions & 0 deletions services/products-api/src/services/products.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Product } from "@guria.dev/aws-js-practitioner-commons/models/Product";
import { products } from "@guria.dev/aws-js-practitioner-commons/mocks";

class ProductsService {
constructor(private products: Product[]) {}

public getProducts(): Product[] {
return this.products;
}
}

export default new ProductsService(products);
Loading

0 comments on commit 7a180ea

Please sign in to comment.