Skip to content

Commit

Permalink
Merge pull request #58 from zhumeisongsong/feature/user-service
Browse files Browse the repository at this point in the history
feat: ✨ import getUserUseCase in UsersService
  • Loading branch information
zhumeisongsong authored Nov 20, 2024
2 parents 8f1b3fb + 84db568 commit d3ba6a6
Show file tree
Hide file tree
Showing 18 changed files with 140 additions and 140 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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). <br/>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). <br/>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.<br/> 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.<br/> Maintain entity independence from database and framework. |
Expand Down
22 changes: 19 additions & 3 deletions apps/users/src/users/users.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ApolloFederationDriverConfig>({
driver: ApolloFederationDriver,
Expand All @@ -27,6 +42,7 @@ import { DatabaseModule } from '@shared/infrastructure-mongoose';
plugins: [ApolloServerPluginInlineTrace()],
}),
DatabaseModule,
MongooseModule.forFeature([{ name: UserDocument.name, schema: UserSchema }])
],
})
export class UsersModule {}
2 changes: 1 addition & 1 deletion libs/users/application/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
36 changes: 0 additions & 36 deletions libs/users/application/src/lib/use-case/get-user.use-case.spec.ts

This file was deleted.

9 changes: 0 additions & 9 deletions libs/users/application/src/lib/use-case/get-user.use-case.ts

This file was deleted.

35 changes: 35 additions & 0 deletions libs/users/application/src/lib/use-cases/get-user.use-case.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
19 changes: 19 additions & 0 deletions libs/users/application/src/lib/use-cases/get-user.use-case.ts
Original file line number Diff line number Diff line change
@@ -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<User | null> {
const user = await this.usersRepository.findById(id);

if (!user) {
return null;
}
return user;
}
}
37 changes: 27 additions & 10 deletions libs/users/application/src/lib/users.service.spec.ts
Original file line number Diff line number Diff line change
@@ -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>(UsersService);
getUserUseCase = module.get<GetUserUseCase>(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();
});
});
});
11 changes: 5 additions & 6 deletions libs/users/application/src/lib/users.service.ts
Original file line number Diff line number Diff line change
@@ -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<User | null> {
return this.getUserUseCase.execute(id);
}
}
3 changes: 1 addition & 2 deletions libs/users/domain/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
// user
export * from './lib/user.entity';
export * from './lib/user.repository';
export * from './lib/users.repository';
5 changes: 0 additions & 5 deletions libs/users/domain/src/lib/user.repository.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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' },
Expand All @@ -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();
});
});
7 changes: 7 additions & 0 deletions libs/users/domain/src/lib/users.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { User } from './user.entity';

export interface UsersRepository {
findById(id: string): Promise<User | null>;
}

export const USERS_REPOSITORY = Symbol('USERS_REPOSITORY');
2 changes: 1 addition & 1 deletion libs/users/infrastructure/mongoose/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from './lib/mongoose-user.repository';
export * from './lib/mongoose-users.repository';
export * from './lib/user.schema';
Original file line number Diff line number Diff line change
@@ -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<UserDocument>;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
MongooseUserRepository,
MongooseUsersRepository,
{
provide: getModelToken(UserDocument.name),
useValue: {
Expand All @@ -22,7 +22,7 @@ describe('MongooseUserRepository', () => {
],
}).compile();

repository = module.get<MongooseUserRepository>(MongooseUserRepository);
repository = module.get<MongooseUsersRepository>(MongooseUsersRepository);
userModel = module.get<Model<UserDocument>>(getModelToken(UserDocument.name));
});

Expand Down
Original file line number Diff line number Diff line change
@@ -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<UserDocument>,
) {}

async findById(id: string): Promise<User | null> {
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
});
Loading

0 comments on commit d3ba6a6

Please sign in to comment.