Skip to content

Commit

Permalink
Merge pull request #5 from kidp2h/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
kidp2h authored Jul 29, 2024
2 parents adf2418 + fd426e5 commit 7f5300b
Show file tree
Hide file tree
Showing 37 changed files with 416 additions and 40 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
name: Nest.JS clean architecture CI
on:
push:
branches: ['*']
branches: [main, develop]
pull_request:
branches: [main]
branches: [main, develop]
jobs:
lint-commit:
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
pnpm lint-staged
pnpm lint-staged && pnpm test

2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default tseslint.config(
'@typescript-eslint/no-explicit-any': 'off',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'no-console': 'warn',
'no-console': 'error',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-namespace': 'off',
},
Expand Down
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,7 @@
"lint-staged": {
"*.ts": [
"pnpm lint",
"pnpm format",
"pnpm run test",
"pnpm run test:e2e"
"pnpm format"
]
}
}
46 changes: 46 additions & 0 deletions src/application/di/auth.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { AuthFacadeUsecase } from '@/domain/auth';
import { AuthorizeUsecase } from '@/domain/auth/usecases';
import { UserRepository } from '@/domain/user';
import { AuthController } from '@/presentation/controllers/auth';

import { UserModel } from '@/infrastructure/typeorm/models';
import { UserEntity } from '@/domain/user';
import { UserRepositoryImpl } from '@/infrastructure/typeorm/repositories';
import { Module } from '@nestjs/common';
import { Mapper } from '@/core';
import { UserMapper } from '@/infrastructure/typeorm/mappers';
import { CacheService } from '@/infrastructure/redis/cache';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({})
export class AuthFacadeModule {
static register() {
return {
module: AuthFacadeModule,
imports: [TypeOrmModule.forFeature([UserModel])],
providers: [
AuthFacadeUsecase,
AuthorizeUsecase,

{
provide: UserRepository,
useClass: UserRepositoryImpl,
},

{
provide: Mapper<UserEntity, UserModel>,
useClass: UserMapper,
},
],
exports: [AuthFacadeUsecase, AuthorizeUsecase],
};
}
}

@Module({
imports: [AuthFacadeModule.register()],
controllers: [AuthController],
providers: [CacheService],
exports: [],
})
export class AuthModule {}
2 changes: 2 additions & 0 deletions src/application/di/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export { PresentationModule } from './presentation.module';
export { TypeOrmModule } from './typeorm.module';
export { AppModule } from './app.module';
export { AuthModule } from './auth.module';
export { UserModule } from './user.module';
4 changes: 2 additions & 2 deletions src/application/di/presentation.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Module } from '@nestjs/common';
import { UserFacadeUsecase } from '@/domain/user';
import { UserModule } from './user.module';
import { AuthModule } from './auth.module';

@Module({
imports: [UserModule],
imports: [UserModule, AuthModule],
controllers: [],
providers: [],
})
Expand Down
22 changes: 14 additions & 8 deletions src/application/di/user.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,33 @@ import { UserController } from '@/presentation/controllers/user';
import { Module } from '@nestjs/common';

import { TypeOrmModule } from '@nestjs/typeorm';
import { UserModel as TypeormUserModel } from '@/infrastructure/typeorm/models';
import { UserModel } from '@/infrastructure/typeorm/models';
import { UserEntity } from '@/domain/user';
import { UserRepositoryImpl as RepoTypeorm } from '@/infrastructure/typeorm/repositories';
import { UserRepositoryImpl } from '@/infrastructure/typeorm/repositories';

import { Mapper } from '@/core';
import { UserMapper as TypeormUserMapper } from '@/infrastructure/typeorm/mappers';
import { UserMapper } from '@/infrastructure/typeorm/mappers';

