From 567ff6add6182cd5f5cd68110030325c8e1a4062 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Thu, 22 Dec 2022 17:29:01 -0500 Subject: [PATCH 01/62] init commit - initialize package.json - add .gitignore with v1 values - add .nvmrc and set to node16 - add init README with some scripts - add start, build, and infrastructure scripts - install deps for establishing and running the project initially --- .../.gitignore | 9 +++++ .../serverless-framework-sqs-dynamodb/.nvmrc | 1 + .../README.md | 11 +++++ .../package.json | 40 +++++++++++++++++++ 4 files changed, 61 insertions(+) create mode 100644 starters/serverless-framework-sqs-dynamodb/.gitignore create mode 100644 starters/serverless-framework-sqs-dynamodb/.nvmrc create mode 100644 starters/serverless-framework-sqs-dynamodb/README.md create mode 100644 starters/serverless-framework-sqs-dynamodb/package.json diff --git a/starters/serverless-framework-sqs-dynamodb/.gitignore b/starters/serverless-framework-sqs-dynamodb/.gitignore new file mode 100644 index 000000000..a0dac4303 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/.gitignore @@ -0,0 +1,9 @@ +# package directories +node_modules +jspm_packages + +# Serverless directories +.serverless + +# dyanmodb directories +.dynamodb diff --git a/starters/serverless-framework-sqs-dynamodb/.nvmrc b/starters/serverless-framework-sqs-dynamodb/.nvmrc new file mode 100644 index 000000000..53d838af2 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/.nvmrc @@ -0,0 +1 @@ +lts/gallium diff --git a/starters/serverless-framework-sqs-dynamodb/README.md b/starters/serverless-framework-sqs-dynamodb/README.md new file mode 100644 index 000000000..9ce2c10da --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/README.md @@ -0,0 +1,11 @@ +# Serverless Framework, SQS, DynamoDB Kit + +## Available Commands + +- `build` bundles the project using the serverless packaging serverless. The produced artifacts will ship bundles shipped to AWS on deployment. You can optionally pass `--analyze ` to run the bundle analzyer and visualize the results to understand your handler bundles. +- `start` runs the `serverless-offline` provided server for local development and testing. Be sure to have the local docker infrastructure running to emulate the related services. +- `test` +- `infrastructure:up` creates docker container and related images and runs them in the background. This should only be needed once during initial setup. +- `infrastructure:down` deletes the docker container and related images. +- `infrastructure:start` starts the docker container. +- `infrastructure:stop` stops the docker container. diff --git a/starters/serverless-framework-sqs-dynamodb/package.json b/starters/serverless-framework-sqs-dynamodb/package.json new file mode 100644 index 000000000..effb783d8 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/package.json @@ -0,0 +1,40 @@ +{ + "name": "serverless-framework-sqs-dynamodb", + "version": "1.0.0", + "description": "A starter kit utilizing the Serverless Framework in conjunction with AWS DynamoDB & SQS.", + "author": "Dustin Goodman", + "license": "MIT", + "keywords": [ + "serverless-framework", + "dynamodb", + "sqs" + ], + "scripts": { + "build": "sls package", + "start": "SLS_DEBUG=* sls offline start", + "test": "test", + "infrastructure:up": "docker compose up -d", + "infrastructure:down": "docker compose down", + "infrastructure:start": "docker compose start", + "infrastructure:stop": "docker compose stop" + }, + "dependencies": { + "@aws-sdk/client-dynamodb": "^3.236.0" + }, + "devDependencies": { + "@types/aws-lambda": "^8.10.109", + "@types/node": "^18.11.17", + "@types/serverless": "^3.12.9", + "esbuild": "^0.16.10", + "esbuild-node-externals": "^1.6.0", + "esbuild-visualizer": "^0.4.0", + "serverless": "^3.26.0", + "serverless-analyze-bundle-plugin": "^1.2.1", + "serverless-dynamodb-local": "^0.2.40", + "serverless-esbuild": "^1.34.0", + "serverless-offline": "^12.0.3", + "serverless-offline-sqs": "^7.3.2", + "ts-node": "^10.9.1", + "typescript": "^4.9.4" + } +} From f0fed2ed12bd9e467f408206ad6ccbe9e2007249 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Thu, 22 Dec 2022 17:31:09 -0500 Subject: [PATCH 02/62] add docker-compose.yml with dynamodb and elasticmq config --- .../docker-compose.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 starters/serverless-framework-sqs-dynamodb/docker-compose.yml diff --git a/starters/serverless-framework-sqs-dynamodb/docker-compose.yml b/starters/serverless-framework-sqs-dynamodb/docker-compose.yml new file mode 100644 index 000000000..78253f4f3 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3' +services: + dynamodb: + image: amazon/dynamodb-local + hostname: dynamodb + restart: always + volumes: + - ./.dynamodb:/home/dynamodblocal/data + ports: + - 8000:8000 + command: '-jar DynamoDBLocal.jar -sharedDb -dbPath /home/dynamodblocal/data/' + + sqs: + image: softwaremill/elasticmq:1.1.1 + ports: + - 9324:9324 + - 9325:9325 From fd662b27a825e42843b6a619db2591c04ff90bd3 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Thu, 22 Dec 2022 17:31:25 -0500 Subject: [PATCH 03/62] add tsconfig --- .../tsconfig.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 starters/serverless-framework-sqs-dynamodb/tsconfig.json diff --git a/starters/serverless-framework-sqs-dynamodb/tsconfig.json b/starters/serverless-framework-sqs-dynamodb/tsconfig.json new file mode 100644 index 000000000..903aff67c --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "rootDir": ".", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node16", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "importHelpers": false, + "target": "ES2018", + "module": "node16", + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "baseUrl": ".", + "paths": {} + }, + "exclude": ["node_modules", "tmp"] +} From 916f815e9d3ce1ddbd69d54ddd596d17da925efe Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Thu, 22 Dec 2022 17:31:39 -0500 Subject: [PATCH 04/62] add .editorconfig --- .../serverless-framework-sqs-dynamodb/.editorconfig | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 starters/serverless-framework-sqs-dynamodb/.editorconfig diff --git a/starters/serverless-framework-sqs-dynamodb/.editorconfig b/starters/serverless-framework-sqs-dynamodb/.editorconfig new file mode 100644 index 000000000..529be2114 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/.editorconfig @@ -0,0 +1,12 @@ +# Editor configuration, see http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = tab +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.yml] +indent_style = space From 0f2bd12aaba196f51bfceb09e18e6c124be1092a Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Thu, 22 Dec 2022 17:34:45 -0500 Subject: [PATCH 05/62] add serverless configuration --- .../esbuild-plugins.ts | 4 + .../package.json | 3 +- .../serverless.ts | 94 +++++++++++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 starters/serverless-framework-sqs-dynamodb/esbuild-plugins.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/serverless.ts diff --git a/starters/serverless-framework-sqs-dynamodb/esbuild-plugins.ts b/starters/serverless-framework-sqs-dynamodb/esbuild-plugins.ts new file mode 100644 index 000000000..610288a7a --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/esbuild-plugins.ts @@ -0,0 +1,4 @@ +import { nodeExternalsPlugin } from 'esbuild-node-externals'; + +// default export should be an array of plugins +module.exports = [nodeExternalsPlugin()]; diff --git a/starters/serverless-framework-sqs-dynamodb/package.json b/starters/serverless-framework-sqs-dynamodb/package.json index effb783d8..af9f56212 100644 --- a/starters/serverless-framework-sqs-dynamodb/package.json +++ b/starters/serverless-framework-sqs-dynamodb/package.json @@ -36,5 +36,6 @@ "serverless-offline-sqs": "^7.3.2", "ts-node": "^10.9.1", "typescript": "^4.9.4" - } + }, + "sideEffects": false } diff --git a/starters/serverless-framework-sqs-dynamodb/serverless.ts b/starters/serverless-framework-sqs-dynamodb/serverless.ts new file mode 100644 index 000000000..614a3ffea --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/serverless.ts @@ -0,0 +1,94 @@ +import type { Serverless } from 'serverless/aws'; + +const serverlessConfiguration: Serverless = { + service: 'serverless-framework-sqs-dynamodb', + frameworkVersion: '3', + useDotenv: true, + plugins: ['serverless-esbuild', 'serverless-analyze-bundle-plugin', 'serverless-dynamodb-local', 'serverless-offline'], + custom: { + dynamodb: { + stages: ['dev'], + start: { + docker: true, + port: 8000, + inMemory: true, + migrate: true, + seed: true, + convertEmptyValues: true, + noStart: true, + }, + }, + esbuild: { + packager: 'yarn', + plugins: './esbuild-plugins.ts', + bundle: true, + minify: true, + sourcemap: true, + }, + 'serverless-offline': { + httpPort: 4000, + lambdaPort: 4002, + }, + 'serverless-offline-sqs': { + autoCreate: true, + apiVersion: '2012-11-05', + endpoint: 'http://0.0.0.0:9324', + region: 'us-east-1', + accessKeyId: 'root', + secretAccessKey: 'root', + skipCacheInvalidation: false, + }, + }, + package: { + individually: true, + patterns: ['src/handlers/*.ts', '!src/handlers/*.test.ts', '!node_modules/**'], + excludeDevDependencies: true, + }, + provider: { + name: 'aws', + runtime: 'nodejs16.x', + // profile: '', + stage: "${opt:stage, 'dev'}", + region: "${opt:region, 'us-east-1'}", + memorySize: 512, // default: 1024MB + timeout: 29, // default: max allowable for Gateway + httpApi: { + // TODO: update to be more restrictive for real apps: https://www.serverless.com/framework/docs/providers/aws/events/http-api/#cors-setup + cors: true, + }, + environment: { + REGION: '${aws:region}', + SLS_STAGE: '${sls:stage}', + }, + iam: { + role: { + statements: [ + { + Effect: 'Allow', + Action: ['lambda:InvokeFunction'], + Resource: 'arn:aws:lambda:*:*:*', + }, + ], + }, + }, + tracing: { + apiGateway: true, + lambda: true, + }, + }, + functions: { + healthcheck: { + handler: 'src/handlers/healthcheck.handler', + events: [ + { + httpApi: { + path: '/healthcheck', + method: 'get', + }, + }, + ], + }, + }, +}; + +module.exports = serverlessConfiguration; From 734e6bf1faaaf098076be9ee977a56e75ab9b7de Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Thu, 22 Dec 2022 17:34:56 -0500 Subject: [PATCH 06/62] add initial healthcheck --- .../src/handlers/healthcheck.test.ts | 18 ++++++++++++++++++ .../src/handlers/healthcheck.ts | 8 ++++++++ 2 files changed, 26 insertions(+) create mode 100644 starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.ts diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.test.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.test.ts new file mode 100644 index 000000000..7b835e1f7 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.test.ts @@ -0,0 +1,18 @@ +import { APIGatewayProxyEvent, Context, Callback } from 'aws-lambda'; +import { handler } from './healthcheck'; + +describe('healtcheck', () => { + let subject; + + beforeAll(async () => { + subject = await handler({} as APIGatewayProxyEvent, {} as Context, {} as Callback); + }); + + it('returns a 200 statusCode', () => { + expect(subject.statusCode).toBe(200); + }); + + it('returns a working message', () => { + expect(subject.body).toEqual('public-api is working!'); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.ts new file mode 100644 index 000000000..629c0a969 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.ts @@ -0,0 +1,8 @@ +import { APIGatewayProxyHandler } from 'aws-lambda'; + +export const handler: APIGatewayProxyHandler = async (/* _event, _context */) => { + return { + statusCode: 200, + body: 'public-api is working!', + }; +}; From 92fea914eae1dc476bccad502e58645d2bae5fde Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Thu, 22 Dec 2022 18:52:26 -0500 Subject: [PATCH 07/62] add project linting --- .../.eslintrc.js | 116 ++++++++++++++++++ .../.eslintrc.json | 40 ++++++ .../.prettierignore | 10 ++ .../.prettierrc.json | 8 ++ .../README.md | 3 + .../package.json | 14 ++- .../serverless.ts | 11 +- .../src/handlers/healthcheck.test.ts | 22 ++-- .../src/handlers/healthcheck.ts | 10 +- .../tsconfig.json | 34 ++--- 10 files changed, 231 insertions(+), 37 deletions(-) create mode 100644 starters/serverless-framework-sqs-dynamodb/.eslintrc.js create mode 100644 starters/serverless-framework-sqs-dynamodb/.eslintrc.json create mode 100644 starters/serverless-framework-sqs-dynamodb/.prettierignore create mode 100644 starters/serverless-framework-sqs-dynamodb/.prettierrc.json diff --git a/starters/serverless-framework-sqs-dynamodb/.eslintrc.js b/starters/serverless-framework-sqs-dynamodb/.eslintrc.js new file mode 100644 index 000000000..514fad6cd --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/.eslintrc.js @@ -0,0 +1,116 @@ +module.exports = { + env: { + es2021: true, + node: true, + }, + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], + overrides: [ + { + files: ['**/*.test.ts', 'jest.setupEnvironment.js'], + env: { + jest: true, + }, + rules: { + 'no-unused-expressions': 'off', + }, + }, + ], + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + plugins: ['@typescript-eslint', 'import', 'promise'], + rules: { + 'array-callback-return': 'error', + 'block-scoped-var': 'error', + 'camelcase': ['error', { properties: 'never' }], + 'consistent-return': 'error', + 'curly': ['error', 'multi-line'], + 'dot-notation': 'error', + 'eqeqeq': ['error', 'allow-null'], + 'guard-for-in': 'error', + 'indent': ['error', 'tab'], + 'linebreak-style': ['error', 'unix'], + 'new-cap': 'error', + 'no-array-constructor': 'error', + 'no-caller': 'error', + 'no-confusing-arrow': ['error', { allowParens: false }], + 'no-duplicate-imports': 'error', + 'no-else-return': 'error', + 'no-eval': 'error', + 'no-extra-bind': 'error', + 'no-extra-label': 'error', + 'no-implied-eval': 'error', + 'no-iterator': 'error', + 'no-label-var': 'error', + 'no-lone-blocks': 'error', + 'no-lonely-if': 'error', + 'no-loop-func': 'error', + 'no-multi-str': 'error', + 'no-nested-ternary': 'error', + 'no-new': 'error', + 'no-new-func': 'error', + 'no-new-object': 'error', + 'no-new-wrappers': 'error', + 'no-octal-escape': 'error', + 'no-proto': 'error', + 'no-restricted-syntax': ['error', 'ForInStatement', 'WithStatement'], + 'no-return-assign': 'error', + 'no-script-url': 'error', + 'no-self-compare': 'error', + 'no-sequences': 'error', + 'no-shadow': 'error', + 'no-throw-literal': 'error', + 'no-undef-init': 'error', + 'no-unneeded-ternary': ['error'], + 'no-unused-expressions': ['error'], + 'no-unused-vars': ['error', { vars: 'local', args: 'after-used' }], + 'no-useless-computed-key': 'error', + 'no-useless-concat': 'error', + 'no-useless-constructor': 'error', + 'no-useless-rename': [ + 'error', + { ignoreDestructuring: false, ignoreImport: false, ignoreExport: false }, + ], + 'no-var': 'error', + 'no-void': 'error', + 'object-shorthand': ['error', 'always', { ignoreConstructors: false, avoidQuotes: true }], + 'one-var': ['error', 'never'], + 'operator-assignment': ['error', 'always'], + 'prefer-arrow-callback': ['error', { allowNamedFunctions: false, allowUnboundThis: true }], + 'prefer-const': ['error', { destructuring: 'any', ignoreReadBeforeAssign: true }], + 'prefer-rest-params': 'error', + 'prefer-spread': 'error', + 'prefer-template': 'error', + 'quotes': ['error', 'single', { avoidEscape: true }], + 'radix': 'error', + 'require-atomic-updates': 'off', // Reports false positives: https://github.com/eslint/eslint/issues/11899 + 'semi': ['error', 'always'], + 'spaced-comment': ['error', 'always'], + 'strict': ['error', 'safe'], + 'vars-on-top': 'error', + 'yoda': 'error', + + // import plugin settings + 'import/export': 'error', + 'import/first': ['error', 'absolute-first'], + 'import/newline-after-import': 'error', + 'import/no-duplicates': 'error', + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: ['**/*.test.ts', 'prettier.config.js', 'esbuild-plugins.ts'], + }, + ], + 'import/no-mutable-exports': 'error', + 'import/no-named-as-default': 'error', + 'import/no-named-as-default-member': 'error', + 'import/no-unresolved': ['error', { commonjs: true }], + }, + settings: { + 'import/resolver': { + typescript: {}, + }, + }, +}; diff --git a/starters/serverless-framework-sqs-dynamodb/.eslintrc.json b/starters/serverless-framework-sqs-dynamodb/.eslintrc.json new file mode 100644 index 000000000..ec21f3ce3 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/.eslintrc.json @@ -0,0 +1,40 @@ +{ + "env": { + "es2021": true, + "node": true + }, + "extends": "eslint:recommended", + "overrides": [ + { + "files": ["**/*.test.ts", "jest.setupEnvironment.js"], + "env": { + "jest": true + }, + "rules": { + "no-unused-expressions": "off" + } + } + ], + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": ["import", "promise"], + "rules": { + "array-callback-return": "error", + "block-scoped-var": "error", + "camelcase": ["error", { "properties": "never" }], + "consistent-return": "error", + "curly": ["error", "multi-line"], + "dot-notation": "error", + "eqeqeq": ["error", "allow-null"], + "guard-for-in": "error", + + // import plugin settings + "import/export": "error", + "import/imports-first": ["error", "absolute-first"], + "import/newline-after-import": "error", + "import/no-amd": "error", + "import/no-duplicates": "error" + } +} diff --git a/starters/serverless-framework-sqs-dynamodb/.prettierignore b/starters/serverless-framework-sqs-dynamodb/.prettierignore new file mode 100644 index 000000000..6561b65d5 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/.prettierignore @@ -0,0 +1,10 @@ +# Ignore artifacts: +build +coverage +node_modules +.dynamodb +.serverless + +# yaml +*.yaml +*.yml diff --git a/starters/serverless-framework-sqs-dynamodb/.prettierrc.json b/starters/serverless-framework-sqs-dynamodb/.prettierrc.json new file mode 100644 index 000000000..065e5fbf1 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/.prettierrc.json @@ -0,0 +1,8 @@ +{ + "endOfLine": "lf", + "printWidth": 100, + "quoteProps": "consistent", + "singleQuote": true, + "trailingComma": "es5", + "useTabs": true +} diff --git a/starters/serverless-framework-sqs-dynamodb/README.md b/starters/serverless-framework-sqs-dynamodb/README.md index 9ce2c10da..59e175b23 100644 --- a/starters/serverless-framework-sqs-dynamodb/README.md +++ b/starters/serverless-framework-sqs-dynamodb/README.md @@ -5,6 +5,9 @@ - `build` bundles the project using the serverless packaging serverless. The produced artifacts will ship bundles shipped to AWS on deployment. You can optionally pass `--analyze ` to run the bundle analzyer and visualize the results to understand your handler bundles. - `start` runs the `serverless-offline` provided server for local development and testing. Be sure to have the local docker infrastructure running to emulate the related services. - `test` +- `lint` runs `eslint` under the hood. You can use all the eslint available command line arguments. To lint the entire project, run `yarn lint .`, or equivalent. You can affix `--fix` to auto-correct linting issues that eslint can handle. +- `format:check` runs prettier format checking on all project files. +- `format:write` runs prettier format writing on all project files. - `infrastructure:up` creates docker container and related images and runs them in the background. This should only be needed once during initial setup. - `infrastructure:down` deletes the docker container and related images. - `infrastructure:start` starts the docker container. diff --git a/starters/serverless-framework-sqs-dynamodb/package.json b/starters/serverless-framework-sqs-dynamodb/package.json index af9f56212..f418a9970 100644 --- a/starters/serverless-framework-sqs-dynamodb/package.json +++ b/starters/serverless-framework-sqs-dynamodb/package.json @@ -13,6 +13,9 @@ "build": "sls package", "start": "SLS_DEBUG=* sls offline start", "test": "test", + "lint": "eslint", + "format:check": "prettier --check .", + "format:write": "prettier --write .", "infrastructure:up": "docker compose up -d", "infrastructure:down": "docker compose down", "infrastructure:start": "docker compose start", @@ -25,9 +28,18 @@ "@types/aws-lambda": "^8.10.109", "@types/node": "^18.11.17", "@types/serverless": "^3.12.9", + "@typescript-eslint/eslint-plugin": "^5.0.0", + "@typescript-eslint/parser": "^5.47.0", "esbuild": "^0.16.10", "esbuild-node-externals": "^1.6.0", - "esbuild-visualizer": "^0.4.0", + "esbuild-visualizer": "^0.3.1", + "eslint": "^8.0.1", + "eslint-config-prettier": "^8.5.0", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-n": "^15.0.0", + "eslint-plugin-promise": "^6.0.0", + "prettier": "^2.8.1", "serverless": "^3.26.0", "serverless-analyze-bundle-plugin": "^1.2.1", "serverless-dynamodb-local": "^0.2.40", diff --git a/starters/serverless-framework-sqs-dynamodb/serverless.ts b/starters/serverless-framework-sqs-dynamodb/serverless.ts index 614a3ffea..180bfaa4c 100644 --- a/starters/serverless-framework-sqs-dynamodb/serverless.ts +++ b/starters/serverless-framework-sqs-dynamodb/serverless.ts @@ -4,9 +4,14 @@ const serverlessConfiguration: Serverless = { service: 'serverless-framework-sqs-dynamodb', frameworkVersion: '3', useDotenv: true, - plugins: ['serverless-esbuild', 'serverless-analyze-bundle-plugin', 'serverless-dynamodb-local', 'serverless-offline'], + plugins: [ + 'serverless-esbuild', + 'serverless-analyze-bundle-plugin', + 'serverless-dynamodb-local', + 'serverless-offline', + ], custom: { - dynamodb: { + 'dynamodb': { stages: ['dev'], start: { docker: true, @@ -18,7 +23,7 @@ const serverlessConfiguration: Serverless = { noStart: true, }, }, - esbuild: { + 'esbuild': { packager: 'yarn', plugins: './esbuild-plugins.ts', bundle: true, diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.test.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.test.ts index 7b835e1f7..4ec104c6c 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.test.ts @@ -1,18 +1,18 @@ -import { APIGatewayProxyEvent, Context, Callback } from 'aws-lambda'; +import type { APIGatewayProxyEvent, Context, Callback } from 'aws-lambda'; import { handler } from './healthcheck'; describe('healtcheck', () => { - let subject; + let subject; - beforeAll(async () => { - subject = await handler({} as APIGatewayProxyEvent, {} as Context, {} as Callback); - }); + beforeAll(async () => { + subject = await handler({} as APIGatewayProxyEvent, {} as Context, {} as Callback); + }); - it('returns a 200 statusCode', () => { - expect(subject.statusCode).toBe(200); - }); + it('returns a 200 statusCode', () => { + expect(subject.statusCode).toBe(200); + }); - it('returns a working message', () => { - expect(subject.body).toEqual('public-api is working!'); - }); + it('returns a working message', () => { + expect(subject.body).toEqual('public-api is working!'); + }); }); diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.ts index 629c0a969..21a073c56 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.ts @@ -1,8 +1,8 @@ -import { APIGatewayProxyHandler } from 'aws-lambda'; +import type { APIGatewayProxyHandler } from 'aws-lambda'; export const handler: APIGatewayProxyHandler = async (/* _event, _context */) => { - return { - statusCode: 200, - body: 'public-api is working!', - }; + return { + statusCode: 200, + body: 'public-api is working!', + }; }; diff --git a/starters/serverless-framework-sqs-dynamodb/tsconfig.json b/starters/serverless-framework-sqs-dynamodb/tsconfig.json index 903aff67c..19f4c4444 100644 --- a/starters/serverless-framework-sqs-dynamodb/tsconfig.json +++ b/starters/serverless-framework-sqs-dynamodb/tsconfig.json @@ -1,19 +1,19 @@ { - "compileOnSave": false, - "compilerOptions": { - "rootDir": ".", - "sourceMap": true, - "declaration": false, - "moduleResolution": "node16", - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "importHelpers": false, - "target": "ES2018", - "module": "node16", - "skipLibCheck": true, - "skipDefaultLibCheck": true, - "baseUrl": ".", - "paths": {} - }, - "exclude": ["node_modules", "tmp"] + "compileOnSave": false, + "compilerOptions": { + "rootDir": ".", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node16", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "importHelpers": false, + "target": "ES2018", + "module": "node16", + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "baseUrl": ".", + "paths": {} + }, + "exclude": ["node_modules", "tmp"] } From b94767c82a00fecc484c9fd5e5d42a2ff4ee7393 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Thu, 22 Dec 2022 23:22:52 -0500 Subject: [PATCH 08/62] add jest and related config --- .../jest.config.ts | 16 ++++++++++++++++ .../package.json | 5 ++++- .../tsconfig.json | 5 +++-- 3 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 starters/serverless-framework-sqs-dynamodb/jest.config.ts diff --git a/starters/serverless-framework-sqs-dynamodb/jest.config.ts b/starters/serverless-framework-sqs-dynamodb/jest.config.ts new file mode 100644 index 000000000..68ec37934 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/jest.config.ts @@ -0,0 +1,16 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + collectCoverage: true, + coverageDirectory: './coverage', + coverageReporters: ['html', 'json'], + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: 100, + }, + }, + preset: 'ts-jest', + testEnvironment: 'node', +}; diff --git a/starters/serverless-framework-sqs-dynamodb/package.json b/starters/serverless-framework-sqs-dynamodb/package.json index f418a9970..ecf8ca0fa 100644 --- a/starters/serverless-framework-sqs-dynamodb/package.json +++ b/starters/serverless-framework-sqs-dynamodb/package.json @@ -12,7 +12,7 @@ "scripts": { "build": "sls package", "start": "SLS_DEBUG=* sls offline start", - "test": "test", + "test": "jest", "lint": "eslint", "format:check": "prettier --check .", "format:write": "prettier --write .", @@ -26,6 +26,7 @@ }, "devDependencies": { "@types/aws-lambda": "^8.10.109", + "@types/jest": "^29.2.4", "@types/node": "^18.11.17", "@types/serverless": "^3.12.9", "@typescript-eslint/eslint-plugin": "^5.0.0", @@ -39,6 +40,7 @@ "eslint-plugin-import": "^2.25.2", "eslint-plugin-n": "^15.0.0", "eslint-plugin-promise": "^6.0.0", + "jest": "^29.3.1", "prettier": "^2.8.1", "serverless": "^3.26.0", "serverless-analyze-bundle-plugin": "^1.2.1", @@ -46,6 +48,7 @@ "serverless-esbuild": "^1.34.0", "serverless-offline": "^12.0.3", "serverless-offline-sqs": "^7.3.2", + "ts-jest": "^29.0.3", "ts-node": "^10.9.1", "typescript": "^4.9.4" }, diff --git a/starters/serverless-framework-sqs-dynamodb/tsconfig.json b/starters/serverless-framework-sqs-dynamodb/tsconfig.json index 19f4c4444..af550d747 100644 --- a/starters/serverless-framework-sqs-dynamodb/tsconfig.json +++ b/starters/serverless-framework-sqs-dynamodb/tsconfig.json @@ -4,11 +4,12 @@ "rootDir": ".", "sourceMap": true, "declaration": false, - "moduleResolution": "node16", + "moduleResolution": "Node16", + "esModuleInterop": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "importHelpers": false, - "target": "ES2018", + "target": "ESNext", "module": "node16", "skipLibCheck": true, "skipDefaultLibCheck": true, From 873a7e6b7259d955f259e1038edee50610f67e19 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Thu, 22 Dec 2022 23:25:12 -0500 Subject: [PATCH 09/62] add comment to serverless config about profiles --- starters/serverless-framework-sqs-dynamodb/serverless.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/starters/serverless-framework-sqs-dynamodb/serverless.ts b/starters/serverless-framework-sqs-dynamodb/serverless.ts index 180bfaa4c..3a0e16c8d 100644 --- a/starters/serverless-framework-sqs-dynamodb/serverless.ts +++ b/starters/serverless-framework-sqs-dynamodb/serverless.ts @@ -52,7 +52,7 @@ const serverlessConfiguration: Serverless = { provider: { name: 'aws', runtime: 'nodejs16.x', - // profile: '', + // profile: '', // assumes default aws profile by default stage: "${opt:stage, 'dev'}", region: "${opt:region, 'us-east-1'}", memorySize: 512, // default: 1024MB From 154e8b1e806a88f1eb35ca71be816547b018ac00 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Fri, 23 Dec 2022 01:23:04 -0500 Subject: [PATCH 10/62] add isOffline util --- .../src/utils/is-offline/index.ts | 1 + .../src/utils/is-offline/is-offline.test.ts | 26 +++++++++++++++++++ .../src/utils/is-offline/is-offline.ts | 7 +++++ 3 files changed, 34 insertions(+) create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/is-offline/index.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/is-offline/is-offline.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/is-offline/is-offline.ts diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/is-offline/index.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/is-offline/index.ts new file mode 100644 index 000000000..d90ad1d50 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/is-offline/index.ts @@ -0,0 +1 @@ +export { isOffline } from './is-offline'; diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/is-offline/is-offline.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/is-offline/is-offline.test.ts new file mode 100644 index 000000000..16dd2f7bf --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/is-offline/is-offline.test.ts @@ -0,0 +1,26 @@ +import { isOffline } from './is-offline'; + +describe('isOffline ', () => { + const env = process.env; + + beforeEach(() => { + jest.resetModules(); + process.env = { ...env }; + }); + + afterEach(() => { + process.env = env; + }); + + // run each of the 3 possible scenarios + const cases = [ + ['true', true], + ['false', false], + ['undefined', false], + ]; + test.each(cases)('when process.env.IS_OFFLINE set to %p, returns %p', (a, b) => { + // set env to test case + process.env.IS_OFFLINE = String(a); + expect(isOffline()).toEqual(b); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/is-offline/is-offline.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/is-offline/is-offline.ts new file mode 100644 index 000000000..fe226e57c --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/is-offline/is-offline.ts @@ -0,0 +1,7 @@ +/** + * Utility function for checking where functions are being run locally via serverless offline + * or if they're running on infrastructure. Helpful for detecting which connection string to use. + * + * @returns boolean are we running locally or on infra? + */ +export const isOffline = (): boolean => process.env.IS_OFFLINE === 'true'; From 53bfa4769668a712ea701ec4d75e0d163837249c Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Fri, 23 Dec 2022 01:23:14 -0500 Subject: [PATCH 11/62] add dynamodb client --- .../src/utils/dynamodb/getClient.test.ts | 93 +++++++++++++++++++ .../src/utils/dynamodb/getClient.ts | 28 ++++++ 2 files changed, 121 insertions(+) create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.ts diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.test.ts new file mode 100644 index 000000000..d2f1ae0d4 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.test.ts @@ -0,0 +1,93 @@ +import { DynamoDB } from '@aws-sdk/client-dynamodb'; + +describe('getClient', () => { + const OLD_ENV = process.env; + let subject; + let client: DynamoDB; + + describe('when IS_OFFLINE is true', () => { + beforeAll(() => { + jest.resetModules(); + client = require('@aws-sdk/client-dynamodb').DynamoDB; + const { getClient } = require('./getClient'); + process.env = { + ...OLD_ENV, + IS_OFFLINE: 'true', + }; + subject = getClient(); + }); + + afterAll(() => { + process.env = OLD_ENV; + }); + + it('returns an DynamoDB', () => { + expect(subject).toEqual(expect.any(client)); + }); + + it('sets the endpoint to localhost', async () => { + expect(subject.config.isCustomEndpoint).toBe(true); + const { hostname } = await subject.config.endpoint(); + expect(hostname).toMatch(/localhost/); + }); + }); + + describe('when IS_OFFLINE is false', () => { + beforeAll(() => { + jest.resetModules(); + client = require('@aws-sdk/client-dynamodb').DynamoDB; + const { getClient } = require('./getClient'); + process.env = { + ...OLD_ENV, + IS_OFFLINE: 'false', + }; + subject = getClient(); + }); + + afterAll(() => { + process.env = OLD_ENV; + }); + + it('returns an LambdaClient', () => { + expect(subject).toEqual(expect.any(client)); + }); + + it('uses the default AWS Lambda endpoint', async () => { + expect(subject.config.isCustomEndpoint).toBe(false); + }); + }); + + describe('when called twice', () => { + let dynamodb; + + beforeAll(() => { + jest.resetModules(); + + dynamodb = require('@aws-sdk/client-dynamodb'); + jest.doMock('@aws-sdk/client-dynamodb', () => ({ + DynamoDB: jest.fn().mockImplementation(() => new dynamodb.DynamoDB({})), + })); + client = require('@aws-sdk/client-dynamodb').DynamoDB; + + const { getClient } = require('./getClient'); + process.env = { + ...OLD_ENV, + IS_OFFLINE: 'true', + }; + getClient(); + subject = getClient(); + }); + + afterAll(() => { + process.env = OLD_ENV; + }); + + it('runs the constructor once', () => { + expect(client).toHaveBeenCalledTimes(1); + }); + + it('returns a cached client', () => { + expect(subject).toEqual(expect.any(dynamodb.DynamoDB)); + }); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.ts new file mode 100644 index 000000000..c1414ac53 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.ts @@ -0,0 +1,28 @@ +import { DynamoDB, DynamoDBClientConfig } from '@aws-sdk/client-dynamodb'; +import { isOffline } from '@utils/is-offline'; + +let cachedClient: DynamoDB; + +export const getClient = () => { + if (cachedClient) { + return cachedClient; + } + + const { REGION } = process.env; + const config: DynamoDBClientConfig = { + apiVersion: '2031', + region: REGION, + }; + + if (isOffline()) { + config.endpoint = 'http://localhost:8000'; + // needs to be set if AWS credentials aren't configured + config.credentials = { + accessKeyId: 'DEFAULT_ACCESS_KEY', + secretAccessKey: 'DEFAULT_SECRET', + }; + } + + cachedClient = new DynamoDB(config); + return cachedClient; +}; From c5cec691ae2ed0c16c0585327ae274cba60c5e04 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Fri, 23 Dec 2022 01:23:50 -0500 Subject: [PATCH 12/62] update dynamodb setup --- .../.gitignore | 6 +-- .../README.md | 4 ++ .../docker-compose.yml | 12 +++++ .../serverless.ts | 45 +++++++++++-------- 4 files changed, 45 insertions(+), 22 deletions(-) diff --git a/starters/serverless-framework-sqs-dynamodb/.gitignore b/starters/serverless-framework-sqs-dynamodb/.gitignore index a0dac4303..d674c2f59 100644 --- a/starters/serverless-framework-sqs-dynamodb/.gitignore +++ b/starters/serverless-framework-sqs-dynamodb/.gitignore @@ -1,9 +1,7 @@ -# package directories +coverage node_modules jspm_packages -# Serverless directories .serverless - -# dyanmodb directories .dynamodb +.esbuild diff --git a/starters/serverless-framework-sqs-dynamodb/README.md b/starters/serverless-framework-sqs-dynamodb/README.md index 59e175b23..b3056b66a 100644 --- a/starters/serverless-framework-sqs-dynamodb/README.md +++ b/starters/serverless-framework-sqs-dynamodb/README.md @@ -12,3 +12,7 @@ - `infrastructure:down` deletes the docker container and related images. - `infrastructure:start` starts the docker container. - `infrastructure:stop` stops the docker container. + +## DynamoDB + +View Local Admin Tool at http://localhost:8001/ diff --git a/starters/serverless-framework-sqs-dynamodb/docker-compose.yml b/starters/serverless-framework-sqs-dynamodb/docker-compose.yml index 78253f4f3..2b8b61ece 100644 --- a/starters/serverless-framework-sqs-dynamodb/docker-compose.yml +++ b/starters/serverless-framework-sqs-dynamodb/docker-compose.yml @@ -1,5 +1,6 @@ version: '3' services: + # configures DynamoDB resource for persistent storage dynamodb: image: amazon/dynamodb-local hostname: dynamodb @@ -10,6 +11,17 @@ services: - 8000:8000 command: '-jar DynamoDBLocal.jar -sharedDb -dbPath /home/dynamodblocal/data/' + # configures DynamoDB admin shell for the browser + dynamodb-admin: + image: aaronshaf/dynamodb-admin + ports: + - "8001:8001" + environment: + DYNAMO_ENDPOINT: "http://dynamodb:8000" + depends_on: + - dynamodb + + # configures ElasticMQ to emulate SQS behaviors sqs: image: softwaremill/elasticmq:1.1.1 ports: diff --git a/starters/serverless-framework-sqs-dynamodb/serverless.ts b/starters/serverless-framework-sqs-dynamodb/serverless.ts index 3a0e16c8d..91e5367df 100644 --- a/starters/serverless-framework-sqs-dynamodb/serverless.ts +++ b/starters/serverless-framework-sqs-dynamodb/serverless.ts @@ -4,25 +4,8 @@ const serverlessConfiguration: Serverless = { service: 'serverless-framework-sqs-dynamodb', frameworkVersion: '3', useDotenv: true, - plugins: [ - 'serverless-esbuild', - 'serverless-analyze-bundle-plugin', - 'serverless-dynamodb-local', - 'serverless-offline', - ], + plugins: ['serverless-esbuild', 'serverless-analyze-bundle-plugin', 'serverless-offline'], custom: { - 'dynamodb': { - stages: ['dev'], - start: { - docker: true, - port: 8000, - inMemory: true, - migrate: true, - seed: true, - convertEmptyValues: true, - noStart: true, - }, - }, 'esbuild': { packager: 'yarn', plugins: './esbuild-plugins.ts', @@ -94,6 +77,32 @@ const serverlessConfiguration: Serverless = { ], }, }, + resources: { + Resources: { + technologiesTable: { + Type: 'AWS::DynamoDB::Table', + Properties: { + TableName: 'technologiesTable', + AttributeDefinitions: [ + { + AttributeName: 'id', + AttributeType: 'S', + }, + ], + KeySchema: [ + { + AttributeName: 'id', + KeyType: 'HASH', + }, + ], + ProvisionedThroughput: { + ReadCapacityUnits: 1, + WriteCapacityUnits: 1, + }, + }, + }, + }, + }, }; module.exports = serverlessConfiguration; From 0ff732d8d613e99238314a68383d62c9ca915f54 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Fri, 23 Dec 2022 01:24:05 -0500 Subject: [PATCH 13/62] remove serverless-dynamodb-local --- starters/serverless-framework-sqs-dynamodb/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/starters/serverless-framework-sqs-dynamodb/package.json b/starters/serverless-framework-sqs-dynamodb/package.json index ecf8ca0fa..9e509958f 100644 --- a/starters/serverless-framework-sqs-dynamodb/package.json +++ b/starters/serverless-framework-sqs-dynamodb/package.json @@ -44,7 +44,6 @@ "prettier": "^2.8.1", "serverless": "^3.26.0", "serverless-analyze-bundle-plugin": "^1.2.1", - "serverless-dynamodb-local": "^0.2.40", "serverless-esbuild": "^1.34.0", "serverless-offline": "^12.0.3", "serverless-offline-sqs": "^7.3.2", From 055cb96769b82b9279f55f287c3074bc9b2df1a0 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Fri, 23 Dec 2022 01:24:14 -0500 Subject: [PATCH 14/62] update eslint config --- .../.eslintrc.js | 8 +++- .../.eslintrc.json | 40 ------------------- 2 files changed, 7 insertions(+), 41 deletions(-) delete mode 100644 starters/serverless-framework-sqs-dynamodb/.eslintrc.json diff --git a/starters/serverless-framework-sqs-dynamodb/.eslintrc.js b/starters/serverless-framework-sqs-dynamodb/.eslintrc.js index 514fad6cd..992756094 100644 --- a/starters/serverless-framework-sqs-dynamodb/.eslintrc.js +++ b/starters/serverless-framework-sqs-dynamodb/.eslintrc.js @@ -12,6 +12,7 @@ module.exports = { }, rules: { 'no-unused-expressions': 'off', + '@typescript-eslint/no-var-requires': 'off', }, }, ], @@ -100,7 +101,12 @@ module.exports = { 'import/no-extraneous-dependencies': [ 'error', { - devDependencies: ['**/*.test.ts', 'prettier.config.js', 'esbuild-plugins.ts'], + devDependencies: [ + '**/*.test.ts', + 'prettier.config.js', + 'jest.config.ts', + 'esbuild-plugins.ts', + ], }, ], 'import/no-mutable-exports': 'error', diff --git a/starters/serverless-framework-sqs-dynamodb/.eslintrc.json b/starters/serverless-framework-sqs-dynamodb/.eslintrc.json deleted file mode 100644 index ec21f3ce3..000000000 --- a/starters/serverless-framework-sqs-dynamodb/.eslintrc.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "env": { - "es2021": true, - "node": true - }, - "extends": "eslint:recommended", - "overrides": [ - { - "files": ["**/*.test.ts", "jest.setupEnvironment.js"], - "env": { - "jest": true - }, - "rules": { - "no-unused-expressions": "off" - } - } - ], - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, - "plugins": ["import", "promise"], - "rules": { - "array-callback-return": "error", - "block-scoped-var": "error", - "camelcase": ["error", { "properties": "never" }], - "consistent-return": "error", - "curly": ["error", "multi-line"], - "dot-notation": "error", - "eqeqeq": ["error", "allow-null"], - "guard-for-in": "error", - - // import plugin settings - "import/export": "error", - "import/imports-first": ["error", "absolute-first"], - "import/newline-after-import": "error", - "import/no-amd": "error", - "import/no-duplicates": "error" - } -} From 2858ec581f10a19e0360f77e301634ad9513d2e7 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Fri, 23 Dec 2022 01:24:28 -0500 Subject: [PATCH 15/62] add path aliases --- .../serverless-framework-sqs-dynamodb/jest.config.ts | 10 ++++++++-- .../serverless-framework-sqs-dynamodb/tsconfig.json | 5 ++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/starters/serverless-framework-sqs-dynamodb/jest.config.ts b/starters/serverless-framework-sqs-dynamodb/jest.config.ts index 68ec37934..395a88f1f 100644 --- a/starters/serverless-framework-sqs-dynamodb/jest.config.ts +++ b/starters/serverless-framework-sqs-dynamodb/jest.config.ts @@ -1,5 +1,7 @@ -/** @type {import('ts-jest').JestConfigWithTsJest} */ -module.exports = { +import { pathsToModuleNameMapper, JestConfigWithTsJest } from 'ts-jest'; +import { compilerOptions } from './tsconfig.json'; + +const jestConfig: JestConfigWithTsJest = { collectCoverage: true, coverageDirectory: './coverage', coverageReporters: ['html', 'json'], @@ -11,6 +13,10 @@ module.exports = { statements: 100, }, }, + modulePaths: [compilerOptions.baseUrl], + moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths), preset: 'ts-jest', testEnvironment: 'node', }; + +export default jestConfig; diff --git a/starters/serverless-framework-sqs-dynamodb/tsconfig.json b/starters/serverless-framework-sqs-dynamodb/tsconfig.json index af550d747..3e0ce91bc 100644 --- a/starters/serverless-framework-sqs-dynamodb/tsconfig.json +++ b/starters/serverless-framework-sqs-dynamodb/tsconfig.json @@ -11,10 +11,13 @@ "importHelpers": false, "target": "ESNext", "module": "node16", + "resolveJsonModule": true, "skipLibCheck": true, "skipDefaultLibCheck": true, "baseUrl": ".", - "paths": {} + "paths": { + "@utils/*": ["src/utils/*"] + } }, "exclude": ["node_modules", "tmp"] } From c204411f3c278f650fca621466ac3f5205d251a1 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Fri, 23 Dec 2022 11:31:21 -0500 Subject: [PATCH 16/62] setup .env --- starters/serverless-framework-sqs-dynamodb/.env.example | 1 + starters/serverless-framework-sqs-dynamodb/.gitignore | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 starters/serverless-framework-sqs-dynamodb/.env.example diff --git a/starters/serverless-framework-sqs-dynamodb/.env.example b/starters/serverless-framework-sqs-dynamodb/.env.example new file mode 100644 index 000000000..453a9c56f --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/.env.example @@ -0,0 +1 @@ +IS_OFFLINE=true diff --git a/starters/serverless-framework-sqs-dynamodb/.gitignore b/starters/serverless-framework-sqs-dynamodb/.gitignore index d674c2f59..1b022ad28 100644 --- a/starters/serverless-framework-sqs-dynamodb/.gitignore +++ b/starters/serverless-framework-sqs-dynamodb/.gitignore @@ -5,3 +5,5 @@ jspm_packages .serverless .dynamodb .esbuild + +.env From a071b262ab8c39c919b622e12244cea321afbb0a Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Fri, 23 Dec 2022 11:32:12 -0500 Subject: [PATCH 17/62] fix path aliases --- starters/serverless-framework-sqs-dynamodb/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/starters/serverless-framework-sqs-dynamodb/tsconfig.json b/starters/serverless-framework-sqs-dynamodb/tsconfig.json index 3e0ce91bc..fda057736 100644 --- a/starters/serverless-framework-sqs-dynamodb/tsconfig.json +++ b/starters/serverless-framework-sqs-dynamodb/tsconfig.json @@ -16,7 +16,8 @@ "skipDefaultLibCheck": true, "baseUrl": ".", "paths": { - "@utils/*": ["src/utils/*"] + "@/types/*": ["src/types/*"], + "@/utils/*": ["src/utils/*"] } }, "exclude": ["node_modules", "tmp"] From 74c90c89636b0eee76a7c226d29cceb2559f4ca4 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Fri, 23 Dec 2022 11:32:44 -0500 Subject: [PATCH 18/62] update dynamodb/getClient to use correct client --- .../src/utils/dynamodb/getClient.test.ts | 14 +++++++------- .../src/utils/dynamodb/getClient.ts | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.test.ts index d2f1ae0d4..0734abf26 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.test.ts @@ -1,14 +1,14 @@ -import { DynamoDB } from '@aws-sdk/client-dynamodb'; +import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; describe('getClient', () => { const OLD_ENV = process.env; let subject; - let client: DynamoDB; + let client: DynamoDBClient; describe('when IS_OFFLINE is true', () => { beforeAll(() => { jest.resetModules(); - client = require('@aws-sdk/client-dynamodb').DynamoDB; + client = require('@aws-sdk/client-dynamodb').DynamoDBClient; const { getClient } = require('./getClient'); process.env = { ...OLD_ENV, @@ -35,7 +35,7 @@ describe('getClient', () => { describe('when IS_OFFLINE is false', () => { beforeAll(() => { jest.resetModules(); - client = require('@aws-sdk/client-dynamodb').DynamoDB; + client = require('@aws-sdk/client-dynamodb').DynamoDBClient; const { getClient } = require('./getClient'); process.env = { ...OLD_ENV, @@ -65,9 +65,9 @@ describe('getClient', () => { dynamodb = require('@aws-sdk/client-dynamodb'); jest.doMock('@aws-sdk/client-dynamodb', () => ({ - DynamoDB: jest.fn().mockImplementation(() => new dynamodb.DynamoDB({})), + DynamoDBClient: jest.fn().mockImplementation(() => new dynamodb.DynamoDBClient({})), })); - client = require('@aws-sdk/client-dynamodb').DynamoDB; + client = require('@aws-sdk/client-dynamodb').DynamoDBClient; const { getClient } = require('./getClient'); process.env = { @@ -87,7 +87,7 @@ describe('getClient', () => { }); it('returns a cached client', () => { - expect(subject).toEqual(expect.any(dynamodb.DynamoDB)); + expect(subject).toEqual(expect.any(dynamodb.DynamoDBClient)); }); }); }); diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.ts index c1414ac53..6dfa0256d 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.ts @@ -1,7 +1,7 @@ -import { DynamoDB, DynamoDBClientConfig } from '@aws-sdk/client-dynamodb'; -import { isOffline } from '@utils/is-offline'; +import { DynamoDBClient, DynamoDBClientConfig } from '@aws-sdk/client-dynamodb'; +import { isOffline } from '@/utils/is-offline'; -let cachedClient: DynamoDB; +let cachedClient: DynamoDBClient; export const getClient = () => { if (cachedClient) { @@ -23,6 +23,6 @@ export const getClient = () => { }; } - cachedClient = new DynamoDB(config); + cachedClient = new DynamoDBClient(config); return cachedClient; }; From 0fc18c6b93d560b43110f3cb813c9aef3540fb85 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Fri, 23 Dec 2022 11:32:51 -0500 Subject: [PATCH 19/62] add technology type --- .../src/types/technology.ts | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 starters/serverless-framework-sqs-dynamodb/src/types/technology.ts diff --git a/starters/serverless-framework-sqs-dynamodb/src/types/technology.ts b/starters/serverless-framework-sqs-dynamodb/src/types/technology.ts new file mode 100644 index 000000000..aa3cb096a --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/types/technology.ts @@ -0,0 +1,6 @@ +export type Technology = { + id: string; + displayName: string; + description: string; + url: string; +}; From 7c32890d5fce0aec06b42e3638d6f9970c589220 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Fri, 23 Dec 2022 11:33:00 -0500 Subject: [PATCH 20/62] add technologies seed file --- .../db/technologies-seed.json | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 starters/serverless-framework-sqs-dynamodb/db/technologies-seed.json diff --git a/starters/serverless-framework-sqs-dynamodb/db/technologies-seed.json b/starters/serverless-framework-sqs-dynamodb/db/technologies-seed.json new file mode 100644 index 000000000..f17758b7f --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/db/technologies-seed.json @@ -0,0 +1,20 @@ +[ + { + "id": "Ds2UJdz4-QSHIO2MblWy5", + "displayName": "Serverless Framework", + "description": "All-in-one development solution for auto-scaling apps on AWS Lambda", + "url": "https://www.serverless.com/" + }, + { + "id": "gny3_Bq335TIUMp1YaDZR", + "displayName": "AWS DynamoDB", + "description": "Fast, flexible NoSQL database service for single-digit millisecond performance at any scale", + "url": "https://aws.amazon.com/dynamodb/" + }, + { + "id": "hkUFI46rClB1Jsz0dswx4", + "displayName": "AWS SQS", + "description": "Fully managed message queuing for microservices, distributed systems, and serverless applications", + "url": "https://aws.amazon.com/sqs/" + } +] From e1b9c89195bf7e84d33014c04bea0562ddaf8cb3 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Fri, 23 Dec 2022 11:33:21 -0500 Subject: [PATCH 21/62] readd serverless-dynamodb-local and fix config --- .../package.json | 8 ++- .../serverless.ts | 57 +++++++++++++++++-- 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/starters/serverless-framework-sqs-dynamodb/package.json b/starters/serverless-framework-sqs-dynamodb/package.json index 9e509958f..04e49abc8 100644 --- a/starters/serverless-framework-sqs-dynamodb/package.json +++ b/starters/serverless-framework-sqs-dynamodb/package.json @@ -16,21 +16,26 @@ "lint": "eslint", "format:check": "prettier --check .", "format:write": "prettier --write .", + "db:sync": "sls dynamodb migrate", + "db:seed": "sls dynamodb seed", "infrastructure:up": "docker compose up -d", "infrastructure:down": "docker compose down", "infrastructure:start": "docker compose start", "infrastructure:stop": "docker compose stop" }, "dependencies": { - "@aws-sdk/client-dynamodb": "^3.236.0" + "@aws-sdk/client-dynamodb": "^3.236.0", + "nanoid": "^4.0.0" }, "devDependencies": { + "@serverless/typescript": "^3.25.0", "@types/aws-lambda": "^8.10.109", "@types/jest": "^29.2.4", "@types/node": "^18.11.17", "@types/serverless": "^3.12.9", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.47.0", + "dotenv": "^16.0.3", "esbuild": "^0.16.10", "esbuild-node-externals": "^1.6.0", "esbuild-visualizer": "^0.3.1", @@ -44,6 +49,7 @@ "prettier": "^2.8.1", "serverless": "^3.26.0", "serverless-analyze-bundle-plugin": "^1.2.1", + "serverless-dynamodb-local": "^0.2.40", "serverless-esbuild": "^1.34.0", "serverless-offline": "^12.0.3", "serverless-offline-sqs": "^7.3.2", diff --git a/starters/serverless-framework-sqs-dynamodb/serverless.ts b/starters/serverless-framework-sqs-dynamodb/serverless.ts index 91e5367df..f21cd9988 100644 --- a/starters/serverless-framework-sqs-dynamodb/serverless.ts +++ b/starters/serverless-framework-sqs-dynamodb/serverless.ts @@ -1,11 +1,34 @@ -import type { Serverless } from 'serverless/aws'; +import type { AWS } from '@serverless/typescript'; -const serverlessConfiguration: Serverless = { +const serverlessConfiguration: AWS = { service: 'serverless-framework-sqs-dynamodb', frameworkVersion: '3', useDotenv: true, - plugins: ['serverless-esbuild', 'serverless-analyze-bundle-plugin', 'serverless-offline'], + plugins: [ + 'serverless-esbuild', + 'serverless-analyze-bundle-plugin', + 'serverless-dynamodb-local', + 'serverless-offline', + ], custom: { + 'dynamodb': { + stages: ['dev'], + start: { + port: 8000, + convertEmptyValues: true, + noStart: true, + }, + seed: { + core: { + sources: [ + { + table: '${param:technologiesTable}', + sources: ['./db/technologies-seed.json'], + }, + ], + }, + }, + }, 'esbuild': { packager: 'yarn', plugins: './esbuild-plugins.ts', @@ -32,12 +55,23 @@ const serverlessConfiguration: Serverless = { patterns: ['src/handlers/*.ts', '!src/handlers/*.test.ts', '!node_modules/**'], excludeDevDependencies: true, }, + params: { + production: { + technologiesTable: 'technologies-production', + }, + staging: { + technologiesTable: 'technologies-staging', + }, + dev: { + technologiesTable: 'technologies-dev', + }, + }, provider: { name: 'aws', runtime: 'nodejs16.x', // profile: '', // assumes default aws profile by default stage: "${opt:stage, 'dev'}", - region: "${opt:region, 'us-east-1'}", + region: "${opt:region, 'us-east-1'}" as AWS['provider']['region'], // Temp fix. See https://github.com/serverless/typescript/issues/11. memorySize: 512, // default: 1024MB timeout: 29, // default: max allowable for Gateway httpApi: { @@ -47,6 +81,8 @@ const serverlessConfiguration: Serverless = { environment: { REGION: '${aws:region}', SLS_STAGE: '${sls:stage}', + // DynamoDB Tables + TECHNOLOGIES_TABLE: '${param:technologiesTable}', }, iam: { role: { @@ -56,6 +92,17 @@ const serverlessConfiguration: Serverless = { Action: ['lambda:InvokeFunction'], Resource: 'arn:aws:lambda:*:*:*', }, + { + Effect: 'Allow', + Action: [ + 'dynamodb:Scan', + 'dynamodb:Query', + 'dynamodb:GetItem', + 'dynamodb:PutItem', + 'dynamodb:UpdateItem', + ], + Resource: 'arn:aws:dynamodb:*:*:table/${param:technologiesTable}', + }, ], }, }, @@ -82,7 +129,7 @@ const serverlessConfiguration: Serverless = { technologiesTable: { Type: 'AWS::DynamoDB::Table', Properties: { - TableName: 'technologiesTable', + TableName: '${param:technologiesTable}', AttributeDefinitions: [ { AttributeName: 'id', From 9dac4e5f5ab3837cd372ededb70a78617b8da9aa Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Fri, 23 Dec 2022 12:50:17 -0500 Subject: [PATCH 22/62] update README --- .../README.md | 143 +++++++++++++++++- 1 file changed, 141 insertions(+), 2 deletions(-) diff --git a/starters/serverless-framework-sqs-dynamodb/README.md b/starters/serverless-framework-sqs-dynamodb/README.md index b3056b66a..35f24064a 100644 --- a/starters/serverless-framework-sqs-dynamodb/README.md +++ b/starters/serverless-framework-sqs-dynamodb/README.md @@ -1,18 +1,157 @@ # Serverless Framework, SQS, DynamoDB Kit +## Table of contents + +- [Overview](#overview) +- [Installation](#installation) + - [Pre-requisites](#pre-requisites) + - [CLI (Recommended)](#cli-recommended) + - [Manual](#manual) + - [Getting Started](#getting-started) +- [Available Commands](#available-commands) + - [General Commands](#general-commands) + - [DynamoDB Commands](#dynamodb-commands) + - [Infrastructure Commands](#infrastructure-commands) +- [DynamoDB](#dynamodb) + +## Overview + +A starter kit utilizing the Serverless Framework in conjunction with AWS DynamoDB & SQS. This kit aims to establish a baseline Serverless Framework project with reasonable configurations that is as-is deployable to AWS. It also provides a fully offline development mode for rapid iteration. + +## Installation + +### Pre-requisites + +- Node.js v16 +- npm or yarn or pnpm +- Docker & docker-compose + +### CLI (Recommended) + +1. Init the project + +```shell +npm create @this-dot/starter -- --kit serverless-framework-sqs-dynamodb +``` + +or + +```shell +yarn create @this-dot/starter -- --kit serverless-framework-sqs-dynamodb +``` + +2. Follow the prompt to name your new project. +3. `cd` into your project directory. + +### Manual + +1. Clone the starter.dev repo + +```shell +git clone https://github.com/thisdot/starter.dev.git +``` + +2. Copy and rename the `starters/serverless-framework-sqs-dynamodb` directory to the name of your new project. +3. `cd` into your project directory + +### Getting Started + +This README uses `yarn` for commands. If you're using `npm` or `pnpm`, utilize the equivalent version of the commands. + +1. Create a `.env` file: + +```bash +cp .env.example .env +``` + +2. Run `yarn` to install deps +3. Standup the project infrastructure using docker via: + +```bash +yarn infrastructure:up +``` + +4. Sync database tables and seed the project via: + +```bash +yarn db:sync +yarn db:seed +``` + +5. Start the local development server: + +```bash +yarn start +``` + +6. Make changes and enjoy building your new backend! + ## Available Commands +### General Commands + - `build` bundles the project using the serverless packaging serverless. The produced artifacts will ship bundles shipped to AWS on deployment. You can optionally pass `--analyze ` to run the bundle analzyer and visualize the results to understand your handler bundles. - `start` runs the `serverless-offline` provided server for local development and testing. Be sure to have the local docker infrastructure running to emulate the related services. -- `test` +- `test` runs `jest` under the hood. - `lint` runs `eslint` under the hood. You can use all the eslint available command line arguments. To lint the entire project, run `yarn lint .`, or equivalent. You can affix `--fix` to auto-correct linting issues that eslint can handle. - `format:check` runs prettier format checking on all project files. - `format:write` runs prettier format writing on all project files. + +### DynamoDB Commands + +- `db:sync` syncs all table definitions with DynamoDB instance to ensure resources are correct. Will output warnings for pre-existing tables. +- `db:seed` adds seed data to the DynamoDB. See https://github.com/99x/serverless-dynamodb-local#seeding-sls-dynamodb-seed for more information. + +### Infrastructure Commands + - `infrastructure:up` creates docker container and related images and runs them in the background. This should only be needed once during initial setup. - `infrastructure:down` deletes the docker container and related images. - `infrastructure:start` starts the docker container. - `infrastructure:stop` stops the docker container. +## Project Structure + +TODO: insert tree structure + +## Serverless Configuration + +This kit uses the TypeScript option for configuration. It is type checked using the `@serverless/typescript` definitions over the DefinitelyTyped definitions because DefinitelyTyped is currently behind on its definition. However, the `@serverless/typescript` types have known issues with certain fields are noted directly in the configuration. + +Things to note: + +- profile field +- httpApi cors +- defined stages: dev, staging, production +- esbuild +- bundle analyzer +- package patterns + ## DynamoDB -View Local Admin Tool at http://localhost:8001/ +This kit utilizes [AWS DynamoDB](https://aws.amazon.com/dynamodb/) for data persistence. It uses the [serverless-dynamodb-local](https://github.com/99x/serverless-dynamodb-local) plugin for table creation and seeding. The rest of the database is run via docker-compose using the `amazon/dynamodb-local` image. + +To help manage the database locally, the [`aaronshaf/dynamodb-admin`](https://github.com/aaronshaf/dynamodb-admin) image is used to provide an GUI interface for managing your database. After the infrastructure is stood up, you can visit the admin tool locally at http://localhost:8001/. + +### Defining New Tables + +To create a new table, define it via the `serverless.ts` resources section. It utilizes the [DynamoDB Cloudformation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html) to define data. Table names should be defined per environment through the Serverless `params` config. Once your table is defined, use the `yarn db:sync` command to create the table. + +### Defining New Seeds + +Seed files should be placed in the `db` directory. They can be formatted as JSON or raw AWS AttributeValues. The seed files should be added to the `custom.dynamodb.seed` section of the config. The `seed` key takes seed target groups which contain a list of source objects for your seed data. Target groups can be arbitrarily named per your needs, but the structure should be: + +```typescript +seed: { + // example target group + core: { + sources: [ + { + table: '', + sources: [' Date: Sat, 24 Dec 2022 15:55:58 -0500 Subject: [PATCH 23/62] update jest ignore paths to avoid build --- starters/serverless-framework-sqs-dynamodb/jest.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/starters/serverless-framework-sqs-dynamodb/jest.config.ts b/starters/serverless-framework-sqs-dynamodb/jest.config.ts index 395a88f1f..fa1994e5f 100644 --- a/starters/serverless-framework-sqs-dynamodb/jest.config.ts +++ b/starters/serverless-framework-sqs-dynamodb/jest.config.ts @@ -14,6 +14,7 @@ const jestConfig: JestConfigWithTsJest = { }, }, modulePaths: [compilerOptions.baseUrl], + modulePathIgnorePatterns: ['/.esbuild/'], moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths), preset: 'ts-jest', testEnvironment: 'node', From 047280f996e0793fea2a3fb51769b3e6fa0ec961 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sat, 24 Dec 2022 15:56:20 -0500 Subject: [PATCH 24/62] add dynamodb and uuid packages --- starters/serverless-framework-sqs-dynamodb/package.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/starters/serverless-framework-sqs-dynamodb/package.json b/starters/serverless-framework-sqs-dynamodb/package.json index 04e49abc8..8f4523dd5 100644 --- a/starters/serverless-framework-sqs-dynamodb/package.json +++ b/starters/serverless-framework-sqs-dynamodb/package.json @@ -11,7 +11,7 @@ ], "scripts": { "build": "sls package", - "start": "SLS_DEBUG=* sls offline start", + "start": "SLS_DEBUG=* sls offline start --verbose", "test": "jest", "lint": "eslint", "format:check": "prettier --check .", @@ -25,7 +25,9 @@ }, "dependencies": { "@aws-sdk/client-dynamodb": "^3.236.0", - "nanoid": "^4.0.0" + "@aws-sdk/util-dynamodb": "^3.238.0", + "uuid": "^9.0.0", + "zod": "^3.20.2" }, "devDependencies": { "@serverless/typescript": "^3.25.0", @@ -33,6 +35,7 @@ "@types/jest": "^29.2.4", "@types/node": "^18.11.17", "@types/serverless": "^3.12.9", + "@types/uuid": "^9.0.0", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.47.0", "dotenv": "^16.0.3", From 7a20a73f0818835d49e76f5350e6bfde705d41e0 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sat, 24 Dec 2022 15:56:57 -0500 Subject: [PATCH 25/62] fix module reloading for lamdba --- starters/serverless-framework-sqs-dynamodb/serverless.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/starters/serverless-framework-sqs-dynamodb/serverless.ts b/starters/serverless-framework-sqs-dynamodb/serverless.ts index f21cd9988..44745b43c 100644 --- a/starters/serverless-framework-sqs-dynamodb/serverless.ts +++ b/starters/serverless-framework-sqs-dynamodb/serverless.ts @@ -39,6 +39,7 @@ const serverlessConfiguration: AWS = { 'serverless-offline': { httpPort: 4000, lambdaPort: 4002, + reloadHandler: true, }, 'serverless-offline-sqs': { autoCreate: true, From ebbe95cb6bf7fc140342adff4cefdb6f19f74f12 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sat, 24 Dec 2022 15:57:16 -0500 Subject: [PATCH 26/62] enable typescript strict mod and new models alias --- starters/serverless-framework-sqs-dynamodb/tsconfig.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/starters/serverless-framework-sqs-dynamodb/tsconfig.json b/starters/serverless-framework-sqs-dynamodb/tsconfig.json index fda057736..4c4c98529 100644 --- a/starters/serverless-framework-sqs-dynamodb/tsconfig.json +++ b/starters/serverless-framework-sqs-dynamodb/tsconfig.json @@ -14,8 +14,10 @@ "resolveJsonModule": true, "skipLibCheck": true, "skipDefaultLibCheck": true, + "strict": true, "baseUrl": ".", "paths": { + "@/models/*": ["src/models/*"], "@/types/*": ["src/types/*"], "@/utils/*": ["src/utils/*"] } From 2905aca249bb67d5e2f27de926e53f3facf541e2 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sat, 24 Dec 2022 15:57:28 -0500 Subject: [PATCH 27/62] update seed to use uuid Replaced nanoid with uuid because serverless cannot support ESM and nanoid latest only supports ESM with their latest version and the maintainer hasn't guaranteed that they will backport all fixes. --- .../db/technologies-seed.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/starters/serverless-framework-sqs-dynamodb/db/technologies-seed.json b/starters/serverless-framework-sqs-dynamodb/db/technologies-seed.json index f17758b7f..c8132de23 100644 --- a/starters/serverless-framework-sqs-dynamodb/db/technologies-seed.json +++ b/starters/serverless-framework-sqs-dynamodb/db/technologies-seed.json @@ -1,20 +1,20 @@ [ { - "id": "Ds2UJdz4-QSHIO2MblWy5", + "id": "629202fd-00bc-46f2-9f67-3ddeb149b931", "displayName": "Serverless Framework", "description": "All-in-one development solution for auto-scaling apps on AWS Lambda", - "url": "https://www.serverless.com/" + "websiteUrl": "https://www.serverless.com/" }, { - "id": "gny3_Bq335TIUMp1YaDZR", + "id": "dfb3378f-a991-4e45-8e23-59e27768f96f", "displayName": "AWS DynamoDB", "description": "Fast, flexible NoSQL database service for single-digit millisecond performance at any scale", - "url": "https://aws.amazon.com/dynamodb/" + "websiteUrl": "https://aws.amazon.com/dynamodb/" }, { - "id": "hkUFI46rClB1Jsz0dswx4", + "id": "ca9085fa-45a6-4d56-b6ee-8d6c59bebbfa", "displayName": "AWS SQS", "description": "Fully managed message queuing for microservices, distributed systems, and serverless applications", - "url": "https://aws.amazon.com/sqs/" + "websiteUrl": "https://aws.amazon.com/sqs/" } ] From 1c00f24beda6c8271c36194c639f993afbb2ae62 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sat, 24 Dec 2022 15:58:51 -0500 Subject: [PATCH 28/62] add process.env type defns --- .../src/types/environment.d.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 starters/serverless-framework-sqs-dynamodb/src/types/environment.d.ts diff --git a/starters/serverless-framework-sqs-dynamodb/src/types/environment.d.ts b/starters/serverless-framework-sqs-dynamodb/src/types/environment.d.ts new file mode 100644 index 000000000..c61c5a9b9 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/types/environment.d.ts @@ -0,0 +1,11 @@ +declare global { + namespace NodeJS { + interface ProcessEnv { + REGION: string; + SLS_STAGE: string; + TECHNOLOGIES_TABLE: string; + } + } +} + +export {}; From f0726d915ad8acc8811d46e65a4d8f3be995eaec Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sat, 24 Dec 2022 15:59:16 -0500 Subject: [PATCH 29/62] add technology CRUD endpoint config to serverless --- .../serverless.ts | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/starters/serverless-framework-sqs-dynamodb/serverless.ts b/starters/serverless-framework-sqs-dynamodb/serverless.ts index 44745b43c..287509aaa 100644 --- a/starters/serverless-framework-sqs-dynamodb/serverless.ts +++ b/starters/serverless-framework-sqs-dynamodb/serverless.ts @@ -124,6 +124,61 @@ const serverlessConfiguration: AWS = { }, ], }, + technology_index: { + handler: 'src/handlers/technology.index', + events: [ + { + httpApi: { + path: '/technology', + method: 'get', + }, + }, + ], + }, + technology_create: { + handler: 'src/handlers/technology.create', + events: [ + { + httpApi: { + path: '/technology', + method: 'post', + }, + }, + ], + }, + technology_show: { + handler: 'src/handlers/technology.show', + events: [ + { + httpApi: { + path: '/technology/{id}', + method: 'get', + }, + }, + ], + }, + technology_update: { + handler: 'src/handlers/technology.put', + events: [ + { + httpApi: { + path: '/technology/{id}', + method: 'put', + }, + }, + ], + }, + technology_destroy: { + handler: 'src/handlers/technology.destroy', + events: [ + { + httpApi: { + path: '/technology/{id}', + method: 'delete', + }, + }, + ], + }, }, resources: { Resources: { From b634c66e3b4a0a3ef55c77bc99cb1042b8cf9428 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sat, 24 Dec 2022 16:55:08 -0500 Subject: [PATCH 30/62] fix handler reference in update path --- starters/serverless-framework-sqs-dynamodb/serverless.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/starters/serverless-framework-sqs-dynamodb/serverless.ts b/starters/serverless-framework-sqs-dynamodb/serverless.ts index 287509aaa..76019fdab 100644 --- a/starters/serverless-framework-sqs-dynamodb/serverless.ts +++ b/starters/serverless-framework-sqs-dynamodb/serverless.ts @@ -158,7 +158,7 @@ const serverlessConfiguration: AWS = { ], }, technology_update: { - handler: 'src/handlers/technology.put', + handler: 'src/handlers/technology.update', events: [ { httpApi: { From 17423d119675bb55626401446a57fb0a141d57f3 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sat, 24 Dec 2022 16:55:28 -0500 Subject: [PATCH 31/62] add create & update zod schemas + types --- .../src/types/technology.ts | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/starters/serverless-framework-sqs-dynamodb/src/types/technology.ts b/starters/serverless-framework-sqs-dynamodb/src/types/technology.ts index aa3cb096a..c90b6ef82 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/types/technology.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/types/technology.ts @@ -1,6 +1,24 @@ +import { z } from 'zod'; + export type Technology = { id: string; displayName: string; description: string; - url: string; + websiteUrl: string; }; + +export const TechnologyCreateSchema = z.object({ + displayName: z.string(), + description: z.string().min(10), + websiteUrl: z.string().url(), +}); + +export type TechnologyCreate = z.infer; + +export const TechnologyUpdateSchema = z.object({ + displayName: z.string().optional(), + description: z.string().min(10).optional(), + websiteUrl: z.string().url().optional(), +}); + +export type TechnologyUpdate = z.infer; From 2c59c352b5e099f63d6033dcb63d9db2ab191943 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sat, 24 Dec 2022 17:27:11 -0500 Subject: [PATCH 32/62] add getErrorMessage util --- .../src/utils/error/getErrorMessage.test.ts | 29 +++++++++++++++++++ .../src/utils/error/getErrorMessage.ts | 29 +++++++++++++++++++ .../src/utils/error/index.ts | 1 + 3 files changed, 59 insertions(+) create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/error/getErrorMessage.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/error/getErrorMessage.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/error/index.ts diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/error/getErrorMessage.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/error/getErrorMessage.test.ts new file mode 100644 index 000000000..ea13a68c8 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/error/getErrorMessage.test.ts @@ -0,0 +1,29 @@ +import { getErrorMessage } from './getErrorMessage'; + +type CircularType = { obj?: unknown }; +const circularExample: CircularType = {}; +circularExample.obj = circularExample; + +describe('getErrorMessage', () => { + describe.each([ + { + label: 'error object with message', + input: new Error('generic error message'), + expected: 'generic error message', + }, + { + label: 'generic thrown error', + input: 'non-error error message', + expected: '"non-error error message"', + }, + { + label: 'generic thrown error with parsing issue', + input: circularExample, + expected: '[object Object]', + }, + ])('return $label', ({ input, expected }) => { + test(`return ${expected}`, () => { + expect(getErrorMessage(input)).toEqual(expected); + }); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/error/getErrorMessage.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/error/getErrorMessage.ts new file mode 100644 index 000000000..b3e238e27 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/error/getErrorMessage.ts @@ -0,0 +1,29 @@ +// sourced from https://kentcdodds.com/blog/get-a-catch-block-error-message-with-typescript +type ErrorWithMessage = { + message: string; +}; + +function isErrorWithMessage(error: unknown): error is ErrorWithMessage { + return ( + typeof error === 'object' && + error !== null && + 'message' in error && + typeof (error as Record).message === 'string' + ); +} + +function toErrorWithMessage(maybeError: unknown): ErrorWithMessage { + if (isErrorWithMessage(maybeError)) return maybeError; + + try { + return new Error(JSON.stringify(maybeError)); + } catch { + // fallback in case there's an error stringifying the maybeError + // like with circular references for example. + return new Error(String(maybeError)); + } +} + +export function getErrorMessage(error: unknown) { + return toErrorWithMessage(error).message; +} diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/error/index.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/error/index.ts new file mode 100644 index 000000000..50ca645d9 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/error/index.ts @@ -0,0 +1 @@ +export { getErrorMessage } from './getErrorMessage'; From b2eb85098a899c6a86e7e703c2e54f1187d301df Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sat, 24 Dec 2022 19:38:11 -0500 Subject: [PATCH 33/62] fix tests for strict mode --- .../src/handlers/healthcheck.test.ts | 10 ++++--- .../src/utils/dynamodb/getClient.test.ts | 26 +++++++------------ .../src/utils/dynamodb/getClient.ts | 2 +- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.test.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.test.ts index 4ec104c6c..7eaca36b6 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.test.ts @@ -1,11 +1,15 @@ -import type { APIGatewayProxyEvent, Context, Callback } from 'aws-lambda'; +import type { APIGatewayProxyEvent, Context, Callback, APIGatewayProxyResult } from 'aws-lambda'; import { handler } from './healthcheck'; describe('healtcheck', () => { - let subject; + let subject: APIGatewayProxyResult; beforeAll(async () => { - subject = await handler({} as APIGatewayProxyEvent, {} as Context, {} as Callback); + subject = (await handler( + {} as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; }); it('returns a 200 statusCode', () => { diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.test.ts index 0734abf26..6024a58eb 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.test.ts @@ -2,7 +2,7 @@ import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; describe('getClient', () => { const OLD_ENV = process.env; - let subject; + let subject: DynamoDBClient; let client: DynamoDBClient; describe('when IS_OFFLINE is true', () => { @@ -21,12 +21,14 @@ describe('getClient', () => { process.env = OLD_ENV; }); - it('returns an DynamoDB', () => { + it('returns an DynamoDBClient', () => { expect(subject).toEqual(expect.any(client)); }); it('sets the endpoint to localhost', async () => { - expect(subject.config.isCustomEndpoint).toBe(true); + if (!subject.config.endpoint) { + fail('client misconfigured'); + } const { hostname } = await subject.config.endpoint(); expect(hostname).toMatch(/localhost/); }); @@ -48,27 +50,21 @@ describe('getClient', () => { process.env = OLD_ENV; }); - it('returns an LambdaClient', () => { + it('returns an DynamoDBClient', () => { expect(subject).toEqual(expect.any(client)); }); it('uses the default AWS Lambda endpoint', async () => { - expect(subject.config.isCustomEndpoint).toBe(false); + expect(subject.config.endpoint).toBeUndefined(); }); }); describe('when called twice', () => { - let dynamodb; - - beforeAll(() => { + beforeAll(async () => { jest.resetModules(); - dynamodb = require('@aws-sdk/client-dynamodb'); - jest.doMock('@aws-sdk/client-dynamodb', () => ({ - DynamoDBClient: jest.fn().mockImplementation(() => new dynamodb.DynamoDBClient({})), - })); + jest.mock('@aws-sdk/client-dynamodb'); client = require('@aws-sdk/client-dynamodb').DynamoDBClient; - const { getClient } = require('./getClient'); process.env = { ...OLD_ENV, @@ -85,9 +81,5 @@ describe('getClient', () => { it('runs the constructor once', () => { expect(client).toHaveBeenCalledTimes(1); }); - - it('returns a cached client', () => { - expect(subject).toEqual(expect.any(dynamodb.DynamoDBClient)); - }); }); }); diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.ts index 6dfa0256d..485c7002a 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.ts @@ -3,7 +3,7 @@ import { isOffline } from '@/utils/is-offline'; let cachedClient: DynamoDBClient; -export const getClient = () => { +export const getClient = (): DynamoDBClient => { if (cachedClient) { return cachedClient; } From f45bf79ff30f336beac0b8fe547fe61ea69efd6d Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sat, 24 Dec 2022 20:50:10 -0500 Subject: [PATCH 34/62] disable no-empty-function lint rule --- starters/serverless-framework-sqs-dynamodb/.eslintrc.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/starters/serverless-framework-sqs-dynamodb/.eslintrc.js b/starters/serverless-framework-sqs-dynamodb/.eslintrc.js index 992756094..4677dedac 100644 --- a/starters/serverless-framework-sqs-dynamodb/.eslintrc.js +++ b/starters/serverless-framework-sqs-dynamodb/.eslintrc.js @@ -113,6 +113,9 @@ module.exports = { 'import/no-named-as-default': 'error', 'import/no-named-as-default-member': 'error', 'import/no-unresolved': ['error', { commonjs: true }], + + // typescript settings + '@typescript-eslint/no-empty-function': false, }, settings: { 'import/resolver': { From 368e1fd90b226c3eba7fd07d0da9dbb439952e34 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sat, 24 Dec 2022 20:50:27 -0500 Subject: [PATCH 35/62] add aws-sdk-client-mock --- starters/serverless-framework-sqs-dynamodb/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/starters/serverless-framework-sqs-dynamodb/package.json b/starters/serverless-framework-sqs-dynamodb/package.json index 8f4523dd5..5daa97017 100644 --- a/starters/serverless-framework-sqs-dynamodb/package.json +++ b/starters/serverless-framework-sqs-dynamodb/package.json @@ -38,6 +38,7 @@ "@types/uuid": "^9.0.0", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.47.0", + "aws-sdk-client-mock": "^2.0.1", "dotenv": "^16.0.3", "esbuild": "^0.16.10", "esbuild-node-externals": "^1.6.0", From e45ff75bfdae8ad6592ffeffda8dc3575a7e4f01 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sat, 24 Dec 2022 20:59:29 -0500 Subject: [PATCH 36/62] add dynamodb utils --- .../src/utils/dynamodb/deleteItem.test.ts | 75 +++++++++++++ .../src/utils/dynamodb/deleteItem.ts | 25 +++++ .../src/utils/dynamodb/getItem.test.ts | 75 +++++++++++++ .../src/utils/dynamodb/getItem.ts | 28 +++++ .../src/utils/dynamodb/index.ts | 4 + .../src/utils/dynamodb/putItem.test.ts | 52 +++++++++ .../src/utils/dynamodb/putItem.ts | 20 ++++ .../src/utils/dynamodb/scan.test.ts | 102 ++++++++++++++++++ .../src/utils/dynamodb/scan.ts | 22 ++++ 9 files changed, 403 insertions(+) create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getItem.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getItem.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/index.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/putItem.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/putItem.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.ts diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.test.ts new file mode 100644 index 000000000..6a6dd613d --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.test.ts @@ -0,0 +1,75 @@ +import { DeleteItemCommand } from '@aws-sdk/client-dynamodb'; +import { mockClient } from 'aws-sdk-client-mock'; +import { getClient } from './getClient'; +import { deleteItem } from './deleteItem'; + +describe('deleteItem', () => { + let subject: Record | null; + const ddbMock = mockClient(getClient()); + + afterAll(() => { + ddbMock.restore(); + jest.resetAllMocks(); + }); + + describe('when item exists', () => { + beforeAll(async () => { + ddbMock.on(DeleteItemCommand).resolves({ + Attributes: { + description: { + S: 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + }, + id: { S: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff' }, + websiteUrl: { S: 'https://jestjs.io/' }, + displayName: { S: 'Jest' }, + }, + }); + subject = await deleteItem('technology-test', { + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + }); + }); + + it('returns the old unmarshalled item', () => { + expect(subject).toEqual({ + description: + 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + websiteUrl: 'https://jestjs.io/', + displayName: 'Jest', + }); + }); + }); + + describe('when item does not exist', () => { + beforeAll(async () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + ddbMock.on(DeleteItemCommand).resolves({ + Attributes: undefined, + }); + subject = await deleteItem('technology-test', { + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + }); + }); + + it('returns null', () => { + expect(subject).toBeNull(); + }); + }); + + describe('when error occurs', () => { + beforeAll(async () => { + ddbMock.on(DeleteItemCommand).rejects('mock error'); + subject = await deleteItem('technology-test', { + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + }); + }); + + it('returns null', () => { + expect(subject).toBeNull(); + }); + + it('logs the error', () => { + expect(console.error).toHaveBeenCalledWith('dynamodb.deleteItem Error - mock error'); + }); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.ts new file mode 100644 index 000000000..cd0485fe4 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.ts @@ -0,0 +1,25 @@ +import { DeleteItemCommand } from '@aws-sdk/client-dynamodb'; +import { marshall, unmarshall } from '@aws-sdk/util-dynamodb'; +import { getErrorMessage } from '@/utils/error'; +import { getClient } from './getClient'; + +export const deleteItem = async (tableName: string, key: Record) => { + const command = new DeleteItemCommand({ + TableName: tableName, + Key: marshall(key), + ReturnValues: 'ALL_OLD', + }); + const client = getClient(); + + try { + const response = await client.send(command); + + if (!response || !response.Attributes) { + return null; + } + return unmarshall(response.Attributes); + } catch (err) { + console.error(`dynamodb.deleteItem Error - ${getErrorMessage(err)}`); + return null; + } +}; diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getItem.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getItem.test.ts new file mode 100644 index 000000000..2b8416e6b --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getItem.test.ts @@ -0,0 +1,75 @@ +import { GetItemCommand } from '@aws-sdk/client-dynamodb'; +import { mockClient } from 'aws-sdk-client-mock'; +import { getClient } from './getClient'; +import { getItem } from './getItem'; + +describe('getItem', () => { + let subject: Record | null; + const ddbMock = mockClient(getClient()); + + afterAll(() => { + ddbMock.restore(); + jest.resetAllMocks(); + }); + + describe('when item is found', () => { + beforeAll(async () => { + ddbMock.on(GetItemCommand).resolves({ + Item: { + description: { + S: 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + }, + id: { S: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff' }, + websiteUrl: { S: 'https://jestjs.io/' }, + displayName: { S: 'Jest' }, + }, + }); + subject = await getItem('technology-test', { + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + }); + }); + + it('returns the unmarshalled item', () => { + expect(subject).toEqual({ + description: + 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + websiteUrl: 'https://jestjs.io/', + displayName: 'Jest', + }); + }); + }); + + describe('when item is not found', () => { + beforeAll(async () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + ddbMock.on(GetItemCommand).resolves({ + Item: undefined, + }); + subject = await getItem('technology-test', { + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + }); + }); + + it('returns null', () => { + expect(subject).toBeNull(); + }); + }); + + describe('when error occurs', () => { + beforeAll(async () => { + ddbMock.on(GetItemCommand).rejects('mock error'); + subject = await getItem('technology-test', { + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + }); + }); + + it('returns null', () => { + expect(subject).toBeNull(); + }); + + it('logs the error', () => { + expect(console.error).toHaveBeenCalledWith('dynamodb.getItem Error - mock error'); + }); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getItem.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getItem.ts new file mode 100644 index 000000000..5a7540193 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getItem.ts @@ -0,0 +1,28 @@ +import { GetItemCommand, GetItemCommandInput } from '@aws-sdk/client-dynamodb'; +import { marshall, unmarshall } from '@aws-sdk/util-dynamodb'; +import { getErrorMessage } from '@/utils/error'; +import { getClient } from './getClient'; + +export const getItem = async ( + tableName: string, + key: Record, + projection: GetItemCommandInput['ProjectionExpression'] = undefined +): Promise | null> => { + const command = new GetItemCommand({ + TableName: tableName, + Key: marshall(key), + ProjectionExpression: projection, + }); + const client = getClient(); + + try { + const response = await client.send(command); + if (!response || !response.Item) { + return null; + } + return unmarshall(response.Item); + } catch (err) { + console.error(`dynamodb.getItem Error - ${getErrorMessage(err)}`); + return null; + } +}; diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/index.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/index.ts new file mode 100644 index 000000000..3657d8734 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/index.ts @@ -0,0 +1,4 @@ +export { scan } from './scan'; +export { getItem } from './getItem'; +export { putItem } from './putItem'; +export { deleteItem } from './deleteItem'; diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/putItem.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/putItem.test.ts new file mode 100644 index 000000000..fcebc83b8 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/putItem.test.ts @@ -0,0 +1,52 @@ +import { PutItemCommand } from '@aws-sdk/client-dynamodb'; +import { mockClient } from 'aws-sdk-client-mock'; +import { getClient } from './getClient'; +import { putItem } from './putItem'; + +describe('putItem', () => { + let subject: boolean; + const ddbMock = mockClient(getClient()); + + afterAll(() => { + ddbMock.restore(); + jest.resetAllMocks(); + }); + + describe('when putting an item', () => { + beforeAll(async () => { + ddbMock.on(PutItemCommand).resolves({ + Attributes: undefined, + }); + subject = await putItem('technology-test', { + description: + 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + websiteUrl: 'https://jestjs.io/', + displayName: 'Jest', + }); + }); + + it('returns true', () => { + expect(subject).toEqual(true); + }); + }); + + describe('when error occurs', () => { + beforeAll(async () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + ddbMock.on(PutItemCommand).rejects('mock error'); + subject = await putItem('technology-test', { + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + displayName: 'Jest Test', + }); + }); + + it('returns false', () => { + expect(subject).toEqual(false); + }); + + it('logs the error', () => { + expect(console.error).toHaveBeenCalledWith('dynamodb.putItem Error - mock error'); + }); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/putItem.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/putItem.ts new file mode 100644 index 000000000..145a24daa --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/putItem.ts @@ -0,0 +1,20 @@ +import { PutItemCommand } from '@aws-sdk/client-dynamodb'; +import { marshall } from '@aws-sdk/util-dynamodb'; +import { getErrorMessage } from '@/utils/error'; +import { getClient } from './getClient'; + +export const putItem = async (tableName: string, item: Record) => { + const command = new PutItemCommand({ + TableName: tableName, + Item: marshall(item), + }); + const client = getClient(); + + try { + await client.send(command); + return true; + } catch (err) { + console.error(`dynamodb.putItem Error - ${getErrorMessage(err)}`); + return false; + } +}; diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.test.ts new file mode 100644 index 000000000..5f14aaa34 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.test.ts @@ -0,0 +1,102 @@ +import { ScanCommand } from '@aws-sdk/client-dynamodb'; +import { mockClient } from 'aws-sdk-client-mock'; +import { getClient } from './getClient'; +import { scan } from './scan'; + +describe('scan', () => { + let subject: Record[] | null; + const ddbMock = mockClient(getClient()); + + afterAll(() => { + ddbMock.restore(); + jest.resetAllMocks(); + }); + + describe('when items are found', () => { + beforeAll(async () => { + ddbMock.on(ScanCommand).resolves({ + Items: [ + { + description: { + S: 'Fully managed message queuing for microservices, distributed systems, and serverless applications', + }, + id: { S: 'ca9085fa-45a6-4d56-b6ee-8d6c59bebbfa' }, + websiteUrl: { S: 'https://aws.amazon.com/sqs/' }, + displayName: { S: 'AWS SQS' }, + }, + { + description: { + S: 'Fast, flexible NoSQL database service for single-digit millisecond performance at any scale', + }, + id: { S: 'dfb3378f-a991-4e45-8e23-59e27768f96f' }, + websiteUrl: { S: 'https://aws.amazon.com/dynamodb/' }, + displayName: { S: 'AWS DynamoDB' }, + }, + { + description: { + S: 'All-in-one development solution for auto-scaling apps on AWS Lambda', + }, + id: { S: '629202fd-00bc-46f2-9f67-3ddeb149b931' }, + websiteUrl: { S: 'https://www.serverless.com/' }, + displayName: { S: 'Serverless Framework' }, + }, + ], + }); + subject = await scan('technology-test'); + }); + + it('returns the unmarshalled items', () => { + expect(subject).toEqual([ + { + description: + 'Fully managed message queuing for microservices, distributed systems, and serverless applications', + id: 'ca9085fa-45a6-4d56-b6ee-8d6c59bebbfa', + websiteUrl: 'https://aws.amazon.com/sqs/', + displayName: 'AWS SQS', + }, + { + description: + 'Fast, flexible NoSQL database service for single-digit millisecond performance at any scale', + id: 'dfb3378f-a991-4e45-8e23-59e27768f96f', + websiteUrl: 'https://aws.amazon.com/dynamodb/', + displayName: 'AWS DynamoDB', + }, + { + description: 'All-in-one development solution for auto-scaling apps on AWS Lambda', + id: '629202fd-00bc-46f2-9f67-3ddeb149b931', + websiteUrl: 'https://www.serverless.com/', + displayName: 'Serverless Framework', + }, + ]); + }); + }); + + describe('when items are not found', () => { + beforeAll(async () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + ddbMock.on(ScanCommand).resolves({ + Items: undefined, + }); + subject = await scan('technology-test'); + }); + + it('returns empty array', () => { + expect(subject).toEqual([]); + }); + }); + + describe('when error occurs', () => { + beforeAll(async () => { + ddbMock.on(ScanCommand).rejects('mock error'); + subject = await scan('technology-test'); + }); + + it('returns null', () => { + expect(subject).toBeNull(); + }); + + it('logs the error', () => { + expect(console.error).toHaveBeenCalledWith('dynamodb.scan Error - mock error'); + }); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.ts new file mode 100644 index 000000000..894bf8790 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.ts @@ -0,0 +1,22 @@ +import { ScanCommand } from '@aws-sdk/client-dynamodb'; +import { unmarshall } from '@aws-sdk/util-dynamodb'; +import { getErrorMessage } from '@/utils/error'; +import { getClient } from './getClient'; + +export const scan = async (tableName: string) => { + const command = new ScanCommand({ + TableName: tableName, + }); + const client = getClient(); + + try { + const response = await client.send(command); + if (!response || !response.Items) { + return []; + } + return response.Items.map((item) => unmarshall(item)); + } catch (err) { + console.error(`dynamodb.scan Error - ${getErrorMessage(err)}`); + return null; + } +}; From 398eac5306434c1bc9184680df6d9b0e22f7be6d Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sat, 24 Dec 2022 21:13:54 -0500 Subject: [PATCH 37/62] add redis to project * add docker-compose config * add new env variables for redis cache url * expose redis getClient function * add ioredis-mock --- .../.env.example | 3 ++ .../.gitignore | 4 +-- .../docker-compose.yml | 12 ++++++++ .../jest.config.ts | 1 + .../jest.setup.ts | 4 +++ .../package.json | 6 +++- .../serverless.ts | 1 + .../src/types/environment.d.ts | 2 ++ .../src/utils/redis/getClient.test.ts | 29 +++++++++++++++++++ .../src/utils/redis/getClient.ts | 13 +++++++++ .../src/utils/redis/index.ts | 1 + 11 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 starters/serverless-framework-sqs-dynamodb/jest.setup.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/redis/getClient.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/redis/getClient.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/redis/index.ts diff --git a/starters/serverless-framework-sqs-dynamodb/.env.example b/starters/serverless-framework-sqs-dynamodb/.env.example index 453a9c56f..65240b684 100644 --- a/starters/serverless-framework-sqs-dynamodb/.env.example +++ b/starters/serverless-framework-sqs-dynamodb/.env.example @@ -1 +1,4 @@ +# indicate to serverless if you're offline mode or not IS_OFFLINE=true +# connection info for Redis used for caching +REDIS_CACHE_URL=redis://:sOmE_sEcUrE_pAsS@localhost:6379 diff --git a/starters/serverless-framework-sqs-dynamodb/.gitignore b/starters/serverless-framework-sqs-dynamodb/.gitignore index 1b022ad28..fc5caa0eb 100644 --- a/starters/serverless-framework-sqs-dynamodb/.gitignore +++ b/starters/serverless-framework-sqs-dynamodb/.gitignore @@ -2,8 +2,8 @@ coverage node_modules jspm_packages -.serverless .dynamodb .esbuild - .env +.redis +.serverless diff --git a/starters/serverless-framework-sqs-dynamodb/docker-compose.yml b/starters/serverless-framework-sqs-dynamodb/docker-compose.yml index 2b8b61ece..c875f9266 100644 --- a/starters/serverless-framework-sqs-dynamodb/docker-compose.yml +++ b/starters/serverless-framework-sqs-dynamodb/docker-compose.yml @@ -21,6 +21,18 @@ services: depends_on: - dynamodb + # configure Redis for caching + redis_cache: + image: 'redis:alpine' + command: redis-server --save 20 1 --loglevel warning --requirepass sOmE_sEcUrE_pAsS + ports: + - '6379:6379' + volumes: + - ./.redis/cache_data:/var/lib/redis + - ./.redis/cache_conf:/usr/local/etc/redis/redis.conf + environment: + - REDIS_REPLICATION_MODE=master + # configures ElasticMQ to emulate SQS behaviors sqs: image: softwaremill/elasticmq:1.1.1 diff --git a/starters/serverless-framework-sqs-dynamodb/jest.config.ts b/starters/serverless-framework-sqs-dynamodb/jest.config.ts index fa1994e5f..9e33bbd21 100644 --- a/starters/serverless-framework-sqs-dynamodb/jest.config.ts +++ b/starters/serverless-framework-sqs-dynamodb/jest.config.ts @@ -18,6 +18,7 @@ const jestConfig: JestConfigWithTsJest = { moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths), preset: 'ts-jest', testEnvironment: 'node', + setupFiles: ['./jest.setup.ts'], }; export default jestConfig; diff --git a/starters/serverless-framework-sqs-dynamodb/jest.setup.ts b/starters/serverless-framework-sqs-dynamodb/jest.setup.ts new file mode 100644 index 000000000..8531e333b --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/jest.setup.ts @@ -0,0 +1,4 @@ +import * as dotenv from 'dotenv'; +dotenv.config(); + +jest.mock('ioredis', () => require('ioredis-mock')); diff --git a/starters/serverless-framework-sqs-dynamodb/package.json b/starters/serverless-framework-sqs-dynamodb/package.json index 5daa97017..ae1256877 100644 --- a/starters/serverless-framework-sqs-dynamodb/package.json +++ b/starters/serverless-framework-sqs-dynamodb/package.json @@ -12,7 +12,7 @@ "scripts": { "build": "sls package", "start": "SLS_DEBUG=* sls offline start --verbose", - "test": "jest", + "test": "jest --runInBand", "lint": "eslint", "format:check": "prettier --check .", "format:write": "prettier --write .", @@ -26,6 +26,8 @@ "dependencies": { "@aws-sdk/client-dynamodb": "^3.236.0", "@aws-sdk/util-dynamodb": "^3.238.0", + "ioredis": "^5.2.4", + "ioredis-mock": "^8.2.2", "uuid": "^9.0.0", "zod": "^3.20.2" }, @@ -49,7 +51,9 @@ "eslint-plugin-import": "^2.25.2", "eslint-plugin-n": "^15.0.0", "eslint-plugin-promise": "^6.0.0", + "i": "^0.3.7", "jest": "^29.3.1", + "npm": "^9.2.0", "prettier": "^2.8.1", "serverless": "^3.26.0", "serverless-analyze-bundle-plugin": "^1.2.1", diff --git a/starters/serverless-framework-sqs-dynamodb/serverless.ts b/starters/serverless-framework-sqs-dynamodb/serverless.ts index 76019fdab..5f72e1799 100644 --- a/starters/serverless-framework-sqs-dynamodb/serverless.ts +++ b/starters/serverless-framework-sqs-dynamodb/serverless.ts @@ -83,6 +83,7 @@ const serverlessConfiguration: AWS = { REGION: '${aws:region}', SLS_STAGE: '${sls:stage}', // DynamoDB Tables + REDIS_CACHE_URL: '${env:REDIS_CACHE_URL}', TECHNOLOGIES_TABLE: '${param:technologiesTable}', }, iam: { diff --git a/starters/serverless-framework-sqs-dynamodb/src/types/environment.d.ts b/starters/serverless-framework-sqs-dynamodb/src/types/environment.d.ts index c61c5a9b9..d022fbe97 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/types/environment.d.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/types/environment.d.ts @@ -3,6 +3,8 @@ declare global { interface ProcessEnv { REGION: string; SLS_STAGE: string; + + REDIS_CACHE_URL: string; TECHNOLOGIES_TABLE: string; } } diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/redis/getClient.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/redis/getClient.test.ts new file mode 100644 index 000000000..1a33a55ba --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/redis/getClient.test.ts @@ -0,0 +1,29 @@ +import Redis from 'ioredis'; +import { getClient } from './getClient'; + +describe('redis.getClient()', () => { + let subject: Redis; + + describe('when called once', () => { + beforeAll(async () => { + jest.resetModules(); + subject = await getClient('test', 'fakeurl'); + }); + + it('returns a Redis instance', () => { + expect(subject).toEqual(expect.any(Redis)); + }); + }); + + describe('when called twice', () => { + beforeAll(async () => { + jest.resetModules(); + await getClient('test', 'fakeurl'); + subject = await getClient('test', 'fakeurl'); + }); + + it('returns a Redis client', () => { + expect(subject).toEqual(expect.any(Redis)); + }); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/redis/getClient.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/redis/getClient.ts new file mode 100644 index 000000000..3525e73fc --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/redis/getClient.ts @@ -0,0 +1,13 @@ +import Redis from 'ioredis'; + +let cachedClients: Record = {}; + +export const getClient = async (type: string, url: string): Promise => { + if (cachedClients[type]) { + return cachedClients[type]; + } + + const newClient = new Redis(`${url}/0?allowUsernameInURI=true`); + cachedClients[type] = newClient; + return newClient; +}; diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/redis/index.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/redis/index.ts new file mode 100644 index 000000000..075c17190 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/redis/index.ts @@ -0,0 +1 @@ +export { getClient } from './getClient'; From 08ed410e818d0d270cbb916dd9854afa720ebfae Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sat, 24 Dec 2022 21:14:03 -0500 Subject: [PATCH 38/62] add cache utils * install cachified * create redis adapter for ioredis per https://github.com/Xiphe/cachified/issues/3#issuecomment-1288884378 * add cache reader * add cache setter * add cache key purger --- .../package.json | 1 + .../src/utils/cache/addToCache.test.ts | 39 ++++++++++++ .../src/utils/cache/addToCache.ts | 13 ++++ .../src/utils/cache/constants.ts | 1 + .../src/utils/cache/getClient.test.ts | 34 +++++++++++ .../src/utils/cache/getClient.ts | 33 ++++++++++ .../src/utils/cache/index.ts | 2 + .../src/utils/cache/removeFromCache.test.ts | 18 ++++++ .../src/utils/cache/removeFromCache.ts | 6 ++ .../src/utils/cache/useCache.test.ts | 60 +++++++++++++++++++ .../src/utils/cache/useCache.ts | 17 ++++++ 11 files changed, 224 insertions(+) create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/cache/addToCache.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/cache/addToCache.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/cache/constants.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/cache/getClient.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/cache/getClient.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/cache/index.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/cache/removeFromCache.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/cache/removeFromCache.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/cache/useCache.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/cache/useCache.ts diff --git a/starters/serverless-framework-sqs-dynamodb/package.json b/starters/serverless-framework-sqs-dynamodb/package.json index ae1256877..281a28275 100644 --- a/starters/serverless-framework-sqs-dynamodb/package.json +++ b/starters/serverless-framework-sqs-dynamodb/package.json @@ -26,6 +26,7 @@ "dependencies": { "@aws-sdk/client-dynamodb": "^3.236.0", "@aws-sdk/util-dynamodb": "^3.238.0", + "cachified": "^3.0.1", "ioredis": "^5.2.4", "ioredis-mock": "^8.2.2", "uuid": "^9.0.0", diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/addToCache.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/addToCache.test.ts new file mode 100644 index 000000000..c88d509a0 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/addToCache.test.ts @@ -0,0 +1,39 @@ +import Redis from 'ioredis'; +import { getClient as getRedisClient } from '@/utils/redis'; +import { DEFAULT_CACHE_TIME } from './constants'; +import { addToCache } from './addToCache'; + +describe('cache.addToCache()', () => { + let redisClient: Redis; + + beforeAll(async () => { + jest.resetModules(); + jest.useFakeTimers(); + redisClient = await getRedisClient('cache', process.env.REDIS_CACHE_URL); + jest.spyOn(redisClient, 'set'); + await addToCache('test-add', { + a: 1, + }); + }); + + afterAll(() => { + jest.clearAllTimers(); + }); + + it('sets a value to the cache', () => { + expect(redisClient.set).toHaveBeenCalledWith( + 'test-add', + JSON.stringify({ + value: { + a: 1, + }, + metadata: { + createdTime: Date.now(), + ttl: DEFAULT_CACHE_TIME, + }, + }), + 'PX', + DEFAULT_CACHE_TIME + ); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/addToCache.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/addToCache.ts new file mode 100644 index 000000000..ae4678083 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/addToCache.ts @@ -0,0 +1,13 @@ +import { DEFAULT_CACHE_TIME } from './constants'; +import { getClient } from './getClient'; + +export async function addToCache(key: string, value: unknown, ttl: number = DEFAULT_CACHE_TIME) { + const cache = await getClient(); + await cache.set(key, { + value, + metadata: { + createdTime: Date.now(), + ttl, + }, + }); +} diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/constants.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/constants.ts new file mode 100644 index 000000000..fa92b08fa --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/constants.ts @@ -0,0 +1 @@ +export const DEFAULT_CACHE_TIME = 300_000; // 5 minutes = 1000 ms/s * 60 s/min * 5 min diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/getClient.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/getClient.test.ts new file mode 100644 index 000000000..8f67e434a --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/getClient.test.ts @@ -0,0 +1,34 @@ +import { Cache } from 'cachified'; +import Redis from 'ioredis'; +import { getClient } from './getClient'; + +describe('cache.getClient()', () => { + let subject: Cache; + + describe('when called once', () => { + beforeAll(async () => { + jest.resetModules(); + subject = await getClient(); + }); + + it('returns a cache instance', () => { + expect(subject).toHaveProperty('set'); + expect(subject).toHaveProperty('get'); + expect(subject).toHaveProperty('delete'); + }); + }); + + describe('when called twice', () => { + beforeAll(async () => { + jest.resetModules(); + await getClient(); + subject = await getClient(); + }); + + it('returns a cache instance', () => { + expect(subject).toHaveProperty('set'); + expect(subject).toHaveProperty('get'); + expect(subject).toHaveProperty('delete'); + }); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/getClient.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/getClient.ts new file mode 100644 index 000000000..8d2b5d28a --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/getClient.ts @@ -0,0 +1,33 @@ +import { Cache, totalTtl } from 'cachified'; +import { getClient as getRedisClient } from '@/utils/redis'; + +let cachedClient: Cache; +export const getClient = async () => { + if (cachedClient) { + return cachedClient; + } + + const redisClient = await getRedisClient('cache', process.env.REDIS_CACHE_URL); + cachedClient = { + name: redisClient.options?.name || 'Redis', + async get(key) { + const value = await redisClient.get(key); + if (!value) { + return null; + } + return JSON.parse(value); + }, + async set(key, value) { + const ttl = totalTtl(value?.metadata); + if (ttl > 0 && ttl < Infinity) { + return redisClient.set(key, JSON.stringify(value), 'PX', ttl); + } + return redisClient.set(key, JSON.stringify(value)); + }, + async delete(key) { + return redisClient.del(key); + }, + }; + + return cachedClient; +}; diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/index.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/index.ts new file mode 100644 index 000000000..a88344985 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/index.ts @@ -0,0 +1,2 @@ +export { useCache } from './useCache'; +export { removeFromCache } from './removeFromCache'; diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/removeFromCache.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/removeFromCache.test.ts new file mode 100644 index 000000000..f91f20b25 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/removeFromCache.test.ts @@ -0,0 +1,18 @@ +import Redis from 'ioredis'; +import { getClient as getRedisClient } from '@/utils/redis'; +import { removeFromCache } from './removeFromCache'; + +describe('cache.removeFromCache()', () => { + let redisClient: Redis; + + beforeAll(async () => { + jest.resetModules(); + redisClient = await getRedisClient('cache', process.env.REDIS_CACHE_URL); + jest.spyOn(redisClient, 'del'); + await removeFromCache('test-del'); + }); + + it('deletes a value', () => { + expect(redisClient.del).toHaveBeenCalledWith('test-del'); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/removeFromCache.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/removeFromCache.ts new file mode 100644 index 000000000..6220a3921 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/removeFromCache.ts @@ -0,0 +1,6 @@ +import { getClient } from './getClient'; + +export async function removeFromCache(key: string) { + const cache = await getClient(); + await cache.delete(key); +} diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/useCache.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/useCache.test.ts new file mode 100644 index 000000000..6ff012a70 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/useCache.test.ts @@ -0,0 +1,60 @@ +import { useCache } from './useCache'; + +describe('cache.useCache()', () => { + let subject: string; + + describe('when using default ttl', () => { + beforeAll(async () => { + jest.resetModules(); + subject = await useCache({ + key: 'test-with-ttl', + getFreshValue() { + return 'test-value'; + }, + }); + }); + + it('returns a fresh fetched value', () => { + expect(subject).toEqual('test-value'); + }); + }); + + describe('when no ttl', () => { + beforeAll(async () => { + jest.resetModules(); + subject = await useCache({ + key: 'test-no-ttl', + getFreshValue() { + return 'test-value'; + }, + ttl: 0, + }); + }); + + it('returns a fresh fetched value', () => { + expect(subject).toEqual('test-value'); + }); + }); + + describe('when called twice', () => { + beforeAll(async () => { + jest.resetModules(); + await useCache({ + key: 'test-double', + getFreshValue() { + return 'test-value'; + }, + }); + subject = await useCache({ + key: 'test-double', + getFreshValue() { + return 'test-value'; + }, + }); + }); + + it('returns a fresh fetched value', () => { + expect(subject).toEqual('test-value'); + }); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/useCache.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/useCache.ts new file mode 100644 index 000000000..f3691d042 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/useCache.ts @@ -0,0 +1,17 @@ +import { cachified, CachifiedOptions } from 'cachified'; +import { DEFAULT_CACHE_TIME } from './constants'; +import { getClient } from './getClient'; + +export async function useCache({ + key, + getFreshValue, + ttl = DEFAULT_CACHE_TIME, +}: Pick, 'key' | 'getFreshValue' | 'ttl'>) { + const cache = await getClient(); + return cachified({ + cache, + key, + getFreshValue, + ttl, + }); +} From ce12f61624b5393dbce2a4ed4940eeefc939a067 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Tue, 27 Dec 2022 11:05:20 -0500 Subject: [PATCH 39/62] update test describes per https://bettertests.js.org/ --- .../src/utils/dynamodb/deleteItem.test.ts | 2 +- .../src/utils/dynamodb/getClient.test.ts | 2 +- .../src/utils/dynamodb/getItem.test.ts | 2 +- .../src/utils/dynamodb/putItem.test.ts | 2 +- .../src/utils/dynamodb/scan.test.ts | 2 +- .../src/utils/error/getErrorMessage.test.ts | 2 +- .../src/utils/is-offline/is-offline.test.ts | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.test.ts index 6a6dd613d..bb6027c3f 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.test.ts @@ -3,7 +3,7 @@ import { mockClient } from 'aws-sdk-client-mock'; import { getClient } from './getClient'; import { deleteItem } from './deleteItem'; -describe('deleteItem', () => { +describe('dynamodb.deleteItem()', () => { let subject: Record | null; const ddbMock = mockClient(getClient()); diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.test.ts index 6024a58eb..ccfbfca59 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.test.ts @@ -1,6 +1,6 @@ import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; -describe('getClient', () => { +describe('dynamodb.getClient()', () => { const OLD_ENV = process.env; let subject: DynamoDBClient; let client: DynamoDBClient; diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getItem.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getItem.test.ts index 2b8416e6b..3913aa9df 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getItem.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getItem.test.ts @@ -3,7 +3,7 @@ import { mockClient } from 'aws-sdk-client-mock'; import { getClient } from './getClient'; import { getItem } from './getItem'; -describe('getItem', () => { +describe('dynamodb.getItem()', () => { let subject: Record | null; const ddbMock = mockClient(getClient()); diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/putItem.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/putItem.test.ts index fcebc83b8..f88e5e666 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/putItem.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/putItem.test.ts @@ -3,7 +3,7 @@ import { mockClient } from 'aws-sdk-client-mock'; import { getClient } from './getClient'; import { putItem } from './putItem'; -describe('putItem', () => { +describe('dynamodb.putItem()', () => { let subject: boolean; const ddbMock = mockClient(getClient()); diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.test.ts index 5f14aaa34..5c822060f 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.test.ts @@ -3,7 +3,7 @@ import { mockClient } from 'aws-sdk-client-mock'; import { getClient } from './getClient'; import { scan } from './scan'; -describe('scan', () => { +describe('dynamodb.scan()', () => { let subject: Record[] | null; const ddbMock = mockClient(getClient()); diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/error/getErrorMessage.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/error/getErrorMessage.test.ts index ea13a68c8..6935bf3e1 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/error/getErrorMessage.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/error/getErrorMessage.test.ts @@ -4,7 +4,7 @@ type CircularType = { obj?: unknown }; const circularExample: CircularType = {}; circularExample.obj = circularExample; -describe('getErrorMessage', () => { +describe('getErrorMessage()', () => { describe.each([ { label: 'error object with message', diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/is-offline/is-offline.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/is-offline/is-offline.test.ts index 16dd2f7bf..cbc7d6488 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/is-offline/is-offline.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/is-offline/is-offline.test.ts @@ -1,6 +1,6 @@ import { isOffline } from './is-offline'; -describe('isOffline ', () => { +describe('isOffline()', () => { const env = process.env; beforeEach(() => { From ad0f533a18030d18b06b07208a0391fb6b764d7b Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Fri, 30 Dec 2022 22:31:25 -0600 Subject: [PATCH 40/62] add tests to technology functions - fix bugs in dynamodb utils for error cases - add ability to run check value on useCache - do not persist in useCache if check value fails - add full technology object schema for validation --- .../src/models/technology/create.test.ts | 56 +++++++++ .../src/models/technology/create.ts | 19 +++ .../src/models/technology/destroy.test.ts | 39 ++++++ .../src/models/technology/destroy.ts | 9 ++ .../src/models/technology/get.test.ts | 39 ++++++ .../src/models/technology/get.ts | 18 +++ .../src/models/technology/getAll.test.ts | 70 +++++++++++ .../src/models/technology/getAll.ts | 6 + .../src/models/technology/getCacheKey.ts | 3 + .../src/models/technology/index.ts | 5 + .../src/models/technology/update.test.ts | 113 ++++++++++++++++++ .../src/models/technology/update.ts | 24 ++++ .../src/types/technology.ts | 7 ++ .../src/utils/cache/index.ts | 1 + .../src/utils/cache/useCache.test.ts | 24 +++- .../src/utils/cache/useCache.ts | 32 +++-- .../src/utils/dynamodb/index.ts | 1 + .../src/utils/dynamodb/scan.test.ts | 2 +- .../src/utils/dynamodb/scan.ts | 2 +- 19 files changed, 459 insertions(+), 11 deletions(-) create mode 100644 starters/serverless-framework-sqs-dynamodb/src/models/technology/create.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/models/technology/create.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/models/technology/destroy.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/models/technology/destroy.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/models/technology/get.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/models/technology/get.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/models/technology/getAll.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/models/technology/getAll.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/models/technology/getCacheKey.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/models/technology/index.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/models/technology/update.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/models/technology/update.ts diff --git a/starters/serverless-framework-sqs-dynamodb/src/models/technology/create.test.ts b/starters/serverless-framework-sqs-dynamodb/src/models/technology/create.test.ts new file mode 100644 index 000000000..360c1dd12 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/models/technology/create.test.ts @@ -0,0 +1,56 @@ +import { PutItemCommand } from '@aws-sdk/client-dynamodb'; +import { mockClient } from 'aws-sdk-client-mock'; +import { getClient } from '@/utils/dynamodb'; +import { create } from './create'; + +describe('technology.create()', () => { + let subject: Record | null; + + const ddbMock = mockClient(getClient()); + + afterAll(() => { + ddbMock.restore(); + jest.resetAllMocks(); + }); + + describe('when successfully creates the record', () => { + beforeAll(async () => { + ddbMock.on(PutItemCommand).resolves({ + Attributes: undefined, + }); + subject = await create({ + description: + 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + websiteUrl: 'https://jestjs.io/', + displayName: 'Jest', + }); + }); + + it('returns the newly created technology', () => { + expect(subject).toEqual({ + description: + 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + id: expect.any(String), + websiteUrl: 'https://jestjs.io/', + displayName: 'Jest', + }); + }); + }); + + describe('when fails to create the record', () => { + beforeAll(async () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + ddbMock.on(PutItemCommand).rejects('mock error'); + subject = await create({ + description: + 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + websiteUrl: 'https://jestjs.io/', + displayName: 'Jest', + }); + }); + + it('returns null', () => { + expect(subject).toBeNull(); + }); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/models/technology/create.ts b/starters/serverless-framework-sqs-dynamodb/src/models/technology/create.ts new file mode 100644 index 000000000..25ffe19ed --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/models/technology/create.ts @@ -0,0 +1,19 @@ +import { v4 as uuidv4 } from 'uuid'; +import { TechnologyCreate } from '@/types/technology'; +import { putItem } from '@/utils/dynamodb'; +import { addToCache } from '@/utils/cache'; +import { getCacheKey } from './getCacheKey'; + +export const create = async (payload: TechnologyCreate) => { + const newTechnology = { + id: uuidv4(), + ...payload, + }; + const didPersist = await putItem(process.env.TECHNOLOGIES_TABLE, newTechnology); + if (didPersist) { + await addToCache(getCacheKey(newTechnology.id), newTechnology); + return newTechnology; + } + + return null; +}; diff --git a/starters/serverless-framework-sqs-dynamodb/src/models/technology/destroy.test.ts b/starters/serverless-framework-sqs-dynamodb/src/models/technology/destroy.test.ts new file mode 100644 index 000000000..596a31c3a --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/models/technology/destroy.test.ts @@ -0,0 +1,39 @@ +import { DeleteItemCommand } from '@aws-sdk/client-dynamodb'; +import { mockClient } from 'aws-sdk-client-mock'; +import { getClient } from '@/utils/dynamodb'; +import { destroy } from './destroy'; + +describe('technology.destroy()', () => { + let subject: Record | null; + + const ddbMock = mockClient(getClient()); + + beforeAll(async () => { + ddbMock.on(DeleteItemCommand).resolves({ + Attributes: { + description: { + S: 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + }, + id: { S: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff' }, + websiteUrl: { S: 'https://jestjs.io/' }, + displayName: { S: 'Jest' }, + }, + }); + subject = await destroy('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff'); + }); + + afterAll(() => { + ddbMock.restore(); + jest.resetAllMocks(); + }); + + it('returns the deleted technology', () => { + expect(subject).toEqual({ + description: + 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + websiteUrl: 'https://jestjs.io/', + displayName: 'Jest', + }); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/models/technology/destroy.ts b/starters/serverless-framework-sqs-dynamodb/src/models/technology/destroy.ts new file mode 100644 index 000000000..ba1196caf --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/models/technology/destroy.ts @@ -0,0 +1,9 @@ +import { removeFromCache } from '@/utils/cache'; +import { deleteItem } from '@/utils/dynamodb'; +import { getCacheKey } from './getCacheKey'; + +export const destroy = async (key: string) => { + const item = await deleteItem(process.env.TECHNOLOGIES_TABLE, { id: key }); + await removeFromCache(getCacheKey(key)); + return item; +}; diff --git a/starters/serverless-framework-sqs-dynamodb/src/models/technology/get.test.ts b/starters/serverless-framework-sqs-dynamodb/src/models/technology/get.test.ts new file mode 100644 index 000000000..5e17e2a37 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/models/technology/get.test.ts @@ -0,0 +1,39 @@ +import { GetItemCommand } from '@aws-sdk/client-dynamodb'; +import { mockClient } from 'aws-sdk-client-mock'; +import { getClient } from '@/utils/dynamodb'; +import { get } from './get'; + +describe('technology.get()', () => { + let subject: Record | null; + + const ddbMock = mockClient(getClient()); + + beforeAll(async () => { + ddbMock.on(GetItemCommand).resolves({ + Item: { + description: { + S: 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + }, + id: { S: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff' }, + websiteUrl: { S: 'https://jestjs.io/' }, + displayName: { S: 'Jest' }, + }, + }); + subject = await get('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff'); + }); + + afterAll(() => { + ddbMock.restore(); + jest.resetAllMocks(); + }); + + it('returns the requested technology', () => { + expect(subject).toEqual({ + description: + 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + websiteUrl: 'https://jestjs.io/', + displayName: 'Jest', + }); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/models/technology/get.ts b/starters/serverless-framework-sqs-dynamodb/src/models/technology/get.ts new file mode 100644 index 000000000..d88c54b39 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/models/technology/get.ts @@ -0,0 +1,18 @@ +import { useCache } from '@/utils/cache'; +import { getItem } from '@/utils/dynamodb'; +import { TechnologySchema } from '@/types/technology'; +import { getCacheKey } from './getCacheKey'; +import { getErrorMessage } from '@/utils/error/getErrorMessage'; + +export const get = async (key: string) => { + return useCache({ + key: getCacheKey(key), + getFreshValue: async () => { + const item = await getItem(process.env.TECHNOLOGIES_TABLE, { id: key }); + return item; + }, + checkValue(value: unknown) { + TechnologySchema.parse(value); + }, + }); +}; diff --git a/starters/serverless-framework-sqs-dynamodb/src/models/technology/getAll.test.ts b/starters/serverless-framework-sqs-dynamodb/src/models/technology/getAll.test.ts new file mode 100644 index 000000000..cc248834b --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/models/technology/getAll.test.ts @@ -0,0 +1,70 @@ +import { ScanCommand } from '@aws-sdk/client-dynamodb'; +import { mockClient } from 'aws-sdk-client-mock'; +import { getClient } from '@/utils/dynamodb'; +import { getAll } from './getAll'; + +describe('technology.getAll()', () => { + let subject: Record[]; + + const ddbMock = mockClient(getClient()); + + afterAll(() => { + ddbMock.restore(); + jest.resetAllMocks(); + }); + + describe('when no items in table', () => { + beforeAll(async () => { + ddbMock.on(ScanCommand).resolves({ + Items: [ + { + description: { + S: 'Fully managed message queuing for microservices, distributed systems, and serverless applications', + }, + id: { S: 'ca9085fa-45a6-4d56-b6ee-8d6c59bebbfa' }, + websiteUrl: { S: 'https://aws.amazon.com/sqs/' }, + displayName: { S: 'AWS SQS' }, + }, + { + description: { + S: 'Fast, flexible NoSQL database service for single-digit millisecond performance at any scale', + }, + id: { S: 'dfb3378f-a991-4e45-8e23-59e27768f96f' }, + websiteUrl: { S: 'https://aws.amazon.com/dynamodb/' }, + displayName: { S: 'AWS DynamoDB' }, + }, + { + description: { + S: 'All-in-one development solution for auto-scaling apps on AWS Lambda', + }, + id: { S: '629202fd-00bc-46f2-9f67-3ddeb149b931' }, + websiteUrl: { S: 'https://www.serverless.com/' }, + displayName: { S: 'Serverless Framework' }, + }, + ], + }); + subject = await getAll(); + }); + + it('returns the requested technology', () => { + expect(subject).toHaveLength(3); + const actual = subject.map((tech) => tech.displayName); + expect(actual).toContain('AWS SQS'); + expect(actual).toContain('AWS DynamoDB'); + expect(actual).toContain('Serverless Framework'); + }); + }); + + describe('when items in table', () => { + beforeAll(async () => { + ddbMock.on(ScanCommand).resolves({ + Items: undefined, + }); + subject = await getAll(); + }); + + it('returns the requested technology', () => { + expect(subject).toHaveLength(0); + }); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/models/technology/getAll.ts b/starters/serverless-framework-sqs-dynamodb/src/models/technology/getAll.ts new file mode 100644 index 000000000..817b179a1 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/models/technology/getAll.ts @@ -0,0 +1,6 @@ +import { scan } from '@/utils/dynamodb'; + +export const getAll = async () => { + const items = await scan(process.env.TECHNOLOGIES_TABLE); + return items; +}; diff --git a/starters/serverless-framework-sqs-dynamodb/src/models/technology/getCacheKey.ts b/starters/serverless-framework-sqs-dynamodb/src/models/technology/getCacheKey.ts new file mode 100644 index 000000000..abbeb62c4 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/models/technology/getCacheKey.ts @@ -0,0 +1,3 @@ +export function getCacheKey(key: string) { + return `technology-${key}`; +} diff --git a/starters/serverless-framework-sqs-dynamodb/src/models/technology/index.ts b/starters/serverless-framework-sqs-dynamodb/src/models/technology/index.ts new file mode 100644 index 000000000..dd60c7a76 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/models/technology/index.ts @@ -0,0 +1,5 @@ +export { getAll } from './getAll'; +export { create } from './create'; +export { get } from './get'; +export { update } from './update'; +export { destroy } from './destroy'; diff --git a/starters/serverless-framework-sqs-dynamodb/src/models/technology/update.test.ts b/starters/serverless-framework-sqs-dynamodb/src/models/technology/update.test.ts new file mode 100644 index 000000000..e829a7a85 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/models/technology/update.test.ts @@ -0,0 +1,113 @@ +import { + GetItemCommand, + PutItemCommand, + ServiceInputTypes, + ServiceOutputTypes, +} from '@aws-sdk/client-dynamodb'; +import { AwsStub, mockClient } from 'aws-sdk-client-mock'; +import { getClient } from '@/utils/dynamodb'; +import { update } from './update'; + +describe('technology.update()', () => { + let subject: Record | null; + let ddbMock: AwsStub; + + beforeAll(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterAll(() => { + jest.resetAllMocks(); + }); + + describe('when no existing record', () => { + beforeAll(async () => { + ddbMock = mockClient(getClient()); + ddbMock.on(GetItemCommand).resolvesOnce({ + Item: undefined, + }); + subject = await update('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', { + displayName: 'Jest v29', + }); + }); + + afterAll(() => { + ddbMock.restore(); + }); + + it('returns null', () => { + expect(subject).toBeNull(); + }); + }); + + describe('when record updated successfully', () => { + beforeAll(async () => { + ddbMock = mockClient(getClient()); + ddbMock.on(GetItemCommand).resolvesOnce({ + Item: { + description: { + S: 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + }, + id: { S: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff' }, + websiteUrl: { S: 'https://jestjs.io/' }, + displayName: { S: 'Jest' }, + }, + }); + ddbMock.on(PutItemCommand).resolves({ + Attributes: { + description: { + S: 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + }, + id: { S: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff' }, + websiteUrl: { S: 'https://jestjs.io/' }, + displayName: { S: 'Jest v29' }, + }, + }); + subject = await update('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', { + displayName: 'Jest v29', + }); + }); + + afterAll(() => { + ddbMock.restore(); + }); + + it('returns the updated record', () => { + expect(subject).toEqual({ + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + displayName: 'Jest v29', + description: + 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + websiteUrl: 'https://jestjs.io/', + }); + }); + }); + + describe('when record update fails', () => { + beforeAll(async () => { + ddbMock = mockClient(getClient()); + ddbMock.on(GetItemCommand).resolvesOnce({ + Item: { + description: { + S: 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + }, + id: { S: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff' }, + websiteUrl: { S: 'https://jestjs.io/' }, + displayName: { S: 'Jest' }, + }, + }); + ddbMock.on(PutItemCommand).rejects('mock error'); + subject = await update('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', { + displayName: 'Jest v29', + }); + }); + + afterAll(() => { + ddbMock.restore(); + }); + + it('returns null', () => { + expect(subject).toBeNull(); + }); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/models/technology/update.ts b/starters/serverless-framework-sqs-dynamodb/src/models/technology/update.ts new file mode 100644 index 000000000..b017474a1 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/models/technology/update.ts @@ -0,0 +1,24 @@ +import { TechnologyUpdate } from '@/types/technology'; +import { addToCache } from '@/utils/cache/addToCache'; +import { putItem } from '@/utils/dynamodb'; +import { get } from './get'; + +export const update = async (id: string, payload: TechnologyUpdate) => { + const existingTechnology = await get(id); + if (!existingTechnology) { + return null; + } + + const updatedTechnology = { + id, + ...existingTechnology, + ...payload, + }; + + const response = await putItem(process.env.TECHNOLOGIES_TABLE, updatedTechnology); + if (!response) { + return null; + } + await addToCache(id, updatedTechnology); + return updatedTechnology; +}; diff --git a/starters/serverless-framework-sqs-dynamodb/src/types/technology.ts b/starters/serverless-framework-sqs-dynamodb/src/types/technology.ts index c90b6ef82..61322b184 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/types/technology.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/types/technology.ts @@ -7,6 +7,13 @@ export type Technology = { websiteUrl: string; }; +export const TechnologySchema = z.object({ + id: z.string().uuid(), + displayName: z.string(), + description: z.string().min(10), + websiteUrl: z.string().url(), +}); + export const TechnologyCreateSchema = z.object({ displayName: z.string(), description: z.string().min(10), diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/index.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/index.ts index a88344985..502145fd0 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/index.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/index.ts @@ -1,2 +1,3 @@ export { useCache } from './useCache'; +export { addToCache } from './addToCache'; export { removeFromCache } from './removeFromCache'; diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/useCache.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/useCache.test.ts index 6ff012a70..de3084e01 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/useCache.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/useCache.test.ts @@ -1,7 +1,29 @@ import { useCache } from './useCache'; describe('cache.useCache()', () => { - let subject: string; + let subject: string | null; + + describe('when fails checkValue', () => { + beforeAll(async () => { + jest.resetModules(); + jest.spyOn(console, 'error').mockImplementation(() => {}); + subject = await useCache({ + key: 'test-with-ttl', + getFreshValue() { + return 'test-value'; + }, + checkValue(value: unknown) { + if (typeof value === 'string') { + return value.startsWith('tests-'); + } + }, + }); + }); + + it('returns null', () => { + expect(subject).toBeNull(); + }); + }); describe('when using default ttl', () => { beforeAll(async () => { diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/useCache.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/useCache.ts index f3691d042..45d317c58 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/useCache.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/useCache.ts @@ -1,17 +1,33 @@ import { cachified, CachifiedOptions } from 'cachified'; +import { getErrorMessage } from '../error/getErrorMessage'; import { DEFAULT_CACHE_TIME } from './constants'; import { getClient } from './getClient'; +import { removeFromCache } from './removeFromCache'; export async function useCache({ key, getFreshValue, ttl = DEFAULT_CACHE_TIME, -}: Pick, 'key' | 'getFreshValue' | 'ttl'>) { - const cache = await getClient(); - return cachified({ - cache, - key, - getFreshValue, - ttl, - }); + checkValue = undefined, +}: Omit, 'cache'>) { + try { + const cache = await getClient(); + const cachifiedOptions: CachifiedOptions = { + cache, + key, + getFreshValue, + ttl, + }; + + if (checkValue) { + cachifiedOptions.checkValue = checkValue; + } + + const cacheValue = await cachified(cachifiedOptions); + return cacheValue; + } catch (err) { + await removeFromCache(key); + console.error(getErrorMessage(err)); + return null; + } } diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/index.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/index.ts index 3657d8734..4b5d7df77 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/index.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/index.ts @@ -1,3 +1,4 @@ +export { getClient } from './getClient'; export { scan } from './scan'; export { getItem } from './getItem'; export { putItem } from './putItem'; diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.test.ts index 5c822060f..1b5f20915 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.test.ts @@ -92,7 +92,7 @@ describe('dynamodb.scan()', () => { }); it('returns null', () => { - expect(subject).toBeNull(); + expect(subject).toEqual([]); }); it('logs the error', () => { diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.ts index 894bf8790..3d6b61102 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.ts @@ -17,6 +17,6 @@ export const scan = async (tableName: string) => { return response.Items.map((item) => unmarshall(item)); } catch (err) { console.error(`dynamodb.scan Error - ${getErrorMessage(err)}`); - return null; + return []; } }; From 0f1c5aed303e570cf8e07698927e3c93a589c197 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sat, 31 Dec 2022 00:00:32 -0600 Subject: [PATCH 41/62] add technology api handlers --- .../package.json | 1 + .../src/handlers/technology.test.ts | 568 ++++++++++++++++++ .../src/handlers/technology.ts | 109 ++++ .../src/models/technology/index.ts | 1 + 4 files changed, 679 insertions(+) create mode 100644 starters/serverless-framework-sqs-dynamodb/src/handlers/technology.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/handlers/technology.ts diff --git a/starters/serverless-framework-sqs-dynamodb/package.json b/starters/serverless-framework-sqs-dynamodb/package.json index 281a28275..0bee5e225 100644 --- a/starters/serverless-framework-sqs-dynamodb/package.json +++ b/starters/serverless-framework-sqs-dynamodb/package.json @@ -27,6 +27,7 @@ "@aws-sdk/client-dynamodb": "^3.236.0", "@aws-sdk/util-dynamodb": "^3.238.0", "cachified": "^3.0.1", + "http-status-codes": "^2.2.0", "ioredis": "^5.2.4", "ioredis-mock": "^8.2.2", "uuid": "^9.0.0", diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/technology.test.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology.test.ts new file mode 100644 index 000000000..eebb02f1f --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology.test.ts @@ -0,0 +1,568 @@ +import { APIGatewayProxyResult, APIGatewayProxyEvent, Context, Callback } from 'aws-lambda'; +import { + ScanCommand, + GetItemCommand, + PutItemCommand, + DeleteItemCommand, + ServiceInputTypes, + ServiceOutputTypes, +} from '@aws-sdk/client-dynamodb'; +import { AwsStub, mockClient } from 'aws-sdk-client-mock'; +import { getClient } from '@/utils/dynamodb'; +import { removeFromCache } from '@/utils/cache'; +import * as technology from '@/models/technology'; +import { getCacheKey } from '@/models/technology'; +import { index, create, show, update, destroy } from './technology'; + +describe('GET /technology', () => { + let subject: APIGatewayProxyResult; + let ddbMock: AwsStub; + + beforeAll(async () => { + ddbMock = mockClient(getClient()); + ddbMock.on(ScanCommand).resolves({ + Items: [], + }); + subject = (await index( + {} as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(() => { + ddbMock.restore(); + }); + + it('returns 200 status', () => { + expect(subject.statusCode).toEqual(200); + }); + + it('returns a list of technologies', () => { + expect(JSON.parse(subject.body)).toEqual([]); + }); +}); + +describe('POST /technology', () => { + let subject: APIGatewayProxyResult; + let ddbMock: AwsStub; + + describe('when record is created', () => { + beforeAll(async () => { + ddbMock = mockClient(getClient()); + ddbMock.on(PutItemCommand).resolves({ + Attributes: undefined, + }); + subject = (await create( + { + body: JSON.stringify({ + description: + 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + websiteUrl: 'https://jestjs.io/', + displayName: 'Valid Test', + }), + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(async () => { + ddbMock.restore(); + await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); + }); + + it('returns 201 status', () => { + expect(subject.statusCode).toEqual(201); + }); + + it('returns the created record', () => { + expect(JSON.parse(subject.body)).toEqual({ + description: + 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + id: expect.any(String), + websiteUrl: 'https://jestjs.io/', + displayName: 'Valid Test', + }); + }); + }); + + describe('when invalid inputs', () => { + beforeAll(async () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + subject = (await create( + {} as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(async () => { + jest.restoreAllMocks(); + await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); + }); + + it('returns 400 status', () => { + expect(subject.statusCode).toEqual(400); + }); + + it('returns form error', () => { + const parsedResp = JSON.parse(subject.body); + expect(parsedResp).toHaveProperty('formErrors'); + expect(parsedResp).toHaveProperty('fieldErrors'); + }); + }); + + describe('when error occurs', () => { + beforeAll(async () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(technology, 'create').mockRejectedValue('fail'); + subject = (await create( + { + body: JSON.stringify({ + description: + 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + websiteUrl: 'https://jestjs.io/', + displayName: 'Valid Test', + }), + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(async () => { + jest.restoreAllMocks(); + await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); + }); + + it('returns 500 status', () => { + expect(subject.statusCode).toEqual(500); + }); + + it('returns server error message', () => { + expect(subject.body).toEqual(expect.stringContaining('Server Error:')); + }); + }); +}); + +describe('GET /technology/:id', () => { + let subject: APIGatewayProxyResult; + let ddbMock: AwsStub; + + beforeAll(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + describe('when no id provided', () => { + beforeAll(async () => { + subject = (await show( + { + pathParameters: { + id: null, + }, + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + it('returns 400 status', () => { + expect(subject.statusCode).toEqual(400); + }); + + it('returns bad request message', () => { + expect(subject.body).toEqual('Bad Request: no id provided'); + }); + }); + + describe('when record not found', () => { + beforeAll(async () => { + ddbMock = mockClient(getClient()); + ddbMock.on(GetItemCommand).resolves({ + Item: undefined, + }); + subject = (await show( + { + pathParameters: { + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + }, + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(async () => { + ddbMock.restore(); + await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); + }); + + it('returns 404 status', () => { + expect(subject.statusCode).toEqual(404); + }); + + it('returns null', () => { + expect(JSON.parse(subject.body)).toBeNull(); + }); + }); + + describe('when record is found', () => { + beforeAll(async () => { + ddbMock = mockClient(getClient()); + ddbMock.on(GetItemCommand).resolves({ + Item: { + description: { + S: 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + }, + id: { S: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff' }, + websiteUrl: { S: 'https://jestjs.io/' }, + displayName: { S: 'Jest' }, + }, + }); + subject = (await show( + { + pathParameters: { + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + }, + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(async () => { + ddbMock.restore(); + await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); + }); + + it('returns 200 status', () => { + expect(subject.statusCode).toEqual(200); + }); + + it('returns the requested record', () => { + expect(JSON.parse(subject.body)).toEqual({ + description: + 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + websiteUrl: 'https://jestjs.io/', + displayName: 'Jest', + }); + }); + }); + + describe('when error occurs', () => { + beforeAll(async () => { + jest.spyOn(technology, 'get').mockRejectedValue('fail'); + subject = (await show( + { + pathParameters: { + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + }, + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(async () => { + jest.restoreAllMocks(); + await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); + }); + + it('returns 500 status', () => { + expect(subject.statusCode).toEqual(500); + }); + + it('returns server error message', () => { + expect(subject.body).toEqual(expect.stringContaining('Server Error:')); + }); + }); +}); + +describe('PUT /technology/:id', () => { + let subject: APIGatewayProxyResult; + let ddbMock: AwsStub; + + describe('when no id provided', () => { + beforeAll(async () => { + subject = (await update( + { + pathParameters: { + id: null, + }, + body: JSON.stringify({ + displayName: 'Valid Test', + }), + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + it('returns 400 status', () => { + expect(subject.statusCode).toEqual(400); + }); + + it('returns bad request message', () => { + expect(subject.body).toEqual('Bad Request: no id provided'); + }); + }); + + describe('when record not found', () => { + beforeAll(async () => { + ddbMock = mockClient(getClient()); + ddbMock.on(GetItemCommand).resolves({ + Item: undefined, + }); + jest.spyOn(console, 'error').mockImplementation(() => {}); + subject = (await update( + { + pathParameters: { + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + }, + body: JSON.stringify({ + displayName: 'Valid Test', + }), + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(async () => { + ddbMock.restore(); + await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); + jest.resetAllMocks(); + }); + + it('returns 404 status', () => { + expect(subject.statusCode).toEqual(404); + }); + + it('returns null', () => { + expect(JSON.parse(subject.body)).toBeNull(); + }); + }); + + describe('when record is found', () => { + beforeAll(async () => { + ddbMock = mockClient(getClient()); + ddbMock.on(GetItemCommand).resolvesOnce({ + Item: { + description: { + S: 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + }, + id: { S: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff' }, + websiteUrl: { S: 'https://jestjs.io/' }, + displayName: { S: 'Jest' }, + }, + }); + ddbMock.on(PutItemCommand).resolves({ + Attributes: { + description: { + S: 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + }, + id: { S: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff' }, + websiteUrl: { S: 'https://jestjs.io/' }, + displayName: { S: 'Valid Test' }, + }, + }); + subject = (await update( + { + pathParameters: { + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + }, + body: JSON.stringify({ + displayName: 'Valid Test', + }), + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(async () => { + ddbMock.restore(); + await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); + }); + + it('returns 200 status', () => { + expect(subject.statusCode).toEqual(200); + }); + + it('returns the updated record', () => { + expect(JSON.parse(subject.body)).toEqual({ + description: + 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + websiteUrl: 'https://jestjs.io/', + displayName: 'Valid Test', + }); + }); + }); + + describe('when invalid inputs', () => { + beforeAll(async () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + subject = (await update( + { + pathParameters: { + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + }, + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(async () => { + jest.restoreAllMocks(); + await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); + }); + + it('returns 400 status', () => { + expect(subject.statusCode).toEqual(400); + }); + + it('returns form error', () => { + const parsedResp = JSON.parse(subject.body); + expect(parsedResp).toHaveProperty('formErrors'); + expect(parsedResp).toHaveProperty('fieldErrors'); + }); + }); + + describe('when error occurs', () => { + beforeAll(async () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(technology, 'update').mockRejectedValue('fail'); + subject = (await update( + { + pathParameters: { + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + }, + body: JSON.stringify({ + displayName: 'Error Test', + }), + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(async () => { + jest.restoreAllMocks(); + await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); + }); + + it('returns 500 status', () => { + expect(subject.statusCode).toEqual(500); + }); + + it('returns server error message', () => { + expect(subject.body).toEqual(expect.stringContaining('Server Error:')); + }); + }); +}); + +describe('DELETE /technology/:id', () => { + let subject: APIGatewayProxyResult; + let ddbMock: AwsStub; + + describe('when no id provided', () => { + beforeAll(async () => { + subject = (await destroy( + { + pathParameters: { + id: null, + }, + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + it('returns 400 status', () => { + expect(subject.statusCode).toEqual(400); + }); + + it('returns bad request message', () => { + expect(subject.body).toEqual('Bad Request: no id provided'); + }); + }); + + describe('when record maybe deleted', () => { + beforeAll(async () => { + ddbMock = mockClient(getClient()); + ddbMock.on(DeleteItemCommand).resolves({ + Attributes: { + description: { + S: 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + }, + id: { S: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff' }, + websiteUrl: { S: 'https://jestjs.io/' }, + displayName: { S: 'Jest' }, + }, + }); + subject = (await destroy( + { + pathParameters: { + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + }, + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(async () => { + ddbMock.restore(); + }); + + it('returns 200 status', () => { + expect(subject.statusCode).toEqual(200); + }); + + it('returns deleted object if it exists', () => { + expect(JSON.parse(subject.body)).toEqual({ + description: + 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + websiteUrl: 'https://jestjs.io/', + displayName: 'Jest', + }); + }); + }); + + describe('when error occurs', () => { + beforeAll(async () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(technology, 'destroy').mockRejectedValue('fail'); + subject = (await destroy( + { + pathParameters: { + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + }, + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(async () => { + jest.restoreAllMocks(); + }); + + it('returns 500 status', () => { + expect(subject.statusCode).toEqual(500); + }); + + it('returns server error message', () => { + expect(subject.body).toEqual(expect.stringContaining('Server Error:')); + }); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/technology.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology.ts new file mode 100644 index 000000000..21dfa7c9b --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology.ts @@ -0,0 +1,109 @@ +import type { APIGatewayProxyHandler } from 'aws-lambda'; +import { ZodError } from 'zod'; +import { TechnologyCreateSchema, TechnologyUpdateSchema } from '@/types/technology'; +import { + getAll as getAllTechnology, + create as createTechnology, + get as getTechnology, + update as updateTechnology, + destroy as destroyTechnology, +} from '@/models/technology'; +import { getErrorMessage } from '@/utils/error'; +import { StatusCodes } from 'http-status-codes'; + +export const index: APIGatewayProxyHandler = async () => { + const technologies = await getAllTechnology(); + return responseHelper(StatusCodes.OK, technologies); +}; + +export const create: APIGatewayProxyHandler = async (event) => { + try { + const payload = event.body ? JSON.parse(event.body) : null; + TechnologyCreateSchema.parse(payload); + const technology = await createTechnology(payload); + + return responseHelper(StatusCodes.CREATED, technology); + } catch (err) { + console.error(err); + if (err instanceof ZodError) { + return responseHelper(StatusCodes.BAD_REQUEST, err.flatten()); + } + return responseHelper( + StatusCodes.INTERNAL_SERVER_ERROR, + `Server Error: ${getErrorMessage(err)}` + ); + } +}; + +export const show: APIGatewayProxyHandler = async (event) => { + const id = event.pathParameters?.id; + if (!id) { + return responseHelper(StatusCodes.BAD_REQUEST, 'Bad Request: no id provided'); + } + + try { + const technology = await getTechnology(id); + if (!technology) { + return responseHelper(StatusCodes.NOT_FOUND, null); + } + return responseHelper(StatusCodes.OK, technology); + } catch (err) { + console.error(err); + return responseHelper( + StatusCodes.INTERNAL_SERVER_ERROR, + `Server Error: ${getErrorMessage(err)}` + ); + } +}; + +export const update: APIGatewayProxyHandler = async (event) => { + const id = event.pathParameters?.id; + if (!id) { + return responseHelper(StatusCodes.BAD_REQUEST, 'Bad Request: no id provided'); + } + + try { + const payload = event.body ? JSON.parse(event.body) : null; + TechnologyUpdateSchema.parse(payload); + const technology = await updateTechnology(id, payload); + if (!technology) { + return responseHelper(StatusCodes.NOT_FOUND, null); + } + + return responseHelper(StatusCodes.OK, technology); + } catch (err) { + console.error(err); + if (err instanceof ZodError) { + return responseHelper(StatusCodes.BAD_REQUEST, err.flatten()); + } + return responseHelper( + StatusCodes.INTERNAL_SERVER_ERROR, + `Server Error: ${getErrorMessage(err)}` + ); + } +}; + +export const destroy: APIGatewayProxyHandler = async (event) => { + const id = event.pathParameters?.id; + if (!id) { + return responseHelper(StatusCodes.BAD_REQUEST, 'Bad Request: no id provided'); + } + + try { + const technology = await destroyTechnology(id); + return responseHelper(StatusCodes.OK, technology); + } catch (err) { + console.error(err); + return responseHelper( + StatusCodes.INTERNAL_SERVER_ERROR, + `Server Error: ${getErrorMessage(err)}` + ); + } +}; + +function responseHelper(statusCode: number, resp: unknown) { + return { + statusCode, + body: typeof resp === 'string' ? resp : JSON.stringify(resp), + }; +} diff --git a/starters/serverless-framework-sqs-dynamodb/src/models/technology/index.ts b/starters/serverless-framework-sqs-dynamodb/src/models/technology/index.ts index dd60c7a76..6b9229a8a 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/models/technology/index.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/models/technology/index.ts @@ -1,3 +1,4 @@ +export { getCacheKey } from './getCacheKey'; export { getAll } from './getAll'; export { create } from './create'; export { get } from './get'; From 8536b17d7cd3ea6b9df7469418dc30db81d39bf5 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sat, 31 Dec 2022 21:09:46 -0600 Subject: [PATCH 42/62] fix errors in eslint config --- starters/serverless-framework-sqs-dynamodb/.eslintrc.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/starters/serverless-framework-sqs-dynamodb/.eslintrc.js b/starters/serverless-framework-sqs-dynamodb/.eslintrc.js index 4677dedac..532b14f77 100644 --- a/starters/serverless-framework-sqs-dynamodb/.eslintrc.js +++ b/starters/serverless-framework-sqs-dynamodb/.eslintrc.js @@ -105,6 +105,7 @@ module.exports = { '**/*.test.ts', 'prettier.config.js', 'jest.config.ts', + 'jest.setup.ts', 'esbuild-plugins.ts', ], }, @@ -112,10 +113,10 @@ module.exports = { 'import/no-mutable-exports': 'error', 'import/no-named-as-default': 'error', 'import/no-named-as-default-member': 'error', - 'import/no-unresolved': ['error', { commonjs: true }], + 'import/no-unresolved': ['error', { commonjs: false }], // typescript settings - '@typescript-eslint/no-empty-function': false, + '@typescript-eslint/no-empty-function': 'off', }, settings: { 'import/resolver': { From 8e2467c03252cf2dab04ce78ab524bba9624be8a Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sat, 31 Dec 2022 21:11:19 -0600 Subject: [PATCH 43/62] fix lint and bundle optimizations --- .../jest.setup.ts | 1 + .../serverless.ts | 10 +- .../src/handlers/technology.test.ts | 568 ------------------ .../src/handlers/technology.ts | 109 ---- .../src/handlers/technology_create.test.ts | 111 ++++ .../src/handlers/technology_create.ts | 26 + .../src/handlers/technology_destroy.test.ts | 104 ++++ .../src/handlers/technology_destroy.ts | 23 + .../src/handlers/technology_index.test.ts | 34 ++ .../src/handlers/technology_index.ts | 9 + .../src/handlers/technology_show.test.ts | 146 +++++ .../src/handlers/technology_show.ts | 26 + .../src/handlers/technology_update.test.ts | 198 ++++++ .../src/handlers/technology_update.ts | 34 ++ .../src/models/technology/create.test.ts | 4 +- .../src/models/technology/create.ts | 4 +- .../src/models/technology/destroy.test.ts | 4 +- .../src/models/technology/destroy.ts | 4 +- .../src/models/technology/get.test.ts | 4 +- .../src/models/technology/get.ts | 5 +- .../src/models/technology/getAll.test.ts | 4 +- .../src/models/technology/getAll.ts | 2 +- .../src/models/technology/index.ts | 6 - .../src/models/technology/update.test.ts | 4 +- .../src/models/technology/update.ts | 2 +- .../src/utils/cache/addToCache.test.ts | 2 +- .../src/utils/cache/getClient.test.ts | 4 +- .../src/utils/cache/getClient.ts | 2 +- .../src/utils/cache/index.ts | 3 - .../src/utils/cache/removeFromCache.test.ts | 2 +- .../src/utils/cache/useCache.test.ts | 3 +- .../src/utils/cache/useCache.ts | 2 +- .../src/utils/dynamodb/deleteItem.test.ts | 2 +- .../src/utils/dynamodb/deleteItem.ts | 2 +- .../src/utils/dynamodb/getClient.ts | 2 +- .../src/utils/dynamodb/getItem.test.ts | 2 +- .../src/utils/dynamodb/getItem.ts | 2 +- .../src/utils/dynamodb/index.ts | 5 - .../src/utils/dynamodb/putItem.test.ts | 2 +- .../src/utils/dynamodb/putItem.ts | 2 +- .../src/utils/dynamodb/scan.test.ts | 2 +- .../src/utils/dynamodb/scan.ts | 2 +- .../src/utils/error/index.ts | 1 - .../src/utils/is-offline/index.ts | 1 - .../isOffline.test.ts} | 2 +- .../is-offline.ts => isOffline/isOffline.ts} | 0 .../src/utils/redis/getClient.ts | 2 +- .../src/utils/redis/index.ts | 1 - .../responseHelper/responseHelper.test.ts | 35 ++ .../utils/responseHelper/responseHelper.ts | 6 + 50 files changed, 794 insertions(+), 737 deletions(-) delete mode 100644 starters/serverless-framework-sqs-dynamodb/src/handlers/technology.test.ts delete mode 100644 starters/serverless-framework-sqs-dynamodb/src/handlers/technology.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/handlers/technology_create.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/handlers/technology_create.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/handlers/technology_destroy.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/handlers/technology_destroy.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/handlers/technology_index.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/handlers/technology_index.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/handlers/technology_show.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/handlers/technology_show.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/handlers/technology_update.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/handlers/technology_update.ts delete mode 100644 starters/serverless-framework-sqs-dynamodb/src/models/technology/index.ts delete mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/cache/index.ts delete mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/index.ts delete mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/error/index.ts delete mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/is-offline/index.ts rename starters/serverless-framework-sqs-dynamodb/src/utils/{is-offline/is-offline.test.ts => isOffline/isOffline.test.ts} (92%) rename starters/serverless-framework-sqs-dynamodb/src/utils/{is-offline/is-offline.ts => isOffline/isOffline.ts} (100%) delete mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/redis/index.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/responseHelper/responseHelper.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/responseHelper/responseHelper.ts diff --git a/starters/serverless-framework-sqs-dynamodb/jest.setup.ts b/starters/serverless-framework-sqs-dynamodb/jest.setup.ts index 8531e333b..6ac0123f2 100644 --- a/starters/serverless-framework-sqs-dynamodb/jest.setup.ts +++ b/starters/serverless-framework-sqs-dynamodb/jest.setup.ts @@ -1,4 +1,5 @@ import * as dotenv from 'dotenv'; + dotenv.config(); jest.mock('ioredis', () => require('ioredis-mock')); diff --git a/starters/serverless-framework-sqs-dynamodb/serverless.ts b/starters/serverless-framework-sqs-dynamodb/serverless.ts index 5f72e1799..17fe9383d 100644 --- a/starters/serverless-framework-sqs-dynamodb/serverless.ts +++ b/starters/serverless-framework-sqs-dynamodb/serverless.ts @@ -126,7 +126,7 @@ const serverlessConfiguration: AWS = { ], }, technology_index: { - handler: 'src/handlers/technology.index', + handler: 'src/handlers/technology_index.handler', events: [ { httpApi: { @@ -137,7 +137,7 @@ const serverlessConfiguration: AWS = { ], }, technology_create: { - handler: 'src/handlers/technology.create', + handler: 'src/handlers/technology_create.handler', events: [ { httpApi: { @@ -148,7 +148,7 @@ const serverlessConfiguration: AWS = { ], }, technology_show: { - handler: 'src/handlers/technology.show', + handler: 'src/handlers/technology_show.handler', events: [ { httpApi: { @@ -159,7 +159,7 @@ const serverlessConfiguration: AWS = { ], }, technology_update: { - handler: 'src/handlers/technology.update', + handler: 'src/handlers/technology_update.handler', events: [ { httpApi: { @@ -170,7 +170,7 @@ const serverlessConfiguration: AWS = { ], }, technology_destroy: { - handler: 'src/handlers/technology.destroy', + handler: 'src/handlers/technology_destroy.handler', events: [ { httpApi: { diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/technology.test.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology.test.ts deleted file mode 100644 index eebb02f1f..000000000 --- a/starters/serverless-framework-sqs-dynamodb/src/handlers/technology.test.ts +++ /dev/null @@ -1,568 +0,0 @@ -import { APIGatewayProxyResult, APIGatewayProxyEvent, Context, Callback } from 'aws-lambda'; -import { - ScanCommand, - GetItemCommand, - PutItemCommand, - DeleteItemCommand, - ServiceInputTypes, - ServiceOutputTypes, -} from '@aws-sdk/client-dynamodb'; -import { AwsStub, mockClient } from 'aws-sdk-client-mock'; -import { getClient } from '@/utils/dynamodb'; -import { removeFromCache } from '@/utils/cache'; -import * as technology from '@/models/technology'; -import { getCacheKey } from '@/models/technology'; -import { index, create, show, update, destroy } from './technology'; - -describe('GET /technology', () => { - let subject: APIGatewayProxyResult; - let ddbMock: AwsStub; - - beforeAll(async () => { - ddbMock = mockClient(getClient()); - ddbMock.on(ScanCommand).resolves({ - Items: [], - }); - subject = (await index( - {} as APIGatewayProxyEvent, - {} as Context, - {} as Callback - )) as APIGatewayProxyResult; - }); - - afterAll(() => { - ddbMock.restore(); - }); - - it('returns 200 status', () => { - expect(subject.statusCode).toEqual(200); - }); - - it('returns a list of technologies', () => { - expect(JSON.parse(subject.body)).toEqual([]); - }); -}); - -describe('POST /technology', () => { - let subject: APIGatewayProxyResult; - let ddbMock: AwsStub; - - describe('when record is created', () => { - beforeAll(async () => { - ddbMock = mockClient(getClient()); - ddbMock.on(PutItemCommand).resolves({ - Attributes: undefined, - }); - subject = (await create( - { - body: JSON.stringify({ - description: - 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', - websiteUrl: 'https://jestjs.io/', - displayName: 'Valid Test', - }), - } as unknown as APIGatewayProxyEvent, - {} as Context, - {} as Callback - )) as APIGatewayProxyResult; - }); - - afterAll(async () => { - ddbMock.restore(); - await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); - }); - - it('returns 201 status', () => { - expect(subject.statusCode).toEqual(201); - }); - - it('returns the created record', () => { - expect(JSON.parse(subject.body)).toEqual({ - description: - 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', - id: expect.any(String), - websiteUrl: 'https://jestjs.io/', - displayName: 'Valid Test', - }); - }); - }); - - describe('when invalid inputs', () => { - beforeAll(async () => { - jest.spyOn(console, 'error').mockImplementation(() => {}); - subject = (await create( - {} as unknown as APIGatewayProxyEvent, - {} as Context, - {} as Callback - )) as APIGatewayProxyResult; - }); - - afterAll(async () => { - jest.restoreAllMocks(); - await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); - }); - - it('returns 400 status', () => { - expect(subject.statusCode).toEqual(400); - }); - - it('returns form error', () => { - const parsedResp = JSON.parse(subject.body); - expect(parsedResp).toHaveProperty('formErrors'); - expect(parsedResp).toHaveProperty('fieldErrors'); - }); - }); - - describe('when error occurs', () => { - beforeAll(async () => { - jest.spyOn(console, 'error').mockImplementation(() => {}); - jest.spyOn(technology, 'create').mockRejectedValue('fail'); - subject = (await create( - { - body: JSON.stringify({ - description: - 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', - websiteUrl: 'https://jestjs.io/', - displayName: 'Valid Test', - }), - } as unknown as APIGatewayProxyEvent, - {} as Context, - {} as Callback - )) as APIGatewayProxyResult; - }); - - afterAll(async () => { - jest.restoreAllMocks(); - await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); - }); - - it('returns 500 status', () => { - expect(subject.statusCode).toEqual(500); - }); - - it('returns server error message', () => { - expect(subject.body).toEqual(expect.stringContaining('Server Error:')); - }); - }); -}); - -describe('GET /technology/:id', () => { - let subject: APIGatewayProxyResult; - let ddbMock: AwsStub; - - beforeAll(() => { - jest.spyOn(console, 'error').mockImplementation(() => {}); - }); - - afterAll(() => { - jest.restoreAllMocks(); - }); - - describe('when no id provided', () => { - beforeAll(async () => { - subject = (await show( - { - pathParameters: { - id: null, - }, - } as unknown as APIGatewayProxyEvent, - {} as Context, - {} as Callback - )) as APIGatewayProxyResult; - }); - - it('returns 400 status', () => { - expect(subject.statusCode).toEqual(400); - }); - - it('returns bad request message', () => { - expect(subject.body).toEqual('Bad Request: no id provided'); - }); - }); - - describe('when record not found', () => { - beforeAll(async () => { - ddbMock = mockClient(getClient()); - ddbMock.on(GetItemCommand).resolves({ - Item: undefined, - }); - subject = (await show( - { - pathParameters: { - id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', - }, - } as unknown as APIGatewayProxyEvent, - {} as Context, - {} as Callback - )) as APIGatewayProxyResult; - }); - - afterAll(async () => { - ddbMock.restore(); - await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); - }); - - it('returns 404 status', () => { - expect(subject.statusCode).toEqual(404); - }); - - it('returns null', () => { - expect(JSON.parse(subject.body)).toBeNull(); - }); - }); - - describe('when record is found', () => { - beforeAll(async () => { - ddbMock = mockClient(getClient()); - ddbMock.on(GetItemCommand).resolves({ - Item: { - description: { - S: 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', - }, - id: { S: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff' }, - websiteUrl: { S: 'https://jestjs.io/' }, - displayName: { S: 'Jest' }, - }, - }); - subject = (await show( - { - pathParameters: { - id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', - }, - } as unknown as APIGatewayProxyEvent, - {} as Context, - {} as Callback - )) as APIGatewayProxyResult; - }); - - afterAll(async () => { - ddbMock.restore(); - await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); - }); - - it('returns 200 status', () => { - expect(subject.statusCode).toEqual(200); - }); - - it('returns the requested record', () => { - expect(JSON.parse(subject.body)).toEqual({ - description: - 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', - id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', - websiteUrl: 'https://jestjs.io/', - displayName: 'Jest', - }); - }); - }); - - describe('when error occurs', () => { - beforeAll(async () => { - jest.spyOn(technology, 'get').mockRejectedValue('fail'); - subject = (await show( - { - pathParameters: { - id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', - }, - } as unknown as APIGatewayProxyEvent, - {} as Context, - {} as Callback - )) as APIGatewayProxyResult; - }); - - afterAll(async () => { - jest.restoreAllMocks(); - await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); - }); - - it('returns 500 status', () => { - expect(subject.statusCode).toEqual(500); - }); - - it('returns server error message', () => { - expect(subject.body).toEqual(expect.stringContaining('Server Error:')); - }); - }); -}); - -describe('PUT /technology/:id', () => { - let subject: APIGatewayProxyResult; - let ddbMock: AwsStub; - - describe('when no id provided', () => { - beforeAll(async () => { - subject = (await update( - { - pathParameters: { - id: null, - }, - body: JSON.stringify({ - displayName: 'Valid Test', - }), - } as unknown as APIGatewayProxyEvent, - {} as Context, - {} as Callback - )) as APIGatewayProxyResult; - }); - - it('returns 400 status', () => { - expect(subject.statusCode).toEqual(400); - }); - - it('returns bad request message', () => { - expect(subject.body).toEqual('Bad Request: no id provided'); - }); - }); - - describe('when record not found', () => { - beforeAll(async () => { - ddbMock = mockClient(getClient()); - ddbMock.on(GetItemCommand).resolves({ - Item: undefined, - }); - jest.spyOn(console, 'error').mockImplementation(() => {}); - subject = (await update( - { - pathParameters: { - id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', - }, - body: JSON.stringify({ - displayName: 'Valid Test', - }), - } as unknown as APIGatewayProxyEvent, - {} as Context, - {} as Callback - )) as APIGatewayProxyResult; - }); - - afterAll(async () => { - ddbMock.restore(); - await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); - jest.resetAllMocks(); - }); - - it('returns 404 status', () => { - expect(subject.statusCode).toEqual(404); - }); - - it('returns null', () => { - expect(JSON.parse(subject.body)).toBeNull(); - }); - }); - - describe('when record is found', () => { - beforeAll(async () => { - ddbMock = mockClient(getClient()); - ddbMock.on(GetItemCommand).resolvesOnce({ - Item: { - description: { - S: 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', - }, - id: { S: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff' }, - websiteUrl: { S: 'https://jestjs.io/' }, - displayName: { S: 'Jest' }, - }, - }); - ddbMock.on(PutItemCommand).resolves({ - Attributes: { - description: { - S: 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', - }, - id: { S: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff' }, - websiteUrl: { S: 'https://jestjs.io/' }, - displayName: { S: 'Valid Test' }, - }, - }); - subject = (await update( - { - pathParameters: { - id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', - }, - body: JSON.stringify({ - displayName: 'Valid Test', - }), - } as unknown as APIGatewayProxyEvent, - {} as Context, - {} as Callback - )) as APIGatewayProxyResult; - }); - - afterAll(async () => { - ddbMock.restore(); - await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); - }); - - it('returns 200 status', () => { - expect(subject.statusCode).toEqual(200); - }); - - it('returns the updated record', () => { - expect(JSON.parse(subject.body)).toEqual({ - description: - 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', - id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', - websiteUrl: 'https://jestjs.io/', - displayName: 'Valid Test', - }); - }); - }); - - describe('when invalid inputs', () => { - beforeAll(async () => { - jest.spyOn(console, 'error').mockImplementation(() => {}); - subject = (await update( - { - pathParameters: { - id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', - }, - } as unknown as APIGatewayProxyEvent, - {} as Context, - {} as Callback - )) as APIGatewayProxyResult; - }); - - afterAll(async () => { - jest.restoreAllMocks(); - await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); - }); - - it('returns 400 status', () => { - expect(subject.statusCode).toEqual(400); - }); - - it('returns form error', () => { - const parsedResp = JSON.parse(subject.body); - expect(parsedResp).toHaveProperty('formErrors'); - expect(parsedResp).toHaveProperty('fieldErrors'); - }); - }); - - describe('when error occurs', () => { - beforeAll(async () => { - jest.spyOn(console, 'error').mockImplementation(() => {}); - jest.spyOn(technology, 'update').mockRejectedValue('fail'); - subject = (await update( - { - pathParameters: { - id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', - }, - body: JSON.stringify({ - displayName: 'Error Test', - }), - } as unknown as APIGatewayProxyEvent, - {} as Context, - {} as Callback - )) as APIGatewayProxyResult; - }); - - afterAll(async () => { - jest.restoreAllMocks(); - await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); - }); - - it('returns 500 status', () => { - expect(subject.statusCode).toEqual(500); - }); - - it('returns server error message', () => { - expect(subject.body).toEqual(expect.stringContaining('Server Error:')); - }); - }); -}); - -describe('DELETE /technology/:id', () => { - let subject: APIGatewayProxyResult; - let ddbMock: AwsStub; - - describe('when no id provided', () => { - beforeAll(async () => { - subject = (await destroy( - { - pathParameters: { - id: null, - }, - } as unknown as APIGatewayProxyEvent, - {} as Context, - {} as Callback - )) as APIGatewayProxyResult; - }); - - it('returns 400 status', () => { - expect(subject.statusCode).toEqual(400); - }); - - it('returns bad request message', () => { - expect(subject.body).toEqual('Bad Request: no id provided'); - }); - }); - - describe('when record maybe deleted', () => { - beforeAll(async () => { - ddbMock = mockClient(getClient()); - ddbMock.on(DeleteItemCommand).resolves({ - Attributes: { - description: { - S: 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', - }, - id: { S: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff' }, - websiteUrl: { S: 'https://jestjs.io/' }, - displayName: { S: 'Jest' }, - }, - }); - subject = (await destroy( - { - pathParameters: { - id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', - }, - } as unknown as APIGatewayProxyEvent, - {} as Context, - {} as Callback - )) as APIGatewayProxyResult; - }); - - afterAll(async () => { - ddbMock.restore(); - }); - - it('returns 200 status', () => { - expect(subject.statusCode).toEqual(200); - }); - - it('returns deleted object if it exists', () => { - expect(JSON.parse(subject.body)).toEqual({ - description: - 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', - id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', - websiteUrl: 'https://jestjs.io/', - displayName: 'Jest', - }); - }); - }); - - describe('when error occurs', () => { - beforeAll(async () => { - jest.spyOn(console, 'error').mockImplementation(() => {}); - jest.spyOn(technology, 'destroy').mockRejectedValue('fail'); - subject = (await destroy( - { - pathParameters: { - id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', - }, - } as unknown as APIGatewayProxyEvent, - {} as Context, - {} as Callback - )) as APIGatewayProxyResult; - }); - - afterAll(async () => { - jest.restoreAllMocks(); - }); - - it('returns 500 status', () => { - expect(subject.statusCode).toEqual(500); - }); - - it('returns server error message', () => { - expect(subject.body).toEqual(expect.stringContaining('Server Error:')); - }); - }); -}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/technology.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology.ts deleted file mode 100644 index 21dfa7c9b..000000000 --- a/starters/serverless-framework-sqs-dynamodb/src/handlers/technology.ts +++ /dev/null @@ -1,109 +0,0 @@ -import type { APIGatewayProxyHandler } from 'aws-lambda'; -import { ZodError } from 'zod'; -import { TechnologyCreateSchema, TechnologyUpdateSchema } from '@/types/technology'; -import { - getAll as getAllTechnology, - create as createTechnology, - get as getTechnology, - update as updateTechnology, - destroy as destroyTechnology, -} from '@/models/technology'; -import { getErrorMessage } from '@/utils/error'; -import { StatusCodes } from 'http-status-codes'; - -export const index: APIGatewayProxyHandler = async () => { - const technologies = await getAllTechnology(); - return responseHelper(StatusCodes.OK, technologies); -}; - -export const create: APIGatewayProxyHandler = async (event) => { - try { - const payload = event.body ? JSON.parse(event.body) : null; - TechnologyCreateSchema.parse(payload); - const technology = await createTechnology(payload); - - return responseHelper(StatusCodes.CREATED, technology); - } catch (err) { - console.error(err); - if (err instanceof ZodError) { - return responseHelper(StatusCodes.BAD_REQUEST, err.flatten()); - } - return responseHelper( - StatusCodes.INTERNAL_SERVER_ERROR, - `Server Error: ${getErrorMessage(err)}` - ); - } -}; - -export const show: APIGatewayProxyHandler = async (event) => { - const id = event.pathParameters?.id; - if (!id) { - return responseHelper(StatusCodes.BAD_REQUEST, 'Bad Request: no id provided'); - } - - try { - const technology = await getTechnology(id); - if (!technology) { - return responseHelper(StatusCodes.NOT_FOUND, null); - } - return responseHelper(StatusCodes.OK, technology); - } catch (err) { - console.error(err); - return responseHelper( - StatusCodes.INTERNAL_SERVER_ERROR, - `Server Error: ${getErrorMessage(err)}` - ); - } -}; - -export const update: APIGatewayProxyHandler = async (event) => { - const id = event.pathParameters?.id; - if (!id) { - return responseHelper(StatusCodes.BAD_REQUEST, 'Bad Request: no id provided'); - } - - try { - const payload = event.body ? JSON.parse(event.body) : null; - TechnologyUpdateSchema.parse(payload); - const technology = await updateTechnology(id, payload); - if (!technology) { - return responseHelper(StatusCodes.NOT_FOUND, null); - } - - return responseHelper(StatusCodes.OK, technology); - } catch (err) { - console.error(err); - if (err instanceof ZodError) { - return responseHelper(StatusCodes.BAD_REQUEST, err.flatten()); - } - return responseHelper( - StatusCodes.INTERNAL_SERVER_ERROR, - `Server Error: ${getErrorMessage(err)}` - ); - } -}; - -export const destroy: APIGatewayProxyHandler = async (event) => { - const id = event.pathParameters?.id; - if (!id) { - return responseHelper(StatusCodes.BAD_REQUEST, 'Bad Request: no id provided'); - } - - try { - const technology = await destroyTechnology(id); - return responseHelper(StatusCodes.OK, technology); - } catch (err) { - console.error(err); - return responseHelper( - StatusCodes.INTERNAL_SERVER_ERROR, - `Server Error: ${getErrorMessage(err)}` - ); - } -}; - -function responseHelper(statusCode: number, resp: unknown) { - return { - statusCode, - body: typeof resp === 'string' ? resp : JSON.stringify(resp), - }; -} diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_create.test.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_create.test.ts new file mode 100644 index 000000000..01fea1a71 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_create.test.ts @@ -0,0 +1,111 @@ +import type { APIGatewayProxyResult, APIGatewayProxyEvent, Context, Callback } from 'aws-lambda'; +import { PutItemCommand, ServiceInputTypes, ServiceOutputTypes } from '@aws-sdk/client-dynamodb'; +import { AwsStub, mockClient } from 'aws-sdk-client-mock'; +import { getClient } from '@/utils/dynamodb/getClient'; +import { removeFromCache } from '@/utils/cache/removeFromCache'; +import * as technologyCreate from '@/models/technology/create'; +import { getCacheKey } from '@/models/technology/getCacheKey'; +import { handler } from './technology_create'; + +describe('POST /technology', () => { + let subject: APIGatewayProxyResult; + let ddbMock: AwsStub; + + describe('when record is created', () => { + beforeAll(async () => { + ddbMock = mockClient(getClient()); + ddbMock.on(PutItemCommand).resolves({ + Attributes: undefined, + }); + subject = (await handler( + { + body: JSON.stringify({ + description: + 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + websiteUrl: 'https://jestjs.io/', + displayName: 'Valid Test', + }), + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(async () => { + ddbMock.restore(); + await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); + }); + + it('returns 201 status', () => { + expect(subject.statusCode).toEqual(201); + }); + + it('returns the created record', () => { + expect(JSON.parse(subject.body)).toEqual({ + description: + 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + id: expect.any(String), + websiteUrl: 'https://jestjs.io/', + displayName: 'Valid Test', + }); + }); + }); + + describe('when invalid inputs', () => { + beforeAll(async () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + subject = (await handler( + {} as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(async () => { + jest.restoreAllMocks(); + await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); + }); + + it('returns 400 status', () => { + expect(subject.statusCode).toEqual(400); + }); + + it('returns form error', () => { + const parsedResp = JSON.parse(subject.body); + expect(parsedResp).toHaveProperty('formErrors'); + expect(parsedResp).toHaveProperty('fieldErrors'); + }); + }); + + describe('when error occurs', () => { + beforeAll(async () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(technologyCreate, 'create').mockRejectedValue('fail'); + subject = (await handler( + { + body: JSON.stringify({ + description: + 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + websiteUrl: 'https://jestjs.io/', + displayName: 'Valid Test', + }), + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(async () => { + jest.restoreAllMocks(); + await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); + }); + + it('returns 500 status', () => { + expect(subject.statusCode).toEqual(500); + }); + + it('returns server error message', () => { + expect(subject.body).toEqual(expect.stringContaining('Server Error:')); + }); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_create.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_create.ts new file mode 100644 index 000000000..59f35a0d2 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_create.ts @@ -0,0 +1,26 @@ +import type { APIGatewayProxyHandler } from 'aws-lambda'; +import { ZodError } from 'zod'; +import { StatusCodes } from 'http-status-codes'; +import { TechnologyCreateSchema } from '@/types/technology'; +import { create as createTechnology } from '@/models/technology/create'; +import { responseHelper } from '@/utils/responseHelper/responseHelper'; +import { getErrorMessage } from '@/utils/error/getErrorMessage'; + +export const handler: APIGatewayProxyHandler = async (event) => { + try { + const payload = event.body ? JSON.parse(event.body) : null; + TechnologyCreateSchema.parse(payload); + const technology = await createTechnology(payload); + + return responseHelper(StatusCodes.CREATED, technology); + } catch (err) { + console.error(err); + if (err instanceof ZodError) { + return responseHelper(StatusCodes.BAD_REQUEST, err.flatten()); + } + return responseHelper( + StatusCodes.INTERNAL_SERVER_ERROR, + `Server Error: ${getErrorMessage(err)}` + ); + } +}; diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_destroy.test.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_destroy.test.ts new file mode 100644 index 000000000..de15f7b43 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_destroy.test.ts @@ -0,0 +1,104 @@ +import type { APIGatewayProxyResult, APIGatewayProxyEvent, Context, Callback } from 'aws-lambda'; +import { DeleteItemCommand, ServiceInputTypes, ServiceOutputTypes } from '@aws-sdk/client-dynamodb'; +import { AwsStub, mockClient } from 'aws-sdk-client-mock'; +import { getClient } from '@/utils/dynamodb/getClient'; +import * as technologyDestroy from '@/models/technology/destroy'; +import { handler } from './technology_destroy'; + +describe('DELETE /technology/:id', () => { + let subject: APIGatewayProxyResult; + let ddbMock: AwsStub; + + describe('when no id provided', () => { + beforeAll(async () => { + subject = (await handler( + { + pathParameters: { + id: null, + }, + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + it('returns 400 status', () => { + expect(subject.statusCode).toEqual(400); + }); + + it('returns bad request message', () => { + expect(subject.body).toEqual('Bad Request: no id provided'); + }); + }); + + describe('when record maybe deleted', () => { + beforeAll(async () => { + ddbMock = mockClient(getClient()); + ddbMock.on(DeleteItemCommand).resolves({ + Attributes: { + description: { + S: 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + }, + id: { S: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff' }, + websiteUrl: { S: 'https://jestjs.io/' }, + displayName: { S: 'Jest' }, + }, + }); + subject = (await handler( + { + pathParameters: { + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + }, + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(async () => { + ddbMock.restore(); + }); + + it('returns 200 status', () => { + expect(subject.statusCode).toEqual(200); + }); + + it('returns deleted object if it exists', () => { + expect(JSON.parse(subject.body)).toEqual({ + description: + 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + websiteUrl: 'https://jestjs.io/', + displayName: 'Jest', + }); + }); + }); + + describe('when error occurs', () => { + beforeAll(async () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(technologyDestroy, 'destroy').mockRejectedValue('fail'); + subject = (await handler( + { + pathParameters: { + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + }, + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(async () => { + jest.restoreAllMocks(); + }); + + it('returns 500 status', () => { + expect(subject.statusCode).toEqual(500); + }); + + it('returns server error message', () => { + expect(subject.body).toEqual(expect.stringContaining('Server Error:')); + }); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_destroy.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_destroy.ts new file mode 100644 index 000000000..77318857e --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_destroy.ts @@ -0,0 +1,23 @@ +import type { APIGatewayProxyHandler } from 'aws-lambda'; +import { StatusCodes } from 'http-status-codes'; +import { destroy as destroyTechnology } from '@/models/technology/destroy'; +import { responseHelper } from '@/utils/responseHelper/responseHelper'; +import { getErrorMessage } from '@/utils/error/getErrorMessage'; + +export const handler: APIGatewayProxyHandler = async (event) => { + const id = event.pathParameters?.id; + if (!id) { + return responseHelper(StatusCodes.BAD_REQUEST, 'Bad Request: no id provided'); + } + + try { + const technology = await destroyTechnology(id); + return responseHelper(StatusCodes.OK, technology); + } catch (err) { + console.error(err); + return responseHelper( + StatusCodes.INTERNAL_SERVER_ERROR, + `Server Error: ${getErrorMessage(err)}` + ); + } +}; diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_index.test.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_index.test.ts new file mode 100644 index 000000000..ba45d702b --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_index.test.ts @@ -0,0 +1,34 @@ +import type { APIGatewayProxyResult, APIGatewayProxyEvent, Context, Callback } from 'aws-lambda'; +import { ScanCommand, ServiceInputTypes, ServiceOutputTypes } from '@aws-sdk/client-dynamodb'; +import { AwsStub, mockClient } from 'aws-sdk-client-mock'; +import { getClient } from '@/utils/dynamodb/getClient'; +import { handler } from './technology_index'; + +describe('GET /technology', () => { + let subject: APIGatewayProxyResult; + let ddbMock: AwsStub; + + beforeAll(async () => { + ddbMock = mockClient(getClient()); + ddbMock.on(ScanCommand).resolves({ + Items: [], + }); + subject = (await handler( + {} as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(() => { + ddbMock.restore(); + }); + + it('returns 200 status', () => { + expect(subject.statusCode).toEqual(200); + }); + + it('returns a list of technologies', () => { + expect(JSON.parse(subject.body)).toEqual([]); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_index.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_index.ts new file mode 100644 index 000000000..e0d14176e --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_index.ts @@ -0,0 +1,9 @@ +import type { APIGatewayProxyHandler } from 'aws-lambda'; +import { StatusCodes } from 'http-status-codes'; +import { getAll as getAllTechnology } from '@/models/technology/getAll'; +import { responseHelper } from '@/utils/responseHelper/responseHelper'; + +export const handler: APIGatewayProxyHandler = async () => { + const technologies = await getAllTechnology(); + return responseHelper(StatusCodes.OK, technologies); +}; diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_show.test.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_show.test.ts new file mode 100644 index 000000000..52df9431a --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_show.test.ts @@ -0,0 +1,146 @@ +import type { APIGatewayProxyResult, APIGatewayProxyEvent, Context, Callback } from 'aws-lambda'; +import { GetItemCommand, ServiceInputTypes, ServiceOutputTypes } from '@aws-sdk/client-dynamodb'; +import { AwsStub, mockClient } from 'aws-sdk-client-mock'; +import { getClient } from '@/utils/dynamodb/getClient'; +import { removeFromCache } from '@/utils/cache/removeFromCache'; +import * as technologyGet from '@/models/technology/get'; +import { getCacheKey } from '@/models/technology/getCacheKey'; +import { handler } from './technology_show'; + +describe('GET /technology/:id', () => { + let subject: APIGatewayProxyResult; + let ddbMock: AwsStub; + + beforeAll(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + describe('when no id provided', () => { + beforeAll(async () => { + subject = (await handler( + { + pathParameters: { + id: null, + }, + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + it('returns 400 status', () => { + expect(subject.statusCode).toEqual(400); + }); + + it('returns bad request message', () => { + expect(subject.body).toEqual('Bad Request: no id provided'); + }); + }); + + describe('when record not found', () => { + beforeAll(async () => { + ddbMock = mockClient(getClient()); + ddbMock.on(GetItemCommand).resolves({ + Item: undefined, + }); + subject = (await handler( + { + pathParameters: { + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + }, + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(async () => { + ddbMock.restore(); + await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); + }); + + it('returns 404 status', () => { + expect(subject.statusCode).toEqual(404); + }); + + it('returns null', () => { + expect(JSON.parse(subject.body)).toBeNull(); + }); + }); + + describe('when record is found', () => { + beforeAll(async () => { + ddbMock = mockClient(getClient()); + ddbMock.on(GetItemCommand).resolves({ + Item: { + description: { + S: 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + }, + id: { S: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff' }, + websiteUrl: { S: 'https://jestjs.io/' }, + displayName: { S: 'Jest' }, + }, + }); + subject = (await handler( + { + pathParameters: { + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + }, + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(async () => { + ddbMock.restore(); + await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); + }); + + it('returns 200 status', () => { + expect(subject.statusCode).toEqual(200); + }); + + it('returns the requested record', () => { + expect(JSON.parse(subject.body)).toEqual({ + description: + 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + websiteUrl: 'https://jestjs.io/', + displayName: 'Jest', + }); + }); + }); + + describe('when error occurs', () => { + beforeAll(async () => { + jest.spyOn(technologyGet, 'get').mockRejectedValue('fail'); + subject = (await handler( + { + pathParameters: { + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + }, + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(async () => { + jest.restoreAllMocks(); + await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); + }); + + it('returns 500 status', () => { + expect(subject.statusCode).toEqual(500); + }); + + it('returns server error message', () => { + expect(subject.body).toEqual(expect.stringContaining('Server Error:')); + }); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_show.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_show.ts new file mode 100644 index 000000000..6c7ba4c0b --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_show.ts @@ -0,0 +1,26 @@ +import type { APIGatewayProxyHandler } from 'aws-lambda'; +import { StatusCodes } from 'http-status-codes'; +import { get as getTechnology } from '@/models/technology/get'; +import { responseHelper } from '@/utils/responseHelper/responseHelper'; +import { getErrorMessage } from '@/utils/error/getErrorMessage'; + +export const handler: APIGatewayProxyHandler = async (event) => { + const id = event.pathParameters?.id; + if (!id) { + return responseHelper(StatusCodes.BAD_REQUEST, 'Bad Request: no id provided'); + } + + try { + const technology = await getTechnology(id); + if (!technology) { + return responseHelper(StatusCodes.NOT_FOUND, null); + } + return responseHelper(StatusCodes.OK, technology); + } catch (err) { + console.error(err); + return responseHelper( + StatusCodes.INTERNAL_SERVER_ERROR, + `Server Error: ${getErrorMessage(err)}` + ); + } +}; diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_update.test.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_update.test.ts new file mode 100644 index 000000000..e39a32329 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_update.test.ts @@ -0,0 +1,198 @@ +import type { APIGatewayProxyResult, APIGatewayProxyEvent, Context, Callback } from 'aws-lambda'; +import { + GetItemCommand, + PutItemCommand, + ServiceInputTypes, + ServiceOutputTypes, +} from '@aws-sdk/client-dynamodb'; +import { AwsStub, mockClient } from 'aws-sdk-client-mock'; +import { getClient } from '@/utils/dynamodb/getClient'; +import { removeFromCache } from '@/utils/cache/removeFromCache'; +import * as technologyUpdate from '@/models/technology/update'; +import { getCacheKey } from '@/models/technology/getCacheKey'; +import { handler } from './technology_update'; + +describe('PUT /technology/:id', () => { + let subject: APIGatewayProxyResult; + let ddbMock: AwsStub; + + describe('when no id provided', () => { + beforeAll(async () => { + subject = (await handler( + { + pathParameters: { + id: null, + }, + body: JSON.stringify({ + displayName: 'Valid Test', + }), + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + it('returns 400 status', () => { + expect(subject.statusCode).toEqual(400); + }); + + it('returns bad request message', () => { + expect(subject.body).toEqual('Bad Request: no id provided'); + }); + }); + + describe('when record not found', () => { + beforeAll(async () => { + ddbMock = mockClient(getClient()); + ddbMock.on(GetItemCommand).resolves({ + Item: undefined, + }); + jest.spyOn(console, 'error').mockImplementation(() => {}); + subject = (await handler( + { + pathParameters: { + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + }, + body: JSON.stringify({ + displayName: 'Valid Test', + }), + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(async () => { + ddbMock.restore(); + await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); + jest.resetAllMocks(); + }); + + it('returns 404 status', () => { + expect(subject.statusCode).toEqual(404); + }); + + it('returns null', () => { + expect(JSON.parse(subject.body)).toBeNull(); + }); + }); + + describe('when record is found', () => { + beforeAll(async () => { + ddbMock = mockClient(getClient()); + ddbMock.on(GetItemCommand).resolvesOnce({ + Item: { + description: { + S: 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + }, + id: { S: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff' }, + websiteUrl: { S: 'https://jestjs.io/' }, + displayName: { S: 'Jest' }, + }, + }); + ddbMock.on(PutItemCommand).resolves({ + Attributes: { + description: { + S: 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + }, + id: { S: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff' }, + websiteUrl: { S: 'https://jestjs.io/' }, + displayName: { S: 'Valid Test' }, + }, + }); + subject = (await handler( + { + pathParameters: { + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + }, + body: JSON.stringify({ + displayName: 'Valid Test', + }), + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(async () => { + ddbMock.restore(); + await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); + }); + + it('returns 200 status', () => { + expect(subject.statusCode).toEqual(200); + }); + + it('returns the updated record', () => { + expect(JSON.parse(subject.body)).toEqual({ + description: + 'Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!', + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + websiteUrl: 'https://jestjs.io/', + displayName: 'Valid Test', + }); + }); + }); + + describe('when invalid inputs', () => { + beforeAll(async () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + subject = (await handler( + { + pathParameters: { + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + }, + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(async () => { + jest.restoreAllMocks(); + await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); + }); + + it('returns 400 status', () => { + expect(subject.statusCode).toEqual(400); + }); + + it('returns form error', () => { + const parsedResp = JSON.parse(subject.body); + expect(parsedResp).toHaveProperty('formErrors'); + expect(parsedResp).toHaveProperty('fieldErrors'); + }); + }); + + describe('when error occurs', () => { + beforeAll(async () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(technologyUpdate, 'update').mockRejectedValue('fail'); + subject = (await handler( + { + pathParameters: { + id: '87af19b1-aa0d-4178-a30c-2fa8cd1f2cff', + }, + body: JSON.stringify({ + displayName: 'Error Test', + }), + } as unknown as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(async () => { + jest.restoreAllMocks(); + await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); + }); + + it('returns 500 status', () => { + expect(subject.statusCode).toEqual(500); + }); + + it('returns server error message', () => { + expect(subject.body).toEqual(expect.stringContaining('Server Error:')); + }); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_update.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_update.ts new file mode 100644 index 000000000..0f92f5d0f --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_update.ts @@ -0,0 +1,34 @@ +import type { APIGatewayProxyHandler } from 'aws-lambda'; +import { ZodError } from 'zod'; +import { StatusCodes } from 'http-status-codes'; +import { TechnologyUpdateSchema } from '@/types/technology'; +import { update as updateTechnology } from '@/models/technology/update'; +import { responseHelper } from '@/utils/responseHelper/responseHelper'; +import { getErrorMessage } from '@/utils/error/getErrorMessage'; + +export const handler: APIGatewayProxyHandler = async (event) => { + const id = event.pathParameters?.id; + if (!id) { + return responseHelper(StatusCodes.BAD_REQUEST, 'Bad Request: no id provided'); + } + + try { + const payload = event.body ? JSON.parse(event.body) : null; + TechnologyUpdateSchema.parse(payload); + const technology = await updateTechnology(id, payload); + if (!technology) { + return responseHelper(StatusCodes.NOT_FOUND, null); + } + + return responseHelper(StatusCodes.OK, technology); + } catch (err) { + console.error(err); + if (err instanceof ZodError) { + return responseHelper(StatusCodes.BAD_REQUEST, err.flatten()); + } + return responseHelper( + StatusCodes.INTERNAL_SERVER_ERROR, + `Server Error: ${getErrorMessage(err)}` + ); + } +}; diff --git a/starters/serverless-framework-sqs-dynamodb/src/models/technology/create.test.ts b/starters/serverless-framework-sqs-dynamodb/src/models/technology/create.test.ts index 360c1dd12..7c5c5a4b1 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/models/technology/create.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/models/technology/create.test.ts @@ -1,10 +1,10 @@ import { PutItemCommand } from '@aws-sdk/client-dynamodb'; import { mockClient } from 'aws-sdk-client-mock'; -import { getClient } from '@/utils/dynamodb'; +import { getClient } from '@/utils/dynamodb/getClient'; import { create } from './create'; describe('technology.create()', () => { - let subject: Record | null; + let subject: Awaited>; const ddbMock = mockClient(getClient()); diff --git a/starters/serverless-framework-sqs-dynamodb/src/models/technology/create.ts b/starters/serverless-framework-sqs-dynamodb/src/models/technology/create.ts index 25ffe19ed..9a212c961 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/models/technology/create.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/models/technology/create.ts @@ -1,7 +1,7 @@ import { v4 as uuidv4 } from 'uuid'; import { TechnologyCreate } from '@/types/technology'; -import { putItem } from '@/utils/dynamodb'; -import { addToCache } from '@/utils/cache'; +import { putItem } from '@/utils/dynamodb/putItem'; +import { addToCache } from '@/utils/cache/addToCache'; import { getCacheKey } from './getCacheKey'; export const create = async (payload: TechnologyCreate) => { diff --git a/starters/serverless-framework-sqs-dynamodb/src/models/technology/destroy.test.ts b/starters/serverless-framework-sqs-dynamodb/src/models/technology/destroy.test.ts index 596a31c3a..48e9c83f1 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/models/technology/destroy.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/models/technology/destroy.test.ts @@ -1,10 +1,10 @@ import { DeleteItemCommand } from '@aws-sdk/client-dynamodb'; import { mockClient } from 'aws-sdk-client-mock'; -import { getClient } from '@/utils/dynamodb'; +import { getClient } from '@/utils/dynamodb/getClient'; import { destroy } from './destroy'; describe('technology.destroy()', () => { - let subject: Record | null; + let subject: Awaited>; const ddbMock = mockClient(getClient()); diff --git a/starters/serverless-framework-sqs-dynamodb/src/models/technology/destroy.ts b/starters/serverless-framework-sqs-dynamodb/src/models/technology/destroy.ts index ba1196caf..20a3e650a 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/models/technology/destroy.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/models/technology/destroy.ts @@ -1,5 +1,5 @@ -import { removeFromCache } from '@/utils/cache'; -import { deleteItem } from '@/utils/dynamodb'; +import { removeFromCache } from '@/utils/cache/removeFromCache'; +import { deleteItem } from '@/utils/dynamodb/deleteItem'; import { getCacheKey } from './getCacheKey'; export const destroy = async (key: string) => { diff --git a/starters/serverless-framework-sqs-dynamodb/src/models/technology/get.test.ts b/starters/serverless-framework-sqs-dynamodb/src/models/technology/get.test.ts index 5e17e2a37..c1d51e467 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/models/technology/get.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/models/technology/get.test.ts @@ -1,10 +1,10 @@ import { GetItemCommand } from '@aws-sdk/client-dynamodb'; import { mockClient } from 'aws-sdk-client-mock'; -import { getClient } from '@/utils/dynamodb'; +import { getClient } from '@/utils/dynamodb/getClient'; import { get } from './get'; describe('technology.get()', () => { - let subject: Record | null; + let subject: Awaited>; const ddbMock = mockClient(getClient()); diff --git a/starters/serverless-framework-sqs-dynamodb/src/models/technology/get.ts b/starters/serverless-framework-sqs-dynamodb/src/models/technology/get.ts index d88c54b39..8a20195f8 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/models/technology/get.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/models/technology/get.ts @@ -1,8 +1,7 @@ -import { useCache } from '@/utils/cache'; -import { getItem } from '@/utils/dynamodb'; +import { useCache } from '@/utils/cache/useCache'; +import { getItem } from '@/utils/dynamodb/getItem'; import { TechnologySchema } from '@/types/technology'; import { getCacheKey } from './getCacheKey'; -import { getErrorMessage } from '@/utils/error/getErrorMessage'; export const get = async (key: string) => { return useCache({ diff --git a/starters/serverless-framework-sqs-dynamodb/src/models/technology/getAll.test.ts b/starters/serverless-framework-sqs-dynamodb/src/models/technology/getAll.test.ts index cc248834b..916a3b581 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/models/technology/getAll.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/models/technology/getAll.test.ts @@ -1,10 +1,10 @@ import { ScanCommand } from '@aws-sdk/client-dynamodb'; import { mockClient } from 'aws-sdk-client-mock'; -import { getClient } from '@/utils/dynamodb'; +import { getClient } from '@/utils/dynamodb/getClient'; import { getAll } from './getAll'; describe('technology.getAll()', () => { - let subject: Record[]; + let subject: Awaited>; const ddbMock = mockClient(getClient()); diff --git a/starters/serverless-framework-sqs-dynamodb/src/models/technology/getAll.ts b/starters/serverless-framework-sqs-dynamodb/src/models/technology/getAll.ts index 817b179a1..418cebea2 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/models/technology/getAll.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/models/technology/getAll.ts @@ -1,4 +1,4 @@ -import { scan } from '@/utils/dynamodb'; +import { scan } from '@/utils/dynamodb/scan'; export const getAll = async () => { const items = await scan(process.env.TECHNOLOGIES_TABLE); diff --git a/starters/serverless-framework-sqs-dynamodb/src/models/technology/index.ts b/starters/serverless-framework-sqs-dynamodb/src/models/technology/index.ts deleted file mode 100644 index 6b9229a8a..000000000 --- a/starters/serverless-framework-sqs-dynamodb/src/models/technology/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { getCacheKey } from './getCacheKey'; -export { getAll } from './getAll'; -export { create } from './create'; -export { get } from './get'; -export { update } from './update'; -export { destroy } from './destroy'; diff --git a/starters/serverless-framework-sqs-dynamodb/src/models/technology/update.test.ts b/starters/serverless-framework-sqs-dynamodb/src/models/technology/update.test.ts index e829a7a85..0847e8d12 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/models/technology/update.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/models/technology/update.test.ts @@ -5,11 +5,11 @@ import { ServiceOutputTypes, } from '@aws-sdk/client-dynamodb'; import { AwsStub, mockClient } from 'aws-sdk-client-mock'; -import { getClient } from '@/utils/dynamodb'; +import { getClient } from '@/utils/dynamodb/getClient'; import { update } from './update'; describe('technology.update()', () => { - let subject: Record | null; + let subject: Awaited>; let ddbMock: AwsStub; beforeAll(() => { diff --git a/starters/serverless-framework-sqs-dynamodb/src/models/technology/update.ts b/starters/serverless-framework-sqs-dynamodb/src/models/technology/update.ts index b017474a1..cd647657f 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/models/technology/update.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/models/technology/update.ts @@ -1,6 +1,6 @@ import { TechnologyUpdate } from '@/types/technology'; import { addToCache } from '@/utils/cache/addToCache'; -import { putItem } from '@/utils/dynamodb'; +import { putItem } from '@/utils/dynamodb/putItem'; import { get } from './get'; export const update = async (id: string, payload: TechnologyUpdate) => { diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/addToCache.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/addToCache.test.ts index c88d509a0..c8aa97751 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/addToCache.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/addToCache.test.ts @@ -1,5 +1,5 @@ import Redis from 'ioredis'; -import { getClient as getRedisClient } from '@/utils/redis'; +import { getClient as getRedisClient } from '@/utils/redis/getClient'; import { DEFAULT_CACHE_TIME } from './constants'; import { addToCache } from './addToCache'; diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/getClient.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/getClient.test.ts index 8f67e434a..d606dc434 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/getClient.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/getClient.test.ts @@ -1,9 +1,7 @@ -import { Cache } from 'cachified'; -import Redis from 'ioredis'; import { getClient } from './getClient'; describe('cache.getClient()', () => { - let subject: Cache; + let subject: Awaited>; describe('when called once', () => { beforeAll(async () => { diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/getClient.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/getClient.ts index 8d2b5d28a..10699c143 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/getClient.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/getClient.ts @@ -1,5 +1,5 @@ import { Cache, totalTtl } from 'cachified'; -import { getClient as getRedisClient } from '@/utils/redis'; +import { getClient as getRedisClient } from '@/utils/redis/getClient'; let cachedClient: Cache; export const getClient = async () => { diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/index.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/index.ts deleted file mode 100644 index 502145fd0..000000000 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { useCache } from './useCache'; -export { addToCache } from './addToCache'; -export { removeFromCache } from './removeFromCache'; diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/removeFromCache.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/removeFromCache.test.ts index f91f20b25..c732d5a0a 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/removeFromCache.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/removeFromCache.test.ts @@ -1,5 +1,5 @@ import Redis from 'ioredis'; -import { getClient as getRedisClient } from '@/utils/redis'; +import { getClient as getRedisClient } from '@/utils/redis/getClient'; import { removeFromCache } from './removeFromCache'; describe('cache.removeFromCache()', () => { diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/useCache.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/useCache.test.ts index de3084e01..c57080053 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/useCache.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/useCache.test.ts @@ -1,7 +1,7 @@ import { useCache } from './useCache'; describe('cache.useCache()', () => { - let subject: string | null; + let subject: Awaited>; describe('when fails checkValue', () => { beforeAll(async () => { @@ -16,6 +16,7 @@ describe('cache.useCache()', () => { if (typeof value === 'string') { return value.startsWith('tests-'); } + return true; }, }); }); diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/useCache.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/useCache.ts index 45d317c58..bee9526af 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/useCache.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/useCache.ts @@ -1,5 +1,5 @@ import { cachified, CachifiedOptions } from 'cachified'; -import { getErrorMessage } from '../error/getErrorMessage'; +import { getErrorMessage } from '@/utils/error/getErrorMessage'; import { DEFAULT_CACHE_TIME } from './constants'; import { getClient } from './getClient'; import { removeFromCache } from './removeFromCache'; diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.test.ts index bb6027c3f..c49a88324 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.test.ts @@ -4,7 +4,7 @@ import { getClient } from './getClient'; import { deleteItem } from './deleteItem'; describe('dynamodb.deleteItem()', () => { - let subject: Record | null; + let subject: Awaited>; const ddbMock = mockClient(getClient()); afterAll(() => { diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.ts index cd0485fe4..be56381a0 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.ts @@ -1,6 +1,6 @@ import { DeleteItemCommand } from '@aws-sdk/client-dynamodb'; import { marshall, unmarshall } from '@aws-sdk/util-dynamodb'; -import { getErrorMessage } from '@/utils/error'; +import { getErrorMessage } from '@/utils/error/getErrorMessage'; import { getClient } from './getClient'; export const deleteItem = async (tableName: string, key: Record) => { diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.ts index 485c7002a..af0d3c77d 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getClient.ts @@ -1,5 +1,5 @@ import { DynamoDBClient, DynamoDBClientConfig } from '@aws-sdk/client-dynamodb'; -import { isOffline } from '@/utils/is-offline'; +import { isOffline } from '@/utils/isOffline/isOffline'; let cachedClient: DynamoDBClient; diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getItem.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getItem.test.ts index 3913aa9df..b3f79507e 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getItem.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getItem.test.ts @@ -4,7 +4,7 @@ import { getClient } from './getClient'; import { getItem } from './getItem'; describe('dynamodb.getItem()', () => { - let subject: Record | null; + let subject: Awaited>; const ddbMock = mockClient(getClient()); afterAll(() => { diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getItem.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getItem.ts index 5a7540193..c9f489e94 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getItem.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/getItem.ts @@ -1,6 +1,6 @@ import { GetItemCommand, GetItemCommandInput } from '@aws-sdk/client-dynamodb'; import { marshall, unmarshall } from '@aws-sdk/util-dynamodb'; -import { getErrorMessage } from '@/utils/error'; +import { getErrorMessage } from '@/utils/error/getErrorMessage'; import { getClient } from './getClient'; export const getItem = async ( diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/index.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/index.ts deleted file mode 100644 index 4b5d7df77..000000000 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { getClient } from './getClient'; -export { scan } from './scan'; -export { getItem } from './getItem'; -export { putItem } from './putItem'; -export { deleteItem } from './deleteItem'; diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/putItem.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/putItem.test.ts index f88e5e666..8e0d86c7c 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/putItem.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/putItem.test.ts @@ -4,7 +4,7 @@ import { getClient } from './getClient'; import { putItem } from './putItem'; describe('dynamodb.putItem()', () => { - let subject: boolean; + let subject: Awaited>; const ddbMock = mockClient(getClient()); afterAll(() => { diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/putItem.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/putItem.ts index 145a24daa..ccfa4887b 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/putItem.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/putItem.ts @@ -1,6 +1,6 @@ import { PutItemCommand } from '@aws-sdk/client-dynamodb'; import { marshall } from '@aws-sdk/util-dynamodb'; -import { getErrorMessage } from '@/utils/error'; +import { getErrorMessage } from '@/utils/error/getErrorMessage'; import { getClient } from './getClient'; export const putItem = async (tableName: string, item: Record) => { diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.test.ts index 1b5f20915..af8f79d96 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.test.ts @@ -4,7 +4,7 @@ import { getClient } from './getClient'; import { scan } from './scan'; describe('dynamodb.scan()', () => { - let subject: Record[] | null; + let subject: Awaited>; const ddbMock = mockClient(getClient()); afterAll(() => { diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.ts index 3d6b61102..08f12ddf5 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/scan.ts @@ -1,6 +1,6 @@ import { ScanCommand } from '@aws-sdk/client-dynamodb'; import { unmarshall } from '@aws-sdk/util-dynamodb'; -import { getErrorMessage } from '@/utils/error'; +import { getErrorMessage } from '@/utils/error/getErrorMessage'; import { getClient } from './getClient'; export const scan = async (tableName: string) => { diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/error/index.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/error/index.ts deleted file mode 100644 index 50ca645d9..000000000 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/error/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { getErrorMessage } from './getErrorMessage'; diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/is-offline/index.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/is-offline/index.ts deleted file mode 100644 index d90ad1d50..000000000 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/is-offline/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { isOffline } from './is-offline'; diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/is-offline/is-offline.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/isOffline/isOffline.test.ts similarity index 92% rename from starters/serverless-framework-sqs-dynamodb/src/utils/is-offline/is-offline.test.ts rename to starters/serverless-framework-sqs-dynamodb/src/utils/isOffline/isOffline.test.ts index cbc7d6488..70071d67a 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/is-offline/is-offline.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/isOffline/isOffline.test.ts @@ -1,4 +1,4 @@ -import { isOffline } from './is-offline'; +import { isOffline } from './isOffline'; describe('isOffline()', () => { const env = process.env; diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/is-offline/is-offline.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/isOffline/isOffline.ts similarity index 100% rename from starters/serverless-framework-sqs-dynamodb/src/utils/is-offline/is-offline.ts rename to starters/serverless-framework-sqs-dynamodb/src/utils/isOffline/isOffline.ts diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/redis/getClient.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/redis/getClient.ts index 3525e73fc..1312fb63b 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/redis/getClient.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/redis/getClient.ts @@ -1,6 +1,6 @@ import Redis from 'ioredis'; -let cachedClients: Record = {}; +const cachedClients: Record = {}; export const getClient = async (type: string, url: string): Promise => { if (cachedClients[type]) { diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/redis/index.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/redis/index.ts deleted file mode 100644 index 075c17190..000000000 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/redis/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { getClient } from './getClient'; diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/responseHelper/responseHelper.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/responseHelper/responseHelper.test.ts new file mode 100644 index 000000000..8e82b67c1 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/responseHelper/responseHelper.test.ts @@ -0,0 +1,35 @@ +import { responseHelper } from './responseHelper'; + +describe('responseHelper()', () => { + let subject: ReturnType; + + describe('when resp is string', () => { + beforeAll(() => { + subject = responseHelper(200, 'some string'); + }); + + it('returns a response object', () => { + expect(subject).toHaveProperty('statusCode'); + expect(subject).toHaveProperty('body'); + }); + + it('sets body to the provided string', () => { + expect(subject.body).toEqual('some string'); + }); + }); + + describe('when resp is not a string', () => { + beforeAll(() => { + subject = responseHelper(200, null); + }); + + it('returns a response object', () => { + expect(subject).toHaveProperty('statusCode'); + expect(subject).toHaveProperty('body'); + }); + + it('sets body to the JSON stringified value', () => { + expect(subject.body).toEqual('null'); + }); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/responseHelper/responseHelper.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/responseHelper/responseHelper.ts new file mode 100644 index 000000000..72de1651f --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/responseHelper/responseHelper.ts @@ -0,0 +1,6 @@ +export function responseHelper(statusCode: number, resp: unknown) { + return { + statusCode, + body: typeof resp === 'string' ? resp : JSON.stringify(resp), + }; +} From 97bddbcabf647a9ca430e7b4996ae20ca7c57f9a Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sat, 31 Dec 2022 21:24:33 -0600 Subject: [PATCH 44/62] fix install of ioredis-mock --- starters/serverless-framework-sqs-dynamodb/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/starters/serverless-framework-sqs-dynamodb/package.json b/starters/serverless-framework-sqs-dynamodb/package.json index 0bee5e225..e772c1858 100644 --- a/starters/serverless-framework-sqs-dynamodb/package.json +++ b/starters/serverless-framework-sqs-dynamodb/package.json @@ -29,7 +29,6 @@ "cachified": "^3.0.1", "http-status-codes": "^2.2.0", "ioredis": "^5.2.4", - "ioredis-mock": "^8.2.2", "uuid": "^9.0.0", "zod": "^3.20.2" }, @@ -54,6 +53,7 @@ "eslint-plugin-n": "^15.0.0", "eslint-plugin-promise": "^6.0.0", "i": "^0.3.7", + "ioredis-mock": "^8.2.2", "jest": "^29.3.1", "npm": "^9.2.0", "prettier": "^2.8.1", From ee9398dee0bbcd71737babea941683e026998e49 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sat, 31 Dec 2022 21:42:28 -0600 Subject: [PATCH 45/62] add deploy script --- starters/serverless-framework-sqs-dynamodb/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/starters/serverless-framework-sqs-dynamodb/package.json b/starters/serverless-framework-sqs-dynamodb/package.json index e772c1858..75de898ca 100644 --- a/starters/serverless-framework-sqs-dynamodb/package.json +++ b/starters/serverless-framework-sqs-dynamodb/package.json @@ -11,6 +11,7 @@ ], "scripts": { "build": "sls package", + "deploy": "sls deploy", "start": "SLS_DEBUG=* sls offline start --verbose", "test": "jest --runInBand", "lint": "eslint", From 2a6151a05a0dd407eef3cbc065f734b374539ebd Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sat, 31 Dec 2022 21:44:03 -0600 Subject: [PATCH 46/62] update readme --- .../README.md | 69 ++++++++++++++++--- 1 file changed, 60 insertions(+), 9 deletions(-) diff --git a/starters/serverless-framework-sqs-dynamodb/README.md b/starters/serverless-framework-sqs-dynamodb/README.md index 35f24064a..0329d19d9 100644 --- a/starters/serverless-framework-sqs-dynamodb/README.md +++ b/starters/serverless-framework-sqs-dynamodb/README.md @@ -13,6 +13,8 @@ - [DynamoDB Commands](#dynamodb-commands) - [Infrastructure Commands](#infrastructure-commands) - [DynamoDB](#dynamodb) +- [Jest](#jest) +- [Deployment](#deployment) ## Overview @@ -90,7 +92,8 @@ yarn start ### General Commands -- `build` bundles the project using the serverless packaging serverless. The produced artifacts will ship bundles shipped to AWS on deployment. You can optionally pass `--analyze ` to run the bundle analzyer and visualize the results to understand your handler bundles. +- `build` bundles the project using the serverless packaging serverless. The produced artifacts will ship bundles shipped to AWS on deployment. You can optionally pass `--analyze ` to run the bundle analyzer and visualize the results to understand your handler bundles. +- `deploy` ships the project to the configured AWS account using the Serverless Framework CLI command. - `start` runs the `serverless-offline` provided server for local development and testing. Be sure to have the local docker infrastructure running to emulate the related services. - `test` runs `jest` under the hood. - `lint` runs `eslint` under the hood. You can use all the eslint available command line arguments. To lint the entire project, run `yarn lint .`, or equivalent. You can affix `--fix` to auto-correct linting issues that eslint can handle. @@ -111,20 +114,50 @@ yarn start ## Project Structure -TODO: insert tree structure +- `db` contains seed files for database initialization using the `db:seed` script +- `src/handlers` contains Lambda function handlers referenced by the Serverless Configuration. +- `src/models` contains directories of different data models used by the project and exports all available methods. +- `src/types` contains all TypeScript types for the project. +- `src/utils` contains utility functions and wrappers. +- `serverless.ts` - the Serverless Configuration + +**Note:** All tests are co-located with their implementation files. ## Serverless Configuration This kit uses the TypeScript option for configuration. It is type checked using the `@serverless/typescript` definitions over the DefinitelyTyped definitions because DefinitelyTyped is currently behind on its definition. However, the `@serverless/typescript` types have known issues with certain fields are noted directly in the configuration. -Things to note: +It is not compatible with the automated CI/CD of the Serverless Dashboard as they only support the default YAML format. You can read more about [setting up CI/CD using GitHub for this project on the This Dot blog](https://www.thisdot.co/blog/github-actions-for-serverless-framework-deployments). + +### Key Fields to Note + +#### `profile` + +This is left blank and will default to use your system configured AWS default profile. If you want to use a custom profile, set this field with the name of the profile used. This should ideally be selected based on the AWS organization used. + +#### `httpApi.cors` + +This setting was enabled out of the box to help with development quickly. Ideally, these CORS settings should be more restrictive in production environments. You can read more about [this option in the official docs](https://www.serverless.com/framework/docs/providers/aws/events/http-api/#cors-setup). + +#### Defined Stages + +This project comes configured with 3 default stages: `dev`, `staging`, `production`. + +- `dev` should be used for local development. If you need a shared dev environment for your team, it is recommended to add a new `local` stage for local dev and promote `dev` to the shared deployment stage value. +- `staging` should be the pre-production staging environment for all changes. +- `production` should be the production environment. + +#### esbuild + +This project uses [serverless-esbuild](https://www.npmjs.com/package/serverless-esbuild) over its webpack counterpart. The esbuild tool chain is generally faster and requires less dependencies to work out of the box. + +#### Bundle Analyzer -- profile field -- httpApi cors -- defined stages: dev, staging, production -- esbuild -- bundle analyzer -- package patterns +This project ships with the [serverless-analyze-bundle-plugin](https://www.npmjs.com/package/serverless-analyze-bundle-plugin) to allow you to visualize your Lambda bundles. This is an especially important factor when dealing with cold starts and should be monitored. To analyze a function bundle, run `yarn build --analyze `. This will give you a visualization of your function's dependencies and their sizes. + +#### File Patterns + +Barrel files are not used as they increase the size of the final output bundle due to [how esbuild interprets these types of files](https://github.com/evanw/esbuild/issues/2164). As such, it is recommended to split usable chunks of code into individually packaged files and directly imported for use. ## DynamoDB @@ -155,3 +188,21 @@ seed: { ``` Once defined, run `yarn db:seed` to seed your database. See https://github.com/99x/serverless-dynamodb-local#seeding-sls-dynamodb-seed for more information. + +## Jest + +All test files are co-located with their implementations. There are no integration or e2e tests implemented for this project. All tests are implemented as unit tests. + +### Stubbed Databases + +Both Redis and DynamoDB are fully stubbed in the tests. + +Redis is fully stubbed because it does not have the concept of environments or tables so it would require 2 instances to be running. Redis can easily be mocked in memory and there are several existing solutions for this problem. We're using [ioredis](https://github.com/luin/ioredis) for our implementation and its related [ioredis-mock](https://github.com/stipsan/ioredis-mock). + +There is an existing [DynamoDB Local preset for Jest](https://github.com/shelfio/jest-dynamodb). However, it indiscriminately clears the entire database before and after all tests are run. We've [submitted a PR to fix this problem](https://github.com/shelfio/jest-dynamodb/pull/190) which we can utilize once released. In the meantime, we're stubbing the DynamoDB during testing using the [aws-sdk-client-mock](https://github.com/m-radzikowski/aws-sdk-client-mock) to stub the AWS JS v3 SDK. + +## Deployment + +As a serverless implementation, most of the infrastructure will be deployed and configured correctly simply utilizing the `deploy` script provided by this kit which is just an alias for [`serverless deploy`](https://www.serverless.com/framework/docs/providers/aws/cli-reference/deploy). However, the Redis instance is not configurable via the Serverless Configuration and will need to be set up ahead of your first deploy and configured via environment variables. We recommend using [Serverless Framework's interface for AWS Secret Manager](https://www.serverless.com/blog/aws-secrets-management/) for security purposes. + +This entire stack can be deployed via CI tools such as GitHub Actions, CircleCI, etc. and is our recommended approach as this kit is incompatible with the Serverless Dashboard. The Serverless Dashboard CI only works with the configuration in the YAML format which we do not use to give developers type-safety in the config file. From 2efcd59d34f105b6840c2c74ec5f3f101b72ae02 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sat, 31 Dec 2022 23:32:29 -0600 Subject: [PATCH 47/62] add swagger and configure endpoint docs --- .../.gitignore | 1 + .../package.json | 1 + .../serverless.ts | 79 +++++++++++++++++++ .../src/types/api-types.d.ts | 40 ++++++++++ 4 files changed, 121 insertions(+) create mode 100644 starters/serverless-framework-sqs-dynamodb/src/types/api-types.d.ts diff --git a/starters/serverless-framework-sqs-dynamodb/.gitignore b/starters/serverless-framework-sqs-dynamodb/.gitignore index fc5caa0eb..f94275c37 100644 --- a/starters/serverless-framework-sqs-dynamodb/.gitignore +++ b/starters/serverless-framework-sqs-dynamodb/.gitignore @@ -7,3 +7,4 @@ jspm_packages .env .redis .serverless +swagger diff --git a/starters/serverless-framework-sqs-dynamodb/package.json b/starters/serverless-framework-sqs-dynamodb/package.json index 75de898ca..cb0d82b66 100644 --- a/starters/serverless-framework-sqs-dynamodb/package.json +++ b/starters/serverless-framework-sqs-dynamodb/package.json @@ -60,6 +60,7 @@ "prettier": "^2.8.1", "serverless": "^3.26.0", "serverless-analyze-bundle-plugin": "^1.2.1", + "serverless-auto-swagger": "^2.12.0", "serverless-dynamodb-local": "^0.2.40", "serverless-esbuild": "^1.34.0", "serverless-offline": "^12.0.3", diff --git a/starters/serverless-framework-sqs-dynamodb/serverless.ts b/starters/serverless-framework-sqs-dynamodb/serverless.ts index 17fe9383d..748290a21 100644 --- a/starters/serverless-framework-sqs-dynamodb/serverless.ts +++ b/starters/serverless-framework-sqs-dynamodb/serverless.ts @@ -1,16 +1,21 @@ import type { AWS } from '@serverless/typescript'; +import { StatusCodes } from 'http-status-codes'; const serverlessConfiguration: AWS = { service: 'serverless-framework-sqs-dynamodb', frameworkVersion: '3', useDotenv: true, plugins: [ + 'serverless-auto-swagger', 'serverless-esbuild', 'serverless-analyze-bundle-plugin', 'serverless-dynamodb-local', 'serverless-offline', ], custom: { + 'autoswagger': { + excludeStages: ['production'], + }, 'dynamodb': { stages: ['dev'], start: { @@ -132,6 +137,14 @@ const serverlessConfiguration: AWS = { httpApi: { path: '/technology', method: 'get', + // @ts-expect-error Swagger Types not in main ts type + responses: { + [StatusCodes.OK]: { + description: 'Fetched Technologies Successfully', + bodyType: 'Technologies', + }, + }, + swaggerTags: ['Technology'], }, }, ], @@ -143,6 +156,22 @@ const serverlessConfiguration: AWS = { httpApi: { path: '/technology', method: 'post', + // @ts-expect-error Swagger Types not in main ts type + bodyType: 'TechnologyCreateBody', + responses: { + [StatusCodes.CREATED]: { + description: 'Technology Successfully Created', + bodyType: 'Technology', + }, + [StatusCodes.BAD_REQUEST]: { + description: 'Failed to create Technology', + bodyType: 'TechnologyCreateFormError', + }, + [StatusCodes.INTERNAL_SERVER_ERROR]: { + description: 'Server Error', + }, + }, + swaggerTags: ['Technology'], }, }, ], @@ -154,6 +183,23 @@ const serverlessConfiguration: AWS = { httpApi: { path: '/technology/{id}', method: 'get', + // @ts-expect-error Swagger Types not in main ts type + responses: { + [StatusCodes.OK]: { + description: 'Fetched Technology Successfully', + bodyType: 'Technology', + }, + [StatusCodes.BAD_REQUEST]: { + description: 'Invalid Request', + }, + [StatusCodes.NOT_FOUND]: { + description: 'Technology Not Found', + }, + [StatusCodes.INTERNAL_SERVER_ERROR]: { + description: 'Server Error', + }, + }, + swaggerTags: ['Technology'], }, }, ], @@ -165,6 +211,25 @@ const serverlessConfiguration: AWS = { httpApi: { path: '/technology/{id}', method: 'put', + // @ts-expect-error Swagger Types not in main ts type + bodyType: 'TechnologyUpdateBody', + responses: { + [StatusCodes.OK]: { + description: 'Technology Successfully Updated', + bodyType: 'Technology', + }, + [StatusCodes.BAD_REQUEST]: { + description: 'Invalid Request', + bodyType: 'TechnologyUpdateFormError', + }, + [StatusCodes.NOT_FOUND]: { + description: 'Technology Not Found', + }, + [StatusCodes.INTERNAL_SERVER_ERROR]: { + description: 'Server Error', + }, + }, + swaggerTags: ['Technology'], }, }, ], @@ -176,6 +241,20 @@ const serverlessConfiguration: AWS = { httpApi: { path: '/technology/{id}', method: 'delete', + // @ts-expect-error Swagger Types not in main ts type + responses: { + [StatusCodes.OK]: { + description: 'Technology Successfully Deleted', + bodyType: 'Technology', + }, + [StatusCodes.BAD_REQUEST]: { + description: 'Invalid Request', + }, + [StatusCodes.INTERNAL_SERVER_ERROR]: { + description: 'Server Error', + }, + }, + swaggerTags: ['Technology'], }, }, ], diff --git a/starters/serverless-framework-sqs-dynamodb/src/types/api-types.d.ts b/starters/serverless-framework-sqs-dynamodb/src/types/api-types.d.ts new file mode 100644 index 000000000..ebcc4fe92 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/types/api-types.d.ts @@ -0,0 +1,40 @@ +// required to be manually generated because of https://github.com/completecoding/serverless-auto-swagger/issues/45 + +export interface Technology { + id: string; + displayName: string; + description: string; + websiteUrl: string; +} + +export type Technologies = Technology[]; + +export interface TechnologyCreateBody { + displayName: string; + description: string; + websiteUrl: string; +} + +export interface TechnologyCreateFormError { + formErrors: string[]; + fieldErrors: { + displayName?: string[] | undefined; + description?: string[] | undefined; + websiteUrl?: string[] | undefined; + }; +} + +export interface TechnologyUpdateBody { + displayName?: string | undefined; + description?: string | undefined; + websiteUrl?: string | undefined; +} + +export interface TechnologyUpdateFormError { + formErrors: string[]; + fieldErrors: { + displayName?: string[] | undefined; + description?: string[] | undefined; + websiteUrl?: string[] | undefined; + }; +} From 2c7ec1e62dea6455e75a0e2b0db99a69fe624089 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sun, 1 Jan 2023 12:26:26 -0600 Subject: [PATCH 48/62] add queue examples - add job generator - add job processor - add dynamodb stream config and handlers - add sqs utils --- .../package.json | 2 + .../serverless.ts | 85 ++++++++++++++----- .../handlers/example_job_processor.test.ts | 38 +++++++++ .../src/handlers/example_job_processor.ts | 15 ++++ .../handlers/example_stream_processor.test.ts | 84 ++++++++++++++++++ .../src/handlers/example_stream_processor.ts | 22 +++++ .../src/handlers/generate_job.test.ts | 77 +++++++++++++++++ .../src/handlers/generate_job.ts | 14 +++ .../src/utils/sqs/getClient.ts | 20 +++++ .../src/utils/sqs/getQueueUrl.test.ts | 57 +++++++++++++ .../src/utils/sqs/getQueueUrl.ts | 21 +++++ .../src/utils/sqs/sendMessage.test.ts | 58 +++++++++++++ .../src/utils/sqs/sendMessage.ts | 33 +++++++ 13 files changed, 505 insertions(+), 21 deletions(-) create mode 100644 starters/serverless-framework-sqs-dynamodb/src/handlers/example_job_processor.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/handlers/example_job_processor.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/handlers/example_stream_processor.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/handlers/example_stream_processor.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/handlers/generate_job.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/handlers/generate_job.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/sqs/getClient.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/sqs/getQueueUrl.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/sqs/getQueueUrl.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/sqs/sendMessage.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/sqs/sendMessage.ts diff --git a/starters/serverless-framework-sqs-dynamodb/package.json b/starters/serverless-framework-sqs-dynamodb/package.json index cb0d82b66..84f1726eb 100644 --- a/starters/serverless-framework-sqs-dynamodb/package.json +++ b/starters/serverless-framework-sqs-dynamodb/package.json @@ -26,6 +26,7 @@ }, "dependencies": { "@aws-sdk/client-dynamodb": "^3.236.0", + "@aws-sdk/client-sqs": "^3.241.0", "@aws-sdk/util-dynamodb": "^3.238.0", "cachified": "^3.0.1", "http-status-codes": "^2.2.0", @@ -64,6 +65,7 @@ "serverless-dynamodb-local": "^0.2.40", "serverless-esbuild": "^1.34.0", "serverless-offline": "^12.0.3", + "serverless-offline-dynamodb-streams": "^6.2.3", "serverless-offline-sqs": "^7.3.2", "ts-jest": "^29.0.3", "ts-node": "^10.9.1", diff --git a/starters/serverless-framework-sqs-dynamodb/serverless.ts b/starters/serverless-framework-sqs-dynamodb/serverless.ts index 748290a21..f40dc6f69 100644 --- a/starters/serverless-framework-sqs-dynamodb/serverless.ts +++ b/starters/serverless-framework-sqs-dynamodb/serverless.ts @@ -10,6 +10,8 @@ const serverlessConfiguration: AWS = { 'serverless-esbuild', 'serverless-analyze-bundle-plugin', 'serverless-dynamodb-local', + 'serverless-offline-dynamodb-streams', + 'serverless-offline-sqs', 'serverless-offline', ], custom: { @@ -46,6 +48,15 @@ const serverlessConfiguration: AWS = { lambdaPort: 4002, reloadHandler: true, }, + 'serverless-offline-dynamodb-streams': { + apiVersion: '2015-03-31', + endpoint: 'http://0.0.0.0:8000', + region: '${aws:region}', + accessKeyId: 'root', + secretAccessKey: 'root', + skipCacheInvalidation: false, + readInterval: 500, + }, 'serverless-offline-sqs': { autoCreate: true, apiVersion: '2012-11-05', @@ -110,6 +121,11 @@ const serverlessConfiguration: AWS = { ], Resource: 'arn:aws:dynamodb:*:*:table/${param:technologiesTable}', }, + { + Effect: 'Allow', + Action: ['sqs:CreateQueue', 'sqs:SendMessage', 'sqs:GetQueueUrl'], + Resource: 'arn:aws:sqs:*:*:*', + }, ], }, }, @@ -137,14 +153,14 @@ const serverlessConfiguration: AWS = { httpApi: { path: '/technology', method: 'get', - // @ts-expect-error Swagger Types not in main ts type - responses: { - [StatusCodes.OK]: { - description: 'Fetched Technologies Successfully', - bodyType: 'Technologies', - }, - }, - swaggerTags: ['Technology'], + // // @ts-expect-error Swagger Types not in main ts type + // responses: { + // [StatusCodes.OK]: { + // description: 'Fetched Technologies Successfully', + // bodyType: 'Technologies', + // }, + // }, + // swaggerTags: ['Technology'], }, }, ], @@ -241,20 +257,38 @@ const serverlessConfiguration: AWS = { httpApi: { path: '/technology/{id}', method: 'delete', - // @ts-expect-error Swagger Types not in main ts type - responses: { - [StatusCodes.OK]: { - description: 'Technology Successfully Deleted', - bodyType: 'Technology', - }, - [StatusCodes.BAD_REQUEST]: { - description: 'Invalid Request', - }, - [StatusCodes.INTERNAL_SERVER_ERROR]: { - description: 'Server Error', - }, + generate_job: { + handler: 'src/handlers/generate_job.handler', + events: [ + { + httpApi: { + path: '/generate_job', + method: 'post', + }, + }, + ], + }, + example_job_processor: { + handler: 'src/handlers/example_job_processor.handler', + events: [ + { + sqs: { + arn: { + 'Fn::GetAtt': ['ExampleQueue', 'Arn'], + }, + }, + }, + ], + }, + example_stream_processor: { + handler: 'src/handlers/example_stream_processor.handler', + events: [ + { + stream: { + type: 'dynamodb', + arn: { + 'Fn::GetAtt': ['technologiesTable', 'StreamArn'], }, - swaggerTags: ['Technology'], }, }, ], @@ -282,6 +316,15 @@ const serverlessConfiguration: AWS = { ReadCapacityUnits: 1, WriteCapacityUnits: 1, }, + StreamSpecification: { + StreamViewType: 'NEW_AND_OLD_IMAGES', + }, + }, + }, + ExampleQueue: { + Type: 'AWS::SQS::Queue', + Properties: { + QueueName: 'ExampleQueue', }, }, }, diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/example_job_processor.test.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/example_job_processor.test.ts new file mode 100644 index 000000000..90e9a313d --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/example_job_processor.test.ts @@ -0,0 +1,38 @@ +import type { Context, Callback, SQSEvent } from 'aws-lambda'; +import { handler } from './example_job_processor'; + +describe('demo', () => { + let subject: Awaited>; + let logMock: jest.SpyInstance; + + beforeAll(async () => { + logMock = jest.spyOn(console, 'log').mockImplementation(() => ({})); + subject = await handler( + { + Records: [ + { + body: 'Hello', + }, + { + body: 'World', + }, + ], + } as SQSEvent, + {} as Context, + {} as Callback + ); + }); + + afterAll(() => { + logMock.mockReset(); + }); + + it('processes both messages', () => { + expect(console.log).toHaveBeenCalledWith('"Hello"'); + expect(console.log).toHaveBeenCalledWith('"World"'); + }); + + it('returns nothing', () => { + expect(subject).toBeUndefined(); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/example_job_processor.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/example_job_processor.ts new file mode 100644 index 000000000..0ba7a1dda --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/example_job_processor.ts @@ -0,0 +1,15 @@ +import type { SQSHandler, SQSRecord } from 'aws-lambda'; + +export const handler: SQSHandler = async (event) => { + console.log('Example Job Processor Handler initiated'); + + const recordHandler = async (record: SQSRecord) => { + console.log(JSON.stringify(record.body)); + }; + + // Ensuring we await on all the promises is super important to avoid + // accidentally killing the lambda prior to processing being completed. + await Promise.all(event.Records.map(recordHandler)); + + console.log('Example Job Processor Handler completed'); +}; diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/example_stream_processor.test.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/example_stream_processor.test.ts new file mode 100644 index 000000000..70472cc10 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/example_stream_processor.test.ts @@ -0,0 +1,84 @@ +import type { Context, Callback, DynamoDBStreamEvent } from 'aws-lambda'; +import { handler } from './example_stream_processor'; + +describe('demo', () => { + let subject: Awaited>; + let logMock: jest.SpyInstance; + + beforeAll(async () => { + logMock = jest.spyOn(console, 'log').mockImplementation(() => ({})); + subject = await handler( + { + Records: [ + { + eventName: 'INSERT', + dynamodb: { + NewImage: { + id: 1, + name: 'Test', + }, + }, + }, + { + eventName: 'MODIFY', + dynamodb: { + OldImage: { + id: 1, + name: 'Test', + }, + NewImage: { + id: 1, + name: 'Test 2', + }, + }, + }, + { + eventName: 'REMOVE', + dynamodb: { + OldImage: { + id: 1, + name: 'Test 2', + }, + }, + }, + ], + } as DynamoDBStreamEvent, + {} as Context, + {} as Callback + ); + }); + + afterAll(() => { + logMock.mockReset(); + }); + + it('process INSERT messages', () => { + expect(console.log).toHaveBeenCalledWith('Inserted Record', { + id: 1, + name: 'Test', + }); + }); + + it('process MODIFY messages', () => { + expect(console.log).toHaveBeenCalledWith('Updated Record'); + expect(console.log).toHaveBeenCalledWith('New Values', { + id: 1, + name: 'Test 2', + }); + expect(console.log).toHaveBeenCalledWith('Old Values', { + id: 1, + name: 'Test', + }); + }); + + it('process REMOVE messages', () => { + expect(console.log).toHaveBeenCalledWith('Removed Record', { + id: 1, + name: 'Test 2', + }); + }); + + it('returns nothing', () => { + expect(subject).toBeUndefined(); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/example_stream_processor.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/example_stream_processor.ts new file mode 100644 index 000000000..997bd2662 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/example_stream_processor.ts @@ -0,0 +1,22 @@ +import type { DynamoDBStreamHandler, DynamoDBRecord } from 'aws-lambda'; + +export const handler: DynamoDBStreamHandler = async (event) => { + console.log('Example Stream Processor Handler initiated'); + + const recordHandler = async (record: DynamoDBRecord) => { + if (record.eventName === 'INSERT' && record.dynamodb) { + console.log('Inserted Record', record.dynamodb.NewImage); + } else if (record.eventName === 'MODIFY' && record.dynamodb) { + console.log('Updated Record'); + console.log('New Values', record.dynamodb.NewImage); + console.log('Old Values', record.dynamodb.OldImage); + } else if (record.eventName === 'REMOVE' && record.dynamodb) { + console.log('Removed Record', record.dynamodb.OldImage); + } + }; + + // Ensuring we await on all the promises is super important to avoid + // accidentally killing the lambda prior to processing being completed. + await Promise.all(event.Records.map(recordHandler)); + console.log('Example Stream Processor Handler completed'); +}; diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/generate_job.test.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/generate_job.test.ts new file mode 100644 index 000000000..23aae86df --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/generate_job.test.ts @@ -0,0 +1,77 @@ +import type { APIGatewayProxyResult, APIGatewayProxyEvent, Context, Callback } from 'aws-lambda'; +import { sendMessage } from '@/utils/sqs/sendMessage'; +import { handler } from './generate_job'; + +jest.mock('@/utils/sqs/sendMessage'); + +describe('POST /generate_job', () => { + let subject: APIGatewayProxyResult; + let mathRandomMock: jest.SpyInstance; + + const sendMessageMock = jest.mocked(sendMessage); + + beforeAll(() => { + mathRandomMock = jest.spyOn(global.Math, 'random').mockReturnValue(0.11); + }); + + afterAll(() => { + mathRandomMock.mockRestore(); + jest.clearAllMocks(); + }); + + describe('when the message is sent successfully', () => { + beforeAll(async () => { + sendMessageMock.mockResolvedValue({ + success: true, + data: { + MessageId: '123456789', + }, + }); + subject = (await handler( + {} as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(() => { + sendMessageMock.mockClear(); + }); + + it('returns a 201 status code', () => { + expect(subject.statusCode).toBe(201); + }); + + it('returns the returned messaged', () => { + expect(JSON.parse(subject.body)).toEqual({ + MessageId: '123456789', + }); + }); + }); + + describe('when the message is not sent successfully', () => { + beforeAll(async () => { + sendMessageMock.mockResolvedValue({ + success: false, + data: 'bad request', + }); + subject = (await handler( + {} as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(() => { + sendMessageMock.mockClear(); + }); + + it('returns a 400 status code', () => { + expect(subject.statusCode).toBe(400); + }); + + it('returns the returned messaged', () => { + expect(JSON.parse(subject.body)).toEqual('bad request'); + }); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/generate_job.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/generate_job.ts new file mode 100644 index 000000000..498b20477 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/generate_job.ts @@ -0,0 +1,14 @@ +import type { APIGatewayProxyHandler } from 'aws-lambda'; +import { sendMessage } from '@/utils/sqs/sendMessage'; + +export const handler: APIGatewayProxyHandler = async () => { + const resp = await sendMessage('ExampleQueue', { + id: Math.ceil(Math.random() * 100), + message: 'Hello World!', + }); + + return { + statusCode: resp.success ? 201 : 400, + body: JSON.stringify(resp.data), + }; +}; diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/sqs/getClient.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/sqs/getClient.ts new file mode 100644 index 000000000..8762a5ca2 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/sqs/getClient.ts @@ -0,0 +1,20 @@ +import { SQSClient, SQSClientConfig } from '@aws-sdk/client-sqs'; +import { isOffline } from '@/utils/isOffline/isOffline'; + +export type QueueName = 'ExampleQueue'; + +let cachedClient: SQSClient | null = null; + +export const getClient = (): SQSClient => { + if (cachedClient) { + return cachedClient; + } + + const config: SQSClientConfig = {}; + if (isOffline()) { + config.endpoint = 'http://localhost:9324'; + } + + cachedClient = new SQSClient(config); + return cachedClient; +}; diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/sqs/getQueueUrl.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/sqs/getQueueUrl.test.ts new file mode 100644 index 000000000..bf752bcdc --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/sqs/getQueueUrl.test.ts @@ -0,0 +1,57 @@ +import { mockClient } from 'aws-sdk-client-mock'; +import { SQSClient, GetQueueUrlCommand } from '@aws-sdk/client-sqs'; +import { getQueueUrl } from './getQueueUrl'; + +describe('getQueueUrl', () => { + let subject: ReturnType | Awaited>; + let sqsMock: ReturnType; + + beforeAll(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + sqsMock = mockClient(SQSClient); + }); + + afterAll(() => { + sqsMock.restore(); + jest.restoreAllMocks(); + }); + + describe('when valid params are provided', () => { + describe('when queue is found', () => { + beforeAll(async () => { + sqsMock.on(GetQueueUrlCommand).resolves({ + QueueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/ExampleQueue', + }); + subject = await getQueueUrl('ExampleQueue'); + }); + + it('returns QueueUrl', () => { + expect(subject).toEqual('https://sqs.us-east-1.amazonaws.com/123456789012/ExampleQueue'); + }); + }); + + describe('when queue is not found', () => { + beforeAll(() => { + sqsMock.on(GetQueueUrlCommand).resolves({ + QueueUrl: undefined, + }); + subject = getQueueUrl('ExampleQueue'); + }); + + it('throws an error', async () => { + await expect(subject).rejects.toThrow('Queue not found'); + }); + }); + }); + + describe('when invalid params are provided', () => { + beforeAll(() => { + sqsMock.on(GetQueueUrlCommand).rejects('mocked rejection'); + subject = getQueueUrl('ExampleQueue'); + }); + + it('throws an exception with the error message', async () => { + await expect(subject).rejects.toThrow('mocked rejection'); + }); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/sqs/getQueueUrl.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/sqs/getQueueUrl.ts new file mode 100644 index 000000000..a9465e800 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/sqs/getQueueUrl.ts @@ -0,0 +1,21 @@ +import { GetQueueUrlCommand } from '@aws-sdk/client-sqs'; +import { getErrorMessage } from '@/utils/error/getErrorMessage'; +import { getClient, QueueName } from './getClient'; + +export const getQueueUrl = async (queue: QueueName): Promise => { + const command = new GetQueueUrlCommand({ + QueueName: queue, + }); + + try { + const { QueueUrl } = await getClient().send(command); + if (!QueueUrl) { + throw new Error('Queue not found'); + } + return QueueUrl; + } catch (error) { + const message = getErrorMessage(error); + console.error(message); + throw new Error(message); + } +}; diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/sqs/sendMessage.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/sqs/sendMessage.test.ts new file mode 100644 index 000000000..0ac68e02a --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/sqs/sendMessage.test.ts @@ -0,0 +1,58 @@ +import { mockClient } from 'aws-sdk-client-mock'; +import { SQSClient, SendMessageCommand, GetQueueUrlCommand } from '@aws-sdk/client-sqs'; +import { sendMessage } from './sendMessage'; + +describe('sendMessage', () => { + let subject: ReturnType | Awaited>; + let sqsMock: ReturnType; + + beforeAll(() => { + sqsMock = mockClient(SQSClient); + sqsMock.on(GetQueueUrlCommand).resolves({ + QueueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/test-queue', + }); + }); + + afterAll(() => { + sqsMock.restore(); + }); + + describe('when valid params are provided', () => { + let messageId: string; + + beforeAll(async () => { + messageId = '12345678-1111-2222-3333-111122223333'; + sqsMock.on(SendMessageCommand).resolves({ + MessageId: messageId, + }); + subject = await sendMessage('ExampleQueue', { + message: 'Hello World', + }); + }); + + it('sends message to queue', () => { + expect(subject).toEqual({ + success: true, + data: { + MessageId: messageId, + }, + }); + }); + }); + + describe('when invalid params are provided', () => { + beforeAll(async () => { + sqsMock.on(SendMessageCommand).rejects('mocked rejection'); + subject = await sendMessage('ExampleQueue', { + message: 'Hello World', + }); + }); + + it('does not send the message to queue', () => { + expect(subject).toEqual({ + success: false, + data: 'mocked rejection', + }); + }); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/sqs/sendMessage.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/sqs/sendMessage.ts new file mode 100644 index 000000000..a3f636afe --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/sqs/sendMessage.ts @@ -0,0 +1,33 @@ +import { SendMessageCommand } from '@aws-sdk/client-sqs'; +import { getErrorMessage } from '@/utils/error/getErrorMessage'; +import { getClient, QueueName } from './getClient'; +import { getQueueUrl } from './getQueueUrl'; + +export const sendMessage = async ( + queue: QueueName, + message: Record +): Promise<{ + success: boolean; + data: unknown; +}> => { + const client = getClient(); + const queueUrl = await getQueueUrl(queue); + const command = new SendMessageCommand({ + QueueUrl: queueUrl, + MessageBody: JSON.stringify(message), + }); + + try { + const data = await client.send(command); + return { + success: true, + data, + }; + } catch (error) { + const errMessage = getErrorMessage(error); + return { + success: false, + data: errMessage, + }; + } +}; From 858ca82655620c25ed7a14f27d074657477486db Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sun, 1 Jan 2023 12:28:35 -0600 Subject: [PATCH 49/62] update linting rules --- .../serverless-framework-sqs-dynamodb/.eslintignore | 13 +++++++++++++ .../.prettierignore | 3 +++ .../serverless-framework-sqs-dynamodb/package.json | 4 ++-- 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 starters/serverless-framework-sqs-dynamodb/.eslintignore diff --git a/starters/serverless-framework-sqs-dynamodb/.eslintignore b/starters/serverless-framework-sqs-dynamodb/.eslintignore new file mode 100644 index 000000000..9d916c36e --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/.eslintignore @@ -0,0 +1,13 @@ +# Ignore artifacts: +build +coverage +node_modules +.esbuild +.dynamodb +.redis +.serverless +swagger + +# yaml +*.yaml +*.yml diff --git a/starters/serverless-framework-sqs-dynamodb/.prettierignore b/starters/serverless-framework-sqs-dynamodb/.prettierignore index 6561b65d5..9d916c36e 100644 --- a/starters/serverless-framework-sqs-dynamodb/.prettierignore +++ b/starters/serverless-framework-sqs-dynamodb/.prettierignore @@ -2,8 +2,11 @@ build coverage node_modules +.esbuild .dynamodb +.redis .serverless +swagger # yaml *.yaml diff --git a/starters/serverless-framework-sqs-dynamodb/package.json b/starters/serverless-framework-sqs-dynamodb/package.json index 84f1726eb..6acd79e1c 100644 --- a/starters/serverless-framework-sqs-dynamodb/package.json +++ b/starters/serverless-framework-sqs-dynamodb/package.json @@ -9,6 +9,7 @@ "dynamodb", "sqs" ], + "sideEffects": false, "scripts": { "build": "sls package", "deploy": "sls deploy", @@ -70,6 +71,5 @@ "ts-jest": "^29.0.3", "ts-node": "^10.9.1", "typescript": "^4.9.4" - }, - "sideEffects": false + } } From 71449197e6275342bf4ad25bcb38f5e8f3af23f3 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sun, 1 Jan 2023 12:34:36 -0600 Subject: [PATCH 50/62] fix caching bug in update --- .../src/models/technology/update.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/starters/serverless-framework-sqs-dynamodb/src/models/technology/update.ts b/starters/serverless-framework-sqs-dynamodb/src/models/technology/update.ts index cd647657f..b90647d60 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/models/technology/update.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/models/technology/update.ts @@ -2,6 +2,7 @@ import { TechnologyUpdate } from '@/types/technology'; import { addToCache } from '@/utils/cache/addToCache'; import { putItem } from '@/utils/dynamodb/putItem'; import { get } from './get'; +import { getCacheKey } from './getCacheKey'; export const update = async (id: string, payload: TechnologyUpdate) => { const existingTechnology = await get(id); @@ -19,6 +20,6 @@ export const update = async (id: string, payload: TechnologyUpdate) => { if (!response) { return null; } - await addToCache(id, updatedTechnology); + await addToCache(getCacheKey(id), updatedTechnology); return updatedTechnology; }; From cfee8525988b2cd747f83181d00ac43199d4ecbe Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sun, 1 Jan 2023 12:34:55 -0600 Subject: [PATCH 51/62] fix autoswagger config See https://github.com/completecoding/serverless-auto-swagger/issues/16 --- .../serverless.ts | 56 +++++++++++++++---- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/starters/serverless-framework-sqs-dynamodb/serverless.ts b/starters/serverless-framework-sqs-dynamodb/serverless.ts index f40dc6f69..d21a87dd0 100644 --- a/starters/serverless-framework-sqs-dynamodb/serverless.ts +++ b/starters/serverless-framework-sqs-dynamodb/serverless.ts @@ -153,14 +153,14 @@ const serverlessConfiguration: AWS = { httpApi: { path: '/technology', method: 'get', - // // @ts-expect-error Swagger Types not in main ts type - // responses: { - // [StatusCodes.OK]: { - // description: 'Fetched Technologies Successfully', - // bodyType: 'Technologies', - // }, - // }, - // swaggerTags: ['Technology'], + // @ts-expect-error Swagger Types not in main ts type + responseData: { + [StatusCodes.OK]: { + description: 'Fetched Technologies Successfully', + bodyType: 'Technologies', + }, + }, + swaggerTags: ['Technology'], }, }, ], @@ -174,7 +174,7 @@ const serverlessConfiguration: AWS = { method: 'post', // @ts-expect-error Swagger Types not in main ts type bodyType: 'TechnologyCreateBody', - responses: { + responseData: { [StatusCodes.CREATED]: { description: 'Technology Successfully Created', bodyType: 'Technology', @@ -200,7 +200,7 @@ const serverlessConfiguration: AWS = { path: '/technology/{id}', method: 'get', // @ts-expect-error Swagger Types not in main ts type - responses: { + responseData: { [StatusCodes.OK]: { description: 'Fetched Technology Successfully', bodyType: 'Technology', @@ -229,7 +229,7 @@ const serverlessConfiguration: AWS = { method: 'put', // @ts-expect-error Swagger Types not in main ts type bodyType: 'TechnologyUpdateBody', - responses: { + responseData: { [StatusCodes.OK]: { description: 'Technology Successfully Updated', bodyType: 'Technology', @@ -257,6 +257,24 @@ const serverlessConfiguration: AWS = { httpApi: { path: '/technology/{id}', method: 'delete', + // @ts-expect-error Swagger Types not in main ts type + responseData: { + [StatusCodes.OK]: { + description: 'Technology Successfully Deleted', + bodyType: 'Technology', + }, + [StatusCodes.BAD_REQUEST]: { + description: 'Invalid Request', + }, + [StatusCodes.INTERNAL_SERVER_ERROR]: { + description: 'Server Error', + }, + }, + swaggerTags: ['Technology'], + }, + }, + ], + }, generate_job: { handler: 'src/handlers/generate_job.handler', events: [ @@ -264,6 +282,22 @@ const serverlessConfiguration: AWS = { httpApi: { path: '/generate_job', method: 'post', + // @ts-expect-error Swagger Types not in main ts type + bodyType: 'TechnologyCreateBody', + responseData: { + [StatusCodes.CREATED]: { + description: 'Technology Successfully Created', + bodyType: 'Technology', + }, + [StatusCodes.BAD_REQUEST]: { + description: 'Failed to create Technology', + bodyType: 'TechnologyCreateFormError', + }, + [StatusCodes.INTERNAL_SERVER_ERROR]: { + description: 'Server Error', + }, + }, + swaggerTags: ['Technology'], }, }, ], From f5074997a4e11661f9c04d100ed0a57ba0e34a28 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sun, 1 Jan 2023 12:47:46 -0600 Subject: [PATCH 52/62] lock deps --- .../package.json | 86 +++++++++---------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/starters/serverless-framework-sqs-dynamodb/package.json b/starters/serverless-framework-sqs-dynamodb/package.json index 6acd79e1c..bf5c53ec9 100644 --- a/starters/serverless-framework-sqs-dynamodb/package.json +++ b/starters/serverless-framework-sqs-dynamodb/package.json @@ -26,50 +26,50 @@ "infrastructure:stop": "docker compose stop" }, "dependencies": { - "@aws-sdk/client-dynamodb": "^3.236.0", - "@aws-sdk/client-sqs": "^3.241.0", - "@aws-sdk/util-dynamodb": "^3.238.0", - "cachified": "^3.0.1", - "http-status-codes": "^2.2.0", - "ioredis": "^5.2.4", - "uuid": "^9.0.0", - "zod": "^3.20.2" + "@aws-sdk/client-dynamodb": "3.241.0", + "@aws-sdk/client-sqs": "3.241.0", + "@aws-sdk/util-dynamodb": "3.241.0", + "cachified": "3.0.1", + "http-status-codes": "2.2.0", + "ioredis": "5.2.4", + "uuid": "9.0.0", + "zod": "3.20.2" }, "devDependencies": { - "@serverless/typescript": "^3.25.0", - "@types/aws-lambda": "^8.10.109", - "@types/jest": "^29.2.4", - "@types/node": "^18.11.17", - "@types/serverless": "^3.12.9", - "@types/uuid": "^9.0.0", - "@typescript-eslint/eslint-plugin": "^5.0.0", - "@typescript-eslint/parser": "^5.47.0", - "aws-sdk-client-mock": "^2.0.1", - "dotenv": "^16.0.3", - "esbuild": "^0.16.10", - "esbuild-node-externals": "^1.6.0", - "esbuild-visualizer": "^0.3.1", - "eslint": "^8.0.1", - "eslint-config-prettier": "^8.5.0", - "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.25.2", - "eslint-plugin-n": "^15.0.0", - "eslint-plugin-promise": "^6.0.0", - "i": "^0.3.7", - "ioredis-mock": "^8.2.2", - "jest": "^29.3.1", - "npm": "^9.2.0", - "prettier": "^2.8.1", - "serverless": "^3.26.0", - "serverless-analyze-bundle-plugin": "^1.2.1", - "serverless-auto-swagger": "^2.12.0", - "serverless-dynamodb-local": "^0.2.40", - "serverless-esbuild": "^1.34.0", - "serverless-offline": "^12.0.3", - "serverless-offline-dynamodb-streams": "^6.2.3", - "serverless-offline-sqs": "^7.3.2", - "ts-jest": "^29.0.3", - "ts-node": "^10.9.1", - "typescript": "^4.9.4" + "@serverless/typescript": "3.25.0", + "@types/aws-lambda": "8.10.109", + "@types/jest": "29.2.4", + "@types/node": "18.11.17", + "@types/serverless": "3.12.9", + "@types/uuid": "9.0.0", + "@typescript-eslint/eslint-plugin": "5.0.0", + "@typescript-eslint/parser": "5.47.0", + "aws-sdk-client-mock": "2.0.1", + "dotenv": "16.0.3", + "esbuild": "0.16.10", + "esbuild-node-externals": "1.6.0", + "esbuild-visualizer": "0.3.1", + "eslint": "8.0.1", + "eslint-config-prettier": "8.5.0", + "eslint-import-resolver-typescript": "3.5.2", + "eslint-plugin-import": "2.25.2", + "eslint-plugin-n": "15.0.0", + "eslint-plugin-promise": "6.0.0", + "i": "0.3.7", + "ioredis-mock": "8.2.2", + "jest": "29.3.1", + "npm": "9.2.0", + "prettier": "2.8.1", + "serverless": "3.26.0", + "serverless-analyze-bundle-plugin": "1.2.1", + "serverless-auto-swagger": "2.12.0", + "serverless-dynamodb-local": "0.2.40", + "serverless-esbuild": "1.34.0", + "serverless-offline": "12.0.3", + "serverless-offline-dynamodb-streams": "6.2.3", + "serverless-offline-sqs": "7.3.2", + "ts-jest": "29.0.3", + "ts-node": "10.9.1", + "typescript": "4.9.4" } } From 3d3d0213eaa7a236132c5b37d9ee07a1d6059590 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sun, 1 Jan 2023 13:54:13 -0600 Subject: [PATCH 53/62] update healthcheck endpoint --- .../serverless.ts | 20 ++-- .../src/handlers/healthcheck.test.ts | 104 ++++++++++++++++-- .../src/handlers/healthcheck.ts | 41 ++++++- .../src/utils/dynamodb/listTables.test.ts | 56 ++++++++++ .../src/utils/dynamodb/listTables.ts | 19 ++++ .../src/utils/sqs/listQueues.test.ts | 56 ++++++++++ .../src/utils/sqs/listQueues.ts | 19 ++++ 7 files changed, 289 insertions(+), 26 deletions(-) create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/listTables.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/listTables.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/sqs/listQueues.test.ts create mode 100644 starters/serverless-framework-sqs-dynamodb/src/utils/sqs/listQueues.ts diff --git a/starters/serverless-framework-sqs-dynamodb/serverless.ts b/starters/serverless-framework-sqs-dynamodb/serverless.ts index d21a87dd0..b2f446cc7 100644 --- a/starters/serverless-framework-sqs-dynamodb/serverless.ts +++ b/starters/serverless-framework-sqs-dynamodb/serverless.ts @@ -142,6 +142,15 @@ const serverlessConfiguration: AWS = { httpApi: { path: '/healthcheck', method: 'get', + // @ts-expect-error Swagger Types not in main ts type + responseData: { + [StatusCodes.OK]: { + description: 'All systems ready to use', + }, + [StatusCodes.SERVICE_UNAVAILABLE]: { + description: 'Systems are not operating correctly', + }, + }, }, }, ], @@ -283,21 +292,14 @@ const serverlessConfiguration: AWS = { path: '/generate_job', method: 'post', // @ts-expect-error Swagger Types not in main ts type - bodyType: 'TechnologyCreateBody', responseData: { [StatusCodes.CREATED]: { - description: 'Technology Successfully Created', - bodyType: 'Technology', + description: 'Job Generated', }, [StatusCodes.BAD_REQUEST]: { - description: 'Failed to create Technology', - bodyType: 'TechnologyCreateFormError', - }, - [StatusCodes.INTERNAL_SERVER_ERROR]: { - description: 'Server Error', + description: 'Failed to generate job', }, }, - swaggerTags: ['Technology'], }, }, ], diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.test.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.test.ts index 7eaca36b6..71477d871 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.test.ts @@ -1,22 +1,102 @@ import type { APIGatewayProxyEvent, Context, Callback, APIGatewayProxyResult } from 'aws-lambda'; +import { mockClient } from 'aws-sdk-client-mock'; +import { ListTablesCommand } from '@aws-sdk/client-dynamodb'; +import { ListQueuesCommand } from '@aws-sdk/client-sqs'; +import { Redis } from 'ioredis'; +import { getClient as getDynamodbClient } from '@/utils/dynamodb/getClient'; +import { getClient as getSqsClient } from '@/utils/sqs/getClient'; +import * as GetRedisClient from '@/utils/redis/getClient'; import { handler } from './healthcheck'; -describe('healtcheck', () => { +describe('GET /healthcheck', () => { let subject: APIGatewayProxyResult; + let ddbMock: ReturnType; + let sqsMock: ReturnType; - beforeAll(async () => { - subject = (await handler( - {} as APIGatewayProxyEvent, - {} as Context, - {} as Callback - )) as APIGatewayProxyResult; - }); + describe('when all services are functional', () => { + beforeAll(async () => { + ddbMock = mockClient(getDynamodbClient()); + sqsMock = mockClient(getSqsClient()); + + ddbMock.on(ListTablesCommand).resolves({ + TableNames: ['technologies-test'], + }); + sqsMock.on(ListQueuesCommand).resolves({ + QueueUrls: ['http://localhost:9324/000000000000/ExampleQueue'], + }); + + jest.spyOn(GetRedisClient, 'getClient').mockImplementation(async () => { + return { + status: 'ready', + } as Redis; + }); + + subject = (await handler( + {} as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(() => { + ddbMock.restore(); + sqsMock.restore(); + jest.resetAllMocks(); + }); + + it('returns a 200 statusCode', () => { + expect(subject.statusCode).toBe(200); + }); - it('returns a 200 statusCode', () => { - expect(subject.statusCode).toBe(200); + it('returns status', () => { + expect(JSON.parse(subject.body)).toEqual( + expect.objectContaining({ + dynamodbStatus: `Connected with tables: ${['technologies-test']}`, + sqsStatus: `Connected with queues: ${[ + 'http://localhost:9324/000000000000/ExampleQueue', + ]}`, + cacheRedisStatus: 'ready', + }) + ); + }); }); - it('returns a working message', () => { - expect(subject.body).toEqual('public-api is working!'); + describe('when a service is failing', () => { + beforeAll(async () => { + ddbMock = mockClient(getDynamodbClient()); + sqsMock = mockClient(getSqsClient()); + + ddbMock.on(ListTablesCommand).rejects('mock error'); + sqsMock.on(ListQueuesCommand).resolves({ + QueueUrls: ['http://localhost:9324/000000000000/ExampleQueue'], + }); + + jest.spyOn(GetRedisClient, 'getClient').mockImplementation(async () => { + return { + status: 'ready', + } as Redis; + }); + jest.spyOn(console, 'error').mockImplementation(() => {}); + + subject = (await handler( + {} as APIGatewayProxyEvent, + {} as Context, + {} as Callback + )) as APIGatewayProxyResult; + }); + + afterAll(() => { + ddbMock.restore(); + sqsMock.restore(); + jest.resetAllMocks(); + }); + + it('returns a 503 statusCode', () => { + expect(subject.statusCode).toBe(503); + }); + + it('returns status', () => { + expect(subject.body).toEqual('mock error'); + }); }); }); diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.ts index 21a073c56..04fb4f748 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/healthcheck.ts @@ -1,8 +1,39 @@ import type { APIGatewayProxyHandler } from 'aws-lambda'; +import Redis from 'ioredis'; +import { StatusCodes } from 'http-status-codes'; +import { listTables } from '@/utils/dynamodb/listTables'; +import { listQueues } from '@/utils/sqs/listQueues'; +import { getClient as getRedisClient } from '@/utils/redis/getClient'; +import { getErrorMessage } from '@/utils/error/getErrorMessage'; -export const handler: APIGatewayProxyHandler = async (/* _event, _context */) => { - return { - statusCode: 200, - body: 'public-api is working!', - }; +const checkRedisConnection = (client: Redis) => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(client.status); + }, 100); + }); +}; + +export const handler: APIGatewayProxyHandler = async () => { + try { + const [dynamodbTables, sqsQueues, cacheRedisConnected] = await Promise.all([ + listTables(), + listQueues(), + checkRedisConnection(await getRedisClient('cache', process.env.REDIS_CACHE_URL)), + ]); + + return { + statusCode: StatusCodes.OK, + body: JSON.stringify({ + dynamodbStatus: `Connected with tables: ${dynamodbTables}`, + sqsStatus: `Connected with queues: ${sqsQueues}`, + cacheRedisStatus: cacheRedisConnected, + }), + }; + } catch (err) { + return { + statusCode: StatusCodes.SERVICE_UNAVAILABLE, + body: getErrorMessage(err), + }; + } }; diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/listTables.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/listTables.test.ts new file mode 100644 index 000000000..7f32f891e --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/listTables.test.ts @@ -0,0 +1,56 @@ +import { ListTablesCommand } from '@aws-sdk/client-dynamodb'; +import { mockClient } from 'aws-sdk-client-mock'; +import { getClient } from './getClient'; +import { listTables } from './listTables'; + +describe('dynamodb.listTables()', () => { + let subject: Awaited> | ReturnType; + const ddbMock = mockClient(getClient()); + + afterAll(() => { + ddbMock.restore(); + jest.resetAllMocks(); + }); + + describe('when items are found', () => { + beforeAll(async () => { + ddbMock.on(ListTablesCommand).resolves({ + TableNames: ['Table1', 'Table2'], + }); + subject = await listTables(); + }); + + it('returns the table names', () => { + expect(subject).toEqual(['Table1', 'Table2']); + }); + }); + + describe('when items are not found', () => { + beforeAll(async () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + ddbMock.on(ListTablesCommand).resolves({ + TableNames: undefined, + }); + subject = listTables(); + }); + + it('throws error', async () => { + await expect(subject).rejects.toThrow(); + }); + }); + + describe('when error occurs', () => { + beforeAll(async () => { + ddbMock.on(ListTablesCommand).rejects('mock error'); + subject = listTables(); + }); + + it('throws error', async () => { + await expect(subject).rejects.toThrow(); + }); + + it('logs the error', () => { + expect(console.error).toHaveBeenCalledWith('dynamodb.listTables Error - mock error'); + }); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/listTables.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/listTables.ts new file mode 100644 index 000000000..bc9b8221c --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/listTables.ts @@ -0,0 +1,19 @@ +import { ListTablesCommand } from '@aws-sdk/client-dynamodb'; +import { getErrorMessage } from '@/utils/error/getErrorMessage'; +import { getClient } from './getClient'; + +export const listTables = async () => { + const command = new ListTablesCommand({}); + const client = getClient(); + + try { + const response = await client.send(command); + if (!response || !response.TableNames) { + throw new Error('Unable to get tables'); + } + return response.TableNames; + } catch (err) { + console.error(`dynamodb.listTables Error - ${getErrorMessage(err)}`); + throw err; + } +}; diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/sqs/listQueues.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/sqs/listQueues.test.ts new file mode 100644 index 000000000..d11ec76c6 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/sqs/listQueues.test.ts @@ -0,0 +1,56 @@ +import { ListQueuesCommand } from '@aws-sdk/client-sqs'; +import { mockClient } from 'aws-sdk-client-mock'; +import { getClient } from './getClient'; +import { listQueues } from './listQueues'; + +describe('dynamodb.listQueues()', () => { + let subject: Awaited> | ReturnType; + const sqsMock = mockClient(getClient()); + + afterAll(() => { + sqsMock.restore(); + jest.resetAllMocks(); + }); + + describe('when items are found', () => { + beforeAll(async () => { + sqsMock.on(ListQueuesCommand).resolves({ + QueueUrls: ['http://localhost:9324/000000000000/ExampleQueue'], + }); + subject = await listQueues(); + }); + + it('returns the queue names', () => { + expect(subject).toEqual(['http://localhost:9324/000000000000/ExampleQueue']); + }); + }); + + describe('when items are not found', () => { + beforeAll(async () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + sqsMock.on(ListQueuesCommand).resolves({ + QueueUrls: undefined, + }); + subject = listQueues(); + }); + + it('throws error', async () => { + await expect(subject).rejects.toThrow(); + }); + }); + + describe('when error occurs', () => { + beforeAll(async () => { + sqsMock.on(ListQueuesCommand).rejects('mock error'); + subject = listQueues(); + }); + + it('throws error', async () => { + await expect(subject).rejects.toThrow(); + }); + + it('logs the error', () => { + expect(console.error).toHaveBeenCalledWith('sqs.listQueues Error - mock error'); + }); + }); +}); diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/sqs/listQueues.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/sqs/listQueues.ts new file mode 100644 index 000000000..591d7f3d8 --- /dev/null +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/sqs/listQueues.ts @@ -0,0 +1,19 @@ +import { ListQueuesCommand } from '@aws-sdk/client-sqs'; +import { getErrorMessage } from '@/utils/error/getErrorMessage'; +import { getClient } from './getClient'; + +export const listQueues = async () => { + const command = new ListQueuesCommand({}); + const client = getClient(); + + try { + const response = await client.send(command); + if (!response || !response.QueueUrls) { + throw new Error('Unable to get queues'); + } + return response.QueueUrls; + } catch (err) { + console.error(`sqs.listQueues Error - ${getErrorMessage(err)}`); + throw err; + } +}; From 8a090f5aec8457f415719122418926eb9660586f Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sun, 1 Jan 2023 14:14:36 -0600 Subject: [PATCH 54/62] fix permissions --- starters/serverless-framework-sqs-dynamodb/serverless.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/starters/serverless-framework-sqs-dynamodb/serverless.ts b/starters/serverless-framework-sqs-dynamodb/serverless.ts index b2f446cc7..f9bce651e 100644 --- a/starters/serverless-framework-sqs-dynamodb/serverless.ts +++ b/starters/serverless-framework-sqs-dynamodb/serverless.ts @@ -113,6 +113,7 @@ const serverlessConfiguration: AWS = { { Effect: 'Allow', Action: [ + 'dynamodb:ListTables', 'dynamodb:Scan', 'dynamodb:Query', 'dynamodb:GetItem', @@ -123,7 +124,7 @@ const serverlessConfiguration: AWS = { }, { Effect: 'Allow', - Action: ['sqs:CreateQueue', 'sqs:SendMessage', 'sqs:GetQueueUrl'], + Action: ['sqs:ListQueues', 'sqs:CreateQueue', 'sqs:SendMessage', 'sqs:GetQueueUrl'], Resource: 'arn:aws:sqs:*:*:*', }, ], From f6f373d9e50edc9bd6beacc3c87e0c04b52099a2 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sun, 1 Jan 2023 14:15:37 -0600 Subject: [PATCH 55/62] update readme --- .../README.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/starters/serverless-framework-sqs-dynamodb/README.md b/starters/serverless-framework-sqs-dynamodb/README.md index 0329d19d9..4bb9a758b 100644 --- a/starters/serverless-framework-sqs-dynamodb/README.md +++ b/starters/serverless-framework-sqs-dynamodb/README.md @@ -123,6 +123,22 @@ yarn start **Note:** All tests are co-located with their implementation files. +### Default API Routes + +This starter kit ships with a set of RESTful APIs. All routes are served via `http://localhost:4000` when using the local development mode. + +- `GET /healthcheck` returns the status of the databases +- `GET /swagger` returns OpenAPI documentation for the API via Swagger +- `POST /generate_job` generates a job to run on the queue + +**Technology CRUD** + +- `GET /technology` returns all technologies in the database +- `POST /technology` creates a new technology record +- `GET /technology/:id` get a technology by ID +- `PUT /technology/:id` update a technology by ID +- `DELETE /technology/:id` delete a technology by ID + ## Serverless Configuration This kit uses the TypeScript option for configuration. It is type checked using the `@serverless/typescript` definitions over the DefinitelyTyped definitions because DefinitelyTyped is currently behind on its definition. However, the `@serverless/typescript` types have known issues with certain fields are noted directly in the configuration. @@ -189,6 +205,14 @@ seed: { Once defined, run `yarn db:seed` to seed your database. See https://github.com/99x/serverless-dynamodb-local#seeding-sls-dynamodb-seed for more information. +### Streams + +This kit provides a way to create DynamoDB Stream handlers. It provides an out-of-the-box example against the technology table that you can replicate for other use cases. It's great for managing subscriptions or post-commit life cycle operations for records. We utilize the [serverless-offline-dynamodb-streams](https://github.com/CoorpAcademy/serverless-plugins/tree/master/packages/serverless-offline-dynamodb-streams) plugin to provide this functionality offline. + +## SQS + +AWS Simple Queue Service (SQS) is a great way to do asynchronous jobs off the main thread of your Lambdas. This kit configures a default ExampleQueue for use with an example job generator and consumer. + ## Jest All test files are co-located with their implementations. There are no integration or e2e tests implemented for this project. All tests are implemented as unit tests. From f6ea67954a5e2e0c04857d559deec102fdeb2477 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sun, 1 Jan 2023 14:40:15 -0600 Subject: [PATCH 56/62] add new kit to website and CLI --- packages/website/src/config.tsx | 29 +++++++++++-- packages/website/src/icons/DynamoDBIcon.tsx | 32 ++++++++++++++ packages/website/src/icons/SQSIcon.tsx | 43 +++++++++++++++++++ .../src/icons/ServerlessFrameworkIcon.tsx | 18 ++++++++ packages/website/src/icons/index.ts | 5 ++- starter-kits.json | 3 +- .../package.json | 1 + 7 files changed, 125 insertions(+), 6 deletions(-) create mode 100644 packages/website/src/icons/DynamoDBIcon.tsx create mode 100644 packages/website/src/icons/SQSIcon.tsx create mode 100644 packages/website/src/icons/ServerlessFrameworkIcon.tsx diff --git a/packages/website/src/config.tsx b/packages/website/src/config.tsx index 90bde143d..a199e1363 100644 --- a/packages/website/src/config.tsx +++ b/packages/website/src/config.tsx @@ -40,6 +40,9 @@ import { ExpressIcon, PostgresIcon, TypeOrmIcon, + ServerlessFrameworkIcon, + DynamoDBIcon, + SQSIcon, } from './icons'; export interface NavItem { @@ -311,20 +314,38 @@ export const TECHNOLOGIES = [ key: 'express', name: 'Express.js', tags: ['Framework'], - Icon: (props) => + Icon: (props) => , }, { key: 'typeorm', name: 'TypeORM', tags: ['Data Management'], - Icon: (props) => + Icon: (props) => , }, { key: 'postgres', name: 'Postgres', tags: ['Data Management'], - Icon: (props) => - } + Icon: (props) => , + }, + { + key: 'serverless-framework', + name: 'Serverless Framework', + tags: ['Framework'], + Icon: (props) => , + }, + { + key: 'dynamodb', + name: 'AWS DynamoDB', + tags: ['Data Management'], + Icon: (props) => , + }, + { + key: 'sqs', + name: 'AWS SQS', + tags: ['Data Queue'], + Icon: (props) => , + }, ]; export const SPONSORS_ICON = [ diff --git a/packages/website/src/icons/DynamoDBIcon.tsx b/packages/website/src/icons/DynamoDBIcon.tsx new file mode 100644 index 000000000..f530f59fd --- /dev/null +++ b/packages/website/src/icons/DynamoDBIcon.tsx @@ -0,0 +1,32 @@ +import { Props } from './types'; + +export function DynamoDBIcon({ className }: Props) { + return ( + + + + + + + + ); +} diff --git a/packages/website/src/icons/SQSIcon.tsx b/packages/website/src/icons/SQSIcon.tsx new file mode 100644 index 000000000..c1dfec438 --- /dev/null +++ b/packages/website/src/icons/SQSIcon.tsx @@ -0,0 +1,43 @@ +import { Props } from './types'; + +export function SQSIcon({ className }: Props) { + return ( + + + + + + + + + + ); +} diff --git a/packages/website/src/icons/ServerlessFrameworkIcon.tsx b/packages/website/src/icons/ServerlessFrameworkIcon.tsx new file mode 100644 index 000000000..f5d0dadab --- /dev/null +++ b/packages/website/src/icons/ServerlessFrameworkIcon.tsx @@ -0,0 +1,18 @@ +import { Props } from './types'; + +export function ServerlessFrameworkIcon({ className }: Props) { + return ( + + + + ); +} diff --git a/packages/website/src/icons/index.ts b/packages/website/src/icons/index.ts index 2acfab513..4f04ebf8c 100644 --- a/packages/website/src/icons/index.ts +++ b/packages/website/src/icons/index.ts @@ -38,7 +38,10 @@ export { ShareIcon } from './ShareIcon'; export { LinkedinIcon } from './LinkedinIcon'; export { QwikIcon } from './QwikIcon'; export { SolidJsIcon } from './SolidJsIcon'; -export { DenoIcon } from './DenoIcon.tsx'; +export { DenoIcon } from './DenoIcon'; export { ExpressIcon } from './ExpressIcon'; export { PostgresIcon } from './PostgresIcon'; export { TypeOrmIcon } from './TypeOrmIcon'; +export { ServerlessFrameworkIcon } from './ServerlessFrameworkIcon'; +export { DynamoDBIcon } from './DynamoDBIcon'; +export { SQSIcon } from './SQSIcon'; diff --git a/starter-kits.json b/starter-kits.json index c7738e393..53d233046 100644 --- a/starter-kits.json +++ b/starter-kits.json @@ -10,5 +10,6 @@ "solidjs-tailwind": "SolidJs and TailwindCSS", "angular-ngrx-scss": "Angular, NgRx, and SCSS", "deno-oak-denodb": "Deno, Oak, and DenoDB", - "express-typeorm-postgres": "Express.js, TypeOrm, and PostgreSQL" + "express-typeorm-postgres": "Express.js, TypeOrm, and PostgreSQL", + "serverless-framework-sqs-dynamodb": "Serverless Framework, AWS SQS, and AWS DynamoDB" } diff --git a/starters/serverless-framework-sqs-dynamodb/package.json b/starters/serverless-framework-sqs-dynamodb/package.json index bf5c53ec9..5f61599cd 100644 --- a/starters/serverless-framework-sqs-dynamodb/package.json +++ b/starters/serverless-framework-sqs-dynamodb/package.json @@ -10,6 +10,7 @@ "sqs" ], "sideEffects": false, + "hasShowcase": false, "scripts": { "build": "sls package", "deploy": "sls deploy", From c54d8599dec1986ec04c54c70d05904bb10bdb5d Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sun, 15 Jan 2023 13:50:59 -0600 Subject: [PATCH 57/62] update docs to use npm by default --- .../README.md | 22 +++++++++---------- .../serverless.ts | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/starters/serverless-framework-sqs-dynamodb/README.md b/starters/serverless-framework-sqs-dynamodb/README.md index 4bb9a758b..ca21415af 100644 --- a/starters/serverless-framework-sqs-dynamodb/README.md +++ b/starters/serverless-framework-sqs-dynamodb/README.md @@ -58,7 +58,7 @@ git clone https://github.com/thisdot/starter.dev.git ### Getting Started -This README uses `yarn` for commands. If you're using `npm` or `pnpm`, utilize the equivalent version of the commands. +This README uses `npm` for commands. If you're using `yarn` or `pnpm`, utilize the equivalent version of the commands. 1. Create a `.env` file: @@ -66,24 +66,24 @@ This README uses `yarn` for commands. If you're using `npm` or `pnpm`, utilize t cp .env.example .env ``` -2. Run `yarn` to install deps +2. Run `npm i` to install deps 3. Standup the project infrastructure using docker via: ```bash -yarn infrastructure:up +npm run infrastructure:up ``` 4. Sync database tables and seed the project via: ```bash -yarn db:sync -yarn db:seed +npm run db:sync +npm run db:seed ``` 5. Start the local development server: ```bash -yarn start +npm start ``` 6. Make changes and enjoy building your new backend! @@ -96,7 +96,7 @@ yarn start - `deploy` ships the project to the configured AWS account using the Serverless Framework CLI command. - `start` runs the `serverless-offline` provided server for local development and testing. Be sure to have the local docker infrastructure running to emulate the related services. - `test` runs `jest` under the hood. -- `lint` runs `eslint` under the hood. You can use all the eslint available command line arguments. To lint the entire project, run `yarn lint .`, or equivalent. You can affix `--fix` to auto-correct linting issues that eslint can handle. +- `lint` runs `eslint` under the hood. You can use all the eslint available command line arguments. To lint the entire project, run `npm run lint .`, or equivalent. You can affix `--fix` to auto-correct linting issues that eslint can handle. - `format:check` runs prettier format checking on all project files. - `format:write` runs prettier format writing on all project files. @@ -165,11 +165,11 @@ This project comes configured with 3 default stages: `dev`, `staging`, `producti #### esbuild -This project uses [serverless-esbuild](https://www.npmjs.com/package/serverless-esbuild) over its webpack counterpart. The esbuild tool chain is generally faster and requires less dependencies to work out of the box. +This project uses [serverless-esbuild](https://www.npmjs.com/package/serverless-esbuild) over its webpack counterpart. The esbuild tool chain is generally faster and requires less dependencies to work out of the box. Currently, the tooling is configured to use `npm`. If you wish to use `yarn` or `pnpm`, please change the `packager` field in the configuration accordingly. #### Bundle Analyzer -This project ships with the [serverless-analyze-bundle-plugin](https://www.npmjs.com/package/serverless-analyze-bundle-plugin) to allow you to visualize your Lambda bundles. This is an especially important factor when dealing with cold starts and should be monitored. To analyze a function bundle, run `yarn build --analyze `. This will give you a visualization of your function's dependencies and their sizes. +This project ships with the [serverless-analyze-bundle-plugin](https://www.npmjs.com/package/serverless-analyze-bundle-plugin) to allow you to visualize your Lambda bundles. This is an especially important factor when dealing with cold starts and should be monitored. To analyze a function bundle, run `npm run build --analyze `. This will give you a visualization of your function's dependencies and their sizes. #### File Patterns @@ -183,7 +183,7 @@ To help manage the database locally, the [`aaronshaf/dynamodb-admin`](https://gi ### Defining New Tables -To create a new table, define it via the `serverless.ts` resources section. It utilizes the [DynamoDB Cloudformation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html) to define data. Table names should be defined per environment through the Serverless `params` config. Once your table is defined, use the `yarn db:sync` command to create the table. +To create a new table, define it via the `serverless.ts` resources section. It utilizes the [DynamoDB Cloudformation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html) to define data. Table names should be defined per environment through the Serverless `params` config. Once your table is defined, use the `npm run db:sync` command to create the table. ### Defining New Seeds @@ -203,7 +203,7 @@ seed: { } ``` -Once defined, run `yarn db:seed` to seed your database. See https://github.com/99x/serverless-dynamodb-local#seeding-sls-dynamodb-seed for more information. +Once defined, run `npm run db:seed` to seed your database. See https://github.com/99x/serverless-dynamodb-local#seeding-sls-dynamodb-seed for more information. ### Streams diff --git a/starters/serverless-framework-sqs-dynamodb/serverless.ts b/starters/serverless-framework-sqs-dynamodb/serverless.ts index f9bce651e..6c2057744 100644 --- a/starters/serverless-framework-sqs-dynamodb/serverless.ts +++ b/starters/serverless-framework-sqs-dynamodb/serverless.ts @@ -37,7 +37,7 @@ const serverlessConfiguration: AWS = { }, }, 'esbuild': { - packager: 'yarn', + packager: 'npm', plugins: './esbuild-plugins.ts', bundle: true, minify: true, From b33cd7bdf8d649c5eee368ddb0f70e629f6daab7 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sun, 15 Jan 2023 13:51:22 -0600 Subject: [PATCH 58/62] update processors to optimize record handler instantiation --- .../src/handlers/example_job_processor.ts | 8 +++---- .../src/handlers/example_stream_processor.ts | 24 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/example_job_processor.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/example_job_processor.ts index 0ba7a1dda..e2afd8f5f 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/handlers/example_job_processor.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/example_job_processor.ts @@ -1,12 +1,12 @@ import type { SQSHandler, SQSRecord } from 'aws-lambda'; +const recordHandler = async (record: SQSRecord) => { + console.log(JSON.stringify(record.body)); +}; + export const handler: SQSHandler = async (event) => { console.log('Example Job Processor Handler initiated'); - const recordHandler = async (record: SQSRecord) => { - console.log(JSON.stringify(record.body)); - }; - // Ensuring we await on all the promises is super important to avoid // accidentally killing the lambda prior to processing being completed. await Promise.all(event.Records.map(recordHandler)); diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/example_stream_processor.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/example_stream_processor.ts index 997bd2662..8c14eb95b 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/handlers/example_stream_processor.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/example_stream_processor.ts @@ -1,20 +1,20 @@ import type { DynamoDBStreamHandler, DynamoDBRecord } from 'aws-lambda'; +const recordHandler = async (record: DynamoDBRecord) => { + if (record.eventName === 'INSERT' && record.dynamodb) { + console.log('Inserted Record', record.dynamodb.NewImage); + } else if (record.eventName === 'MODIFY' && record.dynamodb) { + console.log('Updated Record'); + console.log('New Values', record.dynamodb.NewImage); + console.log('Old Values', record.dynamodb.OldImage); + } else if (record.eventName === 'REMOVE' && record.dynamodb) { + console.log('Removed Record', record.dynamodb.OldImage); + } +}; + export const handler: DynamoDBStreamHandler = async (event) => { console.log('Example Stream Processor Handler initiated'); - const recordHandler = async (record: DynamoDBRecord) => { - if (record.eventName === 'INSERT' && record.dynamodb) { - console.log('Inserted Record', record.dynamodb.NewImage); - } else if (record.eventName === 'MODIFY' && record.dynamodb) { - console.log('Updated Record'); - console.log('New Values', record.dynamodb.NewImage); - console.log('Old Values', record.dynamodb.OldImage); - } else if (record.eventName === 'REMOVE' && record.dynamodb) { - console.log('Removed Record', record.dynamodb.OldImage); - } - }; - // Ensuring we await on all the promises is super important to avoid // accidentally killing the lambda prior to processing being completed. await Promise.all(event.Records.map(recordHandler)); From 5012a6ac68d487c0629326c13139f015c62631d0 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sun, 15 Jan 2023 15:29:31 -0600 Subject: [PATCH 59/62] address PR feedback --- .../serverless-framework-sqs-dynamodb/.env.example | 2 ++ starters/serverless-framework-sqs-dynamodb/README.md | 10 ++++++---- .../serverless-framework-sqs-dynamodb/serverless.ts | 3 ++- .../src/handlers/example_job_processor.test.ts | 2 +- .../src/handlers/example_stream_processor.test.ts | 5 ++++- .../src/handlers/example_stream_processor.ts | 10 +++++++--- .../src/handlers/generate_job.ts | 3 ++- .../src/handlers/technology_create.test.ts | 5 ----- .../src/models/technology/getAll.ts | 3 +-- .../src/types/environment.d.ts | 1 + .../src/utils/cache/constants.ts | 2 +- .../src/utils/dynamodb/deleteItem.test.ts | 1 + .../src/utils/dynamodb/deleteItem.ts | 1 + .../src/utils/isOffline/isOffline.ts | 2 +- 14 files changed, 30 insertions(+), 20 deletions(-) diff --git a/starters/serverless-framework-sqs-dynamodb/.env.example b/starters/serverless-framework-sqs-dynamodb/.env.example index 65240b684..bbef04e5a 100644 --- a/starters/serverless-framework-sqs-dynamodb/.env.example +++ b/starters/serverless-framework-sqs-dynamodb/.env.example @@ -2,3 +2,5 @@ IS_OFFLINE=true # connection info for Redis used for caching REDIS_CACHE_URL=redis://:sOmE_sEcUrE_pAsS@localhost:6379 +# how long should items last in the cache by default +DEFAULT_CACHE_TIME=300000 diff --git a/starters/serverless-framework-sqs-dynamodb/README.md b/starters/serverless-framework-sqs-dynamodb/README.md index ca21415af..a4f1a17ad 100644 --- a/starters/serverless-framework-sqs-dynamodb/README.md +++ b/starters/serverless-framework-sqs-dynamodb/README.md @@ -60,7 +60,7 @@ git clone https://github.com/thisdot/starter.dev.git This README uses `npm` for commands. If you're using `yarn` or `pnpm`, utilize the equivalent version of the commands. -1. Create a `.env` file: +1. Create a `.env` file. This is to support any variable in the Serverless Configuration being read from `env:` and test running: ```bash cp .env.example .env @@ -92,11 +92,11 @@ npm start ### General Commands -- `build` bundles the project using the serverless packaging serverless. The produced artifacts will ship bundles shipped to AWS on deployment. You can optionally pass `--analyze ` to run the bundle analyzer and visualize the results to understand your handler bundles. +- `build` bundles the project using the Serverless Framework's out of the box `package` command. The produced artifacts will be shipped to AWS on deployment. You can optionally pass `--analyze ` to run the bundle analyzer and visualize the results to understand your handler bundles. - `deploy` ships the project to the configured AWS account using the Serverless Framework CLI command. - `start` runs the `serverless-offline` provided server for local development and testing. Be sure to have the local docker infrastructure running to emulate the related services. - `test` runs `jest` under the hood. -- `lint` runs `eslint` under the hood. You can use all the eslint available command line arguments. To lint the entire project, run `npm run lint .`, or equivalent. You can affix `--fix` to auto-correct linting issues that eslint can handle. +- `lint` runs `eslint` under the hood. You can use all the available eslint command line arguments. To lint the entire project, run `npm run lint .`, or equivalent. You can affix `--fix` to auto-correct linting issues that eslint can handle. - `format:check` runs prettier format checking on all project files. - `format:write` runs prettier format writing on all project files. @@ -141,7 +141,7 @@ This starter kit ships with a set of RESTful APIs. All routes are served via `ht ## Serverless Configuration -This kit uses the TypeScript option for configuration. It is type checked using the `@serverless/typescript` definitions over the DefinitelyTyped definitions because DefinitelyTyped is currently behind on its definition. However, the `@serverless/typescript` types have known issues with certain fields are noted directly in the configuration. +This kit uses the TypeScript option for configuration. It is type checked using the `@serverless/typescript` definitions over the DefinitelyTyped definitions because DefinitelyTyped is currently behind on its definition. However, the `@serverless/typescript` types have known issues with certain fields, which are noted directly in the configuration. It is not compatible with the automated CI/CD of the Serverless Dashboard as they only support the default YAML format. You can read more about [setting up CI/CD using GitHub for this project on the This Dot blog](https://www.thisdot.co/blog/github-actions-for-serverless-framework-deployments). @@ -227,6 +227,8 @@ There is an existing [DynamoDB Local preset for Jest](https://github.com/shelfio ## Deployment + + As a serverless implementation, most of the infrastructure will be deployed and configured correctly simply utilizing the `deploy` script provided by this kit which is just an alias for [`serverless deploy`](https://www.serverless.com/framework/docs/providers/aws/cli-reference/deploy). However, the Redis instance is not configurable via the Serverless Configuration and will need to be set up ahead of your first deploy and configured via environment variables. We recommend using [Serverless Framework's interface for AWS Secret Manager](https://www.serverless.com/blog/aws-secrets-management/) for security purposes. This entire stack can be deployed via CI tools such as GitHub Actions, CircleCI, etc. and is our recommended approach as this kit is incompatible with the Serverless Dashboard. The Serverless Dashboard CI only works with the configuration in the YAML format which we do not use to give developers type-safety in the config file. diff --git a/starters/serverless-framework-sqs-dynamodb/serverless.ts b/starters/serverless-framework-sqs-dynamodb/serverless.ts index 6c2057744..413c424cb 100644 --- a/starters/serverless-framework-sqs-dynamodb/serverless.ts +++ b/starters/serverless-framework-sqs-dynamodb/serverless.ts @@ -98,8 +98,9 @@ const serverlessConfiguration: AWS = { environment: { REGION: '${aws:region}', SLS_STAGE: '${sls:stage}', - // DynamoDB Tables + DEFAULT_CACHE_TIME: '${env:DEFAULT_CACHE_TIME}', REDIS_CACHE_URL: '${env:REDIS_CACHE_URL}', + // DynamoDB Tables TECHNOLOGIES_TABLE: '${param:technologiesTable}', }, iam: { diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/example_job_processor.test.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/example_job_processor.test.ts index 90e9a313d..16981912a 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/handlers/example_job_processor.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/example_job_processor.test.ts @@ -6,7 +6,7 @@ describe('demo', () => { let logMock: jest.SpyInstance; beforeAll(async () => { - logMock = jest.spyOn(console, 'log').mockImplementation(() => ({})); + logMock = jest.spyOn(console, 'log').mockImplementation(() => {}); subject = await handler( { Records: [ diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/example_stream_processor.test.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/example_stream_processor.test.ts index 70472cc10..64763354d 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/handlers/example_stream_processor.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/example_stream_processor.test.ts @@ -6,10 +6,13 @@ describe('demo', () => { let logMock: jest.SpyInstance; beforeAll(async () => { - logMock = jest.spyOn(console, 'log').mockImplementation(() => ({})); + logMock = jest.spyOn(console, 'log').mockImplementation(() => {}); subject = await handler( { Records: [ + { + eventName: 'UNKNOWN', + }, { eventName: 'INSERT', dynamodb: { diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/example_stream_processor.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/example_stream_processor.ts index 8c14eb95b..c6e6cdf7d 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/handlers/example_stream_processor.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/example_stream_processor.ts @@ -1,13 +1,17 @@ import type { DynamoDBStreamHandler, DynamoDBRecord } from 'aws-lambda'; const recordHandler = async (record: DynamoDBRecord) => { - if (record.eventName === 'INSERT' && record.dynamodb) { + if (!record.dynamodb) { + return; + } + + if (record.eventName === 'INSERT') { console.log('Inserted Record', record.dynamodb.NewImage); - } else if (record.eventName === 'MODIFY' && record.dynamodb) { + } else if (record.eventName === 'MODIFY') { console.log('Updated Record'); console.log('New Values', record.dynamodb.NewImage); console.log('Old Values', record.dynamodb.OldImage); - } else if (record.eventName === 'REMOVE' && record.dynamodb) { + } else if (record.eventName === 'REMOVE') { console.log('Removed Record', record.dynamodb.OldImage); } }; diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/generate_job.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/generate_job.ts index 498b20477..5deed8fdc 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/handlers/generate_job.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/generate_job.ts @@ -1,4 +1,5 @@ import type { APIGatewayProxyHandler } from 'aws-lambda'; +import { StatusCodes } from 'http-status-codes'; import { sendMessage } from '@/utils/sqs/sendMessage'; export const handler: APIGatewayProxyHandler = async () => { @@ -8,7 +9,7 @@ export const handler: APIGatewayProxyHandler = async () => { }); return { - statusCode: resp.success ? 201 : 400, + statusCode: resp.success ? StatusCodes.CREATED : StatusCodes.BAD_REQUEST, body: JSON.stringify(resp.data), }; }; diff --git a/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_create.test.ts b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_create.test.ts index 01fea1a71..f4eb77e23 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_create.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/handlers/technology_create.test.ts @@ -2,9 +2,7 @@ import type { APIGatewayProxyResult, APIGatewayProxyEvent, Context, Callback } f import { PutItemCommand, ServiceInputTypes, ServiceOutputTypes } from '@aws-sdk/client-dynamodb'; import { AwsStub, mockClient } from 'aws-sdk-client-mock'; import { getClient } from '@/utils/dynamodb/getClient'; -import { removeFromCache } from '@/utils/cache/removeFromCache'; import * as technologyCreate from '@/models/technology/create'; -import { getCacheKey } from '@/models/technology/getCacheKey'; import { handler } from './technology_create'; describe('POST /technology', () => { @@ -33,7 +31,6 @@ describe('POST /technology', () => { afterAll(async () => { ddbMock.restore(); - await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); }); it('returns 201 status', () => { @@ -63,7 +60,6 @@ describe('POST /technology', () => { afterAll(async () => { jest.restoreAllMocks(); - await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); }); it('returns 400 status', () => { @@ -97,7 +93,6 @@ describe('POST /technology', () => { afterAll(async () => { jest.restoreAllMocks(); - await removeFromCache(getCacheKey('87af19b1-aa0d-4178-a30c-2fa8cd1f2cff')); }); it('returns 500 status', () => { diff --git a/starters/serverless-framework-sqs-dynamodb/src/models/technology/getAll.ts b/starters/serverless-framework-sqs-dynamodb/src/models/technology/getAll.ts index 418cebea2..943d98455 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/models/technology/getAll.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/models/technology/getAll.ts @@ -1,6 +1,5 @@ import { scan } from '@/utils/dynamodb/scan'; export const getAll = async () => { - const items = await scan(process.env.TECHNOLOGIES_TABLE); - return items; + return await scan(process.env.TECHNOLOGIES_TABLE); }; diff --git a/starters/serverless-framework-sqs-dynamodb/src/types/environment.d.ts b/starters/serverless-framework-sqs-dynamodb/src/types/environment.d.ts index d022fbe97..ecffbdf71 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/types/environment.d.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/types/environment.d.ts @@ -4,6 +4,7 @@ declare global { REGION: string; SLS_STAGE: string; + DEFAULT_CACHE_TIME: string; REDIS_CACHE_URL: string; TECHNOLOGIES_TABLE: string; } diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/constants.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/constants.ts index fa92b08fa..868d5ab0f 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/cache/constants.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/cache/constants.ts @@ -1 +1 @@ -export const DEFAULT_CACHE_TIME = 300_000; // 5 minutes = 1000 ms/s * 60 s/min * 5 min +export const DEFAULT_CACHE_TIME = parseInt(process.env.DEFAULT_CACHE_TIME, 10); diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.test.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.test.ts index c49a88324..93cad37ba 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.test.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.test.ts @@ -42,6 +42,7 @@ describe('dynamodb.deleteItem()', () => { describe('when item does not exist', () => { beforeAll(async () => { + jest.spyOn(console, 'warn').mockImplementation(() => {}); jest.spyOn(console, 'error').mockImplementation(() => {}); ddbMock.on(DeleteItemCommand).resolves({ Attributes: undefined, diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.ts index be56381a0..92eee28d8 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/dynamodb/deleteItem.ts @@ -15,6 +15,7 @@ export const deleteItem = async (tableName: string, key: Record const response = await client.send(command); if (!response || !response.Attributes) { + console.warn('dynamodb.deleteItem Warning - ${response}'); return null; } return unmarshall(response.Attributes); diff --git a/starters/serverless-framework-sqs-dynamodb/src/utils/isOffline/isOffline.ts b/starters/serverless-framework-sqs-dynamodb/src/utils/isOffline/isOffline.ts index fe226e57c..c0ba0549f 100644 --- a/starters/serverless-framework-sqs-dynamodb/src/utils/isOffline/isOffline.ts +++ b/starters/serverless-framework-sqs-dynamodb/src/utils/isOffline/isOffline.ts @@ -1,5 +1,5 @@ /** - * Utility function for checking where functions are being run locally via serverless offline + * Utility function for checking if functions are being run locally via serverless offline * or if they're running on infrastructure. Helpful for detecting which connection string to use. * * @returns boolean are we running locally or on infra? From c960a81dc58878c13d969e7be28f28fce71bf787 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Sun, 15 Jan 2023 15:32:47 -0600 Subject: [PATCH 60/62] add notes about aws --- starters/serverless-framework-sqs-dynamodb/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/starters/serverless-framework-sqs-dynamodb/README.md b/starters/serverless-framework-sqs-dynamodb/README.md index a4f1a17ad..c797da948 100644 --- a/starters/serverless-framework-sqs-dynamodb/README.md +++ b/starters/serverless-framework-sqs-dynamodb/README.md @@ -227,7 +227,7 @@ There is an existing [DynamoDB Local preset for Jest](https://github.com/shelfio ## Deployment - +This kit deploys to AWS and its serverless offering. As such, you will need to setup your system with the AWS CLI. You can read more about that in the [official AWS docs](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html). Take note to the section on profiles. This kit assumes it will use the `default` profile out of the box but can be configured as noted above in the [`profile`](#profile) section. As a serverless implementation, most of the infrastructure will be deployed and configured correctly simply utilizing the `deploy` script provided by this kit which is just an alias for [`serverless deploy`](https://www.serverless.com/framework/docs/providers/aws/cli-reference/deploy). However, the Redis instance is not configurable via the Serverless Configuration and will need to be set up ahead of your first deploy and configured via environment variables. We recommend using [Serverless Framework's interface for AWS Secret Manager](https://www.serverless.com/blog/aws-secrets-management/) for security purposes. From d3c47ba77eca5503cf190dc084b79b8b7fa98286 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Tue, 17 Jan 2023 10:09:29 -0600 Subject: [PATCH 61/62] fix docker refernces in readme --- starters/serverless-framework-sqs-dynamodb/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/starters/serverless-framework-sqs-dynamodb/README.md b/starters/serverless-framework-sqs-dynamodb/README.md index c797da948..2a906e15c 100644 --- a/starters/serverless-framework-sqs-dynamodb/README.md +++ b/starters/serverless-framework-sqs-dynamodb/README.md @@ -107,10 +107,10 @@ npm start ### Infrastructure Commands -- `infrastructure:up` creates docker container and related images and runs them in the background. This should only be needed once during initial setup. -- `infrastructure:down` deletes the docker container and related images. -- `infrastructure:start` starts the docker container. -- `infrastructure:stop` stops the docker container. +- `infrastructure:up` creates docker containers and runs them in the background. This should only be needed once during initial setup. +- `infrastructure:down` deletes the docker containers. +- `infrastructure:start` starts the docker containers. +- `infrastructure:stop` stops the docker containers. ## Project Structure From fea89098f25984bc4366422974969095fa5414d4 Mon Sep 17 00:00:00 2001 From: Dustin Goodman Date: Wed, 18 Jan 2023 08:21:30 -0600 Subject: [PATCH 62/62] fix analyzer command --- starters/serverless-framework-sqs-dynamodb/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/starters/serverless-framework-sqs-dynamodb/README.md b/starters/serverless-framework-sqs-dynamodb/README.md index 2a906e15c..04e9932cd 100644 --- a/starters/serverless-framework-sqs-dynamodb/README.md +++ b/starters/serverless-framework-sqs-dynamodb/README.md @@ -169,7 +169,7 @@ This project uses [serverless-esbuild](https://www.npmjs.com/package/serverless- #### Bundle Analyzer -This project ships with the [serverless-analyze-bundle-plugin](https://www.npmjs.com/package/serverless-analyze-bundle-plugin) to allow you to visualize your Lambda bundles. This is an especially important factor when dealing with cold starts and should be monitored. To analyze a function bundle, run `npm run build --analyze `. This will give you a visualization of your function's dependencies and their sizes. +This project ships with the [serverless-analyze-bundle-plugin](https://www.npmjs.com/package/serverless-analyze-bundle-plugin) to allow you to visualize your Lambda bundles. This is an especially important factor when dealing with cold starts and should be monitored. To analyze a function bundle, run `npm run build -- --analyze `. This will give you a visualization of your function's dependencies and their sizes. #### File Patterns