Skip to content

Commit 1d65e04

Browse files
alinelariguetpedrodominguesp
authored andcommitted
feat(employees): adiciona módulo employees com dados mocados e filtro OData
Implementa endpoint RESTful /v1/employees com CRUD completo e parser de filtros OData () suportando contains, startswith, endswith, operadores de comparação (eq, ne, gt, ge, lt, le) para strings, números e datas, além de combinações com 'and'. Dados mocados replicam os 20 employees da POC do po-angular para testes de integração com o componente po-table.
1 parent de3098a commit 1d65e04

File tree

8 files changed

+363
-1
lines changed

8 files changed

+363
-1
lines changed

src/app.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { CitiesModule } from './cities/cities.module';
1212
import { HotelsModule } from './hotels/hotels.module';
1313
import { UploadModule } from './upload/upload.module';
1414
import { BatchDeleteModule } from './batch-delete/batch-delete.module';
15+
import { EmployeesModule } from './employees/employees.module';
1516

1617
@Module({
1718
imports: [
@@ -26,7 +27,8 @@ import { BatchDeleteModule } from './batch-delete/batch-delete.module';
2627
PeopleModule,
2728
CitiesModule,
2829
UploadModule,
29-
BatchDeleteModule
30+
BatchDeleteModule,
31+
EmployeesModule
3032
]
3133
})
3234
export class AppModule {}

