Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ✨ add tasks resolver: getTasks / getUserTasks / createUserTasks/updateUserTasks #101

Merged
merged 22 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6c6ced0
docs: 📝 remove enter space
zhumeisongsong Dec 7, 2024
efb88cd
feat: ✨ add tasks domain
zhumeisongsong Dec 7, 2024
0732c1a
feat: ✨ add mock TasksService
zhumeisongsong Dec 7, 2024
c483610
feat: ✨ add hard code users service
zhumeisongsong Dec 7, 2024
d58a708
feat: ✨ add findUserTasks and fix create/update user tasks
zhumeisongsong Dec 7, 2024
48646b7
fix: 🐛 fix TaskRepository definition
zhumeisongsong Dec 7, 2024
934ee65
feat: ✨ add task resolvers logic
zhumeisongsong Dec 7, 2024
a135c0f
test: 🧪 test error
zhumeisongsong Dec 7, 2024
a40db58
fix: 🐛 delete empty constructor
zhumeisongsong Dec 7, 2024
94fbc9c
refactor: ♻️ add UserTasksService
zhumeisongsong Dec 7, 2024
54f0522
refactor: ♻️ add UserTasksResolver
zhumeisongsong Dec 7, 2024
ba44970
fix: 🐛 Prefer using the primitive `string` as a type name
zhumeisongsong Dec 7, 2024
7ee3959
feat: ✨ Add validation for the id field
zhumeisongsong Dec 9, 2024
83df7bb
refactor: ♻️ creating a base DTO class
zhumeisongsong Dec 9, 2024
6a28280
docs: 📝 add method documentation
zhumeisongsong Dec 9, 2024
2ef1c11
feat: ✨ Add validation decorators for TaskDto
zhumeisongsong Dec 9, 2024
d2ba8cb
feat: ✨ Add validation decorators to UserTaskDto
zhumeisongsong Dec 9, 2024
d171615
feat: ✨ add args validation to findUserTasks
zhumeisongsong Dec 9, 2024
1c6aa0a
refactor: ♻️ name from find to findOne
zhumeisongsong Dec 9, 2024
26523ad
fix: 🐛 findOneByEmail to findByEmail
zhumeisongsong Dec 9, 2024
b30eb07
refactor: ♻️ add UserTasksRepository
zhumeisongsong Dec 9, 2024
0db533f
test: 🧪 pass failed test cases
zhumeisongsong Dec 9, 2024
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
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ You can use `npx nx list` to get a list of installed plugins. Then, run `npx nx
| resolver(interface-adapters) | Handles GraphQL queries and mutations by converting them into calls to the application layer.<br/> Responsible for input validation and response formatting specific to GraphQL. |
| dto(interface-adapters) | Define DTOs for GraphQL schema. |
| infrastructure | Implements the technical capabilities needed to support the higher layers of the application.<br/> Handles database connections, external service integrations, and other technical concerns.<br/> Contains concrete implementations of repository interfaces defined in the domain layer. |

| 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).<br/> Adding validation in the Mongoose schema ensures that any data persisted to the database adheres to the required constraints. This helps maintain data integrity and prevents invalid or duplicate entries at the database level. |
| 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.<br/> Implementing validation in the use-case layer allows you to enforce business logic and provide immediate feedback to users or calling services. This is where you can handle complex validation rules and provide detailed error messages.|
Expand Down
3 changes: 2 additions & 1 deletion libs/tasks/application/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './lib/tasks-application';
export * from './lib/tasks.service';
export * from './lib/user-tasks.service';
7 changes: 0 additions & 7 deletions libs/tasks/application/src/lib/tasks-application.spec.ts

This file was deleted.

3 changes: 0 additions & 3 deletions libs/tasks/application/src/lib/tasks-application.ts

This file was deleted.

26 changes: 26 additions & 0 deletions libs/tasks/application/src/lib/tasks.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Test, TestingModule } from '@nestjs/testing';

import { TasksService } from './tasks.service';

describe('TasksService', () => {
let service: TasksService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [TasksService],
}).compile();

service = module.get<TasksService>(TasksService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});

describe('findAll', () => {
it('should return an array of tasks', async () => {
const result = await service.findAll();
expect(Array.isArray(result)).toBe(true);
});
});
zhumeisongsong marked this conversation as resolved.
Show resolved Hide resolved
});
10 changes: 10 additions & 0 deletions libs/tasks/application/src/lib/tasks.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Injectable } from '@nestjs/common';
import { Task } from '@tasks/domain';

@Injectable()
export class TasksService {
async findAll(): Promise<Task[]> {
// TODO: Implement this
return [];
}
zhumeisongsong marked this conversation as resolved.
Show resolved Hide resolved
}
39 changes: 39 additions & 0 deletions libs/tasks/application/src/lib/user-tasks.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UserTasksService } from './user-tasks.service';

describe('UserTasksService', () => {
let service: UserTasksService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UserTasksService],
}).compile();

service = module.get<UserTasksService>(UserTasksService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});

describe('findMany', () => {
it('should return user tasks', async () => {
const result = await service.findMany('userId', { from: new Date(), to: new Date() });
expect(result).toEqual([]);
});
});
zhumeisongsong marked this conversation as resolved.
Show resolved Hide resolved

describe('createSome', () => {
it('should create user tasks', async () => {
const result = await service.createSome('userId', [{ id: '1', createdAt: new Date() }]);
expect(result).toEqual('success');
});
});
zhumeisongsong marked this conversation as resolved.
Show resolved Hide resolved

describe('updateSome', () => {
it('should update user tasks', async () => {
const result = await service.updateSome('userId', [{ id: '1', updatedAt: new Date() }]);
expect(result).toEqual('success');
});
});
zhumeisongsong marked this conversation as resolved.
Show resolved Hide resolved
});
29 changes: 29 additions & 0 deletions libs/tasks/application/src/lib/user-tasks.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Injectable } from '@nestjs/common';
import { UserTask } from '@tasks/domain';

@Injectable()
export class UserTasksService {
async findMany(
userId: string,
range?: { from: Date; to: Date },
): Promise<UserTask[]> {
// TODO: Implement this
return [];
}
zhumeisongsong marked this conversation as resolved.
Show resolved Hide resolved

async createSome(
userId: string,
tasks: { id: string; createdAt: Date }[],
): Promise<string> {
// TODO: Implement this
return 'success';
}
zhumeisongsong marked this conversation as resolved.
Show resolved Hide resolved

async updateSome(
userId: string,
userTasks: { id: string; updatedAt: Date }[],
): Promise<string> {
// TODO: Implement this
return 'success';
}
}
5 changes: 4 additions & 1 deletion libs/tasks/domain/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export * from './lib/tasks-domain';
export * from './lib/entities/task.entity';
export * from './lib/entities/user-task.entity';

export * from './lib/task.repository';
39 changes: 39 additions & 0 deletions libs/tasks/domain/src/lib/entities/task.entity.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Task } from './task.entity';

describe('Task', () => {
it('should create a task with all properties', () => {
const task = new Task(
'test-id',
'Test Title',
'Test Description',
['category1', 'category2']
);

expect(task.id).toBe('test-id');
expect(task.title).toBe('Test Title');
expect(task.description).toBe('Test Description');
expect(task.categories).toEqual(['category1', 'category2']);
});

it('should allow null description', () => {
const task = new Task(
'test-id',
'Test Title',
null,
['category1']
);

expect(task.description).toBeNull();
});

it('should create a task with empty categories array', () => {
const task = new Task(
'test-id',
'Test Title',
'Test Description',
[]
);

expect(task.categories).toEqual([]);
});
});
8 changes: 8 additions & 0 deletions libs/tasks/domain/src/lib/entities/task.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export class Task {
constructor(
public readonly id: string,
public readonly title: string,
public readonly description: string | null,
public readonly categories: string[],
) {}
}
zhumeisongsong marked this conversation as resolved.
Show resolved Hide resolved
30 changes: 30 additions & 0 deletions libs/tasks/domain/src/lib/entities/user-task.entity.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { User } from '@users/domain';
import { Task } from './task.entity';
import { UserTask } from './user-task.entity';

describe('UserTask', () => {
it('should create a user task instance', () => {
const task = new Task('task-1', 'Test Task', 'Description', ['category1']);
const user = new User('user-1', '[email protected]', null, null);
const now = new Date();

const userTask = new UserTask(
'user-task-1',
now,
null,
'task-1',
task,
'user-1',
user,
);

expect(userTask).toBeDefined();
expect(userTask.id).toBe('user-task-1');
expect(userTask.createdAt).toBe(now);
expect(userTask.updatedAt).toBeNull();
expect(userTask.taskId).toBe('task-1');
expect(userTask.task).toBe(task);
expect(userTask.userId).toBe('user-1');
expect(userTask.user).toBe(user);
});
});
15 changes: 15 additions & 0 deletions libs/tasks/domain/src/lib/entities/user-task.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { User } from '@users/domain';

import { Task } from './task.entity';

export class UserTask {
constructor(
public readonly id: string,
public readonly createdAt: Date,
public readonly updatedAt: Date | null,
public readonly taskId: string,
public readonly task: Task | null,
public readonly userId: string,
public readonly user: User | null,
) {}
}
82 changes: 82 additions & 0 deletions libs/tasks/domain/src/lib/task.repository.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Task } from './entities/task.entity';
import { UserTask } from './entities/user-task.entity';
import { User } from '@users/domain';
import { TaskRepository } from './task.repository';

describe('TaskRepository', () => {
let mockTaskRepository: TaskRepository;
let task: Task;
let user: User;
let userTask: UserTask;

beforeEach(() => {
task = new Task('task-1', 'Test Task', 'Description', ['category1']);
user = new User('user-1', '[email protected]', null, null);
userTask = new UserTask(
'user-task-1',
new Date(),
null,
task.id,
task,
user.id,
user
);

zhumeisongsong marked this conversation as resolved.
Show resolved Hide resolved
mockTaskRepository = {
findAllTasks: jest.fn(),
findUserTasks: jest.fn(),
createUserTasks: jest.fn(),
updateUserTasks: jest.fn()
};
});

describe('findAllTasks', () => {
it('should return all tasks', async () => {
const tasks = [task];
(mockTaskRepository.findAllTasks as jest.Mock).mockResolvedValue(tasks);

const result = await mockTaskRepository.findAllTasks();

expect(result).toEqual(tasks);
expect(mockTaskRepository.findAllTasks).toHaveBeenCalled();
});
});
zhumeisongsong marked this conversation as resolved.
Show resolved Hide resolved

describe('findUserTasks', () => {
it('should return tasks for a specific user', async () => {
const tasks = [task];
(mockTaskRepository.findUserTasks as jest.Mock).mockResolvedValue(tasks);

const result = await mockTaskRepository.findUserTasks(user.id);

expect(result).toEqual(tasks);
expect(mockTaskRepository.findUserTasks).toHaveBeenCalledWith(user.id);
});
});

describe('createUserTasks', () => {
it('should create user tasks', async () => {
const tasks = [{
id: 'task-1',
createdAt: new Date()
}];

await mockTaskRepository.createUserTasks(user.id, tasks);

expect(mockTaskRepository.createUserTasks).toHaveBeenCalledWith(user.id, tasks);
});
});
zhumeisongsong marked this conversation as resolved.
Show resolved Hide resolved

describe('updateUserTasks', () => {
it('should update user tasks', async () => {
const userTasks = [{
id: 'user-task-1',
updatedAt: new Date()
}];

await mockTaskRepository.updateUserTasks(user.id, userTasks);

expect(mockTaskRepository.updateUserTasks).toHaveBeenCalledWith(user.id, userTasks);
});
});
});
30 changes: 30 additions & 0 deletions libs/tasks/domain/src/lib/task.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Task } from './entities/task.entity';

export interface TaskRepository {
findAllTasks(): Promise<Task[]>;

findUserTasks(
userId: string,
range?: { from: Date; to: Date },
): Promise<Task[]>;
zhumeisongsong marked this conversation as resolved.
Show resolved Hide resolved

/**
* Creates tasks for a specific user
* @throws {TaskValidationError} If the tasks are invalid
* @throws {UserNotFoundError} If user doesn't exist
*/
createUserTasks(
userId: string,
tasks: { id: string; createdAt: Date }[],
): Promise<void>;

/**
* Updates existing user tasks
* @throws {TaskNotFoundError} If any task doesn't exist
* @throws {UserNotFoundError} If user doesn't exist
*/
updateUserTasks(
userId: string,
userTasks: { id: string; updatedAt: Date }[],
): Promise<void>;
}
7 changes: 0 additions & 7 deletions libs/tasks/domain/src/lib/tasks-domain.spec.ts

This file was deleted.

3 changes: 0 additions & 3 deletions libs/tasks/domain/src/lib/tasks-domain.ts

This file was deleted.

2 changes: 1 addition & 1 deletion libs/tasks/interface-adapters/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * from './lib/tasks-interface-adapters';
export * from './lib/tasks.module';
14 changes: 14 additions & 0 deletions libs/tasks/interface-adapters/src/lib/dto/base-task.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Field, InputType } from "@nestjs/graphql";
import { IsNotEmpty, IsUUID } from "class-validator";

@InputType({ isAbstract: true })
export abstract class BaseTaskDto {
@Field(() => String)
@IsNotEmpty()
@IsUUID()
id: string;

constructor(id: string) {
this.id = id;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { CreateUserTaskDto } from './create-user-task.dto';

describe('CreateUserTaskDto', () => {
it('should create a CreateUserTaskDto instance', () => {
const id = '123';
const createdAt = new Date();

const dto = new CreateUserTaskDto(id, createdAt);

expect(dto).toBeDefined();
expect(dto.id).toBe(id);
expect(dto.createdAt).toBe(createdAt);
});
});
zhumeisongsong marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading