Add user and employee management enhancements with database schema updates
- Introduced new columns `status`, `created_by`, and `updated_by` in the `users` table to track user status and ownership. - Updated the `employees` table to include a foreign key reference to the `users` table via `user_id`. - Created migration script `0012_users_primary.sql` to apply these changes to the database schema. - Enhanced the `EmployeesService` and `EmployeesRepository` to support user assignments and related data retrieval. - Updated DTOs and service methods to reflect the new user and employee relationships. - Added unit tests to validate the new functionality and ensure data integrity. - Modified existing controllers to accommodate the new fields and relationships in user and employee management.
This commit is contained in:
@@ -10,6 +10,7 @@ import { PhoneNumber } from '../../../common/value-objects/phone-number/phone-nu
|
||||
import { Status } from '../../../common/value-objects/status/status';
|
||||
import { DRIZZLE, type DrizzleDB } from '../../../database/database.module';
|
||||
import { employees, type EmployeeRow } from '../../../database/employees-table';
|
||||
import { users } from '../../../database/schema';
|
||||
import type { EmployeePosition } from './employee-fields';
|
||||
import type {
|
||||
CreateEmployeeInput,
|
||||
@@ -41,7 +42,11 @@ export class EmployeesRepository {
|
||||
.offset(filters.offset);
|
||||
|
||||
return {
|
||||
data: rows.map((row) => this.toDomain(row)),
|
||||
data: await Promise.all(
|
||||
rows.map(async (row) =>
|
||||
this.toDomain(row, await this.loadAssignedUser(row.userId)),
|
||||
),
|
||||
),
|
||||
total: Number(totalRow?.total ?? 0),
|
||||
};
|
||||
}
|
||||
@@ -55,13 +60,13 @@ export class EmployeesRepository {
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Employee | null> {
|
||||
const rows: EmployeeRow[] = await this.db
|
||||
const rows = await this.db
|
||||
.select()
|
||||
.from(employees)
|
||||
.where(eq(employees.id, id))
|
||||
.limit(1);
|
||||
const row = rows[0];
|
||||
return row ? this.toDomain(row) : null;
|
||||
return row ? this.toDomain(row, await this.loadAssignedUser(row.userId)) : null;
|
||||
}
|
||||
|
||||
async findByCode(code: string): Promise<Employee | null> {
|
||||
@@ -71,7 +76,7 @@ export class EmployeesRepository {
|
||||
.where(eq(employees.code, code))
|
||||
.limit(1);
|
||||
const row = rows[0];
|
||||
return row ? this.toDomain(row) : null;
|
||||
return row ? this.toDomain(row, await this.loadAssignedUser(row.userId)) : null;
|
||||
}
|
||||
|
||||
async create(input: CreateEmployeeInput): Promise<Employee> {
|
||||
@@ -83,7 +88,7 @@ export class EmployeesRepository {
|
||||
.values(this.toInsertValues(input, status, now, input.userId))
|
||||
.returning();
|
||||
const row = inserted[0];
|
||||
return this.toDomain(row);
|
||||
return this.toDomain(row, await this.loadAssignedUser(row.userId));
|
||||
} catch (error) {
|
||||
this.rethrowUniqueViolation(error);
|
||||
}
|
||||
@@ -125,11 +130,14 @@ export class EmployeesRepository {
|
||||
position: input.position ?? existing.position,
|
||||
updatedAt: now.value,
|
||||
updatedBy: input.userId,
|
||||
...(input.assignedUserId !== undefined
|
||||
? { userId: input.assignedUserId }
|
||||
: {}),
|
||||
})
|
||||
.where(eq(employees.id, id))
|
||||
.returning();
|
||||
const row = updated[0];
|
||||
return this.toDomain(row);
|
||||
return this.toDomain(row, await this.loadAssignedUser(row.userId));
|
||||
} catch (error) {
|
||||
this.rethrowUniqueViolation(error);
|
||||
}
|
||||
@@ -154,7 +162,7 @@ export class EmployeesRepository {
|
||||
if (!row) {
|
||||
throw new NotFoundException('Employee not found');
|
||||
}
|
||||
return this.toDomain(row);
|
||||
return this.toDomain(row, await this.loadAssignedUser(row.userId));
|
||||
}
|
||||
|
||||
async bulkUpdateStatus(
|
||||
@@ -216,6 +224,9 @@ export class EmployeesRepository {
|
||||
if (filters.status) {
|
||||
parts.push(eq(employees.status, filters.status));
|
||||
}
|
||||
if (filters.userId) {
|
||||
parts.push(eq(employees.userId, filters.userId));
|
||||
}
|
||||
if (filters.search) {
|
||||
const search = or(
|
||||
ilike(employees.code, `%${filters.search}%`),
|
||||
@@ -247,10 +258,41 @@ export class EmployeesRepository {
|
||||
updatedAt: now.value,
|
||||
createdBy: userId,
|
||||
updatedBy: userId,
|
||||
userId: input.assignedUserId ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
private toDomain(row: EmployeeRow): Employee {
|
||||
private selectWithUser() {
|
||||
return this.db
|
||||
.select({
|
||||
employee: employees,
|
||||
user: {
|
||||
id: users.id,
|
||||
username: users.username,
|
||||
},
|
||||
})
|
||||
.from(employees)
|
||||
.leftJoin(users, eq(employees.userId, users.id));
|
||||
}
|
||||
|
||||
private async loadAssignedUser(
|
||||
userId: string | null,
|
||||
): Promise<{ id: string; username: string } | null> {
|
||||
if (!userId) {
|
||||
return null;
|
||||
}
|
||||
const rows = await this.db
|
||||
.select({ id: users.id, username: users.username })
|
||||
.from(users)
|
||||
.where(eq(users.id, userId))
|
||||
.limit(1);
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
private toDomain(
|
||||
row: EmployeeRow,
|
||||
user: { id: string; username: string } | null,
|
||||
): Employee {
|
||||
return {
|
||||
id: row.id,
|
||||
code: row.code,
|
||||
@@ -262,29 +304,39 @@ export class EmployeesRepository {
|
||||
updatedAt: DateTime.fromUnixMs(row.updatedAt),
|
||||
createdBy: row.createdBy,
|
||||
updatedBy: row.updatedBy,
|
||||
userId: row.userId ?? null,
|
||||
user: user?.id ? { id: user.id, username: user.username } : null,
|
||||
};
|
||||
}
|
||||
|
||||
private rethrowUniqueViolation(error: unknown): never {
|
||||
const err = this.unwrapDbError(error);
|
||||
if (err.code === '23505') {
|
||||
const constraint = err.constraint ?? '';
|
||||
if (constraint.includes('user_id')) {
|
||||
throw new ConflictException('User is already assigned to an employee');
|
||||
}
|
||||
throw new ConflictException('Employee code already exists');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
private unwrapDbError(error: unknown): { code?: string } {
|
||||
private unwrapDbError(error: unknown): { code?: string; constraint?: string } {
|
||||
let current: unknown = error;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (!current || typeof current !== 'object') {
|
||||
break;
|
||||
}
|
||||
const obj = current as { code?: string; cause?: unknown };
|
||||
const obj = current as {
|
||||
code?: string;
|
||||
constraint?: string;
|
||||
cause?: unknown;
|
||||
};
|
||||
if (obj.code === '23505' || obj.code === '23503') {
|
||||
return { code: obj.code };
|
||||
return { code: obj.code, constraint: obj.constraint };
|
||||
}
|
||||
current = obj.cause;
|
||||
}
|
||||
return error as { code?: string };
|
||||
return error as { code?: string; constraint?: string };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user