src/employees/db/employees.data.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { Employee } from '../interfaces/employee.interface';
2+
3+
export const employees: Array<Employee> = [
4+
{
5+
id: 1, name: 'Ana Silva', age: 28, city: 'São Paulo',
6+
department: 'TI', salary: 8500, status: 'Ativo', hireDate: '2023-03-15'
7+
},
8+
{
9+
id: 2, name: 'Bruno Costa', age: 35, city: 'Rio de Janeiro',
10+
department: 'RH', salary: 7200, status: 'Ativo', hireDate: '2022-01-10'
11+
},
12+
{
13+
id: 3, name: 'Carla Mendes', age: 42, city: 'São Paulo',
14+
department: 'Financeiro', salary: 12000, status: 'Ativo', hireDate: '2020-06-01'
15+
},
16+
{
17+
id: 4, name: 'Diego Oliveira', age: 31, city: 'Belo Horizonte',
18+
department: 'TI', salary: 9800, status: 'Inativo', hireDate: '2021-09-20'
19+
},
20+
{
21+
id: 5, name: 'Elena Santos', age: 26, city: 'Curitiba',
22+
department: 'Marketing', salary: 6500, status: 'Ativo', hireDate: '2024-02-01'
23+
},
24+
{
25+
id: 6, name: 'Fernando Lima', age: 39, city: 'São Paulo',
26+
department: 'TI', salary: 15000, status: 'Ativo', hireDate: '2019-04-15'
27+
},
28+
{
29+
id: 7, name: 'Gabriela Rocha', age: 33, city: 'Porto Alegre',
30+
department: 'RH', salary: 8000, status: 'Inativo', hireDate: '2022-07-01'
31+
},
32+
{
33+
id: 8, name: 'Hugo Pereira', age: 45, city: 'Rio de Janeiro',
34+
department: 'Financeiro', salary: 14000, status: 'Ativo', hireDate: '2018-11-10'
35+
},
36+
{
37+
id: 9, name: 'Isabela Alves', age: 29, city: 'São Paulo',
38+
department: 'Marketing', salary: 7800, status: 'Ativo', hireDate: '2023-08-20'
39+
},
40+
{
41+
id: 10, name: 'João Ferreira', age: 37, city: 'Brasília',
42+
department: 'TI', salary: 11000, status: 'Ativo', hireDate: '2021-01-05'
43+
},
44+
{
45+
id: 11, name: 'Karen Souza', age: 24, city: 'Curitiba',
46+
department: 'TI', salary: 6000, status: 'Ativo', hireDate: '2024-06-15'
47+
},
48+
{
49+
id: 12, name: 'Lucas Barbosa', age: 41, city: 'São Paulo',
50+
department: 'Financeiro', salary: 13500, status: 'Inativo', hireDate: '2019-02-28'
51+
},
52+
{
53+
id: 13, name: 'Marina Castro', age: 30, city: 'Rio de Janeiro',
54+
department: 'Marketing', salary: 8200, status: 'Ativo', hireDate: '2022-12-01'
55+
},
56+
{
57+
id: 14, name: 'Nelson Dias', age: 48, city: 'Belo Horizonte',
58+
department: 'RH', salary: 9500, status: 'Ativo', hireDate: '2017-05-10'
59+
},
60+
{
61+
id: 15, name: 'Olivia Ribeiro', age: 27, city: 'Porto Alegre',
62+
department: 'TI', salary: 7500, status: 'Ativo', hireDate: '2023-10-01'
63+
},
64+
{
65+
id: 16, name: 'Paulo Cardoso', age: 36, city: 'São Paulo',
66+
department: 'TI', salary: 10500, status: 'Inativo', hireDate: '2020-08-15'
67+
},
68+
{
69+
id: 17, name: 'Raquel Martins', age: 32, city: 'Brasília',
70+
department: 'Financeiro', salary: 9000, status: 'Ativo', hireDate: '2021-11-20'
71+
},
72+
{
73+
id: 18, name: 'Samuel Teixeira', age: 44, city: 'Rio de Janeiro',
74+
department: 'Marketing', salary: 11500, status: 'Ativo', hireDate: '2018-03-01'
75+
},
76+
{
77+
id: 19, name: 'Tatiana Moreira', age: 25, city: 'Curitiba',
78+
department: 'RH', salary: 5800, status: 'Ativo', hireDate: '2024-01-15'
79+
},
80+
{
81+
id: 20, name: 'Victor Nascimento', age: 38, city: 'São Paulo',
82+
department: 'TI', salary: 12500, status: 'Ativo', hireDate: '2020-04-01'
83+
}
84+
];
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { ApiPropertyOptional } from '@nestjs/swagger';
2+
3+
export class CreateEmployeeDto {
4+
5+
@ApiPropertyOptional()
6+
id: number;
7+
8+
@ApiPropertyOptional()
9+
name: string;
10+
11+
@ApiPropertyOptional()
12+
age: number;
13+
14+
@ApiPropertyOptional()
15+
city: string;
16+
17+
@ApiPropertyOptional()
18+
department: string;
19+
20+
@ApiPropertyOptional()
21+
salary: number;
22+
23+
@ApiPropertyOptional()
24+
status: string;
25+
26+
@ApiPropertyOptional()
27+
hireDate: string;
28+
29+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
3+
import { CreateEmployeeDto } from './create-employee.dto';
4+
5+
export class GetEmployeesDto {
6+
7+
@ApiProperty({
8+
type: () => [CreateEmployeeDto],
9+
})
10+
items: Array<CreateEmployeeDto>;
11+
12+
@ApiProperty()
13+
hasNext: boolean;
14+
15+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Controller, Get, Param, Body, Post, Put, Query, Delete } from '@nestjs/common';
2+
import { ApiResponse, ApiParam, ApiTags, ApiBody, ApiQuery } from '@nestjs/swagger';
3+
4+
import { EmployeesService } from './employees.service';
5+
import { CreateEmployeeDto } from './dto/create-employee.dto';
6+
import { GetEmployeesDto } from './dto/get-employees.dto';
7+
8+
@ApiTags('employees')
9+
@Controller('employees')
10+
export class EmployeesController {
11+
12+
constructor(private employeesService: EmployeesService) {}
13+
14+
@ApiResponse({ status: 200, type: GetEmployeesDto })
15+
@ApiQuery({ name: 'search', required: false })
16+
@ApiQuery({ name: '$filter', required: false })
17+
@ApiQuery({ name: 'page', required: false })
18+
@ApiQuery({ name: 'pageSize', required: false })
19+
@Get()
20+
getEmployees(@Query() query) {
21+
const search = query['search'];
22+
const filter = query['$filter'];
23+
const page = query['page'];
24+
const pageSize = query['pageSize'];
25+
26+
return this.employeesService.getEmployees(search, filter, page, pageSize);
27+
}
28+
29+
@ApiResponse({ status: 200, type: CreateEmployeeDto })
30+
@ApiParam({ name: 'id' })
31+
@Get(':id')
32+
getEmployee(@Param('id') id: string) {
33+
return this.employeesService.getEmployee(parseInt(id, 10));
34+
}
35+
36+
@ApiResponse({ status: 201, type: CreateEmployeeDto })
37+
@ApiBody({ type: CreateEmployeeDto })
38+
@Post()
39+
save(@Body() employee: CreateEmployeeDto) {
40+
this.employeesService.save(employee);
41+
}
42+
43+
@ApiResponse({ status: 200, type: CreateEmployeeDto })
44+
@ApiParam({ name: 'id' })
45+
@ApiBody({ type: CreateEmployeeDto })
46+
@Put(':id')
47+
update(@Body() employee: CreateEmployeeDto, @Param('id') id: string) {
48+
this.employeesService.update(parseInt(id, 10), employee);
49+
}
50+
51+
@ApiResponse({ status: 200 })
52+
@ApiParam({ name: 'id' })
53+
@Delete(':id')
54+
deleteEmployee(@Param('id') id: string) {
55+
return this.employeesService.delete(parseInt(id, 10));
56+
}
57+
58+
}

src/employees/employees.module.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Module } from '@nestjs/common';
2+
3+
import { EmployeesController } from './employees.controller';
4+
import { EmployeesService } from './employees.service';
5+
6+
@Module({
7+
controllers: [EmployeesController],
8+
providers: [EmployeesService]
9+
})
10+
export class EmployeesModule {}

