From 699cfcc35adbd091dfc42684bcce0e5f3056a4b0 Mon Sep 17 00:00:00 2001 From: barbedwire Date: Thu, 8 Jun 2023 10:39:21 +0200 Subject: [PATCH 01/22] add dynamoDB read policy to lambdas --- .../lib/aws-shop-nodejs-back-stack.ts | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/product-service/lib/aws-shop-nodejs-back-stack.ts b/product-service/lib/aws-shop-nodejs-back-stack.ts index 4e4fe18..0adbb97 100644 --- a/product-service/lib/aws-shop-nodejs-back-stack.ts +++ b/product-service/lib/aws-shop-nodejs-back-stack.ts @@ -1,5 +1,6 @@ import * as cdk from 'aws-cdk-lib'; import * as lambda from "aws-cdk-lib/aws-lambda"; +import * as iam from "aws-cdk-lib/aws-iam"; 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'; @@ -14,12 +15,24 @@ export class AwsShopNodejsBackStack extends cdk.Stack { const APP_PREFIX = "bw-aws-shop-backend"; super(scope, `${APP_PREFIX}-stack`, props); + const policy = new iam.Policy(this, `${APP_PREFIX}-dynamodb-read-policy`, { + statements: [ + new iam.PolicyStatement({ + actions: ["dynamodb:Query"], + 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, }; - 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", @@ -29,7 +42,7 @@ export class AwsShopNodejsBackStack extends cdk.Stack { }, }); - 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", @@ -39,6 +52,9 @@ export class AwsShopNodejsBackStack extends cdk.Stack { }, }); + getProductListLambda.role?.attachInlinePolicy(policy); + getProductByIdLambda.role?.attachInlinePolicy(policy); + const api = new HttpApi(this, `${APP_PREFIX}-products-api`, { corsPreflight: { allowHeaders: ["*"], @@ -48,14 +64,14 @@ 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] From 7315608ba31712007a5712e217fb8e9efc7cf825 Mon Sep 17 00:00:00 2001 From: barbedwire Date: Thu, 8 Jun 2023 20:26:54 +0200 Subject: [PATCH 02/22] add repository approach files --- .../repository/dynamodb-repository.ts | 46 +++++++++++++++++++ .../src/services/repository/index.ts | 6 +++ .../src/services/repository/types.ts | 12 +++++ 3 files changed, 64 insertions(+) create mode 100644 product-service/src/services/repository/dynamodb-repository.ts create mode 100644 product-service/src/services/repository/index.ts create mode 100644 product-service/src/services/repository/types.ts 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..17a944a --- /dev/null +++ b/product-service/src/services/repository/dynamodb-repository.ts @@ -0,0 +1,46 @@ +import * as aws from 'aws-sdk'; +import { ProductInterface, ProductsRepository} from './types'; +import { log } from 'console'; + +export class DynamoDbRepository implements ProductsRepository { + private dynamo = new aws.DynamoDB.DocumentClient(); + + async getAllProducts(): Promise { + 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 ProductInterface + }); + } + + 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 ProductInterface + }; +} \ 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..5de0836 --- /dev/null +++ b/product-service/src/services/repository/index.ts @@ -0,0 +1,6 @@ +import { ProductInterface, ProductsRepository} from './types'; + +export { + ProductInterface, + ProductsRepository, +} \ 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..ef450d9 --- /dev/null +++ b/product-service/src/services/repository/types.ts @@ -0,0 +1,12 @@ +export interface ProductInterface { + id: string, + title: string, + description: string, + price: number, + count: number, +}; + +export interface ProductsRepository { + getProductById: (id: string) => Promise, + getAllProducts: () => Promise, +} \ No newline at end of file From 679b5e35b3c9cdbffd40440ad73333d687c10f5c Mon Sep 17 00:00:00 2001 From: barbedwire Date: Thu, 8 Jun 2023 20:27:54 +0200 Subject: [PATCH 03/22] refactor product-service --- .../src/services/product-service/index.ts | 10 ++++++++++ .../services/product-service/product-service.ts | 14 ++++++++++++++ .../src/services/product-service/types.ts | 9 +++++++++ 3 files changed, 33 insertions(+) create mode 100644 product-service/src/services/product-service/index.ts create mode 100644 product-service/src/services/product-service/product-service.ts create mode 100644 product-service/src/services/product-service/types.ts 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..a39eb20 --- /dev/null +++ b/product-service/src/services/product-service/product-service.ts @@ -0,0 +1,14 @@ +import * as aws from 'aws-sdk'; +import { ProductsRepository } from '../repository/types'; + +export class ProductService { + constructor(private repository: ProductsRepository) {} + + async getAllProducts() { + return this.repository.getAllProducts(); + }; + + async getProductById(id: string) { + return this.repository.getProductById(id); + } +} \ No newline at end of file diff --git a/product-service/src/services/product-service/types.ts b/product-service/src/services/product-service/types.ts new file mode 100644 index 0000000..edd00c7 --- /dev/null +++ b/product-service/src/services/product-service/types.ts @@ -0,0 +1,9 @@ +import { APIGatewayProxyEvent, APIGatewayProxyEventPathParameters } from "aws-lambda"; + +export interface ProductByIdPathParams extends APIGatewayProxyEventPathParameters { + productId: string, +} + +export interface ProductByIdEvent extends APIGatewayProxyEvent { + pathParameters: ProductByIdPathParams, +} \ No newline at end of file From f997b3c0029951ac1dd04539b36132186628b9e1 Mon Sep 17 00:00:00 2001 From: barbedwire Date: Thu, 8 Jun 2023 20:28:41 +0200 Subject: [PATCH 04/22] revome redundant files --- product-service/src/index.ts | 8 ------- product-service/src/service/definitions.ts | 21 ------------------- product-service/src/service/index.ts | 12 ----------- .../src/service/product-service.ts | 12 ----------- 4 files changed, 53 deletions(-) delete mode 100644 product-service/src/index.ts delete mode 100644 product-service/src/service/definitions.ts delete mode 100644 product-service/src/service/index.ts delete mode 100644 product-service/src/service/product-service.ts 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/src/service/definitions.ts b/product-service/src/service/definitions.ts deleted file mode 100644 index dec2e0e..0000000 --- a/product-service/src/service/definitions.ts +++ /dev/null @@ -1,21 +0,0 @@ -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/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 From 827152ce7a51655b930a359845c343c9f34fcfcc Mon Sep 17 00:00:00 2001 From: barbedwire Date: Thu, 8 Jun 2023 20:29:16 +0200 Subject: [PATCH 05/22] move mocks to product-service directory --- product-service/{ => src}/mocks/products-data.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename product-service/{ => src}/mocks/products-data.json (100%) 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 From 13629d1fd61a1d05928b8a6baefd0dd73cfeff18 Mon Sep 17 00:00:00 2001 From: barbedwire Date: Thu, 8 Jun 2023 20:30:48 +0200 Subject: [PATCH 06/22] refactor handlers to use with repository --- product-service/src/handlers/getProductById.test.ts | 4 ++-- product-service/src/handlers/getProductById.ts | 8 ++++---- product-service/src/handlers/getProductList.test.ts | 2 +- product-service/src/handlers/getProductList.ts | 4 ++-- product-service/src/handlers/index.ts | 9 +++++++++ 5 files changed, 18 insertions(+), 9 deletions(-) create mode 100644 product-service/src/handlers/index.ts diff --git a/product-service/src/handlers/getProductById.test.ts b/product-service/src/handlers/getProductById.test.ts index 5bdc878..fd8fc73 100644 --- a/product-service/src/handlers/getProductById.test.ts +++ b/product-service/src/handlers/getProductById.test.ts @@ -1,7 +1,7 @@ import { APIGatewayProxyResult } from 'aws-lambda'; -import { ProductService } from '../service/product-service'; +import { ProductService } from '../services/product-service'; import { getSingleProduct } from './getProductById'; -import { ProductByIdEvent } from '../service'; +import { ProductByIdEvent } from '../services/product-service'; describe('getProductList tests', () => { const service = new ProductService(); diff --git a/product-service/src/handlers/getProductById.ts b/product-service/src/handlers/getProductById.ts index 0e80f81..9e39ce2 100644 --- a/product-service/src/handlers/getProductById.ts +++ b/product-service/src/handlers/getProductById.ts @@ -1,17 +1,17 @@ import { APIGatewayProxyResult } from "aws-lambda"; -import { ProductServiceInterface, ProductByIdEvent, errorResponse, successResponse } from "../service"; +import { ProductByIdEvent, errorResponse, successResponse, ProductService } from "../services/product-service"; -export const getSingleProduct = (productService: ProductServiceInterface) => +export const getSingleProduct = (productService: ProductService) => async (event: ProductByIdEvent): Promise => { try { const { productId } = event.pathParameters; const product = await productService.getProductById(productId); - + if (!product) { return errorResponse(new Error(`Product with id ${productId} not found`), 404); } - return successResponse({ product }); + return successResponse(product); } catch (err: any) { return errorResponse(err); diff --git a/product-service/src/handlers/getProductList.test.ts b/product-service/src/handlers/getProductList.test.ts index 3478422..297a470 100644 --- a/product-service/src/handlers/getProductList.test.ts +++ b/product-service/src/handlers/getProductList.test.ts @@ -1,5 +1,5 @@ import { APIGatewayProxyResult } from 'aws-lambda'; -import { ProductService } from '../service/product-service'; +import { ProductService } from '../services/product-service'; import { getProductList } from './getProductList'; describe('getProductList tests', () => { diff --git a/product-service/src/handlers/getProductList.ts b/product-service/src/handlers/getProductList.ts index 68c6b4f..449296c 100644 --- a/product-service/src/handlers/getProductList.ts +++ b/product-service/src/handlers/getProductList.ts @@ -1,7 +1,7 @@ import { APIGatewayProxyResult } from "aws-lambda"; -import { ProductServiceInterface, errorResponse, successResponse } from "../service"; +import { ProductService, errorResponse, successResponse } from "../services/product-service"; -export const getProductList = (productService: ProductServiceInterface) => +export const getProductList = (productService: ProductService) => async (): Promise => { try { const products = await productService.getAllProducts(); diff --git a/product-service/src/handlers/index.ts b/product-service/src/handlers/index.ts new file mode 100644 index 0000000..b31775b --- /dev/null +++ b/product-service/src/handlers/index.ts @@ -0,0 +1,9 @@ +import { ProductService } from "../services/product-service"; +import { getProductList } from './getProductList'; +import { getSingleProduct } from './getProductById'; +import { DynamoDbRepository } from "../services/repository/dynamodb-repository"; + +const productService = new ProductService(new DynamoDbRepository()); + +export const getAllProducts = getProductList(productService); +export const getProductById = getSingleProduct(productService); \ No newline at end of file From 790833dca9783dab2377a8589dcc518b924167b2 Mon Sep 17 00:00:00 2001 From: barbedwire Date: Thu, 8 Jun 2023 20:31:20 +0200 Subject: [PATCH 07/22] rework main cdk class --- product-service/lib/aws-shop-nodejs-back-stack.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/product-service/lib/aws-shop-nodejs-back-stack.ts b/product-service/lib/aws-shop-nodejs-back-stack.ts index 0adbb97..6c67ba3 100644 --- a/product-service/lib/aws-shop-nodejs-back-stack.ts +++ b/product-service/lib/aws-shop-nodejs-back-stack.ts @@ -8,8 +8,6 @@ import { NodejsFunction, NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-node import * as dotenv from 'dotenv'; dotenv.config(); -console.log(process.env) - export class AwsShopNodejsBackStack extends cdk.Stack { constructor(scope: Construct, props?: cdk.StackProps) { const APP_PREFIX = "bw-aws-shop-backend"; @@ -18,7 +16,10 @@ export class AwsShopNodejsBackStack extends cdk.Stack { const policy = new iam.Policy(this, `${APP_PREFIX}-dynamodb-read-policy`, { statements: [ new iam.PolicyStatement({ - actions: ["dynamodb:Query"], + actions: [ + "dynamodb:Scan", + "dynamodb:Query" + ], resources: [ `arn:aws:dynamodb:*:*:table/${process.env.DB_TABLE_PRODUCTS}`, `arn:aws:dynamodb:*:*:table/${process.env.DB_TABLE_STOCKS}` @@ -28,7 +29,7 @@ export class AwsShopNodejsBackStack extends cdk.Stack { }); const sharedProps: Partial = { - entry: './src/index.ts', + entry: './src/handlers/index.ts', runtime: lambda.Runtime.NODEJS_18_X, }; From 4c7b53c30a02860afbee1b149a241ec16b48cc1f Mon Sep 17 00:00:00 2001 From: barbedwire Date: Fri, 9 Jun 2023 16:36:21 +0200 Subject: [PATCH 08/22] rename ProductInterface --- .../src/services/repository/dynamodb-repository.ts | 10 +++++----- product-service/src/services/repository/index.ts | 4 ++-- product-service/src/services/repository/types.ts | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/product-service/src/services/repository/dynamodb-repository.ts b/product-service/src/services/repository/dynamodb-repository.ts index 17a944a..af60015 100644 --- a/product-service/src/services/repository/dynamodb-repository.ts +++ b/product-service/src/services/repository/dynamodb-repository.ts @@ -1,11 +1,11 @@ import * as aws from 'aws-sdk'; -import { ProductInterface, ProductsRepository} from './types'; +import { Product, ProductsRepository} from './types'; import { log } from 'console'; export class DynamoDbRepository implements ProductsRepository { private dynamo = new aws.DynamoDB.DocumentClient(); - async getAllProducts(): Promise { + async getAllProducts(): Promise { const productsScanResult = await this.dynamo.scan({ TableName: process.env.TABLE_PRODUCTS! }).promise(); const stocksScanResult = await this.dynamo.scan({ TableName: process.env.TABLE_STOCKS! }).promise(); @@ -17,11 +17,11 @@ export class DynamoDbRepository implements ProductsRepository { return { ...p, count: stockRecord ? stockRecord.count : 0, - } as ProductInterface + } as Product }); } - async getProductById(id: string): Promise { + async getProductById(id: string): Promise { const productsQueryResult = await this.dynamo.query({ ExpressionAttributeValues: { ":id": id }, KeyConditionExpression: "id = :id", @@ -41,6 +41,6 @@ export class DynamoDbRepository implements ProductsRepository { return { ...productsQueryResult.Items![0], count: stocksQueryResult.Items!.length > 0 ? stocksQueryResult.Items![0].count : 0, - } as ProductInterface + } as Product }; } \ No newline at end of file diff --git a/product-service/src/services/repository/index.ts b/product-service/src/services/repository/index.ts index 5de0836..84a8283 100644 --- a/product-service/src/services/repository/index.ts +++ b/product-service/src/services/repository/index.ts @@ -1,6 +1,6 @@ -import { ProductInterface, ProductsRepository} from './types'; +import { Product, ProductsRepository} from './types'; export { - ProductInterface, + Product as ProductInterface, ProductsRepository, } \ No newline at end of file diff --git a/product-service/src/services/repository/types.ts b/product-service/src/services/repository/types.ts index ef450d9..c05bce6 100644 --- a/product-service/src/services/repository/types.ts +++ b/product-service/src/services/repository/types.ts @@ -1,4 +1,4 @@ -export interface ProductInterface { +export interface Product { id: string, title: string, description: string, @@ -7,6 +7,6 @@ export interface ProductInterface { }; export interface ProductsRepository { - getProductById: (id: string) => Promise, - getAllProducts: () => Promise, + getProductById: (id: string) => Promise, + getAllProducts: () => Promise, } \ No newline at end of file From b634e82080c9bd95b4542ff74f91b1a3a441659d Mon Sep 17 00:00:00 2001 From: barbedwire Date: Fri, 9 Jun 2023 19:28:18 +0200 Subject: [PATCH 09/22] add createProduct lambda --- .../lib/aws-shop-nodejs-back-stack.ts | 20 +++++++++++++- product-service/src/handlers/addProduct.ts | 25 ++++++++++++++++++ product-service/src/handlers/index.ts | 4 ++- .../product-service/product-service.ts | 10 ++++--- .../repository/dynamodb-repository.ts | 26 ++++++++++++++++--- .../src/services/repository/types.ts | 1 + 6 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 product-service/src/handlers/addProduct.ts diff --git a/product-service/lib/aws-shop-nodejs-back-stack.ts b/product-service/lib/aws-shop-nodejs-back-stack.ts index 6c67ba3..619b74e 100644 --- a/product-service/lib/aws-shop-nodejs-back-stack.ts +++ b/product-service/lib/aws-shop-nodejs-back-stack.ts @@ -18,7 +18,8 @@ export class AwsShopNodejsBackStack extends cdk.Stack { new iam.PolicyStatement({ actions: [ "dynamodb:Scan", - "dynamodb:Query" + "dynamodb:Query", + "dynamodb:PutItem", ], resources: [ `arn:aws:dynamodb:*:*:table/${process.env.DB_TABLE_PRODUCTS}`, @@ -53,8 +54,19 @@ export class AwsShopNodejsBackStack extends cdk.Stack { }, }); + const createProductLambda = new NodejsFunction(this, `${APP_PREFIX}-create-product-lambda`, { + ...sharedProps, + functionName: "createProduct", + handler: "createProduct", + environment: { + TABLE_PRODUCTS: process.env.DB_TABLE_PRODUCTS!, + TABLE_STOCKS: process.env.DB_TABLE_STOCKS!, + }, + }); + getProductListLambda.role?.attachInlinePolicy(policy); getProductByIdLambda.role?.attachInlinePolicy(policy); + createProductLambda.role?.attachInlinePolicy(policy); const api = new HttpApi(this, `${APP_PREFIX}-products-api`, { corsPreflight: { @@ -77,5 +89,11 @@ export class AwsShopNodejsBackStack extends cdk.Stack { 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/src/handlers/addProduct.ts b/product-service/src/handlers/addProduct.ts new file mode 100644 index 0000000..fc3a373 --- /dev/null +++ b/product-service/src/handlers/addProduct.ts @@ -0,0 +1,25 @@ +import { APIGatewayProxyResult } from "aws-lambda"; +import { + ProductByIdEvent, + errorResponse, + successResponse, + ProductService +} from "../services/product-service"; +import { Product } from "../services/repository/types"; + +export const addProduct = (productService: ProductService) => + async (event: ProductByIdEvent): Promise => { + try { + // TODO check for the empty body + const payload: Product = JSON.parse(event.body!); + + // TODO process a valid result + const result = await productService.createProduct(payload); + + return successResponse(result, 201); + } + catch (err: any) { + console.log('error', err); + return errorResponse(err); + } +} diff --git a/product-service/src/handlers/index.ts b/product-service/src/handlers/index.ts index b31775b..00d59b8 100644 --- a/product-service/src/handlers/index.ts +++ b/product-service/src/handlers/index.ts @@ -1,9 +1,11 @@ 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"; const productService = new ProductService(new DynamoDbRepository()); export const getAllProducts = getProductList(productService); -export const getProductById = getSingleProduct(productService); \ No newline at end of file +export const getProductById = getSingleProduct(productService); +export const createProduct = addProduct(productService); \ 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 index a39eb20..ef8e4c6 100644 --- a/product-service/src/services/product-service/product-service.ts +++ b/product-service/src/services/product-service/product-service.ts @@ -1,14 +1,18 @@ import * as aws from 'aws-sdk'; -import { ProductsRepository } from '../repository/types'; +import { Product, ProductsRepository } from '../repository/types'; export class ProductService { constructor(private repository: ProductsRepository) {} - async getAllProducts() { + async getAllProducts(): Promise { return this.repository.getAllProducts(); }; - async getProductById(id: string) { + 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/services/repository/dynamodb-repository.ts b/product-service/src/services/repository/dynamodb-repository.ts index af60015..d1748c1 100644 --- a/product-service/src/services/repository/dynamodb-repository.ts +++ b/product-service/src/services/repository/dynamodb-repository.ts @@ -1,9 +1,7 @@ -import * as aws from 'aws-sdk'; +import { DynamoDB } from 'aws-sdk'; import { Product, ProductsRepository} from './types'; -import { log } from 'console'; - export class DynamoDbRepository implements ProductsRepository { - private dynamo = new aws.DynamoDB.DocumentClient(); + private dynamo = new DynamoDB.DocumentClient(); async getAllProducts(): Promise { const productsScanResult = await this.dynamo.scan({ TableName: process.env.TABLE_PRODUCTS! }).promise(); @@ -43,4 +41,24 @@ export class DynamoDbRepository implements ProductsRepository { count: stocksQueryResult.Items!.length > 0 ? stocksQueryResult.Items![0].count : 0, } as Product }; + + async createProduct(payload: Product): Promise { + const { id, title, description, price, count } = payload; + + 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(); + + 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 index c05bce6..3b0fe9a 100644 --- a/product-service/src/services/repository/types.ts +++ b/product-service/src/services/repository/types.ts @@ -9,4 +9,5 @@ export interface Product { export interface ProductsRepository { getProductById: (id: string) => Promise, getAllProducts: () => Promise, + createProduct: (payload: Product) => Promise, } \ No newline at end of file From 8bd66a10c6dc0a72a47341c4ee7ea0d53b1d3d78 Mon Sep 17 00:00:00 2001 From: barbedwire Date: Fri, 9 Jun 2023 19:40:13 +0200 Subject: [PATCH 10/22] make aws types refactoring --- product-service/bin/aws-shop-nodejs-back.ts | 17 ++--------- .../lib/aws-shop-nodejs-back-stack.ts | 30 +++++++++++-------- .../src/handlers/getProductById.test.ts | 3 +- .../src/handlers/getProductById.ts | 7 ++++- .../src/handlers/getProductList.test.ts | 3 +- product-service/tsconfig.json | 3 -- 6 files changed, 30 insertions(+), 33 deletions(-) diff --git a/product-service/bin/aws-shop-nodejs-back.ts b/product-service/bin/aws-shop-nodejs-back.ts index 722c5cf..0e88c5c 100644 --- a/product-service/bin/aws-shop-nodejs-back.ts +++ b/product-service/bin/aws-shop-nodejs-back.ts @@ -1,23 +1,10 @@ #!/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/lib/aws-shop-nodejs-back-stack.ts b/product-service/lib/aws-shop-nodejs-back-stack.ts index 619b74e..51e6871 100644 --- a/product-service/lib/aws-shop-nodejs-back-stack.ts +++ b/product-service/lib/aws-shop-nodejs-back-stack.ts @@ -1,21 +1,27 @@ -import * as cdk from 'aws-cdk-lib'; -import * as lambda from "aws-cdk-lib/aws-lambda"; -import * as iam from "aws-cdk-lib/aws-iam"; -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 { + 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(); -export class AwsShopNodejsBackStack extends cdk.Stack { - constructor(scope: Construct, props?: cdk.StackProps) { +envConfig(); +export class AwsShopNodejsBackStack extends Stack { + constructor(scope: Construct, props?: StackProps) { const APP_PREFIX = "bw-aws-shop-backend"; super(scope, `${APP_PREFIX}-stack`, props); - const policy = new iam.Policy(this, `${APP_PREFIX}-dynamodb-read-policy`, { + const policy = new Policy(this, `${APP_PREFIX}-dynamodb-read-policy`, { statements: [ - new iam.PolicyStatement({ + new PolicyStatement({ actions: [ "dynamodb:Scan", "dynamodb:Query", @@ -31,7 +37,7 @@ export class AwsShopNodejsBackStack extends cdk.Stack { const sharedProps: Partial = { entry: './src/handlers/index.ts', - runtime: lambda.Runtime.NODEJS_18_X, + runtime: Runtime.NODEJS_18_X, }; const getProductListLambda = new NodejsFunction(this, `${APP_PREFIX}-get-product-list-lambda`, { diff --git a/product-service/src/handlers/getProductById.test.ts b/product-service/src/handlers/getProductById.test.ts index fd8fc73..56fd2bb 100644 --- a/product-service/src/handlers/getProductById.test.ts +++ b/product-service/src/handlers/getProductById.test.ts @@ -2,9 +2,10 @@ import { APIGatewayProxyResult } from 'aws-lambda'; import { ProductService } from '../services/product-service'; import { getSingleProduct } from './getProductById'; import { ProductByIdEvent } from '../services/product-service'; +import { DynamoDbRepository } from '../services/repository/dynamodb-repository'; describe('getProductList tests', () => { - const service = new ProductService(); + const service = new ProductService(new DynamoDbRepository()); const eventParams = { pathParameters: { productId: '123' }, diff --git a/product-service/src/handlers/getProductById.ts b/product-service/src/handlers/getProductById.ts index 9e39ce2..a150ede 100644 --- a/product-service/src/handlers/getProductById.ts +++ b/product-service/src/handlers/getProductById.ts @@ -1,5 +1,10 @@ import { APIGatewayProxyResult } from "aws-lambda"; -import { ProductByIdEvent, errorResponse, successResponse, ProductService } from "../services/product-service"; +import { + ProductByIdEvent, + errorResponse, + successResponse, + ProductService +} from "../services/product-service"; export const getSingleProduct = (productService: ProductService) => async (event: ProductByIdEvent): Promise => { diff --git a/product-service/src/handlers/getProductList.test.ts b/product-service/src/handlers/getProductList.test.ts index 297a470..cd98c62 100644 --- a/product-service/src/handlers/getProductList.test.ts +++ b/product-service/src/handlers/getProductList.test.ts @@ -1,9 +1,10 @@ import { APIGatewayProxyResult } from 'aws-lambda'; import { ProductService } from '../services/product-service'; import { getProductList } from './getProductList'; +import { DynamoDbRepository } from '../services/repository/dynamodb-repository'; describe('getProductList tests', () => { - const service = new ProductService(); + const service = new ProductService(new DynamoDbRepository()); beforeEach(() => { jest.clearAllMocks(); 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" }, From 6c0b5fea85fa03bbb712089b5bef1665247d292d Mon Sep 17 00:00:00 2001 From: barbedwire Date: Fri, 9 Jun 2023 21:40:48 +0200 Subject: [PATCH 11/22] add payload validation and request logging --- product-service/package-lock.json | 11 +++++++- product-service/package.json | 3 ++- product-service/src/handlers/addProduct.ts | 27 ++++++++++++++----- .../src/handlers/getProductById.ts | 2 ++ .../src/handlers/getProductList.ts | 4 +-- 5 files changed, 36 insertions(+), 11 deletions(-) diff --git a/product-service/package-lock.json b/product-service/package-lock.json index c20c122..064786c 100644 --- a/product-service/package-lock.json +++ b/product-service/package-lock.json @@ -8,7 +8,8 @@ "name": "product-service", "version": "1.0.0", "dependencies": { - "dotenv": "^16.1.4" + "dotenv": "^16.1.4", + "zod": "^3.21.4" }, "devDependencies": { "@aws-cdk/aws-apigatewayv2-alpha": "^2.82.0-alpha.0", @@ -8113,6 +8114,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..4368445 100644 --- a/product-service/package.json +++ b/product-service/package.json @@ -27,6 +27,7 @@ "typescript": "^4.3.0" }, "dependencies": { - "dotenv": "^16.1.4" + "dotenv": "^16.1.4", + "zod": "^3.21.4" } } diff --git a/product-service/src/handlers/addProduct.ts b/product-service/src/handlers/addProduct.ts index fc3a373..6923255 100644 --- a/product-service/src/handlers/addProduct.ts +++ b/product-service/src/handlers/addProduct.ts @@ -1,4 +1,5 @@ -import { APIGatewayProxyResult } from "aws-lambda"; +import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda"; +import { z } from 'zod'; import { ProductByIdEvent, errorResponse, @@ -6,20 +7,32 @@ import { ProductService } from "../services/product-service"; import { Product } from "../services/repository/types"; +import { log } from "console"; + +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: ProductByIdEvent): Promise => { + async (event: APIGatewayProxyEvent): Promise => { try { - // TODO check for the empty body - const payload: Product = JSON.parse(event.body!); + 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'), 400); + } - // TODO process a valid result const result = await productService.createProduct(payload); - return successResponse(result, 201); } catch (err: any) { - console.log('error', err); return errorResponse(err); } } diff --git a/product-service/src/handlers/getProductById.ts b/product-service/src/handlers/getProductById.ts index a150ede..bafcd2d 100644 --- a/product-service/src/handlers/getProductById.ts +++ b/product-service/src/handlers/getProductById.ts @@ -9,6 +9,8 @@ import { 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); diff --git a/product-service/src/handlers/getProductList.ts b/product-service/src/handlers/getProductList.ts index 449296c..1d4b5d3 100644 --- a/product-service/src/handlers/getProductList.ts +++ b/product-service/src/handlers/getProductList.ts @@ -1,8 +1,8 @@ -import { APIGatewayProxyResult } from "aws-lambda"; +import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda"; import { ProductService, errorResponse, successResponse } from "../services/product-service"; export const getProductList = (productService: ProductService) => - async (): Promise => { + async (event: APIGatewayProxyEvent): Promise => { try { const products = await productService.getAllProducts(); return successResponse(products); From eb5bf8b761d189144caaaa3188b2dfa819e58ac4 Mon Sep 17 00:00:00 2001 From: barbedwire Date: Fri, 9 Jun 2023 21:45:59 +0200 Subject: [PATCH 12/22] add status-codes --- product-service/package-lock.json | 6 ++++++ product-service/package.json | 1 + product-service/src/handlers/addProduct.ts | 6 +++--- product-service/src/handlers/getProductById.ts | 3 ++- product-service/src/utils.ts | 5 +++-- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/product-service/package-lock.json b/product-service/package-lock.json index 064786c..09b42da 100644 --- a/product-service/package-lock.json +++ b/product-service/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "dotenv": "^16.1.4", + "http-status-codes": "^2.2.0", "zod": "^3.21.4" }, "devDependencies": { @@ -3868,6 +3869,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", diff --git a/product-service/package.json b/product-service/package.json index 4368445..21ee471 100644 --- a/product-service/package.json +++ b/product-service/package.json @@ -28,6 +28,7 @@ }, "dependencies": { "dotenv": "^16.1.4", + "http-status-codes": "^2.2.0", "zod": "^3.21.4" } } diff --git a/product-service/src/handlers/addProduct.ts b/product-service/src/handlers/addProduct.ts index 6923255..07d4316 100644 --- a/product-service/src/handlers/addProduct.ts +++ b/product-service/src/handlers/addProduct.ts @@ -1,4 +1,5 @@ import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda"; +import { StatusCodes } from 'http-status-codes'; import { z } from 'zod'; import { ProductByIdEvent, @@ -7,7 +8,6 @@ import { ProductService } from "../services/product-service"; import { Product } from "../services/repository/types"; -import { log } from "console"; const ProductSchema = z.object({ id: z.string(), @@ -26,11 +26,11 @@ export const addProduct = (productService: ProductService) => const { success } = ProductSchema.safeParse(payload); if (!success) { - return errorResponse(new Error('Payload is empty or invalid'), 400); + return errorResponse(new Error('Payload is empty or invalid'), StatusCodes.BAD_REQUEST); } const result = await productService.createProduct(payload); - return successResponse(result, 201); + 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 bafcd2d..042974e 100644 --- a/product-service/src/handlers/getProductById.ts +++ b/product-service/src/handlers/getProductById.ts @@ -1,4 +1,5 @@ import { APIGatewayProxyResult } from "aws-lambda"; +import { StatusCodes } from 'http-status-codes'; import { ProductByIdEvent, errorResponse, @@ -15,7 +16,7 @@ export const getSingleProduct = (productService: ProductService) => 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); 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: { From 421cc2470c88359c1836c02b41c752a29d58591d Mon Sep 17 00:00:00 2001 From: barbedwire Date: Sat, 10 Jun 2023 09:37:13 +0200 Subject: [PATCH 13/22] cleanup logging --- product-service/src/handlers/addProduct.ts | 4 +--- product-service/src/handlers/getProductById.ts | 1 - product-service/src/handlers/getProductList.ts | 3 ++- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/product-service/src/handlers/addProduct.ts b/product-service/src/handlers/addProduct.ts index 07d4316..556766a 100644 --- a/product-service/src/handlers/addProduct.ts +++ b/product-service/src/handlers/addProduct.ts @@ -2,7 +2,6 @@ import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda"; import { StatusCodes } from 'http-status-codes'; import { z } from 'zod'; import { - ProductByIdEvent, errorResponse, successResponse, ProductService @@ -20,8 +19,7 @@ const ProductSchema = z.object({ export const addProduct = (productService: ProductService) => async (event: APIGatewayProxyEvent): Promise => { try { - console.log('Incoming request', event); - + console.log('Incoming request', event); const payload: Product = JSON.parse(event.body! || '{}'); const { success } = ProductSchema.safeParse(payload); diff --git a/product-service/src/handlers/getProductById.ts b/product-service/src/handlers/getProductById.ts index 042974e..05f7b26 100644 --- a/product-service/src/handlers/getProductById.ts +++ b/product-service/src/handlers/getProductById.ts @@ -11,7 +11,6 @@ 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); diff --git a/product-service/src/handlers/getProductList.ts b/product-service/src/handlers/getProductList.ts index 1d4b5d3..aeee8c4 100644 --- a/product-service/src/handlers/getProductList.ts +++ b/product-service/src/handlers/getProductList.ts @@ -2,8 +2,9 @@ import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda"; import { ProductService, errorResponse, successResponse } from "../services/product-service"; export const getProductList = (productService: ProductService) => - async (event: APIGatewayProxyEvent): Promise => { + async (event?: APIGatewayProxyEvent): Promise => { try { + console.log('Incoming request', event); const products = await productService.getAllProducts(); return successResponse(products); } From 0cb1c3d9120beedac56df6d66ab17f033fd718ca Mon Sep 17 00:00:00 2001 From: barbedwire Date: Sat, 10 Jun 2023 09:56:45 +0200 Subject: [PATCH 14/22] reorganize and fix tests --- product-service/jest.config.js | 2 +- .../handlers => test}/getProductById.test.ts | 19 ++++++++++--------- .../handlers => test}/getProductList.test.ts | 14 +++++++++----- 3 files changed, 20 insertions(+), 15 deletions(-) rename product-service/{src/handlers => test}/getProductById.test.ts (69%) rename product-service/{src/handlers => test}/getProductList.test.ts (72%) 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/src/handlers/getProductById.test.ts b/product-service/test/getProductById.test.ts similarity index 69% rename from product-service/src/handlers/getProductById.test.ts rename to product-service/test/getProductById.test.ts index 56fd2bb..ad75e73 100644 --- a/product-service/src/handlers/getProductById.test.ts +++ b/product-service/test/getProductById.test.ts @@ -1,10 +1,11 @@ import { APIGatewayProxyResult } from 'aws-lambda'; -import { ProductService } from '../services/product-service'; -import { getSingleProduct } from './getProductById'; -import { ProductByIdEvent } from '../services/product-service'; -import { DynamoDbRepository } from '../services/repository/dynamodb-repository'; +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', () => { +describe('getProductById tests', () => { const service = new ProductService(new DynamoDbRepository()); const eventParams = { @@ -29,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 () => { @@ -38,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'})); }); @@ -47,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 72% rename from product-service/src/handlers/getProductList.test.ts rename to product-service/test/getProductList.test.ts index cd98c62..7b39a0f 100644 --- a/product-service/src/handlers/getProductList.test.ts +++ b/product-service/test/getProductList.test.ts @@ -1,7 +1,8 @@ import { APIGatewayProxyResult } from 'aws-lambda'; -import { ProductService } from '../services/product-service'; -import { getProductList } from './getProductList'; -import { DynamoDbRepository } from '../services/repository/dynamodb-repository'; +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(new DynamoDbRepository()); @@ -28,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)); }); @@ -38,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 From fe453a488bcd894939e4bb4026bc4896be4bc1a0 Mon Sep 17 00:00:00 2001 From: barbedwire Date: Sat, 10 Jun 2023 21:07:51 +0200 Subject: [PATCH 15/22] add addProduct tests --- product-service/test/addProduct.test.ts | 50 +++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 product-service/test/addProduct.test.ts 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 From 2b3e27a3b0e9dda6ffd1284715261844f7fa1be1 Mon Sep 17 00:00:00 2001 From: barbedwire Date: Sat, 10 Jun 2023 21:37:10 +0200 Subject: [PATCH 16/22] add and reorganize db scripts --- .../scripts/{ => dynamo}/fill-tables.sh | 0 .../scripts/postgres/create-tables.sql | 14 ++++++++++++++ product-service/scripts/postgres/fill-tables.sql | 16 ++++++++++++++++ 3 files changed, 30 insertions(+) rename product-service/scripts/{ => dynamo}/fill-tables.sh (100%) create mode 100644 product-service/scripts/postgres/create-tables.sql create mode 100644 product-service/scripts/postgres/fill-tables.sql 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 From 6a94c3eb1056042feb3e021e1c3ac08ac3ee030e Mon Sep 17 00:00:00 2001 From: barbedwire Date: Sat, 10 Jun 2023 21:56:54 +0200 Subject: [PATCH 17/22] install postgres package --- product-service/package-lock.json | 91 +++++++++++++++++++++++++++---- product-service/package.json | 1 + 2 files changed, 81 insertions(+), 11 deletions(-) diff --git a/product-service/package-lock.json b/product-service/package-lock.json index 09b42da..c4693b9 100644 --- a/product-service/package-lock.json +++ b/product-service/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "dotenv": "^16.1.4", "http-status-codes": "^2.2.0", + "pg": "^8.11.0", "zod": "^3.21.4" }, "devDependencies": { @@ -2795,6 +2796,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", @@ -6538,6 +6547,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", @@ -6598,26 +6612,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", @@ -6629,6 +6687,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", @@ -6681,7 +6747,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" } @@ -6690,7 +6755,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" } @@ -6699,7 +6763,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" } @@ -6708,7 +6771,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" }, @@ -7250,6 +7312,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", @@ -8053,7 +8123,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" } diff --git a/product-service/package.json b/product-service/package.json index 21ee471..be45eba 100644 --- a/product-service/package.json +++ b/product-service/package.json @@ -29,6 +29,7 @@ "dependencies": { "dotenv": "^16.1.4", "http-status-codes": "^2.2.0", + "pg": "^8.11.0", "zod": "^3.21.4" } } From 44afba43c4c93b60e6c7619417b1b0bddeeec353 Mon Sep 17 00:00:00 2001 From: barbedwire Date: Mon, 12 Jun 2023 14:01:37 +0200 Subject: [PATCH 18/22] add uuid package --- product-service/package-lock.json | 17 +++++++++++++---- product-service/package.json | 1 + 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/product-service/package-lock.json b/product-service/package-lock.json index c4693b9..677c3e1 100644 --- a/product-service/package-lock.json +++ b/product-service/package-lock.json @@ -11,6 +11,7 @@ "dotenv": "^16.1.4", "http-status-codes": "^2.2.0", "pg": "^8.11.0", + "uuid": "^9.0.0", "zod": "^3.21.4" }, "devDependencies": { @@ -2478,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", @@ -7948,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" } diff --git a/product-service/package.json b/product-service/package.json index be45eba..7347beb 100644 --- a/product-service/package.json +++ b/product-service/package.json @@ -30,6 +30,7 @@ "dotenv": "^16.1.4", "http-status-codes": "^2.2.0", "pg": "^8.11.0", + "uuid": "^9.0.0", "zod": "^3.21.4" } } From 3dd052168e1223ca6cb316fce3417552b9f4a6ad Mon Sep 17 00:00:00 2001 From: barbedwire Date: Mon, 12 Jun 2023 14:02:45 +0200 Subject: [PATCH 19/22] perform cleanup --- product-service/bin/aws-shop-nodejs-back.ts | 1 - .../src/services/repository/index.ts | 2 +- .../repository/postgres-repository.ts | 71 +++++++++++++++++++ .../src/services/repository/types.ts | 2 +- 4 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 product-service/src/services/repository/postgres-repository.ts diff --git a/product-service/bin/aws-shop-nodejs-back.ts b/product-service/bin/aws-shop-nodejs-back.ts index 0e88c5c..d56d7b1 100644 --- a/product-service/bin/aws-shop-nodejs-back.ts +++ b/product-service/bin/aws-shop-nodejs-back.ts @@ -1,4 +1,3 @@ -#!/usr/bin/env node import 'source-map-support/register'; import { App } from 'aws-cdk-lib'; import { AwsShopNodejsBackStack } from '../lib/aws-shop-nodejs-back-stack'; diff --git a/product-service/src/services/repository/index.ts b/product-service/src/services/repository/index.ts index 84a8283..1b568d5 100644 --- a/product-service/src/services/repository/index.ts +++ b/product-service/src/services/repository/index.ts @@ -1,6 +1,6 @@ import { Product, ProductsRepository} from './types'; export { - Product as ProductInterface, + 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 index 3b0fe9a..1dd25ab 100644 --- a/product-service/src/services/repository/types.ts +++ b/product-service/src/services/repository/types.ts @@ -9,5 +9,5 @@ export interface Product { export interface ProductsRepository { getProductById: (id: string) => Promise, getAllProducts: () => Promise, - createProduct: (payload: Product) => Promise, + createProduct: (payload: Product) => Promise, } \ No newline at end of file From 2e0bf8fc867b9708e34e63c4aef6e71d46263b65 Mon Sep 17 00:00:00 2001 From: barbedwire Date: Mon, 12 Jun 2023 14:03:21 +0200 Subject: [PATCH 20/22] add PostgresRepository --- product-service/src/handlers/addProduct.ts | 4 +++ product-service/src/handlers/index.ts | 5 ++- .../product-service/product-service.ts | 3 +- .../repository/dynamodb-repository.ts | 36 +++++++++++-------- 4 files changed, 30 insertions(+), 18 deletions(-) diff --git a/product-service/src/handlers/addProduct.ts b/product-service/src/handlers/addProduct.ts index 556766a..ea2c978 100644 --- a/product-service/src/handlers/addProduct.ts +++ b/product-service/src/handlers/addProduct.ts @@ -28,6 +28,10 @@ export const addProduct = (productService: ProductService) => } 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) { diff --git a/product-service/src/handlers/index.ts b/product-service/src/handlers/index.ts index 00d59b8..852a6a5 100644 --- a/product-service/src/handlers/index.ts +++ b/product-service/src/handlers/index.ts @@ -3,8 +3,11 @@ 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 = new ProductService(new DynamoDbRepository()); +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); diff --git a/product-service/src/services/product-service/product-service.ts b/product-service/src/services/product-service/product-service.ts index ef8e4c6..75a3753 100644 --- a/product-service/src/services/product-service/product-service.ts +++ b/product-service/src/services/product-service/product-service.ts @@ -1,4 +1,3 @@ -import * as aws from 'aws-sdk'; import { Product, ProductsRepository } from '../repository/types'; export class ProductService { @@ -12,7 +11,7 @@ export class ProductService { return this.repository.getProductById(id); } - async createProduct(payload: Product): Promise { + async createProduct(payload: Product): Promise { return this.repository.createProduct(payload); } } \ 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 index d1748c1..2486a4b 100644 --- a/product-service/src/services/repository/dynamodb-repository.ts +++ b/product-service/src/services/repository/dynamodb-repository.ts @@ -4,6 +4,8 @@ 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(); @@ -42,23 +44,27 @@ export class DynamoDbRepository implements ProductsRepository { } as Product }; - async createProduct(payload: Product): Promise { + async createProduct(payload: Product): Promise { const { id, title, description, price, count } = payload; - - 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(); + 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 From b3d62ff5579072f4057a69398575fcd6648d41ee Mon Sep 17 00:00:00 2001 From: barbedwire Date: Mon, 12 Jun 2023 14:03:49 +0200 Subject: [PATCH 21/22] rework stack file --- .../lib/aws-shop-nodejs-back-stack.ts | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/product-service/lib/aws-shop-nodejs-back-stack.ts b/product-service/lib/aws-shop-nodejs-back-stack.ts index 51e6871..ac64dad 100644 --- a/product-service/lib/aws-shop-nodejs-back-stack.ts +++ b/product-service/lib/aws-shop-nodejs-back-stack.ts @@ -2,6 +2,9 @@ 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, @@ -15,11 +18,26 @@ import { config as envConfig } from 'dotenv'; 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!, + } + } + } + constructor(scope: Construct, props?: StackProps) { - const APP_PREFIX = "bw-aws-shop-backend"; + const APP_PREFIX = "bw-aws-shop-backnd"; super(scope, `${APP_PREFIX}-stack`, props); - const policy = new Policy(this, `${APP_PREFIX}-dynamodb-read-policy`, { + const lambdaPolicy = new Policy(this, `${APP_PREFIX}-dynamodb-read-policy`, { statements: [ new PolicyStatement({ actions: [ @@ -31,49 +49,38 @@ export class AwsShopNodejsBackStack extends Stack { `arn:aws:dynamodb:*:*:table/${process.env.DB_TABLE_PRODUCTS}`, `arn:aws:dynamodb:*:*:table/${process.env.DB_TABLE_STOCKS}` ], - }) + }), ], }); const sharedProps: Partial = { entry: './src/handlers/index.ts', runtime: Runtime.NODEJS_18_X, + environment: this.getEnvironment(), }; 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 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", - environment: { - TABLE_PRODUCTS: process.env.DB_TABLE_PRODUCTS!, - TABLE_STOCKS: process.env.DB_TABLE_STOCKS!, - }, + handler: "createProduct", }); - getProductListLambda.role?.attachInlinePolicy(policy); - getProductByIdLambda.role?.attachInlinePolicy(policy); - createProductLambda.role?.attachInlinePolicy(policy); - + getProductListLambda.role?.attachInlinePolicy(lambdaPolicy); + getProductByIdLambda.role?.attachInlinePolicy(lambdaPolicy); + createProductLambda.role?.attachInlinePolicy(lambdaPolicy); + const api = new HttpApi(this, `${APP_PREFIX}-products-api`, { corsPreflight: { allowHeaders: ["*"], From 3b9a138d124a48db1aaa54213acb22fae3a2cf1e Mon Sep 17 00:00:00 2001 From: barbedwire Date: Mon, 12 Jun 2023 14:04:09 +0200 Subject: [PATCH 22/22] change documentation file format --- product-service/product-api.json | 222 +++++++++++++++++++++++++++++++ product-service/product-api.yaml | 71 ---------- 2 files changed, 222 insertions(+), 71 deletions(-) create mode 100644 product-service/product-api.json delete mode 100644 product-service/product-api.yaml 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