diff --git a/README.md b/README.md index 2f70b6a..ab68b9d 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ You can use `npx nx list` to get a list of installed plugins. Then, run `npx nx | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | resolver(interface-adapters) | Define GraphQL schema and resolver. | | dto(interface-adapters) | Define DTOs for GraphQL schema. | -| mongoose(infrastructure) | Implements the repository interfaces defined in the domain layer using Mongoose as the ODM (Object Document Mapper).
Includes Mongoose Schema definitions, database connection management, and concrete implementations of repository interfaces (e.g., MongooseUserRepository). | +| mongoose(infrastructure) | Implements the repository interfaces defined in the domain layer using Mongoose as the ODM (Object Document Mapper).
Includes Mongoose Schema definitions, database connection management, and concrete implementations of repository interfaces (e.g., MongooseUsersRepository). | | service(application) | As the core of the application layer, it mainly interacts with the domain layer and interface-adapter layer.
If you migrate to a non-NestJS architecture in the future (e.g. other frameworks or microservices), the application tier code can be left unaffected. | | use-case(application) | Define business use cases and encapsulate business logic. | | entity(domain) | Define core business entities and business rules.
Maintain entity independence from database and framework. | diff --git a/apps/users/src/users/users.module.ts b/apps/users/src/users/users.module.ts index 1ec9dce..f0ed7b7 100644 --- a/apps/users/src/users/users.module.ts +++ b/apps/users/src/users/users.module.ts @@ -5,12 +5,27 @@ import { } from '@nestjs/apollo'; import { Module } from '@nestjs/common'; import { GraphQLModule } from '@nestjs/graphql'; -import { UsersService } from '@users/application'; -import { UsersResolver } from '@users/interface-adapters'; import { DatabaseModule } from '@shared/infrastructure-mongoose'; +import { UsersService, GetUserUseCase } from '@users/application'; +import { UsersResolver } from '@users/interface-adapters'; +import { + MongooseUsersRepository, + UserDocument, + UserSchema, +} from '@users/infrastructure-mongoose'; +import { MongooseModule } from '@nestjs/mongoose'; +import { USERS_REPOSITORY } from '@users/domain'; @Module({ - providers: [UsersResolver, UsersService], + providers: [ + UsersResolver, + UsersService, + GetUserUseCase, + { + provide: USERS_REPOSITORY, + useClass: MongooseUsersRepository, + }, + ], imports: [ GraphQLModule.forRoot({ driver: ApolloFederationDriver, @@ -27,6 +42,7 @@ import { DatabaseModule } from '@shared/infrastructure-mongoose'; plugins: [ApolloServerPluginInlineTrace()], }), DatabaseModule, + MongooseModule.forFeature([{ name: UserDocument.name, schema: UserSchema }]) ], }) export class UsersModule {} diff --git a/libs/users/application/src/index.ts b/libs/users/application/src/index.ts index c5a7c7d..192fddd 100644 --- a/libs/users/application/src/index.ts +++ b/libs/users/application/src/index.ts @@ -1,4 +1,4 @@ -export * from './lib/use-case/get-user.use-case'; +export * from './lib/use-cases/get-user.use-case'; // service export * from './lib/users.service'; diff --git a/libs/users/application/src/lib/use-case/get-user.use-case.spec.ts b/libs/users/application/src/lib/use-case/get-user.use-case.spec.ts deleted file mode 100644 index 650e7d2..0000000 --- a/libs/users/application/src/lib/use-case/get-user.use-case.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { GetUserUseCase } from './get-user.use-case'; -import { UserRepository } from '@users/domain'; - -describe('GetUserUseCase', () => { - let getUserUseCase: GetUserUseCase; - let userRepository: jest.Mocked; - - beforeEach(() => { - userRepository = { - findById: jest.fn(), - } as unknown as jest.Mocked; - - getUserUseCase = new GetUserUseCase(userRepository); - }); - - describe('execute', () => { - it('should return a user when found', async () => { - const user = { id: '1', name: 'John Doe' }; - userRepository.findById.mockResolvedValue(user); - - const result = await getUserUseCase.execute('1'); - - expect(result).toEqual(user); - expect(userRepository.findById).toHaveBeenCalledWith('1'); - }); - - it('should return null when user is not found', async () => { - userRepository.findById.mockResolvedValue(null); - - const result = await getUserUseCase.execute('1'); - - expect(result).toBeNull(); - expect(userRepository.findById).toHaveBeenCalledWith('1'); - }); - }); -}); \ No newline at end of file diff --git a/libs/users/application/src/lib/use-case/get-user.use-case.ts b/libs/users/application/src/lib/use-case/get-user.use-case.ts deleted file mode 100644 index a922928..0000000 --- a/libs/users/application/src/lib/use-case/get-user.use-case.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { User, UserRepository } from '@users/domain'; - -export class GetUserUseCase { - constructor(private readonly userRepository: UserRepository) {} - - async execute(id: string): Promise { - return this.userRepository.findById(id); - } -} diff --git a/libs/users/application/src/lib/use-cases/get-user.use-case.spec.ts b/libs/users/application/src/lib/use-cases/get-user.use-case.spec.ts new file mode 100644 index 0000000..7debc7a --- /dev/null +++ b/libs/users/application/src/lib/use-cases/get-user.use-case.spec.ts @@ -0,0 +1,35 @@ +import { GetUserUseCase } from './get-user.use-case'; +import { UsersRepository } from '@users/domain'; + +describe('GetUserUseCase', () => { + let getUserUseCase: GetUserUseCase; + let usersRepository: UsersRepository; + + beforeEach(() => { + usersRepository = { + findById: jest.fn(), + }; + getUserUseCase = new GetUserUseCase(usersRepository); + }); + + describe('execute', () => { + it('should return user when found', async () => { + const mockUser = { id: '1', name: 'John Doe' }; + (usersRepository.findById as jest.Mock).mockResolvedValue(mockUser); + + const result = await getUserUseCase.execute('1'); + + expect(result).toEqual(mockUser); + expect(usersRepository.findById).toHaveBeenCalledWith('1'); + }); + + it('should return null when user not found', async () => { + (usersRepository.findById as jest.Mock).mockResolvedValue(null); + + const result = await getUserUseCase.execute('1'); + + expect(result).toBeNull(); + expect(usersRepository.findById).toHaveBeenCalledWith('1'); + }); + }); +}); \ No newline at end of file diff --git a/libs/users/application/src/lib/use-cases/get-user.use-case.ts b/libs/users/application/src/lib/use-cases/get-user.use-case.ts new file mode 100644 index 0000000..be0f1e8 --- /dev/null +++ b/libs/users/application/src/lib/use-cases/get-user.use-case.ts @@ -0,0 +1,19 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { User, USERS_REPOSITORY, UsersRepository } from '@users/domain'; + +@Injectable() +export class GetUserUseCase { + constructor( + @Inject(USERS_REPOSITORY) + private readonly usersRepository: UsersRepository, + ) {} + + async execute(id: string): Promise { + const user = await this.usersRepository.findById(id); + + if (!user) { + return null; + } + return user; + } +} diff --git a/libs/users/application/src/lib/users.service.spec.ts b/libs/users/application/src/lib/users.service.spec.ts index 85a8661..c3879e0 100644 --- a/libs/users/application/src/lib/users.service.spec.ts +++ b/libs/users/application/src/lib/users.service.spec.ts @@ -1,30 +1,47 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { User } from '@users/domain'; - import { UsersService } from './users.service'; +import { GetUserUseCase } from './use-cases/get-user.use-case'; +import { User } from '@users/domain'; describe('UsersService', () => { let service: UsersService; + let getUserUseCase: GetUserUseCase; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [UsersService], + providers: [ + UsersService, + { + provide: GetUserUseCase, + useValue: { + execute: jest.fn(), + }, + }, + ], }).compile(); service = module.get(UsersService); + getUserUseCase = module.get(GetUserUseCase); }); it('should be defined', () => { expect(service).toBeDefined(); }); - it('should return a user by id', () => { - const user: User | undefined = service.findById('1'); - expect(user).toEqual({ id: '1', name: 'John Doe' }); - }); + describe('findById', () => { + it('should return a user if found', async () => { + const user: User = { id: '1', name: 'John Doe' }; + jest.spyOn(getUserUseCase, 'execute').mockResolvedValue(user); + + const result = await service.findById('1'); + expect(result).toEqual(user); + }); + + it('should return null if user not found', async () => { + jest.spyOn(getUserUseCase, 'execute').mockResolvedValue(null); - it('should return undefined if user is not found', () => { - const user: User | undefined = service.findById('3'); - expect(user).toBeUndefined(); + const result = await service.findById('2'); + expect(result).toBeNull(); + }); }); }); diff --git a/libs/users/application/src/lib/users.service.ts b/libs/users/application/src/lib/users.service.ts index b605946..8583406 100644 --- a/libs/users/application/src/lib/users.service.ts +++ b/libs/users/application/src/lib/users.service.ts @@ -1,14 +1,13 @@ import { Injectable } from '@nestjs/common'; import { User } from '@users/domain'; +import { GetUserUseCase } from './use-cases/get-user.use-case'; + @Injectable() export class UsersService { - private users: User[] = [ - { id: '1', name: 'John Doe' }, - { id: '2', name: 'Richard Roe' }, - ]; + constructor(private readonly getUserUseCase: GetUserUseCase) {} - findById(id: string): User | undefined { - return this.users.find((user) => user.id === id); + async findById(id: string): Promise { + return this.getUserUseCase.execute(id); } } diff --git a/libs/users/domain/src/index.ts b/libs/users/domain/src/index.ts index 9602d75..7e85695 100644 --- a/libs/users/domain/src/index.ts +++ b/libs/users/domain/src/index.ts @@ -1,3 +1,2 @@ -// user export * from './lib/user.entity'; -export * from './lib/user.repository'; +export * from './lib/users.repository'; diff --git a/libs/users/domain/src/lib/user.repository.ts b/libs/users/domain/src/lib/user.repository.ts deleted file mode 100644 index a3e9686..0000000 --- a/libs/users/domain/src/lib/user.repository.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { User } from './user.entity'; - -export interface UserRepository { - findById(id: string): Promise; -} diff --git a/libs/users/domain/src/lib/user.repository.spec.ts b/libs/users/domain/src/lib/users.repository.spec.ts similarity index 60% rename from libs/users/domain/src/lib/user.repository.spec.ts rename to libs/users/domain/src/lib/users.repository.spec.ts index dbac875..93f4ec3 100644 --- a/libs/users/domain/src/lib/user.repository.spec.ts +++ b/libs/users/domain/src/lib/users.repository.spec.ts @@ -1,7 +1,7 @@ -import { UserRepository } from './user.repository'; +import { UsersRepository } from './users.repository'; import { User } from './user.entity'; -class MockUserRepository implements UserRepository { +class MockUsersRepository implements UsersRepository { private users: User[] = [ { id: '1', name: 'John Doe' }, { id: '2', name: 'Jane Doe' }, @@ -12,20 +12,20 @@ class MockUserRepository implements UserRepository { } } -describe('UserRepository', () => { - let userRepository: UserRepository; +describe('UsersRepository', () => { + let usersRepository: UsersRepository; beforeEach(() => { - userRepository = new MockUserRepository(); + usersRepository = new MockUsersRepository(); }); test('findById should return a user by id', async () => { - const user = await userRepository.findById('1'); + const user = await usersRepository.findById('1'); expect(user).toEqual({ id: '1', name: 'John Doe' }); }); test('findById should return null if user not found', async () => { - const user = await userRepository.findById('3'); + const user = await usersRepository.findById('3'); expect(user).toBeNull(); }); }); diff --git a/libs/users/domain/src/lib/users.repository.ts b/libs/users/domain/src/lib/users.repository.ts new file mode 100644 index 0000000..4af6be0 --- /dev/null +++ b/libs/users/domain/src/lib/users.repository.ts @@ -0,0 +1,7 @@ +import { User } from './user.entity'; + +export interface UsersRepository { + findById(id: string): Promise; +} + +export const USERS_REPOSITORY = Symbol('USERS_REPOSITORY'); \ No newline at end of file diff --git a/libs/users/infrastructure/mongoose/src/index.ts b/libs/users/infrastructure/mongoose/src/index.ts index ac4244f..9310eb9 100644 --- a/libs/users/infrastructure/mongoose/src/index.ts +++ b/libs/users/infrastructure/mongoose/src/index.ts @@ -1,2 +1,2 @@ -export * from './lib/mongoose-user.repository'; +export * from './lib/mongoose-users.repository'; export * from './lib/user.schema'; \ No newline at end of file diff --git a/libs/users/infrastructure/mongoose/src/lib/mongoose-user.repository.spec.ts b/libs/users/infrastructure/mongoose/src/lib/mongoose-users.repository.spec.ts similarity index 82% rename from libs/users/infrastructure/mongoose/src/lib/mongoose-user.repository.spec.ts rename to libs/users/infrastructure/mongoose/src/lib/mongoose-users.repository.spec.ts index 412edd7..a31ff77 100644 --- a/libs/users/infrastructure/mongoose/src/lib/mongoose-user.repository.spec.ts +++ b/libs/users/infrastructure/mongoose/src/lib/mongoose-users.repository.spec.ts @@ -1,18 +1,18 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getModelToken } from '@nestjs/mongoose'; import { Model } from 'mongoose'; -import { MongooseUserRepository } from './mongoose-user.repository'; +import { MongooseUsersRepository } from './mongoose-users.repository'; import { UserDocument } from './user.schema'; import { User } from '@users/domain'; -describe('MongooseUserRepository', () => { - let repository: MongooseUserRepository; +describe('MongooseUsersRepository', () => { + let repository: MongooseUsersRepository; let userModel: Model; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ - MongooseUserRepository, + MongooseUsersRepository, { provide: getModelToken(UserDocument.name), useValue: { @@ -22,7 +22,7 @@ describe('MongooseUserRepository', () => { ], }).compile(); - repository = module.get(MongooseUserRepository); + repository = module.get(MongooseUsersRepository); userModel = module.get>(getModelToken(UserDocument.name)); }); diff --git a/libs/users/infrastructure/mongoose/src/lib/mongoose-user.repository.ts b/libs/users/infrastructure/mongoose/src/lib/mongoose-users.repository.ts similarity index 56% rename from libs/users/infrastructure/mongoose/src/lib/mongoose-user.repository.ts rename to libs/users/infrastructure/mongoose/src/lib/mongoose-users.repository.ts index 70042a0..ed004d4 100644 --- a/libs/users/infrastructure/mongoose/src/lib/mongoose-user.repository.ts +++ b/libs/users/infrastructure/mongoose/src/lib/mongoose-users.repository.ts @@ -1,16 +1,22 @@ +import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; -import { User, UserRepository } from '@users/domain'; +import { User, UsersRepository } from '@users/domain'; import { Model } from 'mongoose'; import { UserDocument } from './user.schema'; -export class MongooseUserRepository implements UserRepository { +@Injectable() +export class MongooseUsersRepository implements UsersRepository { constructor( @InjectModel(UserDocument.name) private userModel: Model, ) {} async findById(id: string): Promise { const userDoc = await this.userModel.findById(id).exec(); - return userDoc ? new User(userDoc.id, userDoc.name) : null; + + if (!userDoc) { + return null; + } + return new User(userDoc.id, userDoc.name); } } diff --git a/libs/users/interface-adapters/src/lib/resolver/users.resolver.spec.ts b/libs/users/interface-adapters/src/lib/resolver/users.resolver.spec.ts index a7bb40e..1bbf9ec 100644 --- a/libs/users/interface-adapters/src/lib/resolver/users.resolver.spec.ts +++ b/libs/users/interface-adapters/src/lib/resolver/users.resolver.spec.ts @@ -28,42 +28,4 @@ describe('UsersResolver', () => { it('should be defined', () => { expect(resolver).toBeDefined(); }); - - describe('getUser', () => { - it('should return a user by id', () => { - const user: User = { id: '1', name: 'John Doe' }; - jest.spyOn(service, 'findById').mockReturnValue(user); - - expect(resolver.getUser('1')).toEqual(user); - expect(service.findById).toHaveBeenCalledWith('1'); - }); - - it('should return undefined if user not found', () => { - jest.spyOn(service, 'findById').mockReturnValue(undefined); - - expect(resolver.getUser('2')).toBeNull(); - expect(service.findById).toHaveBeenCalledWith('2'); - }); - }); - - describe('resolveReference', () => { - it('should return a user by reference id', () => { - const user: User = { id: '1', name: 'John Doe' }; - jest.spyOn(service, 'findById').mockReturnValue(user); - - expect( - resolver.resolveReference({ __typename: 'User', id: '1' }), - ).toEqual(user); - expect(service.findById).toHaveBeenCalledWith('1'); - }); - - it('should return undefined if user not found by reference id', () => { - jest.spyOn(service, 'findById').mockReturnValue(undefined); - - expect( - resolver.resolveReference({ __typename: 'User', id: '2' }), - ).toBeUndefined(); - expect(service.findById).toHaveBeenCalledWith('2'); - }); - }); }); diff --git a/libs/users/interface-adapters/src/lib/resolver/users.resolver.ts b/libs/users/interface-adapters/src/lib/resolver/users.resolver.ts index ca63644..2d28b8b 100644 --- a/libs/users/interface-adapters/src/lib/resolver/users.resolver.ts +++ b/libs/users/interface-adapters/src/lib/resolver/users.resolver.ts @@ -1,6 +1,5 @@ -import { Args, ID, Query, Resolver, ResolveReference } from '@nestjs/graphql'; +import { Args, ID, Query, Resolver } from '@nestjs/graphql'; import { UsersService } from '@users/application'; -import { User } from '@users/domain'; import { UserDto } from '../dto/user.dto'; @@ -9,16 +8,7 @@ export class UsersResolver { constructor(private usersService: UsersService) {} @Query(() => UserDto, { nullable: true }) - getUser(@Args({ name: 'id', type: () => ID }) id: string): UserDto | null { - const user: User | undefined = this.usersService.findById(id); // Domain entity - return user ? new UserDto(user.id, user.name) : null; - } - - @ResolveReference() - resolveReference(reference: { - __typename: string; - id: string; - }): User | undefined { - return this.usersService.findById(reference.id); + getUser(@Args({ name: 'id', type: () => ID }) id: string): Promise { + return this.usersService.findById(id); } }