Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,14 @@ APP_PORT=4000
AUTH_USERNAME=yourGithubLogin ### It should be your github username
AUTH_PASSWORD=TEST_PASSWORD
APP_URL=http://localhost:4000
###
###

### Database configuration (local development)
DB_HOST=localhost
DB_PORT=5432
DB_NAME=cartdb
DB_USERNAME=postgres
DB_PASSWORD=postgres
### For AWS Lambda, DB_SECRET_ARN is set by CDK and DB_PASSWORD is retrieved from Secrets Manager
# DB_SECRET_ARN=arn:aws:secretsmanager:us-east-1:ACCOUNT_ID:secret:...
DB_SSL=false
2,437 changes: 2,243 additions & 194 deletions package-lock.json

Large diffs are not rendered by default.

16 changes: 12 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"private": true,
"license": "UNLICENSED",
"scripts": {
"prebuild": "rimraf dist",
"prebuild": "./node_modules/.bin/rimraf dist",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
Expand All @@ -21,29 +21,37 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.1045.0",
"@nestjs/common": "^10.0.3",
"@nestjs/config": "^3.0.0",
"@nestjs/core": "^10.0.3",
"@nestjs/jwt": "^10.1.0",
"@nestjs/passport": "^10.0.0",
"@nestjs/platform-express": "^10.0.3",
"@nestjs/typeorm": "^10.0.2",
"@vendia/serverless-express": "^4.12.6",
"aws-lambda": "^1.0.7",
"helmet": "^7.0.0",
"passport": "^0.6.0",
"passport-http": "^0.3.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"pg": "^8.20.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1"
"rxjs": "^7.8.1",
"typeorm": "^0.3.29"
},
"devDependencies": {
"@nestjs/cli": "^10.0.3",
"@nestjs/schematics": "^10.0.1",
"@nestjs/testing": "^10.0.3",
"@types/aws-lambda": "^8.10.161",
"@types/express": "^4.17.17",
"@types/jest": "29.5.2",
"@types/node": "^20.3.1",
"@types/node": "^22.19.19",
"@types/passport-jwt": "^3.0.8",
"@types/passport-local": "^1.0.35",
"@types/pg": "^8.20.0",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "5.60.0",
"@typescript-eslint/parser": "5.60.0",
Expand All @@ -58,7 +66,7 @@
"ts-loader": "^9.4.3",
"ts-node": "10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
"typescript": "^5.9.3"
},
"jest": {
"moduleFileExtensions": [
Expand Down
9 changes: 8 additions & 1 deletion src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@ import { CartModule } from './cart/cart.module';
import { AuthModule } from './auth/auth.module';
import { OrderModule } from './order/order.module';
import { ConfigModule } from '@nestjs/config';
import { DatabaseModule } from './database/database.module';

@Module({
imports: [AuthModule, CartModule, OrderModule, ConfigModule.forRoot()],
imports: [
ConfigModule.forRoot({ isGlobal: true }),
DatabaseModule,
AuthModule,
CartModule,
OrderModule,
],
controllers: [AppController],
providers: [],
})
Expand Down
4 changes: 2 additions & 2 deletions src/auth/strategies/basic.strategy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { Injectable, UnauthorizedException, Inject } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';

import { BasicStrategy as Strategy } from 'passport-http';
Expand All @@ -7,7 +7,7 @@ import { AuthService } from '../auth.service';

@Injectable()
export class BasicStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
constructor(@Inject(AuthService) private authService: AuthService) {
super();
}

Expand Down
49 changes: 28 additions & 21 deletions src/cart/cart.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,21 @@ import {
import { BasicAuthGuard } from '../auth';
import { Order, OrderService } from '../order';
import { AppRequest, getUserIdFromRequest } from '../shared';
import { calculateCartTotal } from './models-rules';
import { CartService } from './services';
import { CartItem } from './models';
import { CartItemEntity, ProductData } from './entities/cart-item.entity';
import { CreateOrderDto, PutCartPayload } from 'src/order/type';

type FrontendCartItem = { product: ProductData; count: number };

function toFrontendItems(items: CartItemEntity[]): FrontendCartItem[] {
return items
.filter((item) => item.product_data)
.map((item) => ({
product: item.product_data as ProductData,
count: item.count,
}));
}

@Controller('api/profile/cart')
export class CartController {
constructor(
Expand All @@ -28,66 +38,63 @@ export class CartController {
// @UseGuards(JwtAuthGuard)
@UseGuards(BasicAuthGuard)
@Get()
findUserCart(@Req() req: AppRequest): CartItem[] {
const cart = this.cartService.findOrCreateByUserId(
async findUserCart(@Req() req: AppRequest): Promise<FrontendCartItem[]> {
const cart = await this.cartService.findOrCreateByUserId(
getUserIdFromRequest(req),
);

return cart.items;
return toFrontendItems(cart.items);
}

// @UseGuards(JwtAuthGuard)
@UseGuards(BasicAuthGuard)
@Put()
updateUserCart(
async updateUserCart(
@Req() req: AppRequest,
@Body() body: PutCartPayload,
): CartItem[] {
): Promise<FrontendCartItem[]> {
// TODO: validate body payload...
const cart = this.cartService.updateByUserId(
const cart = await this.cartService.updateByUserId(
getUserIdFromRequest(req),
body,
);

return cart.items;
return toFrontendItems(cart.items);
}

// @UseGuards(JwtAuthGuard)
@UseGuards(BasicAuthGuard)
@Delete()
@HttpCode(HttpStatus.OK)
clearUserCart(@Req() req: AppRequest) {
this.cartService.removeByUserId(getUserIdFromRequest(req));
async clearUserCart(@Req() req: AppRequest): Promise<void> {
await this.cartService.removeByUserId(getUserIdFromRequest(req));
}

// @UseGuards(JwtAuthGuard)
@UseGuards(BasicAuthGuard)
@Put('order')
checkout(@Req() req: AppRequest, @Body() body: CreateOrderDto) {
async checkout(@Req() req: AppRequest, @Body() body: CreateOrderDto) {
const userId = getUserIdFromRequest(req);
const cart = this.cartService.findByUserId(userId);
const cart = await this.cartService.findByUserId(userId);

if (!(cart && cart.items.length)) {
throw new BadRequestException('Cart is empty');
}

const { id: cartId, items } = cart;
const total = calculateCartTotal(items);
const order = this.orderService.create({
userId,
cartId,
items: items.map(({ product, count }) => ({
productId: product.id,
items: items.map(({ product_id, count }) => ({
productId: product_id,
count,
})),
address: body.address,
total,
total: 0,
});
this.cartService.removeByUserId(userId);
await this.cartService.removeByUserId(userId);

return {
order,
};
return { order };
}

@UseGuards(BasicAuthGuard)
Expand Down
8 changes: 7 additions & 1 deletion src/cart/cart.module.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

import { OrderModule } from '../order/order.module';

import { CartController } from './cart.controller';
import { CartService } from './services';
import { CartEntity } from './entities/cart.entity';
import { CartItemEntity } from './entities/cart-item.entity';

@Module({
imports: [OrderModule],
imports: [
OrderModule,
TypeOrmModule.forFeature([CartEntity, CartItemEntity]),
],
providers: [CartService],
controllers: [CartController],
})
Expand Down
28 changes: 28 additions & 0 deletions src/cart/entities/cart-item.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Entity, Column, ManyToOne, JoinColumn, PrimaryColumn } from 'typeorm';
import { CartEntity } from './cart.entity';

export type ProductData = {
id: string;
title: string;
description: string;
price: number;
};

@Entity('cart_items')
export class CartItemEntity {
@PrimaryColumn({ name: 'cart_id', type: 'uuid' })
cart_id!: string;

@PrimaryColumn({ type: 'varchar' })
product_id!: string;

@Column({ type: 'int' })
count!: number;

@Column({ type: 'simple-json', nullable: true })
product_data!: ProductData | null;

@ManyToOne(() => CartEntity, (cart) => cart.items, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'cart_id' })
cart!: CartEntity;
}
38 changes: 38 additions & 0 deletions src/cart/entities/cart.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
OneToMany,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { CartItemEntity } from './cart-item.entity';

export enum CartStatus {
OPEN = 'OPEN',
ORDERED = 'ORDERED',
}

@Entity('carts')
export class CartEntity {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ type: 'varchar', nullable: false })
user_id: string;

@Column({ type: 'enum', enum: CartStatus, default: CartStatus.OPEN })
status: CartStatus;

@CreateDateColumn()
created_at: Date;

@UpdateDateColumn()
updated_at: Date;

@OneToMany(() => CartItemEntity, (item) => item.cart, {
cascade: true,
eager: true,
})
items: CartItemEntity[];
}
2 changes: 2 additions & 0 deletions src/cart/entities/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './cart.entity';
export * from './cart-item.entity';
Loading