Skip to content

Commit

Permalink
Merge pull request #19 from Guzbyte-tech/feature/User-entity-crud-api
Browse files Browse the repository at this point in the history
User Entity and CRUD APIs
  • Loading branch information
Villarley authored Dec 17, 2024
2 parents 0f1674f + 1ebae95 commit e77b03a
Show file tree
Hide file tree
Showing 13 changed files with 359 additions and 4 deletions.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
"license": "ISC",
"description": "",
"dependencies": {
"axios": "^1.7.9",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"express": "^4.21.1",
"express-validator": "^7.2.0",
"pg": "^8.13.1",
"reflect-metadata": "^0.2.2",
"typeorm": "^0.3.20"
Expand Down
3 changes: 2 additions & 1 deletion src/config/ormconfig.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { DataSource } from 'typeorm';
import { TestEntity } from '../entities/testEntity';
import { User } from '../entities/User';

export const testDataSource = new DataSource({
type: 'sqlite',
database: ':memory:',
synchronize: true,
entities: [TestEntity],
entities: [TestEntity, User]
});
7 changes: 4 additions & 3 deletions src/config/ormconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ const AppDataSource = new DataSource({
url: process.env.DATABASE_URL,
entities: [__dirname + '/../entities/*.ts'],
migrations: [__dirname + '/../migrations/*.ts'],
ssl: {
rejectUnauthorized: false,
},
// ssl: {
// rejectUnauthorized: false,
// },
ssl: false,
synchronize: false, // Set to true only in development; false in production
});

Expand Down
62 changes: 62 additions & 0 deletions src/controllers/UserController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Request, Response } from 'express';
import UserService from '../services/User.service';
import AppDataSource from '../config/ormconfig';

const userService = new UserService(AppDataSource);

export const createUser = async (req: Request, res: Response):Promise<void > => {
try {
const user = await userService.createUser(req.body);
res.status(201).json({success: true, message:"User Created Successfully", data: user});
} catch (error) {
res.status(500).json({ error: error.message });
}
};

export const getUser = async (req: Request, res: Response) :Promise<void> => {
try {
const user = await userService.getUserById(Number(req.params.id));
if (!user) {
res.status(404).json({success: false, message: 'User not found' });
} else {
res.status(200).json({success: true, message:"User Retrieved Successfully", data: user});
}
} catch (error) {
res.status(500).json({success: false, message: "Internal Server Error", error: error.message });
}
};

export const updateUser = async (req: Request, res: Response) : Promise<void> => {
try {
const user = await userService.updateUser(Number(req.params.id), req.body);
if (!user) {
res.status(404).json({success: false, message: 'User not found' });
} else {
res.status(200).json({success: true, message:"User Updated Successfully", data: user});
}
} catch (error) {
res.status(500).json({success: false, message: "Internal Server Error", error: error.message });
}
};

export const deleteUser = async (req: Request, res: Response) :Promise<void> => {
try {
const success = await userService.deleteUser(Number(req.params.id));
if (!success) {
res.status(404).json({success: false, message: 'User not found' });
} else {
res.send(204);
}
} catch (error) {
res.status(500).json({success: false, message: "Internal Server Error", error: error.message });
}
};

export const getAllUsers = async (req: Request, res: Response) :Promise<void> => {
try {
const users = await userService.getAllUsers();
res.status(200).json({success: true, message:"User Retrieved", data: users});
} catch (error) {
res.status(500).json({success: false, message: "Internal Server Error", error: error.message });
}
};
36 changes: 36 additions & 0 deletions src/dtos/UserDTO.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { IsEmail, IsNotEmpty, IsOptional, IsEnum, Matches, MinLength, MaxLength } from 'class-validator';

export class CreateUserDto {
@IsNotEmpty()
@Matches(/^0x[a-fA-F0-9]{40}$/, { message: 'Invalid wallet address format' })
walletAddress: string;

@IsOptional()
@MinLength(2, { message: 'Name is too short' })
@MaxLength(50, { message: 'Name is too long' })
name?: string;

@IsOptional()
@IsEmail({}, { message: 'Invalid email format' })
email?: string;

@IsNotEmpty()
@IsEnum(['buyer', 'seller', 'admin'], { message: 'Role must be buyer, seller, or admin' })
role: 'buyer' | 'seller' | 'admin';
}

export class UpdateUserDto {
@IsOptional()
@MinLength(2, { message: 'Name is too short' })
@MaxLength(50, { message: 'Name is too long' })
name?: string;

@IsOptional()
@IsEmail({}, { message: 'Invalid email format' })
email?: string;

@IsOptional()
@IsEnum(['buyer', 'seller', 'admin'], { message: 'Role must be buyer, seller, or admin' })
role?: 'buyer' | 'seller' | 'admin';
}

25 changes: 25 additions & 0 deletions src/entities/User.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';

