diff --git a/product-service/bin/aws-shop-nodejs-back.ts b/product-service/bin/aws-shop-nodejs-back.ts index 722c5cf..d56d7b1 100644 --- a/product-service/bin/aws-shop-nodejs-back.ts +++ b/product-service/bin/aws-shop-nodejs-back.ts @@ -1,23 +1,9 @@ -#!/usr/bin/env node import 'source-map-support/register'; -import * as cdk from 'aws-cdk-lib'; +import { App } from 'aws-cdk-lib'; import { AwsShopNodejsBackStack } from '../lib/aws-shop-nodejs-back-stack'; -const app = new cdk.App(); +const app = new App(); new AwsShopNodejsBackStack(app, { - /* If you don't specify 'env', this stack will be environment-agnostic. - * Account/Region-dependent features and context lookups will not work, - * but a single synthesized template can be deployed anywhere. */ - - /* Uncomment the next line to specialize this stack for the AWS Account - * and Region that are implied by the current CLI configuration. */ - // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, - - /* Uncomment the next line if you know exactly what Account and Region you - * want to deploy the stack to. */ - // env: { account: '123456789012', region: 'us-east-1' }, - - /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ description: "This stack includes resources needed to deploy aws-shop-backend application" }); \ No newline at end of file diff --git a/product-service/jest.config.js b/product-service/jest.config.js index 311c069..2589aa9 100644 --- a/product-service/jest.config.js +++ b/product-service/jest.config.js @@ -4,6 +4,6 @@ module.exports = { transform: { '^.+\\.tsx?$': 'ts-jest', }, - testMatch: ['/src/**/*.test.(ts|tsx)'], + testMatch: ['/test/*.test.(ts|tsx)'], roots: [''], }; diff --git a/product-service/lib/aws-shop-nodejs-back-stack.ts b/product-service/lib/aws-shop-nodejs-back-stack.ts index 4e4fe18..ac64dad 100644 --- a/product-service/lib/aws-shop-nodejs-back-stack.ts +++ b/product-service/lib/aws-shop-nodejs-back-stack.ts @@ -1,44 +1,86 @@ -import * as cdk from 'aws-cdk-lib'; -import * as lambda from "aws-cdk-lib/aws-lambda"; -import { HttpApi, CorsHttpMethod, HttpMethod, ParameterMapping, MappingValue } from "@aws-cdk/aws-apigatewayv2-alpha"; -import { HttpLambdaIntegration } from "@aws-cdk/aws-apigatewayv2-integrations-alpha"; import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import { Runtime } from "aws-cdk-lib/aws-lambda"; +import { Policy, PolicyStatement } from "aws-cdk-lib/aws-iam"; +import * as ec2 from 'aws-cdk-lib/aws-ec2'; +import * as rds from 'aws-cdk-lib/aws-rds'; +import * as core from 'aws-cdk-lib/core'; +import { + HttpApi, + CorsHttpMethod, + HttpMethod, + ParameterMapping, + MappingValue +} from "@aws-cdk/aws-apigatewayv2-alpha"; +import { HttpLambdaIntegration } from "@aws-cdk/aws-apigatewayv2-integrations-alpha"; import { NodejsFunction, NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-nodejs'; -import * as dotenv from 'dotenv'; +import { config as envConfig } from 'dotenv'; -dotenv.config(); -console.log(process.env) +envConfig(); +export class AwsShopNodejsBackStack extends Stack { + private getEnvironment() { + return { + TABLE_PRODUCTS: process.env.DB_TABLE_PRODUCTS!, + TABLE_STOCKS: process.env.DB_TABLE_STOCKS!, + USE_NOSQL_DB: process.env.USE_NOSQL_DB!, + ...process.env.USE_NOSQL_DB === 'true' ? {} : { + PGHOST: process.env.PGHOST!, + PGPORT: process.env.PGPORT!, + PGDATABASE: process.env.PGDATABASE!, + PGUSER: process.env.PGUSER!, + PGPASSWORD: process.env.PGPASSWORD!, + } + } + } -export class AwsShopNodejsBackStack extends cdk.Stack { - constructor(scope: Construct, props?: cdk.StackProps) { - const APP_PREFIX = "bw-aws-shop-backend"; + constructor(scope: Construct, props?: StackProps) { + const APP_PREFIX = "bw-aws-shop-backnd"; super(scope, `${APP_PREFIX}-stack`, props); + const lambdaPolicy = new Policy(this, `${APP_PREFIX}-dynamodb-read-policy`, { + statements: [ + new PolicyStatement({ + actions: [ + "dynamodb:Scan", + "dynamodb:Query", + "dynamodb:PutItem", + ], + resources: [ + `arn:aws:dynamodb:*:*:table/${process.env.DB_TABLE_PRODUCTS}`, + `arn:aws:dynamodb:*:*:table/${process.env.DB_TABLE_STOCKS}` + ], + }), + ], + }); + const sharedProps: Partial = { - entry: './src/index.ts', - runtime: lambda.Runtime.NODEJS_18_X, + entry: './src/handlers/index.ts', + runtime: Runtime.NODEJS_18_X, + environment: this.getEnvironment(), }; - const getProductList = new NodejsFunction(this, `${APP_PREFIX}-get-product-list-lambda`, { + const getProductListLambda = new NodejsFunction(this, `${APP_PREFIX}-get-product-list-lambda`, { ...sharedProps, functionName: "getProductList", handler: "getAllProducts", - environment: { - TABLE_PRODUCTS: process.env.DB_TABLE_PRODUCTS!, - TABLE_STOCKS: process.env.DB_TABLE_STOCKS!, - }, }); - const getProductById = new NodejsFunction(this, `${APP_PREFIX}-get-product-by-id-lambda`, { + const getProductByIdLambda = new NodejsFunction(this, `${APP_PREFIX}-get-product-by-id-lambda`, { ...sharedProps, functionName: "getProductById", handler: "getProductById", - environment: { - TABLE_PRODUCTS: process.env.DB_TABLE_PRODUCTS!, - TABLE_STOCKS: process.env.DB_TABLE_STOCKS!, - }, }); + const createProductLambda = new NodejsFunction(this, `${APP_PREFIX}-create-product-lambda`, { + ...sharedProps, + functionName: "createProduct", + handler: "createProduct", + }); + + getProductListLambda.role?.attachInlinePolicy(lambdaPolicy); + getProductByIdLambda.role?.attachInlinePolicy(lambdaPolicy); + createProductLambda.role?.attachInlinePolicy(lambdaPolicy); + const api = new HttpApi(this, `${APP_PREFIX}-products-api`, { corsPreflight: { allowHeaders: ["*"], @@ -48,17 +90,23 @@ export class AwsShopNodejsBackStack extends cdk.Stack { }); api.addRoutes({ - integration: new HttpLambdaIntegration(`${APP_PREFIX}-getProductLst-integration`, getProductList), + integration: new HttpLambdaIntegration(`${APP_PREFIX}-getProductLst-integration`, getProductListLambda), path: "/products", methods: [HttpMethod.GET] }); api.addRoutes({ - integration: new HttpLambdaIntegration(`${APP_PREFIX}-getProductById-integration`, getProductById, { + integration: new HttpLambdaIntegration(`${APP_PREFIX}-getProductById-integration`, getProductByIdLambda, { parameterMapping: new ParameterMapping().appendQueryString('productId', MappingValue.requestPathParam('productId'))}), path: "/products/{productId}", methods: [HttpMethod.GET] }); + + api.addRoutes({ + integration: new HttpLambdaIntegration(`${APP_PREFIX}-createProduct-integration`, createProductLambda), + path: "/products", + methods: [HttpMethod.POST] + }); } } diff --git a/product-service/package-lock.json b/product-service/package-lock.json index c20c122..677c3e1 100644 --- a/product-service/package-lock.json +++ b/product-service/package-lock.json @@ -8,7 +8,11 @@ "name": "product-service", "version": "1.0.0", "dependencies": { - "dotenv": "^16.1.4" + "dotenv": "^16.1.4", + "http-status-codes": "^2.2.0", + "pg": "^8.11.0", + "uuid": "^9.0.0", + "zod": "^3.21.4" }, "devDependencies": { "@aws-cdk/aws-apigatewayv2-alpha": "^2.82.0-alpha.0", @@ -2475,6 +2479,15 @@ "node": ">= 10.0.0" } }, + "node_modules/aws-sdk/node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/babel-jest": { "version": "29.5.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.5.0.tgz", @@ -2793,6 +2806,14 @@ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, + "node_modules/buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", + "engines": { + "node": ">=4" + } + }, "node_modules/cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -3867,6 +3888,11 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/http-status-codes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.2.0.tgz", + "integrity": "sha512-feERVo9iWxvnejp3SEfm/+oNG517npqL2/PIA8ORjyOZjGC7TwCRQsZylciLS64i6pJ0wRYz3rkXLRwbtFa8Ng==" + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -6531,6 +6557,11 @@ "node": ">=6" } }, + "node_modules/packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -6591,26 +6622,70 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/pg": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.0.tgz", + "integrity": "sha512-meLUVPn2TWgJyLmy7el3fQQVwft4gU5NGyvV0XbD41iU9Jbg8lCH4zexhIkihDzVHJStlt6r088G6/fWeNjhXA==", + "dependencies": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.6.0", + "pg-pool": "^3.6.0", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.0.tgz", + "integrity": "sha512-tGM8/s6frwuAIyRcJ6nWcIvd3+3NmUKIs6OjviIm1HPPFEt5MzQDOTBQyhPWg/m0kCl95M6gA1JaIXtS8KovOA==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.0.tgz", + "integrity": "sha512-x14ibktcwlHKoHxx9X3uTVW9zIGR41ZB6QNhHb21OPNdCCO3NaRnpJuwKIQSR4u+Yqjx4HCvy7Hh7VSy1U4dGg==" + }, "node_modules/pg-int8": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "dev": true, "engines": { "node": ">=4.0.0" } }, + "node_modules/pg-pool": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.0.tgz", + "integrity": "sha512-clFRf2ksqd+F497kWFyM21tMjeikn60oGDmqMT8UBrynEwVEX/5R5xd2sdvdo1cZCFlguORNpVuqxIj+aK4cfQ==", + "peerDependencies": { + "pg": ">=8.0" + } + }, "node_modules/pg-protocol": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz", - "integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==", - "dev": true + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" }, "node_modules/pg-types": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "dev": true, "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", @@ -6622,6 +6697,14 @@ "node": ">=4" } }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -6674,7 +6757,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "dev": true, "engines": { "node": ">=4" } @@ -6683,7 +6765,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -6692,7 +6773,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -6701,7 +6781,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "dev": true, "dependencies": { "xtend": "^4.0.0" }, @@ -7243,6 +7322,14 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -7871,10 +7958,9 @@ "dev": true }, "node_modules/uuid": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", - "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", - "dev": true, + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", "bin": { "uuid": "dist/bin/uuid" } @@ -8046,7 +8132,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, "engines": { "node": ">=0.4" } @@ -8113,6 +8198,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/product-service/package.json b/product-service/package.json index ebf5e83..7347beb 100644 --- a/product-service/package.json +++ b/product-service/package.json @@ -27,6 +27,10 @@ "typescript": "^4.3.0" }, "dependencies": { - "dotenv": "^16.1.4" + "dotenv": "^16.1.4", + "http-status-codes": "^2.2.0", + "pg": "^8.11.0", + "uuid": "^9.0.0", + "zod": "^3.21.4" } } diff --git a/product-service/product-api.json b/product-service/product-api.json new file mode 100644 index 0000000..7e1604c --- /dev/null +++ b/product-service/product-api.json @@ -0,0 +1,222 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Products Http API", + "description": "Products Http API (Task 3 Serverless)", + "contact": { + "name": "Yevheniy Gandzyuck", + "email": "e.barbedwire@gmail.com" + }, + "version": "1.0.0" + }, + "servers": [ + { + "url": "https://virtserver.swaggerhub.com/bwire/T3-serverless/1.0.0", + "description": "SwaggerHub API Auto Mocking" + }, + { + "url": "https://k7id9o1czl.execute-api.eu-west-1.amazonaws.com" + } + ], + "tags": [ + { + "name": "Products", + "description": "Products api (v1)" + } + ], + "paths": { + "/products": { + "get": { + "tags": [ + "Products" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Products" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + } + } + }, + "400": { + "description": "Bad request. Payload is empty.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/error_response" + } + } + } + }, + "500": { + "description": "Cannot add a product", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/error_response" + } + } + } + } + } + } + }, + "/products/{productId}": { + "get": { + "tags": [ + "Products" + ], + "parameters": [ + { + "name": "productId", + "in": "path", + "description": "product identificator", + "required": true, + "style": "simple", + "explode": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + } + }, + "404": { + "description": "Product not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/error_response" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Product": { + "required": [ + "description", + "id", + "price", + "title" + ], + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "7567ec4b-b10c-48c5-9345-fc73c48a80a2" + }, + "price": { + "type": "number", + "example": 100 + }, + "count": { + "type": "number", + "example": 2 + }, + "description": { + "type": "string", + "example": "This book is interesting to read" + }, + "title": { + "type": "string", + "example": "Refactoring by M. Fowler" + } + } + }, + "error_response": { + "type": "object", + "properties": { + "msg": { + "type": "string", + "example": "Very ugly error" + } + }, + "additionalProperties": false + } + }, + "responses": { + "400": { + "description": "Bad request. Payload is empty.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/error_response" + } + } + } + }, + "404": { + "description": "Product not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/error_response" + } + } + } + }, + "500": { + "description": "Cannot add a product", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/error_response" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/product-service/product-api.yaml b/product-service/product-api.yaml deleted file mode 100644 index 8c0f56b..0000000 --- a/product-service/product-api.yaml +++ /dev/null @@ -1,71 +0,0 @@ -openapi: 3.0.1 -info: - title: Products Http API - description: Products Http API (Task 3 Serverless) - contact: - name: Yevheniy Gandzyuck - email: e.barbedwire@gmail.com - version: 1.0.0 -servers: -- url: https://k7id9o1czl.execute-api.eu-west-1.amazonaws.com -tags: -- name: Products - description: Products api (v1) -paths: - /products: - get: - tags: - - Products - responses: - "200": - description: Success - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Product' - /products/{productId}: - get: - tags: - - Products - parameters: - - name: productId - in: path - description: product identificator - required: true - style: simple - explode: false - schema: - type: string - responses: - "200": - description: Success - content: - application/json: - schema: - $ref: '#/components/schemas/Product' - "404": - description: Product not found -components: - schemas: - Product: - required: - - description - - id - - price - - title - type: object - properties: - id: - type: string - example: 7567ec4b-b10c-48c5-9345-fc73c48a80a2 - price: - type: number - example: 100 - description: - type: string - example: This book is interesting to read - title: - type: string - example: Refactoring by M. Fowler diff --git a/product-service/scripts/fill-tables.sh b/product-service/scripts/dynamo/fill-tables.sh similarity index 100% rename from product-service/scripts/fill-tables.sh rename to product-service/scripts/dynamo/fill-tables.sh diff --git a/product-service/scripts/postgres/create-tables.sql b/product-service/scripts/postgres/create-tables.sql new file mode 100644 index 0000000..86e587c --- /dev/null +++ b/product-service/scripts/postgres/create-tables.sql @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS products( + id UUID PRIMARY KEY, + title TEXT NOT NULL, + description TEXT, + price INT +); + +CREATE TABLE IF NOT EXISTS stocks( + product_id UUID, + count INT, + CONSTRAINT fk_products + FOREIGN KEY(product_id) + REFERENCES products(id) +); \ No newline at end of file diff --git a/product-service/scripts/postgres/fill-tables.sql b/product-service/scripts/postgres/fill-tables.sql new file mode 100644 index 0000000..d46167a --- /dev/null +++ b/product-service/scripts/postgres/fill-tables.sql @@ -0,0 +1,16 @@ +DELETE FROM stocks; +DELETE FROM products; +INSERT INTO products (id, title, description, price) +VALUES + ('7567ec4b-b10c-48c5-9345-fc73c48a80aa', 'Refactoring by M. Fowler', 'Improving the Design of Existing Code shed light on the refactoring process, describing the principles and best practices for its implementation.', 2.4), + ('7567ec4b-b10c-48c5-9345-fc73c48a80a0', 'Pragmatic programmer. The path from apprentice to master. Andrew Hunt, David Thomas', '"Pragmatic programmer. The path from apprentice to master" will tell you everything a person needs to know, starting his way in the field of IT projects. Almost a cult book. You will learn how to deal with software shortcomings, how to create a dynamic, effective and adaptable program, how to form a successful team of programmers.', 10), + ('7567ec4b-b10c-48c5-9345-fc73c48a80a3', 'Perfect code. Master Class. Steve McConnell', '"Perfect code. Master-class" - an updated edition of the time-tested bestseller. A book that makes you think and helps you create the perfect code. And it does not matter if you are a beginner or a pro, in this publication you will definitely find information for growth and work on your project.', 23), + ('7567ec4b-b10c-48c5-9345-fc73c48a80a1', 'At the peak. How to maintain maximum efficiency without burnout. Brad Stahlberg, Steve Magness', 'The book "At the peak. How to maintain maximum efficiency without burnout" is especially necessary for programmers who are accustomed to plunge headlong into work, not keeping track of time and waste of resources such as strength and health.', 15), + ('7567ec4b-b10c-48c5-9345-fc73c48a80a2', 'Programming without fools. Katrin Passig, Johannes Jander', 'This book is interesting to read for both a beginner and an experienced programmer. The authors clearly and humorously talk about the fact that programming is in many ways communication. Programming style, naming, commenting, working with someone else''s code - often agreements develop exactly where there is strict regulation at the programming language level.', 23); + +INSERT INTO stocks (product_id, count) +VALUES + ('7567ec4b-b10c-48c5-9345-fc73c48a80aa', 10), + ('7567ec4b-b10c-48c5-9345-fc73c48a80a3', 18), + ('7567ec4b-b10c-48c5-9345-fc73c48a80a1', 3), + ('7567ec4b-b10c-48c5-9345-fc73c48a80a2', 20) \ No newline at end of file diff --git a/product-service/src/handlers/addProduct.ts b/product-service/src/handlers/addProduct.ts new file mode 100644 index 0000000..ea2c978 --- /dev/null +++ b/product-service/src/handlers/addProduct.ts @@ -0,0 +1,40 @@ +import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda"; +import { StatusCodes } from 'http-status-codes'; +import { z } from 'zod'; +import { + errorResponse, + successResponse, + ProductService +} from "../services/product-service"; +import { Product } from "../services/repository/types"; + +const ProductSchema = z.object({ + id: z.string(), + title: z.string(), + description: z.string(), + count: z.number(), + price: z.number() +}); + +export const addProduct = (productService: ProductService) => + async (event: APIGatewayProxyEvent): Promise => { + try { + console.log('Incoming request', event); + const payload: Product = JSON.parse(event.body! || '{}'); + const { success } = ProductSchema.safeParse(payload); + + if (!success) { + return errorResponse(new Error('Payload is empty or invalid'), StatusCodes.BAD_REQUEST); + } + + const result = await productService.createProduct(payload); + + if (!result) { + return errorResponse(new Error(`Error adding new data`), StatusCodes.INTERNAL_SERVER_ERROR); + } + return successResponse(result, StatusCodes.CREATED); + } + catch (err: any) { + return errorResponse(err); + } +} diff --git a/product-service/src/handlers/getProductById.ts b/product-service/src/handlers/getProductById.ts index 0e80f81..05f7b26 100644 --- a/product-service/src/handlers/getProductById.ts +++ b/product-service/src/handlers/getProductById.ts @@ -1,17 +1,24 @@ import { APIGatewayProxyResult } from "aws-lambda"; -import { ProductServiceInterface, ProductByIdEvent, errorResponse, successResponse } from "../service"; +import { StatusCodes } from 'http-status-codes'; +import { + ProductByIdEvent, + errorResponse, + successResponse, + ProductService +} from "../services/product-service"; -export const getSingleProduct = (productService: ProductServiceInterface) => +export const getSingleProduct = (productService: ProductService) => async (event: ProductByIdEvent): Promise => { try { + console.log('Incoming request', event); const { productId } = event.pathParameters; const product = await productService.getProductById(productId); - + if (!product) { - return errorResponse(new Error(`Product with id ${productId} not found`), 404); + return errorResponse(new Error(`Product with id ${productId} not found`), StatusCodes.BAD_REQUEST); } - return successResponse({ product }); + return successResponse(product); } catch (err: any) { return errorResponse(err); diff --git a/product-service/src/handlers/getProductList.ts b/product-service/src/handlers/getProductList.ts index 68c6b4f..aeee8c4 100644 --- a/product-service/src/handlers/getProductList.ts +++ b/product-service/src/handlers/getProductList.ts @@ -1,9 +1,10 @@ -import { APIGatewayProxyResult } from "aws-lambda"; -import { ProductServiceInterface, errorResponse, successResponse } from "../service"; +import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda"; +import { ProductService, errorResponse, successResponse } from "../services/product-service"; -export const getProductList = (productService: ProductServiceInterface) => - async (): Promise => { +export const getProductList = (productService: ProductService) => + async (event?: APIGatewayProxyEvent): Promise => { try { + console.log('Incoming request', event); const products = await productService.getAllProducts(); return successResponse(products); } diff --git a/product-service/src/handlers/index.ts b/product-service/src/handlers/index.ts new file mode 100644 index 0000000..852a6a5 --- /dev/null +++ b/product-service/src/handlers/index.ts @@ -0,0 +1,14 @@ +import { ProductService } from "../services/product-service"; +import { getProductList } from './getProductList'; +import { getSingleProduct } from './getProductById'; +import { addProduct } from './addProduct'; +import { DynamoDbRepository } from "../services/repository/dynamodb-repository"; +import { PostgresRepository } from "../services/repository/postgres-repository"; + +const productService = process.env.USE_NOSQL_DB === 'true' + ? new ProductService(new DynamoDbRepository()) + : new ProductService(new PostgresRepository()); + +export const getAllProducts = getProductList(productService); +export const getProductById = getSingleProduct(productService); +export const createProduct = addProduct(productService); \ No newline at end of file diff --git a/product-service/src/index.ts b/product-service/src/index.ts deleted file mode 100644 index 88a7725..0000000 --- a/product-service/src/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ProductService } from "./service/product-service"; -import { getProductList } from './handlers/getProductList'; -import { getSingleProduct } from './handlers/getProductById'; - -const productService = new ProductService(); - -export const getAllProducts = getProductList(productService); -export const getProductById = getSingleProduct(productService); \ No newline at end of file diff --git a/product-service/mocks/products-data.json b/product-service/src/mocks/products-data.json similarity index 100% rename from product-service/mocks/products-data.json rename to product-service/src/mocks/products-data.json diff --git a/product-service/src/service/index.ts b/product-service/src/service/index.ts deleted file mode 100644 index 6e56abc..0000000 --- a/product-service/src/service/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ProductInterface, ProductServiceInterface, ProductByIdEvent } from './definitions'; -import { ProductService } from './product-service'; -import { errorResponse, successResponse } from '../utils' - -export { - ProductInterface, - ProductServiceInterface, - ProductService, - ProductByIdEvent, - errorResponse, - successResponse, -} \ No newline at end of file diff --git a/product-service/src/service/product-service.ts b/product-service/src/service/product-service.ts deleted file mode 100644 index 5a582a3..0000000 --- a/product-service/src/service/product-service.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { default as products} from '../../mocks/products-data.json'; -import { ProductServiceInterface } from './definitions'; - -export class ProductService implements ProductServiceInterface { - getProductById(id: string) { - return Promise.resolve(products.find(p => p.id === id)); - } - - public getAllProducts() { - return Promise.resolve(products); - }; -} \ No newline at end of file diff --git a/product-service/src/services/product-service/index.ts b/product-service/src/services/product-service/index.ts new file mode 100644 index 0000000..075c7f3 --- /dev/null +++ b/product-service/src/services/product-service/index.ts @@ -0,0 +1,10 @@ +import { ProductByIdEvent } from './types'; +import { ProductService } from './product-service'; +import { errorResponse, successResponse } from '../../utils' + +export { + ProductService, + ProductByIdEvent, + errorResponse, + successResponse, +} \ No newline at end of file diff --git a/product-service/src/services/product-service/product-service.ts b/product-service/src/services/product-service/product-service.ts new file mode 100644 index 0000000..75a3753 --- /dev/null +++ b/product-service/src/services/product-service/product-service.ts @@ -0,0 +1,17 @@ +import { Product, ProductsRepository } from '../repository/types'; + +export class ProductService { + constructor(private repository: ProductsRepository) {} + + async getAllProducts(): Promise { + return this.repository.getAllProducts(); + }; + + async getProductById(id: string): Promise { + return this.repository.getProductById(id); + } + + async createProduct(payload: Product): Promise { + return this.repository.createProduct(payload); + } +} \ No newline at end of file diff --git a/product-service/src/service/definitions.ts b/product-service/src/services/product-service/types.ts similarity index 51% rename from product-service/src/service/definitions.ts rename to product-service/src/services/product-service/types.ts index dec2e0e..edd00c7 100644 --- a/product-service/src/service/definitions.ts +++ b/product-service/src/services/product-service/types.ts @@ -1,21 +1,9 @@ import { APIGatewayProxyEvent, APIGatewayProxyEventPathParameters } from "aws-lambda"; -export interface ProductInterface { - id: string, - title: string, - description: string, - price: number, -}; - export interface ProductByIdPathParams extends APIGatewayProxyEventPathParameters { productId: string, } export interface ProductByIdEvent extends APIGatewayProxyEvent { pathParameters: ProductByIdPathParams, -} - -export interface ProductServiceInterface { - getProductById: (id: string) => Promise, - getAllProducts: () => Promise, } \ No newline at end of file diff --git a/product-service/src/services/repository/dynamodb-repository.ts b/product-service/src/services/repository/dynamodb-repository.ts new file mode 100644 index 0000000..2486a4b --- /dev/null +++ b/product-service/src/services/repository/dynamodb-repository.ts @@ -0,0 +1,70 @@ +import { DynamoDB } from 'aws-sdk'; +import { Product, ProductsRepository} from './types'; +export class DynamoDbRepository implements ProductsRepository { + private dynamo = new DynamoDB.DocumentClient(); + + async getAllProducts(): Promise { + console.log('getting products'); + + const productsScanResult = await this.dynamo.scan({ TableName: process.env.TABLE_PRODUCTS! }).promise(); + const stocksScanResult = await this.dynamo.scan({ TableName: process.env.TABLE_STOCKS! }).promise(); + + const products = productsScanResult.Items!; + const stocks = stocksScanResult.Items!; + + return products.map(p => { + const stockRecord = stocks.find(s => s.product_id === p.id); + return { + ...p, + count: stockRecord ? stockRecord.count : 0, + } as Product + }); + } + + async getProductById(id: string): Promise { + const productsQueryResult = await this.dynamo.query({ + ExpressionAttributeValues: { ":id": id }, + KeyConditionExpression: "id = :id", + TableName: process.env.TABLE_PRODUCTS! + }).promise(); + + if (productsQueryResult.Items!.length === 0) { + return; + } + + const stocksQueryResult = await this.dynamo.query({ + ExpressionAttributeValues: { ":id": id }, + KeyConditionExpression: "product_id = :id", + TableName: process.env.TABLE_STOCKS! + }).promise(); + + return { + ...productsQueryResult.Items![0], + count: stocksQueryResult.Items!.length > 0 ? stocksQueryResult.Items![0].count : 0, + } as Product + }; + + async createProduct(payload: Product): Promise { + const { id, title, description, price, count } = payload; + + try { + await this.dynamo.transactWrite({ + TransactItems: [{ + Put: { + TableName: process.env.TABLE_PRODUCTS!, + Item: { id, title, description, price }, + } + }, { + Put: { + TableName: process.env.TABLE_STOCKS!, + Item: { product_id: id, count }, + } + }] + }).promise(); + } catch (error) { + return + } + + return { ...payload}; + } +} \ No newline at end of file diff --git a/product-service/src/services/repository/index.ts b/product-service/src/services/repository/index.ts new file mode 100644 index 0000000..1b568d5 --- /dev/null +++ b/product-service/src/services/repository/index.ts @@ -0,0 +1,6 @@ +import { Product, ProductsRepository} from './types'; + +export { + Product, + ProductsRepository, +} \ No newline at end of file diff --git a/product-service/src/services/repository/postgres-repository.ts b/product-service/src/services/repository/postgres-repository.ts new file mode 100644 index 0000000..9e423a2 --- /dev/null +++ b/product-service/src/services/repository/postgres-repository.ts @@ -0,0 +1,71 @@ +import { Client } from 'pg'; +import { parse } from 'uuid'; +import { Product, ProductsRepository} from './types'; + +export class PostgresRepository implements ProductsRepository { + private async getClient(): Promise { + const client = new Client(); + await client.connect(); + return client; + } + + async getAllProducts(): Promise { + const client = await this.getClient(); + const queryText = '\ + SELECT p.*, COALESCE(s.count, 0) count \ + FROM products p \ + INNER JOIN stocks s \ + ON p.id = s.product_id'; + + const result = await client.query(queryText); + return result['rows']; + } + + async getProductById(id: string): Promise { + const client = await this.getClient(); + const queryText = "\ + SELECT p.*, COALESCE(s.count, 0) count \ + FROM products p \ + INNER JOIN stocks s \ + ON p.id = s.product_id \ + WHERE p.id = $1"; + + const result = await client.query({ text: queryText, values: [id]}); + + if (result['rows'].length < 1) { + return; + } + return result['rows'][0]; + } + + async createProduct(payload: Product): Promise { + const client = await this.getClient(); + const { id, title, description, price, count } = payload; + const newId = parse(id); + + const queryTextProducts = "\ + INSERT INTO products(id, title, description, price) \ + VALUES($1, $2, $3, $4);" + const queryTextStocks = "\ + INSERT INTO stocks (product_id, count) \ + VALUES($1, $2)"; + + try { + await client.query('BEGIN') + await client.query({ + text: queryTextProducts, + values: [id, title, description, price], + }); + await client.query({ + text: queryTextStocks, + values: [newId, price], + }); + await client.query('COMMIT') + } catch (error) { + await client.query('ROLLBACK') + return; + } + + return payload; + } +} \ No newline at end of file diff --git a/product-service/src/services/repository/types.ts b/product-service/src/services/repository/types.ts new file mode 100644 index 0000000..1dd25ab --- /dev/null +++ b/product-service/src/services/repository/types.ts @@ -0,0 +1,13 @@ +export interface Product { + id: string, + title: string, + description: string, + price: number, + count: number, +}; + +export interface ProductsRepository { + getProductById: (id: string) => Promise, + getAllProducts: () => Promise, + createProduct: (payload: Product) => Promise, +} \ No newline at end of file diff --git a/product-service/src/utils.ts b/product-service/src/utils.ts index 311d16f..657ac38 100644 --- a/product-service/src/utils.ts +++ b/product-service/src/utils.ts @@ -1,4 +1,5 @@ import { APIGatewayProxyResult } from "aws-lambda"; +import { StatusCodes } from 'http-status-codes'; const defaultHeaders = { 'Access-Control-Allow-Methods': '*', @@ -6,7 +7,7 @@ const defaultHeaders = { 'Access-Control-Allow-Origin': '*' }; -const errorResponse = (err: Error, statusCode: number = 500): APIGatewayProxyResult => { +const errorResponse = (err: Error, statusCode: number = StatusCodes.INTERNAL_SERVER_ERROR): APIGatewayProxyResult => { return { statusCode, headers: { @@ -16,7 +17,7 @@ const errorResponse = (err: Error, statusCode: number = 500): APIGatewayProxyRes } } -const successResponse = (body: Object, statusCode: number = 200): APIGatewayProxyResult => { +const successResponse = (body: Object, statusCode: number = StatusCodes.OK): APIGatewayProxyResult => { return { statusCode, headers: { diff --git a/product-service/test/addProduct.test.ts b/product-service/test/addProduct.test.ts new file mode 100644 index 0000000..f0ab548 --- /dev/null +++ b/product-service/test/addProduct.test.ts @@ -0,0 +1,50 @@ +import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; +import { ProductService } from '../src/services/product-service'; +import { addProduct } from '../src/handlers/addProduct'; +import { DynamoDbRepository } from '../src/services/repository/dynamodb-repository'; +import { StatusCodes } from 'http-status-codes'; + +describe('addProduct tests', () => { + const service = new ProductService(new DynamoDbRepository()); + + const product = { + count: 1, + description: 'Product 1', + id: '123', + price: 45, + title: 'P1', + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('create product, return a valid result', async () => { + const eventParams: APIGatewayProxyEvent = { body: JSON.stringify(product) } as any; + const spyFn = jest.spyOn(service, "createProduct").mockResolvedValue(product); + const result: APIGatewayProxyResult = await addProduct(service)(eventParams); + + expect(spyFn).toBeCalledTimes(1); + expect(spyFn).toBeCalledWith(product); + expect(result.statusCode).toBe(StatusCodes.CREATED); + expect(result.body).toBe(JSON.stringify(product)); + }); + + test('create product, no body - error thrown', async () => { + const eventParams: APIGatewayProxyEvent = {} as any; + const spyFn = jest.spyOn(service, "createProduct").mockResolvedValue(product); + const result: APIGatewayProxyResult = await addProduct(service)(eventParams); + + expect(result.statusCode).toBe(StatusCodes.BAD_REQUEST); + expect(result.body).toBe(JSON.stringify({ message: 'Payload is empty or invalid'})); + }); + + test('create product, incorrect body - error thrown', async () => { + const eventParams: APIGatewayProxyEvent = { body: JSON.stringify({ count: 1}) } as any; + const spyFn = jest.spyOn(service, "createProduct").mockResolvedValue(product); + const result: APIGatewayProxyResult = await addProduct(service)(eventParams); + + expect(result.statusCode).toBe(StatusCodes.BAD_REQUEST); + expect(result.body).toBe(JSON.stringify({ message: 'Payload is empty or invalid'})); + }); +}); \ No newline at end of file diff --git a/product-service/src/handlers/getProductById.test.ts b/product-service/test/getProductById.test.ts similarity index 66% rename from product-service/src/handlers/getProductById.test.ts rename to product-service/test/getProductById.test.ts index 5bdc878..ad75e73 100644 --- a/product-service/src/handlers/getProductById.test.ts +++ b/product-service/test/getProductById.test.ts @@ -1,10 +1,12 @@ import { APIGatewayProxyResult } from 'aws-lambda'; -import { ProductService } from '../service/product-service'; -import { getSingleProduct } from './getProductById'; -import { ProductByIdEvent } from '../service'; +import { ProductService } from '../src/services/product-service'; +import { getSingleProduct } from '../src/handlers/getProductById'; +import { ProductByIdEvent } from '../src/services/product-service'; +import { DynamoDbRepository } from '../src/services/repository/dynamodb-repository'; +import { StatusCodes } from 'http-status-codes'; -describe('getProductList tests', () => { - const service = new ProductService(); +describe('getProductById tests', () => { + const service = new ProductService(new DynamoDbRepository()); const eventParams = { pathParameters: { productId: '123' }, @@ -28,8 +30,8 @@ describe('getProductList tests', () => { expect(spyFn).toBeCalledTimes(1); expect(spyFn).toBeCalledWith('123'); - expect(result.statusCode).toBe(200); - expect(result.body).toBe(JSON.stringify({ product })); + expect(result.statusCode).toBe(StatusCodes.OK); + expect(result.body).toBe(JSON.stringify(product)); }); test('product not found - to handle error', async () => { @@ -37,7 +39,7 @@ describe('getProductList tests', () => { const result: APIGatewayProxyResult = await getSingleProduct(service)(eventParams); expect(spyFn).toBeCalledTimes(1); - expect(result.statusCode).toBe(404); + expect(result.statusCode).toBe(StatusCodes.BAD_REQUEST); expect(result.body).toBe(JSON.stringify({ message: 'Product with id 123 not found'})); }); @@ -46,7 +48,7 @@ describe('getProductList tests', () => { const result: APIGatewayProxyResult = await getSingleProduct(service)(eventParams); expect(spyFn).toBeCalledTimes(1); - expect(result.statusCode).toBe(500); + expect(result.statusCode).toBe(StatusCodes.INTERNAL_SERVER_ERROR); expect(result.body).toBe(JSON.stringify({ message: 'Test error'})); }); }); \ No newline at end of file diff --git a/product-service/src/handlers/getProductList.test.ts b/product-service/test/getProductList.test.ts similarity index 67% rename from product-service/src/handlers/getProductList.test.ts rename to product-service/test/getProductList.test.ts index 3478422..7b39a0f 100644 --- a/product-service/src/handlers/getProductList.test.ts +++ b/product-service/test/getProductList.test.ts @@ -1,9 +1,11 @@ import { APIGatewayProxyResult } from 'aws-lambda'; -import { ProductService } from '../service/product-service'; -import { getProductList } from './getProductList'; +import { ProductService } from '../src/services/product-service'; +import { getProductList } from '../src/handlers/getProductList'; +import { DynamoDbRepository } from '../src/services/repository/dynamodb-repository'; +import { StatusCodes } from 'http-status-codes'; describe('getProductList tests', () => { - const service = new ProductService(); + const service = new ProductService(new DynamoDbRepository()); beforeEach(() => { jest.clearAllMocks(); @@ -27,8 +29,11 @@ describe('getProductList tests', () => { const spyFn = jest.spyOn(service, "getAllProducts").mockResolvedValue(products); const result: APIGatewayProxyResult = await getProductList(service)(); + console.log('result', result); + + expect(spyFn).toBeCalledTimes(1); - expect(result.statusCode).toBe(200); + expect(result.statusCode).toBe(StatusCodes.OK); expect(result.body).toBe(JSON.stringify(products)); }); @@ -37,7 +42,7 @@ describe('getProductList tests', () => { const result: APIGatewayProxyResult = await getProductList(service)(); expect(spyFn).toBeCalledTimes(1); - expect(result.statusCode).toBe(500); + expect(result.statusCode).toBe(StatusCodes.INTERNAL_SERVER_ERROR); expect(result.body).toBe(JSON.stringify({ message: 'Test error'})); }); }); \ No newline at end of file diff --git a/product-service/tsconfig.json b/product-service/tsconfig.json index 2bbbac4..4076810 100644 --- a/product-service/tsconfig.json +++ b/product-service/tsconfig.json @@ -22,9 +22,6 @@ "strictPropertyInitialization": false, "resolveJsonModule": true, "esModuleInterop": true, - "typeRoots": [ - "./node_modules/@types" - ], "types": ["node", "@types/jest"], "outDir": "./out" },