@Module({})
export class UserFacadeModule {
static register() {
return {
module: UserFacadeModule,

imports: [TypeOrmModule.forFeature([TypeormUserModel])],
imports: [TypeOrmModule.forFeature([UserModel])],
providers: [
UserFacadeUsecase,
CreateUserUsecase,
GetUserUsecase,

{
provide: UserRepository,
useClass: RepoTypeorm,
useClass: UserRepositoryImpl,
},

{
provide: Mapper<UserEntity, TypeormUserModel>,
useClass: TypeormUserMapper,
provide: Mapper<UserEntity, UserModel>,
useClass: UserMapper,
},
],
exports: [UserFacadeUsecase, CreateUserUsecase, GetUserUsecase],
Expand All @@ -42,7 +42,13 @@ export class UserFacadeModule {
@Module({
imports: [UserFacadeModule.register()],
controllers: [UserController],
providers: [CacheService],
providers: [
CacheService,
{
provide: Mapper<UserEntity, UserModel>,
useClass: UserMapper,
},
],
exports: [],
})
export class UserModule {}
9 changes: 9 additions & 0 deletions src/application/dtos/auth/authorize.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Length } from 'class-validator';

export class AuthorizeDto {
@Length(4, 20)
username: string;

@Length(6)
password: string;
}
2 changes: 1 addition & 1 deletion src/application/dtos/user/create-user.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IsNotEmpty, IsString, Length } from 'class-validator';
import { IsString, Length } from 'class-validator';

export class CreateUserDto {
@IsString()
Expand Down
6 changes: 3 additions & 3 deletions src/core/mapper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export abstract class Mapper<D, E> {
abstract toEntity(entity: Partial<E>): Partial<D>;
abstract toModel(domain: Partial<D>): Partial<E>;
export abstract class Mapper<E, M> {
abstract toEntity(entity: Partial<M>): Partial<E>;
abstract toModel(domain: Partial<E>): Partial<M>;
}
2 changes: 2 additions & 0 deletions src/core/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ export abstract class Repository<TEntity extends Entity> {
): Promise<Partial<TEntity>>;
abstract delete(id: string): Promise<Partial<TEntity>>;
abstract create(data: Partial<TEntity>): Promise<Partial<TEntity>>;

abstract findOne(filter: Partial<TEntity>): Promise<Partial<TEntity>>;
}
2 changes: 1 addition & 1 deletion src/core/usecase.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export abstract class Usecase<T> {
export abstract class Usecase<T = unknown> {
abstract execute(...args: any[]): Promise<Partial<T>>;
}
15 changes: 15 additions & 0 deletions src/domain/auth/auth.facade.usecase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Inject, Injectable } from '@nestjs/common';
import { AuthorizeUsecase } from './usecases';
import { AuthorizeDto } from '@/application/dtos/auth/authorize.dto';

@Injectable()
export class AuthFacadeUsecase {
constructor(
@Inject(AuthorizeUsecase)
private readonly authorizeUsecase: AuthorizeUsecase,
) {}

authorize(payload: AuthorizeDto) {
return this.authorizeUsecase.execute(payload);
}
}
3 changes: 3 additions & 0 deletions src/domain/auth/auth.usecase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Usecase } from '@/core';

export abstract class AuthUsecase extends Usecase {}
2 changes: 2 additions & 0 deletions src/domain/auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { AuthUsecase } from './auth.usecase';
export { AuthFacadeUsecase } from './auth.facade.usecase';
65 changes: 65 additions & 0 deletions src/domain/auth/usecases/authorize.usecase.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Test } from '@nestjs/testing';
import { AuthUsecase } from '../auth.usecase';
import { AuthorizeUsecase } from './authorize.usecase';
import { UserEntity, UserRepository } from '@/domain/user';
import { faker } from '@faker-js/faker';
import bcrypt from 'bcryptjs';

const mockPayload = {
username: faker.internet.userName(),
password: faker.internet.password(),
};
const mockUser = {
id: faker.string.uuid(),
username: mockPayload.username,
password: bcrypt.hashSync(mockPayload.password, 10),
createdAt: new Date(),
deletedAt: null,
updatedAt: new Date(),
};

describe('AuthorizeUsecase', () => {
let usecase: AuthUsecase;
let repository: UserRepository;

beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
AuthorizeUsecase,
{
provide: UserRepository,
useValue: {
findOne: jest.fn(),
},
},
],
}).compile();
usecase = module.get<AuthUsecase>(AuthorizeUsecase);
repository = module.get<UserRepository>(UserRepository);
});

it('should be defined', () => {
expect(usecase).toBeDefined();
expect(repository).toBeDefined();
});
it('should return null if user not found', async () => {
jest.spyOn(repository, 'findOne').mockResolvedValue(null);
expect(
await usecase.execute({ username: 'username', password: 'password' }),
).toBeNull();
});

it('should return user if user found', async () => {
jest.spyOn(repository, 'findOne').mockResolvedValue(mockUser);
const result = (await usecase.execute(mockPayload)) as UserEntity;
expect(result.id).toEqual(mockUser.id);
});
it('should return null if password is invalid', async () => {
jest.spyOn(repository, 'findOne').mockResolvedValue(mockUser);
const result = (await usecase.execute({
username: 'test',
password: 'wrong password',
})) as UserEntity;
expect(result).toEqual(null);
});
});
22 changes: 22 additions & 0 deletions src/domain/auth/usecases/authorize.usecase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { AuthorizeDto } from '@/application/dtos/auth/authorize.dto';
import { AuthUsecase } from '@/domain/auth';
import { UserRepository } from '@/domain/user';
import { Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs';

@Injectable()
export class AuthorizeUsecase extends AuthUsecase {
constructor(private readonly userRepository: UserRepository) {
super();
}

async execute(payload: AuthorizeDto): Promise<Partial<any>> {
const { username, password } = payload;
const user = await this.userRepository.findOne({ username });

if (!user) return null;
const isValidPassword = bcrypt.compareSync(password, user.password);
if (!isValidPassword) return null;
return user;
}
}
1 change: 1 addition & 0 deletions src/domain/auth/usecases/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AuthorizeUsecase } from './authorize.usecase';
8 changes: 6 additions & 2 deletions src/domain/user/user.entity.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { Entity } from '@/core/entity';
import { Exclude } from 'class-transformer';

export class UserEntity extends Entity {
@Exclude()
public password: string;
constructor(
public id: string,
public username: string,
password: string,

public createdAt: Date,
public updatedAt: Date,
public deletedAt: Date,

public password?: string,
) {
super();

this.password = password;
}
}
4 changes: 2 additions & 2 deletions src/domain/user/user.facade.usecase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ export class UserFacadeUsecase {
@Inject(CreateUserUsecase) private readonly createUserUsecase: UserUsecase,
) {}

async createUser(data: Partial<UserEntity>) {
createUser(data: Partial<UserEntity>) {
return this.createUserUsecase.execute(data);
}

async getUser(id: string) {
getUser(id: string) {
return this.getUserUsecase.execute(id);
}
}
1 change: 1 addition & 0 deletions src/infrastructure/typeorm/mappers/user.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export class UserMapper extends Mapper<UserEntity, UserModel> {
return new UserEntity(
data.id,
data.username,
data.password,
data.createdAt,
data.updatedAt,
data.deletedAt,
Expand Down
2 changes: 1 addition & 1 deletion src/infrastructure/typeorm/models/user.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class UserModel {
username: string;

@Column({
select: false,
select: true,
})
password?: string;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ export class UserRepositoryImpl extends UserRepository {
) {
super();
}

async findOne(filter: Partial<UserEntity>): Promise<Partial<UserEntity>> {
const userTypeorm = await this.userRepository.findOne({
where: filter,
});
return this.userMapper.toEntity(userTypeorm);
}

async findUnique(id: string) {
this.logger.log('[Typeorm] findUnique', id);
if (!uuid.validate(id)) return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,4 +159,12 @@ describe('UserRepository', () => {
const result = await repository.findMany({});
expect(result).toEqual(users);
});
it('should return only one user', async () => {
const user = mockUser;
jest
.spyOn(userRepository, 'findOne')
.mockImplementation(() => Promise.resolve(user));
const result = await repository.findOne({ username: user.username });
expect(result).toEqual(user);
});
});
Loading

0 comments on commit 7f5300b

Please sign in to comment.