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

[Fix] Foreign Key Constraint Violation in project_module_task During Project Module Creation #8776

Merged
merged 11 commits into from
Feb 17, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ export class OrganizationProjectModule extends TenantOrganizationBaseEntity impl
/**
* Task
*/
@ApiPropertyOptional({ type: () => Array, isArray: true, description: 'List of task IDs' })
@IsOptional()
@IsArray()
@MultiORMManyToMany(() => Task, (it) => it.modules, {
/** Defines the database action to perform on update. */
onUpdate: 'CASCADE',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { RoleModule } from '../role/role.module';
import { EmployeeModule } from '../employee/employee.module';
import { OrganizationProjectModuleEmployee } from './organization-project-module-employee.entity';
import { TypeOrmOrganizationProjectModuleEmployeeRepository } from './repository/type-orm-organization-project-module-employee.repository';
import { TaskModule } from '../tasks';

@Module({
imports: [
Expand All @@ -21,6 +22,7 @@ import { TypeOrmOrganizationProjectModuleEmployeeRepository } from './repository
RolePermissionModule,
RoleModule,
EmployeeModule,
TaskModule,
CqrsModule
],
controllers: [OrganizationProjectModuleController],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { BadRequestException, HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { Brackets, FindManyOptions, SelectQueryBuilder, UpdateResult, WhereExpressionBuilder } from 'typeorm';
import { InjectDataSource } from '@nestjs/typeorm';
import {
Brackets,
DataSource,
FindManyOptions,
In,
SelectQueryBuilder,
UpdateResult,
WhereExpressionBuilder
} from 'typeorm';
import {
BaseEntityEnum,
ActorTypeEnum,
Expand All @@ -13,7 +22,8 @@ import {
ProjectModuleStatusEnum,
ActionTypeEnum,
RolesEnum,
IEmployee
IEmployee,
ITask
} from '@gauzy/contracts';
import { isEmpty, isNotEmpty } from '@gauzy/utils';
import { isPostgres } from '@gauzy/config';
Expand All @@ -29,17 +39,20 @@ import { EmployeeService } from '../employee/employee.service';
import { OrganizationProjectModuleEmployee } from './organization-project-module-employee.entity';
import { TypeOrmOrganizationProjectModuleEmployeeRepository } from './repository/type-orm-organization-project-module-employee.repository';
import { MikroOrmOrganizationProjectModuleEmployeeRepository } from './repository/mikro-orm-organization-project-module-employee.repository';
import { TaskService } from '../tasks/task.service';

@Injectable()
export class OrganizationProjectModuleService extends TenantAwareCrudService<OrganizationProjectModule> {
constructor(
@InjectDataSource() private readonly dataSource: DataSource,
readonly typeOrmProjectModuleRepository: TypeOrmOrganizationProjectModuleRepository,
readonly mikroOrmProjectModuleRepository: MikroOrmOrganizationProjectModuleRepository,
readonly typeOrmOrganizationProjectModuleEmployeeRepository: TypeOrmOrganizationProjectModuleEmployeeRepository,
readonly mikroOrmOrganizationProjectModuleEmployeeRepository: MikroOrmOrganizationProjectModuleEmployeeRepository,
private readonly activityLogService: ActivityLogService,
private readonly _roleService: RoleService,
private readonly _employeeService: EmployeeService
private readonly _employeeService: EmployeeService,
private readonly _taskService: TaskService
) {
super(typeOrmProjectModuleRepository, mikroOrmProjectModuleRepository);
}
Expand All @@ -56,79 +69,46 @@ export class OrganizationProjectModuleService extends TenantAwareCrudService<Org
const currentRoleId = RequestContext.currentRoleId();
const { organizationId } = entity;

// Destructure the input data
const { memberIds = [], managerIds = [], ...input } = entity;

try {
try {
await this._roleService.findOneByIdString(currentRoleId, { where: { name: RolesEnum.EMPLOYEE } });

// Add the current employee to the managerIds if they have the EMPLOYEE role and are not already included.
if (!managerIds.includes(employeeId)) {
// If not included, add the employeeId to the managerIds array.
managerIds.push(employeeId);
}
} catch (error) {}
const { memberIds = [], managerIds = [], tasks = [], ...input } = entity;

// Combine memberIds and managerIds into a single array.
const employeeIds = [...memberIds, ...managerIds].filter(Boolean);
await this.addCurrentEmployeeToManagers(managerIds, currentRoleId, employeeId);

// Retrieve a collection of employees based on specified criteria.
const employees = await this._employeeService.findActiveEmployeesByEmployeeIds(
employeeIds,
organizationId,
tenantId
);

// Find the manager role
const managerRole = await this._roleService.findOneByWhereOptions({
name: RolesEnum.MANAGER
});
const employeeIds = [...memberIds, ...managerIds].filter(Boolean);
const employees = await this._employeeService.findActiveEmployeesByEmployeeIds(
employeeIds,
organizationId,
tenantId
);

// Create a Set for faster membership checks
const managerIdsSet = new Set(managerIds);
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();

// Use destructuring to directly extract 'id' from 'employee'
const members = employees.map(({ id: employeeId }) => {
// If the employee is a manager, assign the existing manager with the latest assignedAt date
const isManager = managerIdsSet.has(employeeId);
const assignedAt = new Date();
try {
await queryRunner.startTransaction();

return new OrganizationProjectModuleEmployee({
employeeId,
organizationId,
tenantId,
isManager,
assignedAt,
role: isManager ? managerRole : null
});
});
const existingTasks = await this.getExistingTasks(tasks);
const members = await this.buildModuleMembers(employees, managerIds, organizationId, tenantId);

const projectModule = await super.create({
...input,
members,
creatorId
});

// Generate the activity log
this.activityLogService.logActivity<OrganizationProjectModule>(
BaseEntityEnum.OrganizationProjectModule,
ActionTypeEnum.Created,
ActorTypeEnum.User,
projectModule.id,
projectModule.name,
projectModule,
organizationId,
tenantId
);
await this.assignTasksToModule(existingTasks, projectModule);
await queryRunner.commitTransaction();

this.logModuleActivity(ActionTypeEnum.Created, projectModule, undefined, entity);

return projectModule;
} catch (error) {
// Handle errors and return an appropriate error response
await queryRunner.rollbackTransaction();
throw new HttpException(
`Failed to create organization project module: ${error.message}`,
HttpStatus.BAD_REQUEST
);
} finally {
await queryRunner.release();
}
}

Expand All @@ -146,23 +126,22 @@ export class OrganizationProjectModuleService extends TenantAwareCrudService<Org
const tenantId = RequestContext.currentTenantId() || entity.tenantId;

try {
const { memberIds, managerIds, organizationId } = entity;
const { memberIds, managerIds, organizationId, tasks = [] } = entity;

// Retrieve existing module.
// Retrieve existing module
const existingProjectModule = await this.findOneByIdString(id, {
relations: { parent: true, project: true, teams: true, members: true }
relations: { parent: true, project: true, teams: true, members: true, tasks: true }
});

if (!existingProjectModule) {
throw new BadRequestException('Module not found');
}
console.log(Array.isArray(memberIds), Array.isArray(managerIds));

// Update members and managers if applicable
if (Array.isArray(memberIds) || Array.isArray(managerIds)) {
// Retrieve members and managers IDs
// Combine memberIds and managerIds into a single array
const employeeIds = [...memberIds, ...managerIds].filter(Boolean);
const employeeIds = [...(memberIds || []), ...(managerIds || [])].filter(Boolean);

// Retrieves a collection of employees based on specified criteria.
// Retrieves a collection of employees based on specified criteria
const projectModuleMembers = await this._employeeService.findActiveEmployeesByEmployeeIds(
employeeIds,
organizationId,
Expand All @@ -174,33 +153,47 @@ export class OrganizationProjectModuleService extends TenantAwareCrudService<Org
id,
organizationId,
projectModuleMembers,
managerIds,
memberIds
managerIds || [],
memberIds || []
);
}

// Update tasks logic
if (Array.isArray(tasks)) {
const existingTasks = await this.getExistingTasks(tasks);

// Determine tasks to add
const existingTaskIds = new Set(existingTasks.map((task) => task.id));
const newTasks = tasks.filter((task) => !existingTaskIds.has(task.id));

// Determine tasks to remove
const tasksToRemove = existingProjectModule.tasks.filter(
(task) => !tasks.some((updatedTask) => updatedTask.id === task.id)
);

// Add new tasks
for (const task of newTasks) {
task.modules = [...(task.modules || []), existingProjectModule];
await this._taskService.update(task.id, task);
}

// Remove tasks
for (const task of tasksToRemove) {
task.modules = task.modules?.filter((module) => module.id !== existingProjectModule.id) || [];
await this._taskService.update(task.id, task);
}
}

// Update module with new values
// Update the project module with new values
const updatedProjectModule = await super.create({
...entity,
id
});

// Generate the activity log
this.logModuleActivity(ActionTypeEnum.Updated, updatedProjectModule, existingProjectModule, entity);

this.activityLogService.logActivity<OrganizationProjectModule>(
BaseEntityEnum.OrganizationProjectModule,
ActionTypeEnum.Updated,
ActorTypeEnum.User,
updatedProjectModule.id,
updatedProjectModule.name,
updatedProjectModule,
organizationId,
tenantId,
existingProjectModule,
entity
);

// return updated Module
// Return updated module
return updatedProjectModule;
} catch (error) {
throw new BadRequestException(error);
Expand Down Expand Up @@ -594,4 +587,115 @@ export class OrganizationProjectModuleService extends TenantAwareCrudService<Org
// Wait for all deletions to complete
await Promise.all(deletePromises);
}

/**
* Add the current employee to managerIds if applicable.
* @param managerIds List of manager IDs.
* @param currentRoleId The current role ID of the user.
* @param employeeId The current employee ID.
*/
private async addCurrentEmployeeToManagers(
managerIds: string[],
currentRoleId: string,
employeeId: string
): Promise<void> {
try {
const currentRole = await this._roleService.findOneByIdString(currentRoleId, {
where: { name: RolesEnum.EMPLOYEE }
});
if (currentRole && !managerIds.includes(employeeId)) {
managerIds.push(employeeId);
}
} catch {
// Role is not "EMPLOYEE" or no action needed.
}
}

/**
* Fetch existing tasks related to the project module.
* @param tasks List of tasks to check.
* @returns A list of existing tasks found in the database.
*/
private async getExistingTasks(tasks: ITask[]): Promise<ITask[]> {
const taskIds = tasks.map((task) => task.id);
return this._taskService.find({
where: { id: In(taskIds) },
relations: { modules: true }
});
}

/**
* Build module members from employees and assign manager roles.
* @param employees List of employees to assign as members.
* @param managerIds List of manager IDs.
* @param organizationId The ID of the organization.
* @param tenantId The ID of the tenant.
* @returns A list of organization project module members.
*/
private async buildModuleMembers(
employees: IEmployee[],
managerIds: string[],
organizationId: string,
tenantId: string
): Promise<OrganizationProjectModuleEmployee[]> {
const managerRole = await this._roleService.findOneByWhereOptions({ name: RolesEnum.MANAGER });
const managerIdsSet = new Set(managerIds);

return employees.map(({ id: employeeId }) => {
const isManager = managerIdsSet.has(employeeId);
return new OrganizationProjectModuleEmployee({
employeeId,
organizationId,
tenantId,
isManager,
assignedAt: new Date(),
role: isManager ? managerRole : null
});
});
}

/**
* Assign tasks to the project module.
* @param tasks List of tasks to associate with the module.
* @param projectModule The project module to assign tasks to.
*/
private async assignTasksToModule(tasks: ITask[], projectModule: IOrganizationProjectModule): Promise<void> {
const taskUpdates = tasks.map((task) => {
if (!task.modules) {
task.modules = [];
}
task.modules.push(projectModule);
return this._taskService.update(task.id, { ...task });
});
await Promise.all(taskUpdates);
}

/**
* Log activity for a project module.
* @param projectModule The project module to log.
* @param organizationId The ID of the organization.
* @param tenantId The ID of the tenant.
*/
private logModuleActivity(
action: ActionTypeEnum,
updatedModule: IOrganizationProjectModule,
existingModule?: IOrganizationProjectModule,
changes?: Partial<IOrganizationProjectModuleUpdateInput>
): void {
const tenantId = RequestContext.currentTenantId();
const organizationId = updatedModule.organizationId;

this.activityLogService.logActivity<OrganizationProjectModule>(
BaseEntityEnum.OrganizationProjectModule,
action,
ActorTypeEnum.User,
updatedModule.id,
updatedModule.name,
updatedModule,
organizationId,
tenantId,
existingModule,
changes
);
}
}
Loading
Loading