@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;

@Column({ unique: true })
walletAddress: string;

@Column({ nullable: true })
name: string;

@Column({ nullable: true })
email: string;

@Column()
role: 'buyer' | 'seller' | 'admin';

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import 'reflect-metadata';
import express from 'express';
import AppDataSource from './config/ormconfig';
import userRoutes from './routes/UserRoutes';


const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.json());
app.use('/users', userRoutes);

AppDataSource.initialize()
.then(() => {
Expand All @@ -21,3 +24,6 @@ AppDataSource.initialize()
app.get('/', (req, res) => {
res.send('Hello, world!');
});


export default app; // Export the app for testing
28 changes: 28 additions & 0 deletions src/middleware/userValidation.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { plainToInstance } from 'class-transformer';
import { validate, ValidationError } from 'class-validator';
import { Request, Response, NextFunction } from 'express';

/**
* A validation middleware for validating request body data against a DTO class.
* @param dtoClass The DTO class to validate against.
*/
export function validationMiddleware(dtoClass: any) {
return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
const dtoInstance = plainToInstance(dtoClass, req.body);
const errors: ValidationError[] = await validate(dtoInstance);

if (errors.length > 0) {
const errorDetails = errors.map((error) => ({
property: error.property,
constraints: error.constraints,
}));

res.status(422).json({
message: 'Validation failed',
errors: errorDetails,
});
} else {
next();
}
};
}
16 changes: 16 additions & 0 deletions src/migrations/1734140974017-CreateUserTable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class CreateUserTable1734140974017 implements MigrationInterface {
name = 'CreateUserTable1734140974017'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "test_entity" ("id" SERIAL NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_cc0413536e3afc0e586996bea40" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "users" ("id" SERIAL NOT NULL, "walletAddress" character varying NOT NULL, "name" character varying, "email" character varying, "role" character varying NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_fc71cd6fb73f95244b23e2ef113" UNIQUE ("walletAddress"), CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id"))`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "users"`);
await queryRunner.query(`DROP TABLE "test_entity"`);
}

}
17 changes: 17 additions & 0 deletions src/routes/UserRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Router } from 'express';
import { createUser, deleteUser, getAllUsers, getUser, updateUser } from '../controllers/UserController';
import { validationMiddleware } from '../middleware/userValidation.middleware';
import { CreateUserDto, UpdateUserDto } from '../dtos/UserDTO';



const router = Router();

router.get('/', getAllUsers);
router.post('/create', validationMiddleware(CreateUserDto), createUser);
router.get('/show/:id', getUser);
router.put('/update/:id',validationMiddleware(UpdateUserDto), updateUser);
router.delete('/delete/:id', deleteUser);


export default router;
66 changes: 66 additions & 0 deletions src/services/User.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { QueryFailedError, Repository } from "typeorm";
import { User } from "../entities/User";
import { DataSource } from "typeorm";


export class UserService {
private userRepository: Repository<User>;

constructor(dataSource: DataSource) {
this.userRepository = dataSource.getRepository(User);
}

async createUser(data: Partial<User>): Promise<User> {

try {
const user = this.userRepository.create(data);
return await this.userRepository.save(user);
} catch (error) {
if (error instanceof QueryFailedError && error.message.includes("UNIQUE constraint failed")) {
throw new Error("The wallet address is already in use. Please use a unique wallet address.");
}
if (error.code === '23505' || error.code === 'SQLITE_CONSTRAINT') { // Handle unique constraint errors
if (error.message.includes('UQ_fc71cd6fb73f95244b23e2ef113')) {
throw new Error('The wallet address is already in use. Please use a unique wallet address.');
}
}
throw error;
}

}

async getUserById(id: number): Promise<User | null> {
return await this.userRepository.findOneBy({ id });
}

async updateUser(id: number, data: Partial<User>): Promise<User | null> {
try {
await this.userRepository.update(id, data);
return await this.getUserById(id);
} catch (error) {
if (error.code === '23505' || error.code === 'SQLITE_CONSTRAINT') { // Handle unique constraint errors
if (error.message.includes('UQ_fc71cd6fb73f95244b23e2ef113')) {
throw new Error('The wallet address is already in use. Please use a unique wallet address.');
}
}

if (error.code === '23505' || error.code === 'SQLITE_CONSTRAINT') { // Handle unique constraint errors
if (error.message.includes('UQ_fc71cd6fb73f95244b23e2ef113')) {
throw new Error('The wallet address is already in use. Please use a unique wallet address.');
}
}
throw error;
}
}

async deleteUser(id: number): Promise<boolean> {
const result = await this.userRepository.delete(id);
return result.affected === 1;
}

async getAllUsers(): Promise<User[]> {
return await this.userRepository.find();
}
}

export default UserService;
Loading

0 comments on commit e77b03a

Please sign in to comment.