src/employees/employees.service.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { Injectable, NotFoundException } from '@nestjs/common';
2+
3+
import { employees } from './db/employees.data';
4+
import { Employee } from './interfaces/employee.interface';
5+
import { Utils } from 'src/utils/utils';
6+
7+
@Injectable()
8+
export class EmployeesService {
9+
10+
employees = [...employees];
11+
12+
getEmployees(search?: string, filter?: string, page?: string, pageSize?: string): { items: Array<Employee>, hasNext: boolean } {
13+
let filteredEmployees = this.employees;
14+
15+
if (filter) {
16+
filteredEmployees = this.applyODataFilter(filteredEmployees, filter);
17+
} else if (search) {
18+
filteredEmployees = Utils.filterByAll(search, filteredEmployees);
19+
}
20+
21+
const total = filteredEmployees.length;
22+
const parsedPage = page ? parseInt(page, 10) : undefined;
23+
const parsedPageSize = pageSize ? parseInt(pageSize, 10) : undefined;
24+
filteredEmployees = this.paginate(filteredEmployees, parsedPage, parsedPageSize);
25+
26+
return {
27+
items: filteredEmployees,
28+
hasNext: parsedPageSize ? total > (parsedPageSize * (parsedPage || 1)) : false
29+
};
30+
}
31+
32+
getEmployee(id: number): Employee {
33+
const employee = this.employees.find(e => e.id === id);
34+
35+
if (!employee) {
36+
throw new NotFoundException(`Funcionário ${id} não encontrado!`);
37+
}
38+
39+
return employee;
40+
}
41+
42+
save(employee: Employee) {
43+
const id = this.employees.length > 0 ? Math.max(...this.employees.map(e => e.id)) + 1 : 1;
44+
this.employees.push({ ...employee, id });
45+
}
46+
47+
update(id: number, updatedEmployee: Employee) {
48+
const employee = this.getEmployee(id);
49+
Object.assign(employee, updatedEmployee);
50+
}
51+
52+
delete(id: number) {
53+
const index = this.employees.findIndex(e => e.id === id);
54+
55+
if (index === -1) {
56+
throw new NotFoundException(`Funcionário ${id} não encontrado!`);
57+
}
58+
59+
this.employees.splice(index, 1);
60+
return { message: 'Funcionário removido com sucesso' };
61+
}
62+
63+
private paginate(filteredEmployees: Array<Employee>, page?: number, pageSize?: number) {
64+
if (pageSize || page) {
65+
return Utils.paginate(filteredEmployees, page, pageSize);
66+
}
67+
68+
return filteredEmployees;
69+
}
70+
71+
private applyODataFilter(items: Array<Employee>, filter: string): Array<Employee> {
72+
const conditions = filter.split(/\s+and\s+/i);
73+
74+
return items.filter(item => {
75+
return conditions.every(condition => this.evaluateCondition(item, condition.trim()));
76+
});
77+
}
78+
79+
private evaluateCondition(item: any, condition: string): boolean {
80+
// contains(property, 'value')
81+
const containsMatch = condition.match(/contains\((\w+),\s*'([^']+)'\)/i);
82+
if (containsMatch) {
83+
const value = String(item[containsMatch[1]] || '').toLowerCase();
84+
return value.includes(containsMatch[2].toLowerCase());
85+
}
86+
87+
// startswith(property, 'value')
88+
const startsWithMatch = condition.match(/startswith\((\w+),\s*'([^']+)'\)/i);
89+
if (startsWithMatch) {
90+
const value = String(item[startsWithMatch[1]] || '').toLowerCase();
91+
return value.startsWith(startsWithMatch[2].toLowerCase());
92+
}
93+
94+
// endswith(property, 'value')
95+
const endsWithMatch = condition.match(/endswith\((\w+),\s*'([^']+)'\)/i);
96+
if (endsWithMatch) {
97+
const value = String(item[endsWithMatch[1]] || '').toLowerCase();
98+
return value.endsWith(endsWithMatch[2].toLowerCase());
99+
}
100+
101+
// property op 'string_value'
102+
const stringMatch = condition.match(/(\w+)\s+(eq|ne)\s+'([^']+)'/);
103+
if (stringMatch) {
104+
const value = String(item[stringMatch[1]] || '');
105+
const compareValue = stringMatch[3];
106+
if (stringMatch[2] === 'eq') {
107+
return value.toLowerCase() === compareValue.toLowerCase();
108+
}
109+
return value.toLowerCase() !== compareValue.toLowerCase();
110+
}
111+
112+
// property op date_value (YYYY-MM-DD)
113+
const dateMatch = condition.match(/(\w+)\s+(eq|ne|gt|ge|lt|le)\s+(\d{4}-\d{2}-\d{2})/);
114+
if (dateMatch) {
115+
const value = new Date(item[dateMatch[1]]).getTime();
116+
const compareValue = new Date(dateMatch[3]).getTime();
117+
switch (dateMatch[2]) {
118+
case 'eq': return value === compareValue;
119+
case 'ne': return value !== compareValue;
120+
case 'gt': return value > compareValue;
121+
case 'ge': return value >= compareValue;
122+
case 'lt': return value < compareValue;
123+
case 'le': return value <= compareValue;
124+
}
125+
}
126+
127+
// property op number_value
128+
const numberMatch = condition.match(/(\w+)\s+(eq|ne|gt|ge|lt|le)\s+(\d+(?:\.\d+)?)/);
129+
if (numberMatch) {
130+
const value = Number(item[numberMatch[1]]);
131+
const compareValue = Number(numberMatch[3]);
132+
switch (numberMatch[2]) {
133+
case 'eq': return value === compareValue;
134+
case 'ne': return value !== compareValue;
135+
case 'gt': return value > compareValue;
136+
case 'ge': return value >= compareValue;
137+
case 'lt': return value < compareValue;
138+
case 'le': return value <= compareValue;
139+
}
140+
}
141+
142+
return true;
143+
}
144+
145+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export interface Employee {
2+
3+
id?: number;
4+
5+
name?: string;
6+
7+
age?: number;
8+
9+
city?: string;
10+
11+
department?: string;
12+
13+
salary?: number;
14+
15+
status?: string;
16+
17+
hireDate?: string;
18+
19+
}

0 commit comments

Comments
 (0)