diff --git a/packages/contracts/src/activity-log.model.ts b/packages/contracts/src/activity-log.model.ts index 80283fd2d2e..a2a4d60fdda 100644 --- a/packages/contracts/src/activity-log.model.ts +++ b/packages/contracts/src/activity-log.model.ts @@ -1,6 +1,12 @@ import { ActorTypeEnum, IBasePerTenantAndOrganizationEntityModel, ID } from './base-entity.model'; import { IUser } from './user.model'; +// Define a type for JSON data +export type JsonData = Record | string; + +/** + * Interface representing an activity log entry. + */ export interface IActivityLog extends IBasePerTenantAndOrganizationEntityModel { entity: ActivityLogEntityEnum; // Entity / Table name concerned by activity log entityId: ID; // The ID of the element we are interacting with (a task, an organization, an employee, ...) @@ -14,19 +20,25 @@ export interface IActivityLog extends IBasePerTenantAndOrganizationEntityModel { updatedEntities?: IActivityLogUpdatedValues[]; // Stores updated IDs, or other values for related entities. Eg : {members: ['member_1_ID', 'member_2_ID']}, creator?: IUser; creatorId?: ID; - data?: Record; + data?: JsonData; } -export enum ActionTypeEnum { - CREATED = 'Created', - UPDATED = 'Updated', - DELETED = 'Deleted' +export interface IActivityLogUpdatedValues { + [x: string]: Record; } -export interface IActivityLogUpdatedValues { - [x: string]: any; +/** + * Enum for action types in the activity log. + */ +export enum ActionTypeEnum { + Created = 'Created', + Updated = 'Updated', + Deleted = 'Deleted' } +/** + * Enum for entities that can be involved in activity logs. + */ export enum ActivityLogEntityEnum { Candidate = 'Candidate', Contact = 'Contact', @@ -45,5 +57,9 @@ export enum ActivityLogEntityEnum { OrganizationSprint = 'OrganizationSprint', Task = 'Task', User = 'User' - // Add other entities as we can to use them for activity history } + +/** + * Input type for activity log creation, excluding `creatorId` and `creator`. + */ +export interface IActivityLogInput extends Omit {} diff --git a/packages/contracts/src/base-entity.model.ts b/packages/contracts/src/base-entity.model.ts index e2e6b67dc53..9e030be25a3 100644 --- a/packages/contracts/src/base-entity.model.ts +++ b/packages/contracts/src/base-entity.model.ts @@ -58,6 +58,6 @@ export interface IBasePerTenantAndOrganizationEntityMutationInput extends Partia // Actor type defines if it's User or system performed some action export enum ActorTypeEnum { - SYSTEM = 'SYSTEM', - USER = 'USER' + System = 0, // System performed the action + User = 1 // User performed the action } diff --git a/packages/core/src/activity-log/activity-log.controller.ts b/packages/core/src/activity-log/activity-log.controller.ts new file mode 100644 index 00000000000..448aab59753 --- /dev/null +++ b/packages/core/src/activity-log/activity-log.controller.ts @@ -0,0 +1,29 @@ +import { Controller, Get, Query, UseGuards } from '@nestjs/common'; +import { IActivityLog, IPagination } from '@gauzy/contracts'; +import { Permissions } from '../shared/decorators'; +import { PermissionGuard, TenantPermissionGuard } from '../shared/guards'; +import { UseValidationPipe } from '../shared/pipes'; +import { GetActivityLogsDTO } from './dto/get-activity-logs.dto'; +import { ActivityLogService } from './activity-log.service'; + +@UseGuards(TenantPermissionGuard, PermissionGuard) +@Permissions() +@Controller('/activity-log') +export class ActivityLogController { + constructor(readonly _activityLogService: ActivityLogService) {} + + /** + * Retrieves activity logs based on query parameters. + * Supports filtering, pagination, sorting, and ordering. + * + * @param query Query parameters for filtering, pagination, and ordering. + * @returns A list of activity logs. + */ + @Get('/') + @UseValidationPipe() + async getActivityLogs( + @Query() query: GetActivityLogsDTO + ): Promise> { + return await this._activityLogService.findActivityLogs(query); + } +} diff --git a/packages/core/src/activity-log/activity-log.entity.ts b/packages/core/src/activity-log/activity-log.entity.ts index def5a624831..4b1c9eaa902 100644 --- a/packages/core/src/activity-log/activity-log.entity.ts +++ b/packages/core/src/activity-log/activity-log.entity.ts @@ -1,5 +1,6 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { EntityRepositoryType } from '@mikro-orm/core'; +import { JoinColumn, RelationId } from 'typeorm'; import { IsArray, IsEnum, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator'; import { isMySQL, isPostgres } from '@gauzy/config'; import { @@ -7,14 +8,13 @@ import { ActionTypeEnum, ActorTypeEnum, IActivityLog, - IActivityLogUpdatedValues, ID, - IUser + IUser, + JsonData } from '@gauzy/contracts'; import { TenantOrganizationBaseEntity, User } from '../core/entities/internal'; import { ColumnIndex, MultiORMColumn, MultiORMEntity, MultiORMManyToOne } from '../core/decorators/entity'; import { MikroOrmActivityLogRepository } from './repository/mikro-orm-activity-log.repository'; -import { JoinColumn, RelationId } from 'typeorm'; @MultiORMEntity('activity_log', { mikroOrmRepository: () => MikroOrmActivityLogRepository }) export class ActivityLog extends TenantOrganizationBaseEntity implements IActivityLog { @@ -32,7 +32,7 @@ export class ActivityLog extends TenantOrganizationBaseEntity implements IActivi @IsUUID() @ColumnIndex() @MultiORMColumn() - entityId: string; + entityId: ID; @ApiProperty({ type: () => String, enum: ActionTypeEnum }) @IsNotEmpty() @@ -64,31 +64,31 @@ export class ActivityLog extends TenantOrganizationBaseEntity implements IActivi @IsOptional() @IsArray() @MultiORMColumn({ type: isPostgres() ? 'jsonb' : isMySQL() ? 'json' : 'text', nullable: true }) - previousValues?: IActivityLogUpdatedValues[]; + previousValues?: Record[]; @ApiPropertyOptional({ type: () => Array }) @IsOptional() @IsArray() @MultiORMColumn({ type: isPostgres() ? 'jsonb' : isMySQL() ? 'json' : 'text', nullable: true }) - updatedValues?: IActivityLogUpdatedValues[]; + updatedValues?: Record[]; @ApiPropertyOptional({ type: () => Array }) @IsOptional() @IsArray() @MultiORMColumn({ type: isPostgres() ? 'jsonb' : isMySQL() ? 'json' : 'text', nullable: true }) - previousEntities?: IActivityLogUpdatedValues[]; + previousEntities?: Record[]; @ApiPropertyOptional({ type: () => Array }) @IsOptional() @IsArray() @MultiORMColumn({ type: isPostgres() ? 'jsonb' : isMySQL() ? 'json' : 'text', nullable: true }) - updatedEntities?: IActivityLogUpdatedValues[]; + updatedEntities?: Record[]; @ApiPropertyOptional({ type: () => Object }) @IsOptional() @IsArray() @MultiORMColumn({ type: isPostgres() ? 'jsonb' : isMySQL() ? 'json' : 'text', nullable: true }) - data?: Record; + data?: JsonData; /* |-------------------------------------------------------------------------- @@ -99,8 +99,6 @@ export class ActivityLog extends TenantOrganizationBaseEntity implements IActivi /** * User performed action */ - @ApiPropertyOptional({ type: () => Object }) - @IsOptional() @MultiORMManyToOne(() => User, { /** Indicates if relation column value can be nullable or not. */ nullable: true, @@ -111,9 +109,6 @@ export class ActivityLog extends TenantOrganizationBaseEntity implements IActivi @JoinColumn() creator?: IUser; - @ApiPropertyOptional({ type: () => String }) - @IsOptional() - @IsUUID() @RelationId((it: ActivityLog) => it.creator) @ColumnIndex() @MultiORMColumn({ nullable: true, relationId: true }) diff --git a/packages/core/src/activity-log/activity-log.helper.ts b/packages/core/src/activity-log/activity-log.helper.ts new file mode 100644 index 00000000000..9389d2eb0da --- /dev/null +++ b/packages/core/src/activity-log/activity-log.helper.ts @@ -0,0 +1,37 @@ +import { ActionTypeEnum, ActivityLogEntityEnum } from "@gauzy/contracts"; + +const ActivityTemplates = { + [ActionTypeEnum.Created]: `{action} a new {entity} called "{entityName}"`, + [ActionTypeEnum.Updated]: `{action} {entity} "{entityName}"`, + [ActionTypeEnum.Deleted]: `{action} {entity} "{entityName}"`, +}; + +/** + * Generates an activity description based on the action type, entity, and entity name. + * @param action - The action performed (e.g., CREATED, UPDATED, DELETED). + * @param entity - The type of entity involved in the action (e.g., Project, User). + * @param entityName - The name of the specific entity instance. + * @returns A formatted description string. + */ +export function generateActivityLogDescription( + action: ActionTypeEnum, + entity: ActivityLogEntityEnum, + entityName: string +): string { + // Get the template corresponding to the action + const template = ActivityTemplates[action] || '{action} {entity} "{entityName}"'; + + // Replace placeholders in the template with actual values + return template.replace(/\{(\w+)\}/g, (_, key) => { + switch (key) { + case 'action': + return action; + case 'entity': + return entity; + case 'entityName': + return entityName; + default: + return ''; + } + }); +} diff --git a/packages/core/src/activity-log/activity-log.module.ts b/packages/core/src/activity-log/activity-log.module.ts new file mode 100644 index 00000000000..26bf823dfea --- /dev/null +++ b/packages/core/src/activity-log/activity-log.module.ts @@ -0,0 +1,23 @@ +import { CqrsModule } from '@nestjs/cqrs'; +import { Module } from '@nestjs/common'; +import { MikroOrmModule } from '@mikro-orm/nestjs'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { RolePermissionModule } from '../role-permission/role-permission.module'; +import { ActivityLogController } from './activity-log.controller'; +import { ActivityLog } from './activity-log.entity'; +import { ActivityLogService } from './activity-log.service'; +import { EventHandlers } from './events/handlers'; +import { TypeOrmActivityLogRepository } from './repository/type-orm-activity-log.repository'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ActivityLog]), + MikroOrmModule.forFeature([ActivityLog]), + CqrsModule, + RolePermissionModule + ], + controllers: [ActivityLogController], + providers: [ActivityLogService, TypeOrmActivityLogRepository, ...EventHandlers], + exports: [TypeOrmModule, MikroOrmModule, ActivityLogService, TypeOrmActivityLogRepository] +}) +export class ActivityLogModule {} diff --git a/packages/core/src/activity-log/activity-log.service.ts b/packages/core/src/activity-log/activity-log.service.ts new file mode 100644 index 00000000000..c125074cc7b --- /dev/null +++ b/packages/core/src/activity-log/activity-log.service.ts @@ -0,0 +1,97 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { FindManyOptions, FindOptionsOrder, FindOptionsWhere } from 'typeorm'; +import { IActivityLog, IActivityLogInput, IPagination } from '@gauzy/contracts'; +import { TenantAwareCrudService } from './../core/crud'; +import { RequestContext } from '../core/context'; +import { GetActivityLogsDTO, allowedOrderDirections, allowedOrderFields } from './dto/get-activity-logs.dto'; +import { ActivityLog } from './activity-log.entity'; +import { MikroOrmActivityLogRepository, TypeOrmActivityLogRepository } from './repository'; + +@Injectable() +export class ActivityLogService extends TenantAwareCrudService { + constructor( + readonly typeOrmActivityLogRepository: TypeOrmActivityLogRepository, + readonly mikroOrmActivityLogRepository: MikroOrmActivityLogRepository + ) { + super(typeOrmActivityLogRepository, mikroOrmActivityLogRepository); + } + + /** + * Finds and retrieves activity logs based on the given filter criteria. + * + * @param {GetActivityLogsDTO} filter - Filter criteria to find activity logs, including entity, entityId, action, actorType, isActive, isArchived, orderBy, and order. + * @returns {Promise>} - A promise that resolves to a paginated list of activity logs. + * + * Example usage: + * ``` + * const logs = await findActivityLogs({ + * entity: 'User', + * action: 'CREATE', + * orderBy: 'updatedAt', + * order: 'ASC' + * }); + * ``` + */ + public async findActivityLogs(filter: GetActivityLogsDTO): Promise> { + const { + entity, + entityId, + action, + actorType, + isActive = true, + isArchived = false, + orderBy = 'createdAt', + order = 'DESC', + relations = [], + skip, + take + } = filter; + + // Build the 'where' condition using concise syntax + const where: FindOptionsWhere = { + ...(entity && { entity }), + ...(entityId && { entityId }), + ...(action && { action }), + ...(actorType && { actorType }), + isActive, + isArchived + }; + + // Fallback to default if invalid orderBy/order values are provided + const orderField = allowedOrderFields.includes(orderBy) ? orderBy : 'createdAt'; + const orderDirection = allowedOrderDirections.includes(order.toUpperCase()) ? order.toUpperCase() : 'DESC'; + + // Define order option + const orderOption: FindOptionsOrder = { [orderField]: orderDirection }; + + // Define find options + const findOptions: FindManyOptions = { + where, + order: orderOption, + ...(skip && { skip }), + ...(take && { take }), + ...(relations && { relations }) + }; + + // Retrieve activity logs using the base class method + return await super.findAll(findOptions); + } + + /** + * Creates a new activity log entry with the provided input, while associating it with the current user and tenant. + * + * @param input - The data required to create an activity log entry. + * @returns The created activity log entry. + * @throws BadRequestException when the log creation fails. + */ + async logActivity(input: IActivityLogInput): Promise { + try { + const creatorId = RequestContext.currentUserId(); // Retrieve the current user's ID from the request context + // Create the activity log entry using the provided input along with the tenantId and creatorId + return await super.create({ ...input, creatorId }); + } catch (error) { + console.log('Error while creating activity log:', error); + throw new BadRequestException('Error while creating activity log', error); + } + } +} diff --git a/packages/core/src/activity-log/activity-log.subscriber.ts b/packages/core/src/activity-log/activity-log.subscriber.ts new file mode 100644 index 00000000000..8ff9891d18e --- /dev/null +++ b/packages/core/src/activity-log/activity-log.subscriber.ts @@ -0,0 +1,58 @@ +import { EventSubscriber } from 'typeorm'; +import { isBetterSqlite3, isSqlite } from '@gauzy/config'; +import { BaseEntityEventSubscriber } from '../core/entities/subscribers/base-entity-event.subscriber'; +import { MultiOrmEntityManager } from '../core/entities/subscribers/entity-event-subscriber.types'; +import { ActivityLog } from './activity-log.entity'; + +@EventSubscriber() +export class ActivityLogSubscriber extends BaseEntityEventSubscriber { + /** + * Indicates that this subscriber only listen to ActivityLog events. + */ + listenTo() { + return ActivityLog; + } + + /** + * Called before an ActivityLog entity is inserted or created in the database. + * This method prepares the entity for insertion, particularly by serializing the data property to a JSON string + * for SQLite databases. + * + * @param entity The ActivityLog entity that is about to be created. + * @returns {Promise} A promise that resolves when the pre-creation processing is complete. + */ + async beforeEntityCreate(entity: ActivityLog): Promise { + try { + // Check if the database is SQLite and the entity's metaData is a JavaScript object + if (isSqlite() || isBetterSqlite3()) { + // ToDo: If need convert data to JSON before save + entity.data = JSON.stringify(entity.data); + } + } catch (error) { + // In case of error during JSON serialization, reset metaData to an empty object + entity.data = JSON.stringify({}); + } + } + + /** + * Handles the parsing of JSON data after the ActivityLog entity is loaded from the database. + * This function ensures that if the database is SQLite, the `data` field, stored as a JSON string, + * is parsed back into a JavaScript object. + * + * @param {ActivityLog} entity - The ActivityLog entity that has been loaded from the database. + * @param {MultiOrmEntityManager} [em] - The optional EntityManager instance, if provided. + * @returns {Promise} A promise that resolves once the after-load processing is complete. + */ + async afterEntityLoad(entity: ActivityLog, em?: MultiOrmEntityManager): Promise { + try { + // Check if the database is SQLite and if `data` is a non-null string + if ((isSqlite() || isBetterSqlite3()) && entity.data && typeof entity.data === 'string') { + entity.data = JSON.parse(entity.data); + } + } catch (error) { + // Log the error and reset the data to an empty object if JSON parsing fails + console.error('Error parsing JSON data in afterEntityLoad:', error); + entity.data = {}; + } + } +} diff --git a/packages/core/src/activity-log/dto/get-activity-logs.dto.ts b/packages/core/src/activity-log/dto/get-activity-logs.dto.ts new file mode 100644 index 00000000000..c416cd21c93 --- /dev/null +++ b/packages/core/src/activity-log/dto/get-activity-logs.dto.ts @@ -0,0 +1,57 @@ +import { ApiPropertyOptional, IntersectionType, PickType } from '@nestjs/swagger'; +import { IsEnum, IsIn, IsOptional, IsString, IsUUID } from 'class-validator'; +import { ActionTypeEnum, ActivityLogEntityEnum, ActorTypeEnum, ID } from '@gauzy/contracts'; +import { PaginationParams } from '../../core/crud'; +import { TenantOrganizationBaseDTO } from '../../core/dto'; +import { ActivityLog } from '../activity-log.entity'; + +// Validate 'orderBy' and 'order' parameters with fallbacks +export const allowedOrderFields = ['createdAt', 'updatedAt', 'entity', 'action']; +export const allowedOrderDirections = ['ASC', 'DESC', 'asc', 'desc']; + +/** + * Filters for ActivityLogs + */ +export class GetActivityLogsDTO extends IntersectionType( + TenantOrganizationBaseDTO, + PickType(PaginationParams, ['skip', 'take', 'relations']), + PickType(ActivityLog, ['isActive', 'isArchived']) +) { + // Filter by entity (example: Organization, Task, OrganizationContact) + @ApiPropertyOptional({ type: () => String, enum: ActivityLogEntityEnum }) + @IsOptional() + @IsEnum(ActivityLogEntityEnum) + entity: ActivityLogEntityEnum; + + // Filter by entityId (example: projectId, taskId, organizationContactId) + @ApiPropertyOptional({ type: () => String }) + @IsOptional() + @IsUUID() + entityId: ID; + + // Filter by action (example: CREATED, UPDATED, DELETED) + @ApiPropertyOptional({ type: () => String, enum: ActionTypeEnum }) + @IsOptional() + @IsEnum(ActionTypeEnum) + action: ActionTypeEnum; + + // Filter by actorType (example: SYSTEM, USER) + @ApiPropertyOptional({ type: () => String, enum: ActorTypeEnum }) + @IsOptional() + @IsEnum(ActorTypeEnum) + actorType?: ActorTypeEnum; + + // Filter by orderBy (example: createdAt, updatedAt, entity, action) + @ApiPropertyOptional({ type: () => String, enum: allowedOrderFields }) + @IsOptional() + @IsString() + @IsIn(allowedOrderFields) // Allowed fields + orderBy?: string = 'createdAt'; + + // Filter by order (example: ASC, DESC, asc, desc) + @ApiPropertyOptional({ type: () => String, enum: allowedOrderDirections }) + @IsOptional() + @IsString() + @IsIn(allowedOrderDirections) + order?: 'ASC' | 'DESC' = 'DESC'; +} diff --git a/packages/core/src/activity-log/events/activity-log.event.ts b/packages/core/src/activity-log/events/activity-log.event.ts new file mode 100644 index 00000000000..c95385d4355 --- /dev/null +++ b/packages/core/src/activity-log/events/activity-log.event.ts @@ -0,0 +1,6 @@ +import { IEvent } from '@nestjs/cqrs'; +import { IActivityLogInput } from '@gauzy/contracts'; + +export class ActivityLogEvent implements IEvent { + constructor(public readonly input: IActivityLogInput) {} +} diff --git a/packages/core/src/activity-log/events/handlers/activity-log.handler.ts b/packages/core/src/activity-log/events/handlers/activity-log.handler.ts new file mode 100644 index 00000000000..cd1d8c6ceaf --- /dev/null +++ b/packages/core/src/activity-log/events/handlers/activity-log.handler.ts @@ -0,0 +1,20 @@ +import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; +import { ActivityLogEvent } from '../activity-log.event'; +import { ActivityLogService } from '../../activity-log.service'; + +@EventsHandler(ActivityLogEvent) +export class ActivityLogEventHandler implements IEventHandler { + constructor(readonly activityLogService: ActivityLogService) {} + + /** + * Handles the activity log event by creating a new activity log entry using the provided input data. + * + * @param event - The activity log event containing the input data required to create the log entry. + * @returns A promise that resolves with the created activity log entry. + * + */ + async handle(event: ActivityLogEvent) { + // Extract the input from the event and create a new activity log entry + return await this.activityLogService.logActivity(event.input); + } +} diff --git a/packages/core/src/activity-log/events/handlers/index.ts b/packages/core/src/activity-log/events/handlers/index.ts new file mode 100644 index 00000000000..c575da15c41 --- /dev/null +++ b/packages/core/src/activity-log/events/handlers/index.ts @@ -0,0 +1,3 @@ +import { ActivityLogEventHandler } from './activity-log.handler'; + +export const EventHandlers = [ActivityLogEventHandler]; diff --git a/packages/core/src/activity-log/events/index.ts b/packages/core/src/activity-log/events/index.ts new file mode 100644 index 00000000000..2c2a0251c36 --- /dev/null +++ b/packages/core/src/activity-log/events/index.ts @@ -0,0 +1 @@ +export * from './activity-log.event'; diff --git a/packages/core/src/activity-log/repository/index.ts b/packages/core/src/activity-log/repository/index.ts index 9b525616a1b..1d0cab24080 100644 --- a/packages/core/src/activity-log/repository/index.ts +++ b/packages/core/src/activity-log/repository/index.ts @@ -1 +1,2 @@ export * from './mikro-orm-activity-log.repository'; +export * from './type-orm-activity-log.repository'; diff --git a/packages/core/src/app.module.ts b/packages/core/src/app.module.ts index 2ccc7e1bec0..13e22b0170e 100644 --- a/packages/core/src/app.module.ts +++ b/packages/core/src/app.module.ts @@ -147,6 +147,7 @@ import { GlobalFavoriteModule } from './favorite/global-favorite-service.module' import { CommentModule } from './comment/comment.module'; import { StatsModule } from './stats/stats.module'; // Global Stats Module import { ReactionModule } from './reaction/reaction.module'; +import { ActivityLogModule } from './activity-log/activity-log.module'; const { unleashConfig } = environment; @@ -443,7 +444,8 @@ if (environment.THROTTLE_ENABLED) { GlobalFavoriteModule, StatsModule, // Global Stats Module ReactionModule, - CommentModule + CommentModule, + ActivityLogModule ], controllers: [AppController], providers: [ diff --git a/packages/core/src/comment/commands/handlers/comment.create.handler.ts b/packages/core/src/comment/commands/handlers/comment.create.handler.ts index f7dd8a24b4a..e75a595c7d3 100644 --- a/packages/core/src/comment/commands/handlers/comment.create.handler.ts +++ b/packages/core/src/comment/commands/handlers/comment.create.handler.ts @@ -1,7 +1,7 @@ import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { IComment } from '@gauzy/contracts'; import { CommentService } from '../../comment.service'; import { CommentCreateCommand } from '../comment.create.command'; -import { IComment } from '@gauzy/contracts'; @CommandHandler(CommentCreateCommand) export class CommentCreateHandler implements ICommandHandler { diff --git a/packages/core/src/core/entities/internal.ts b/packages/core/src/core/entities/internal.ts index 74a5d0a3af0..620fa43fb91 100644 --- a/packages/core/src/core/entities/internal.ts +++ b/packages/core/src/core/entities/internal.ts @@ -148,6 +148,7 @@ export * from '../../warehouse/warehouse-product.entity'; export * from '../../warehouse/warehouse.entity'; //core subscribers +export * from '../../activity-log/activity-log.subscriber'; export * from '../../candidate/candidate.subscriber'; export * from '../../custom-smtp/custom-smtp.subscriber'; export * from '../../email-reset/email-reset.subscriber'; diff --git a/packages/core/src/core/entities/subscribers/index.ts b/packages/core/src/core/entities/subscribers/index.ts index 379a2a44e0d..811058e95e1 100644 --- a/packages/core/src/core/entities/subscribers/index.ts +++ b/packages/core/src/core/entities/subscribers/index.ts @@ -4,6 +4,7 @@ export * from './tenant-organization-base-entity.subscriber'; import { MultiORMEnum, getORMType } from '../../utils'; import { ActivitySubscriber, + ActivityLogSubscriber, CandidateSubscriber, CustomSmtpSubscriber, EmailResetSubscriber, @@ -53,6 +54,7 @@ export const coreSubscribers = [ // Add the subscriber only if the ORM type is MikroORM ...(ormType === MultiORMEnum.MikroORM ? [TenantOrganizationBaseEntityEventSubscriber] : []), ActivitySubscriber, + ActivityLogSubscriber, CandidateSubscriber, CustomSmtpSubscriber, EmailResetSubscriber, diff --git a/packages/core/src/core/utils.ts b/packages/core/src/core/utils.ts index a4165910c90..fa98c9c057a 100644 --- a/packages/core/src/core/utils.ts +++ b/packages/core/src/core/utils.ts @@ -690,6 +690,32 @@ export function wrapSerialize(entity: T): T { return wrap(entity).toJSON() as T; } +/** + * Converts the given entity instance to a plain object. + * + * This function creates a shallow copy of the entity, retaining its properties as a plain object, + * making it suitable for use in contexts where a non-class representation is required. + * + * @param entity - The entity instance to be converted to a plain object. + * @returns A plain object representation of the given entity instance. + */ +export function toPlain(entity: any): Record { + return { ...entity }; +} + +/** + * Converts the given entity instance to a JSON object. + * + * This function creates a deep copy of the entity, converting it into a JSON-compatible structure, + * making it suitable for serialization or transferring over a network. + * + * @param entity - The entity instance to be converted to a JSON object. + * @returns A JSON representation of the given entity instance. + */ +export function toJSON(entity: any): Record { + return JSON.parse(JSON.stringify(toPlain(entity))); +} + /** * Replace $ placeholders with ? for mysql, sqlite, and better-sqlite3 * @param query - The SQL query with $ placeholders diff --git a/packages/core/src/database/migrations/1727284713380-AlterActorTypeInCommentAndActivityLogEntities.ts b/packages/core/src/database/migrations/1727284713380-AlterActorTypeInCommentAndActivityLogEntities.ts new file mode 100644 index 00000000000..4c4120a6b06 --- /dev/null +++ b/packages/core/src/database/migrations/1727284713380-AlterActorTypeInCommentAndActivityLogEntities.ts @@ -0,0 +1,349 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { yellow } from 'chalk'; +import { DatabaseTypeEnum } from '@gauzy/config'; + +export class AlterActorTypeInCommentAndActivityLogEntities1727284713380 implements MigrationInterface { + name = 'AlterActorTypeInCommentAndActivityLogEntities1727284713380'; + + /** + * Up Migration + * + * @param queryRunner + */ + public async up(queryRunner: QueryRunner): Promise { + console.log(yellow(this.name + ' start running!')); + + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlUpQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * Down Migration + * + * @param queryRunner + */ + public async down(queryRunner: QueryRunner): Promise { + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlDownQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * PostgresDB Up Migration + * + * @param queryRunner + */ + public async postgresUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "public"."IDX_691ba0d5b57cd5adea2c9cc285"`); + await queryRunner.query(`ALTER TABLE "activity_log" DROP COLUMN "actorType"`); + await queryRunner.query(`ALTER TABLE "activity_log" ADD "actorType" integer`); + await queryRunner.query(`DROP INDEX "public"."IDX_eecd6e41f9acb6bf59e474d518"`); + await queryRunner.query(`ALTER TABLE "comment" DROP COLUMN "actorType"`); + await queryRunner.query(`ALTER TABLE "comment" ADD "actorType" integer`); + await queryRunner.query(`CREATE INDEX "IDX_691ba0d5b57cd5adea2c9cc285" ON "activity_log" ("actorType") `); + await queryRunner.query(`CREATE INDEX "IDX_eecd6e41f9acb6bf59e474d518" ON "comment" ("actorType") `); + } + + /** + * PostgresDB Down Migration + * + * @param queryRunner + */ + public async postgresDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "public"."IDX_eecd6e41f9acb6bf59e474d518"`); + await queryRunner.query(`DROP INDEX "public"."IDX_691ba0d5b57cd5adea2c9cc285"`); + await queryRunner.query(`ALTER TABLE "comment" DROP COLUMN "actorType"`); + await queryRunner.query(`ALTER TABLE "comment" ADD "actorType" character varying`); + await queryRunner.query(`CREATE INDEX "IDX_eecd6e41f9acb6bf59e474d518" ON "comment" ("actorType") `); + await queryRunner.query(`ALTER TABLE "activity_log" DROP COLUMN "actorType"`); + await queryRunner.query(`ALTER TABLE "activity_log" ADD "actorType" character varying`); + await queryRunner.query(`CREATE INDEX "IDX_691ba0d5b57cd5adea2c9cc285" ON "activity_log" ("actorType") `); + } + + /** + * SqliteDB and BetterSQlite3DB Up Migration + * + * @param queryRunner + */ + public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_b6e9a5c3e1ee65a3bcb8a00de2"`); + await queryRunner.query(`DROP INDEX "IDX_691ba0d5b57cd5adea2c9cc285"`); + await queryRunner.query(`DROP INDEX "IDX_695624cb02a5da0e86cd4489c0"`); + await queryRunner.query(`DROP INDEX "IDX_ef0a3bcee9c0305f755d5add13"`); + await queryRunner.query(`DROP INDEX "IDX_c60ac1ac95c2d901afd2f68909"`); + await queryRunner.query(`DROP INDEX "IDX_3e7ec906ac1026a6c9779e82a2"`); + await queryRunner.query(`DROP INDEX "IDX_d42f36e39404cb6455254deb36"`); + await queryRunner.query(`DROP INDEX "IDX_eb63f18992743f35225ae4e77c"`); + await queryRunner.query(`DROP INDEX "IDX_4a88f1b97dd306d919f844828d"`); + await queryRunner.query( + `CREATE TABLE "temporary_activity_log" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entity" varchar NOT NULL, "entityId" varchar NOT NULL, "action" varchar NOT NULL, "actorType" varchar, "description" text, "updatedFields" text, "previousValues" text, "updatedValues" text, "previousEntities" text, "updatedEntities" text, "data" text, "creatorId" varchar, CONSTRAINT "FK_b6e9a5c3e1ee65a3bcb8a00de2b" FOREIGN KEY ("creatorId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_3e7ec906ac1026a6c9779e82a21" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_d42f36e39404cb6455254deb360" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_activity_log"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entity", "entityId", "action", "actorType", "description", "updatedFields", "previousValues", "updatedValues", "previousEntities", "updatedEntities", "data", "creatorId") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entity", "entityId", "action", "actorType", "description", "updatedFields", "previousValues", "updatedValues", "previousEntities", "updatedEntities", "data", "creatorId" FROM "activity_log"` + ); + await queryRunner.query(`DROP TABLE "activity_log"`); + await queryRunner.query(`ALTER TABLE "temporary_activity_log" RENAME TO "activity_log"`); + await queryRunner.query(`CREATE INDEX "IDX_b6e9a5c3e1ee65a3bcb8a00de2" ON "activity_log" ("creatorId") `); + await queryRunner.query(`CREATE INDEX "IDX_691ba0d5b57cd5adea2c9cc285" ON "activity_log" ("actorType") `); + await queryRunner.query(`CREATE INDEX "IDX_695624cb02a5da0e86cd4489c0" ON "activity_log" ("action") `); + await queryRunner.query(`CREATE INDEX "IDX_ef0a3bcee9c0305f755d5add13" ON "activity_log" ("entityId") `); + await queryRunner.query(`CREATE INDEX "IDX_c60ac1ac95c2d901afd2f68909" ON "activity_log" ("entity") `); + await queryRunner.query(`CREATE INDEX "IDX_3e7ec906ac1026a6c9779e82a2" ON "activity_log" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_d42f36e39404cb6455254deb36" ON "activity_log" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_eb63f18992743f35225ae4e77c" ON "activity_log" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_4a88f1b97dd306d919f844828d" ON "activity_log" ("isActive") `); + await queryRunner.query(`DROP INDEX "IDX_e3aebe2bd1c53467a07109be59"`); + await queryRunner.query(`DROP INDEX "IDX_c9409c81aa283c1aae70fd5f4c"`); + await queryRunner.query(`DROP INDEX "IDX_b6bf60ecb9f6c398e349adff52"`); + await queryRunner.query(`DROP INDEX "IDX_eecd6e41f9acb6bf59e474d518"`); + await queryRunner.query(`DROP INDEX "IDX_2950cfa146fc50334efa61a70b"`); + await queryRunner.query(`DROP INDEX "IDX_097e339f6cb990306d19880a4c"`); + await queryRunner.query(`DROP INDEX "IDX_a3422826753d4e6b079dea9834"`); + await queryRunner.query(`DROP INDEX "IDX_8f58834bed39f0f9e85f048eaf"`); + await queryRunner.query(`DROP INDEX "IDX_da3cd25ed3a6ce76770f00c3da"`); + await queryRunner.query(`DROP INDEX "IDX_3620aeff4ac5c977176226017e"`); + await queryRunner.query( + `CREATE TABLE "temporary_comment" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entity" varchar NOT NULL, "entityId" varchar NOT NULL, "comment" text NOT NULL, "actorType" varchar, "resolved" boolean, "resolvedAt" datetime, "editedAt" datetime, "creatorId" varchar, "resolvedById" varchar, "parentId" varchar, CONSTRAINT "FK_e3aebe2bd1c53467a07109be596" FOREIGN KEY ("parentId") REFERENCES "comment" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_c9409c81aa283c1aae70fd5f4c3" FOREIGN KEY ("resolvedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_b6bf60ecb9f6c398e349adff52f" FOREIGN KEY ("creatorId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_a3422826753d4e6b079dea98342" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_8f58834bed39f0f9e85f048eafe" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_comment"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entity", "entityId", "comment", "actorType", "resolved", "resolvedAt", "editedAt", "creatorId", "resolvedById", "parentId") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entity", "entityId", "comment", "actorType", "resolved", "resolvedAt", "editedAt", "creatorId", "resolvedById", "parentId" FROM "comment"` + ); + await queryRunner.query(`DROP TABLE "comment"`); + await queryRunner.query(`ALTER TABLE "temporary_comment" RENAME TO "comment"`); + await queryRunner.query(`CREATE INDEX "IDX_e3aebe2bd1c53467a07109be59" ON "comment" ("parentId") `); + await queryRunner.query(`CREATE INDEX "IDX_c9409c81aa283c1aae70fd5f4c" ON "comment" ("resolvedById") `); + await queryRunner.query(`CREATE INDEX "IDX_b6bf60ecb9f6c398e349adff52" ON "comment" ("creatorId") `); + await queryRunner.query(`CREATE INDEX "IDX_eecd6e41f9acb6bf59e474d518" ON "comment" ("actorType") `); + await queryRunner.query(`CREATE INDEX "IDX_2950cfa146fc50334efa61a70b" ON "comment" ("entityId") `); + await queryRunner.query(`CREATE INDEX "IDX_097e339f6cb990306d19880a4c" ON "comment" ("entity") `); + await queryRunner.query(`CREATE INDEX "IDX_a3422826753d4e6b079dea9834" ON "comment" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_8f58834bed39f0f9e85f048eaf" ON "comment" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_da3cd25ed3a6ce76770f00c3da" ON "comment" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_3620aeff4ac5c977176226017e" ON "comment" ("isActive") `); + await queryRunner.query(`DROP INDEX "IDX_b6e9a5c3e1ee65a3bcb8a00de2"`); + await queryRunner.query(`DROP INDEX "IDX_691ba0d5b57cd5adea2c9cc285"`); + await queryRunner.query(`DROP INDEX "IDX_695624cb02a5da0e86cd4489c0"`); + await queryRunner.query(`DROP INDEX "IDX_ef0a3bcee9c0305f755d5add13"`); + await queryRunner.query(`DROP INDEX "IDX_c60ac1ac95c2d901afd2f68909"`); + await queryRunner.query(`DROP INDEX "IDX_3e7ec906ac1026a6c9779e82a2"`); + await queryRunner.query(`DROP INDEX "IDX_d42f36e39404cb6455254deb36"`); + await queryRunner.query(`DROP INDEX "IDX_eb63f18992743f35225ae4e77c"`); + await queryRunner.query(`DROP INDEX "IDX_4a88f1b97dd306d919f844828d"`); + await queryRunner.query( + `CREATE TABLE "temporary_activity_log" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entity" varchar NOT NULL, "entityId" varchar NOT NULL, "action" varchar NOT NULL, "actorType" integer, "description" text, "updatedFields" text, "previousValues" text, "updatedValues" text, "previousEntities" text, "updatedEntities" text, "data" text, "creatorId" varchar, CONSTRAINT "FK_b6e9a5c3e1ee65a3bcb8a00de2b" FOREIGN KEY ("creatorId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_3e7ec906ac1026a6c9779e82a21" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_d42f36e39404cb6455254deb360" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_activity_log"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entity", "entityId", "action", "actorType", "description", "updatedFields", "previousValues", "updatedValues", "previousEntities", "updatedEntities", "data", "creatorId") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entity", "entityId", "action", "actorType", "description", "updatedFields", "previousValues", "updatedValues", "previousEntities", "updatedEntities", "data", "creatorId" FROM "activity_log"` + ); + await queryRunner.query(`DROP TABLE "activity_log"`); + await queryRunner.query(`ALTER TABLE "temporary_activity_log" RENAME TO "activity_log"`); + await queryRunner.query(`CREATE INDEX "IDX_b6e9a5c3e1ee65a3bcb8a00de2" ON "activity_log" ("creatorId") `); + await queryRunner.query(`CREATE INDEX "IDX_691ba0d5b57cd5adea2c9cc285" ON "activity_log" ("actorType") `); + await queryRunner.query(`CREATE INDEX "IDX_695624cb02a5da0e86cd4489c0" ON "activity_log" ("action") `); + await queryRunner.query(`CREATE INDEX "IDX_ef0a3bcee9c0305f755d5add13" ON "activity_log" ("entityId") `); + await queryRunner.query(`CREATE INDEX "IDX_c60ac1ac95c2d901afd2f68909" ON "activity_log" ("entity") `); + await queryRunner.query(`CREATE INDEX "IDX_3e7ec906ac1026a6c9779e82a2" ON "activity_log" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_d42f36e39404cb6455254deb36" ON "activity_log" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_eb63f18992743f35225ae4e77c" ON "activity_log" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_4a88f1b97dd306d919f844828d" ON "activity_log" ("isActive") `); + await queryRunner.query(`DROP INDEX "IDX_e3aebe2bd1c53467a07109be59"`); + await queryRunner.query(`DROP INDEX "IDX_c9409c81aa283c1aae70fd5f4c"`); + await queryRunner.query(`DROP INDEX "IDX_b6bf60ecb9f6c398e349adff52"`); + await queryRunner.query(`DROP INDEX "IDX_eecd6e41f9acb6bf59e474d518"`); + await queryRunner.query(`DROP INDEX "IDX_2950cfa146fc50334efa61a70b"`); + await queryRunner.query(`DROP INDEX "IDX_097e339f6cb990306d19880a4c"`); + await queryRunner.query(`DROP INDEX "IDX_a3422826753d4e6b079dea9834"`); + await queryRunner.query(`DROP INDEX "IDX_8f58834bed39f0f9e85f048eaf"`); + await queryRunner.query(`DROP INDEX "IDX_da3cd25ed3a6ce76770f00c3da"`); + await queryRunner.query(`DROP INDEX "IDX_3620aeff4ac5c977176226017e"`); + await queryRunner.query( + `CREATE TABLE "temporary_comment" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entity" varchar NOT NULL, "entityId" varchar NOT NULL, "comment" text NOT NULL, "actorType" integer, "resolved" boolean, "resolvedAt" datetime, "editedAt" datetime, "creatorId" varchar, "resolvedById" varchar, "parentId" varchar, CONSTRAINT "FK_e3aebe2bd1c53467a07109be596" FOREIGN KEY ("parentId") REFERENCES "comment" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_c9409c81aa283c1aae70fd5f4c3" FOREIGN KEY ("resolvedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_b6bf60ecb9f6c398e349adff52f" FOREIGN KEY ("creatorId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_a3422826753d4e6b079dea98342" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_8f58834bed39f0f9e85f048eafe" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_comment"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entity", "entityId", "comment", "actorType", "resolved", "resolvedAt", "editedAt", "creatorId", "resolvedById", "parentId") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entity", "entityId", "comment", "actorType", "resolved", "resolvedAt", "editedAt", "creatorId", "resolvedById", "parentId" FROM "comment"` + ); + await queryRunner.query(`DROP TABLE "comment"`); + await queryRunner.query(`ALTER TABLE "temporary_comment" RENAME TO "comment"`); + await queryRunner.query(`CREATE INDEX "IDX_e3aebe2bd1c53467a07109be59" ON "comment" ("parentId") `); + await queryRunner.query(`CREATE INDEX "IDX_c9409c81aa283c1aae70fd5f4c" ON "comment" ("resolvedById") `); + await queryRunner.query(`CREATE INDEX "IDX_b6bf60ecb9f6c398e349adff52" ON "comment" ("creatorId") `); + await queryRunner.query(`CREATE INDEX "IDX_eecd6e41f9acb6bf59e474d518" ON "comment" ("actorType") `); + await queryRunner.query(`CREATE INDEX "IDX_2950cfa146fc50334efa61a70b" ON "comment" ("entityId") `); + await queryRunner.query(`CREATE INDEX "IDX_097e339f6cb990306d19880a4c" ON "comment" ("entity") `); + await queryRunner.query(`CREATE INDEX "IDX_a3422826753d4e6b079dea9834" ON "comment" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_8f58834bed39f0f9e85f048eaf" ON "comment" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_da3cd25ed3a6ce76770f00c3da" ON "comment" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_3620aeff4ac5c977176226017e" ON "comment" ("isActive") `); + } + + /** + * SqliteDB and BetterSQlite3DB Down Migration + * + * @param queryRunner + */ + public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_3620aeff4ac5c977176226017e"`); + await queryRunner.query(`DROP INDEX "IDX_da3cd25ed3a6ce76770f00c3da"`); + await queryRunner.query(`DROP INDEX "IDX_8f58834bed39f0f9e85f048eaf"`); + await queryRunner.query(`DROP INDEX "IDX_a3422826753d4e6b079dea9834"`); + await queryRunner.query(`DROP INDEX "IDX_097e339f6cb990306d19880a4c"`); + await queryRunner.query(`DROP INDEX "IDX_2950cfa146fc50334efa61a70b"`); + await queryRunner.query(`DROP INDEX "IDX_eecd6e41f9acb6bf59e474d518"`); + await queryRunner.query(`DROP INDEX "IDX_b6bf60ecb9f6c398e349adff52"`); + await queryRunner.query(`DROP INDEX "IDX_c9409c81aa283c1aae70fd5f4c"`); + await queryRunner.query(`DROP INDEX "IDX_e3aebe2bd1c53467a07109be59"`); + await queryRunner.query(`ALTER TABLE "comment" RENAME TO "temporary_comment"`); + await queryRunner.query( + `CREATE TABLE "comment" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entity" varchar NOT NULL, "entityId" varchar NOT NULL, "comment" text NOT NULL, "actorType" varchar, "resolved" boolean, "resolvedAt" datetime, "editedAt" datetime, "creatorId" varchar, "resolvedById" varchar, "parentId" varchar, CONSTRAINT "FK_e3aebe2bd1c53467a07109be596" FOREIGN KEY ("parentId") REFERENCES "comment" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_c9409c81aa283c1aae70fd5f4c3" FOREIGN KEY ("resolvedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_b6bf60ecb9f6c398e349adff52f" FOREIGN KEY ("creatorId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_a3422826753d4e6b079dea98342" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_8f58834bed39f0f9e85f048eafe" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "comment"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entity", "entityId", "comment", "actorType", "resolved", "resolvedAt", "editedAt", "creatorId", "resolvedById", "parentId") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entity", "entityId", "comment", "actorType", "resolved", "resolvedAt", "editedAt", "creatorId", "resolvedById", "parentId" FROM "temporary_comment"` + ); + await queryRunner.query(`DROP TABLE "temporary_comment"`); + await queryRunner.query(`CREATE INDEX "IDX_3620aeff4ac5c977176226017e" ON "comment" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_da3cd25ed3a6ce76770f00c3da" ON "comment" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_8f58834bed39f0f9e85f048eaf" ON "comment" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_a3422826753d4e6b079dea9834" ON "comment" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_097e339f6cb990306d19880a4c" ON "comment" ("entity") `); + await queryRunner.query(`CREATE INDEX "IDX_2950cfa146fc50334efa61a70b" ON "comment" ("entityId") `); + await queryRunner.query(`CREATE INDEX "IDX_eecd6e41f9acb6bf59e474d518" ON "comment" ("actorType") `); + await queryRunner.query(`CREATE INDEX "IDX_b6bf60ecb9f6c398e349adff52" ON "comment" ("creatorId") `); + await queryRunner.query(`CREATE INDEX "IDX_c9409c81aa283c1aae70fd5f4c" ON "comment" ("resolvedById") `); + await queryRunner.query(`CREATE INDEX "IDX_e3aebe2bd1c53467a07109be59" ON "comment" ("parentId") `); + await queryRunner.query(`DROP INDEX "IDX_4a88f1b97dd306d919f844828d"`); + await queryRunner.query(`DROP INDEX "IDX_eb63f18992743f35225ae4e77c"`); + await queryRunner.query(`DROP INDEX "IDX_d42f36e39404cb6455254deb36"`); + await queryRunner.query(`DROP INDEX "IDX_3e7ec906ac1026a6c9779e82a2"`); + await queryRunner.query(`DROP INDEX "IDX_c60ac1ac95c2d901afd2f68909"`); + await queryRunner.query(`DROP INDEX "IDX_ef0a3bcee9c0305f755d5add13"`); + await queryRunner.query(`DROP INDEX "IDX_695624cb02a5da0e86cd4489c0"`); + await queryRunner.query(`DROP INDEX "IDX_691ba0d5b57cd5adea2c9cc285"`); + await queryRunner.query(`DROP INDEX "IDX_b6e9a5c3e1ee65a3bcb8a00de2"`); + await queryRunner.query(`ALTER TABLE "activity_log" RENAME TO "temporary_activity_log"`); + await queryRunner.query( + `CREATE TABLE "activity_log" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entity" varchar NOT NULL, "entityId" varchar NOT NULL, "action" varchar NOT NULL, "actorType" varchar, "description" text, "updatedFields" text, "previousValues" text, "updatedValues" text, "previousEntities" text, "updatedEntities" text, "data" text, "creatorId" varchar, CONSTRAINT "FK_b6e9a5c3e1ee65a3bcb8a00de2b" FOREIGN KEY ("creatorId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_3e7ec906ac1026a6c9779e82a21" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_d42f36e39404cb6455254deb360" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "activity_log"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entity", "entityId", "action", "actorType", "description", "updatedFields", "previousValues", "updatedValues", "previousEntities", "updatedEntities", "data", "creatorId") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entity", "entityId", "action", "actorType", "description", "updatedFields", "previousValues", "updatedValues", "previousEntities", "updatedEntities", "data", "creatorId" FROM "temporary_activity_log"` + ); + await queryRunner.query(`DROP TABLE "temporary_activity_log"`); + await queryRunner.query(`CREATE INDEX "IDX_4a88f1b97dd306d919f844828d" ON "activity_log" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_eb63f18992743f35225ae4e77c" ON "activity_log" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_d42f36e39404cb6455254deb36" ON "activity_log" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_3e7ec906ac1026a6c9779e82a2" ON "activity_log" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_c60ac1ac95c2d901afd2f68909" ON "activity_log" ("entity") `); + await queryRunner.query(`CREATE INDEX "IDX_ef0a3bcee9c0305f755d5add13" ON "activity_log" ("entityId") `); + await queryRunner.query(`CREATE INDEX "IDX_695624cb02a5da0e86cd4489c0" ON "activity_log" ("action") `); + await queryRunner.query(`CREATE INDEX "IDX_691ba0d5b57cd5adea2c9cc285" ON "activity_log" ("actorType") `); + await queryRunner.query(`CREATE INDEX "IDX_b6e9a5c3e1ee65a3bcb8a00de2" ON "activity_log" ("creatorId") `); + await queryRunner.query(`DROP INDEX "IDX_3620aeff4ac5c977176226017e"`); + await queryRunner.query(`DROP INDEX "IDX_da3cd25ed3a6ce76770f00c3da"`); + await queryRunner.query(`DROP INDEX "IDX_8f58834bed39f0f9e85f048eaf"`); + await queryRunner.query(`DROP INDEX "IDX_a3422826753d4e6b079dea9834"`); + await queryRunner.query(`DROP INDEX "IDX_097e339f6cb990306d19880a4c"`); + await queryRunner.query(`DROP INDEX "IDX_2950cfa146fc50334efa61a70b"`); + await queryRunner.query(`DROP INDEX "IDX_eecd6e41f9acb6bf59e474d518"`); + await queryRunner.query(`DROP INDEX "IDX_b6bf60ecb9f6c398e349adff52"`); + await queryRunner.query(`DROP INDEX "IDX_c9409c81aa283c1aae70fd5f4c"`); + await queryRunner.query(`DROP INDEX "IDX_e3aebe2bd1c53467a07109be59"`); + await queryRunner.query(`ALTER TABLE "comment" RENAME TO "temporary_comment"`); + await queryRunner.query( + `CREATE TABLE "comment" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entity" varchar NOT NULL, "entityId" varchar NOT NULL, "comment" text NOT NULL, "actorType" varchar, "resolved" boolean, "resolvedAt" datetime, "editedAt" datetime, "creatorId" varchar, "resolvedById" varchar, "parentId" varchar, CONSTRAINT "FK_e3aebe2bd1c53467a07109be596" FOREIGN KEY ("parentId") REFERENCES "comment" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_c9409c81aa283c1aae70fd5f4c3" FOREIGN KEY ("resolvedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_b6bf60ecb9f6c398e349adff52f" FOREIGN KEY ("creatorId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_a3422826753d4e6b079dea98342" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_8f58834bed39f0f9e85f048eafe" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "comment"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entity", "entityId", "comment", "actorType", "resolved", "resolvedAt", "editedAt", "creatorId", "resolvedById", "parentId") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entity", "entityId", "comment", "actorType", "resolved", "resolvedAt", "editedAt", "creatorId", "resolvedById", "parentId" FROM "temporary_comment"` + ); + await queryRunner.query(`DROP TABLE "temporary_comment"`); + await queryRunner.query(`CREATE INDEX "IDX_3620aeff4ac5c977176226017e" ON "comment" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_da3cd25ed3a6ce76770f00c3da" ON "comment" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_8f58834bed39f0f9e85f048eaf" ON "comment" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_a3422826753d4e6b079dea9834" ON "comment" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_097e339f6cb990306d19880a4c" ON "comment" ("entity") `); + await queryRunner.query(`CREATE INDEX "IDX_2950cfa146fc50334efa61a70b" ON "comment" ("entityId") `); + await queryRunner.query(`CREATE INDEX "IDX_eecd6e41f9acb6bf59e474d518" ON "comment" ("actorType") `); + await queryRunner.query(`CREATE INDEX "IDX_b6bf60ecb9f6c398e349adff52" ON "comment" ("creatorId") `); + await queryRunner.query(`CREATE INDEX "IDX_c9409c81aa283c1aae70fd5f4c" ON "comment" ("resolvedById") `); + await queryRunner.query(`CREATE INDEX "IDX_e3aebe2bd1c53467a07109be59" ON "comment" ("parentId") `); + await queryRunner.query(`DROP INDEX "IDX_4a88f1b97dd306d919f844828d"`); + await queryRunner.query(`DROP INDEX "IDX_eb63f18992743f35225ae4e77c"`); + await queryRunner.query(`DROP INDEX "IDX_d42f36e39404cb6455254deb36"`); + await queryRunner.query(`DROP INDEX "IDX_3e7ec906ac1026a6c9779e82a2"`); + await queryRunner.query(`DROP INDEX "IDX_c60ac1ac95c2d901afd2f68909"`); + await queryRunner.query(`DROP INDEX "IDX_ef0a3bcee9c0305f755d5add13"`); + await queryRunner.query(`DROP INDEX "IDX_695624cb02a5da0e86cd4489c0"`); + await queryRunner.query(`DROP INDEX "IDX_691ba0d5b57cd5adea2c9cc285"`); + await queryRunner.query(`DROP INDEX "IDX_b6e9a5c3e1ee65a3bcb8a00de2"`); + await queryRunner.query(`ALTER TABLE "activity_log" RENAME TO "temporary_activity_log"`); + await queryRunner.query( + `CREATE TABLE "activity_log" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entity" varchar NOT NULL, "entityId" varchar NOT NULL, "action" varchar NOT NULL, "actorType" varchar, "description" text, "updatedFields" text, "previousValues" text, "updatedValues" text, "previousEntities" text, "updatedEntities" text, "data" text, "creatorId" varchar, CONSTRAINT "FK_b6e9a5c3e1ee65a3bcb8a00de2b" FOREIGN KEY ("creatorId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_3e7ec906ac1026a6c9779e82a21" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_d42f36e39404cb6455254deb360" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "activity_log"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entity", "entityId", "action", "actorType", "description", "updatedFields", "previousValues", "updatedValues", "previousEntities", "updatedEntities", "data", "creatorId") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entity", "entityId", "action", "actorType", "description", "updatedFields", "previousValues", "updatedValues", "previousEntities", "updatedEntities", "data", "creatorId" FROM "temporary_activity_log"` + ); + await queryRunner.query(`DROP TABLE "temporary_activity_log"`); + await queryRunner.query(`CREATE INDEX "IDX_4a88f1b97dd306d919f844828d" ON "activity_log" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_eb63f18992743f35225ae4e77c" ON "activity_log" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_d42f36e39404cb6455254deb36" ON "activity_log" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_3e7ec906ac1026a6c9779e82a2" ON "activity_log" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_c60ac1ac95c2d901afd2f68909" ON "activity_log" ("entity") `); + await queryRunner.query(`CREATE INDEX "IDX_ef0a3bcee9c0305f755d5add13" ON "activity_log" ("entityId") `); + await queryRunner.query(`CREATE INDEX "IDX_695624cb02a5da0e86cd4489c0" ON "activity_log" ("action") `); + await queryRunner.query(`CREATE INDEX "IDX_691ba0d5b57cd5adea2c9cc285" ON "activity_log" ("actorType") `); + await queryRunner.query(`CREATE INDEX "IDX_b6e9a5c3e1ee65a3bcb8a00de2" ON "activity_log" ("creatorId") `); + } + + /** + * MySQL Up Migration + * + * @param queryRunner + */ + public async mysqlUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX \`IDX_691ba0d5b57cd5adea2c9cc285\` ON \`activity_log\``); + await queryRunner.query(`ALTER TABLE \`activity_log\` DROP COLUMN \`actorType\``); + await queryRunner.query(`ALTER TABLE \`activity_log\` ADD \`actorType\` int NULL`); + await queryRunner.query(`DROP INDEX \`IDX_eecd6e41f9acb6bf59e474d518\` ON \`comment\``); + await queryRunner.query(`ALTER TABLE \`comment\` DROP COLUMN \`actorType\``); + await queryRunner.query(`ALTER TABLE \`comment\` ADD \`actorType\` int NULL`); + await queryRunner.query(`CREATE INDEX \`IDX_691ba0d5b57cd5adea2c9cc285\` ON \`activity_log\` (\`actorType\`)`); + await queryRunner.query(`CREATE INDEX \`IDX_eecd6e41f9acb6bf59e474d518\` ON \`comment\` (\`actorType\`)`); + } + + /** + * MySQL Down Migration + * + * @param queryRunner + */ + public async mysqlDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX \`IDX_eecd6e41f9acb6bf59e474d518\` ON \`comment\``); + await queryRunner.query(`DROP INDEX \`IDX_691ba0d5b57cd5adea2c9cc285\` ON \`activity_log\``); + await queryRunner.query(`ALTER TABLE \`comment\` DROP COLUMN \`actorType\``); + await queryRunner.query(`ALTER TABLE \`comment\` ADD \`actorType\` varchar(255) NULL`); + await queryRunner.query(`CREATE INDEX \`IDX_eecd6e41f9acb6bf59e474d518\` ON \`comment\` (\`actorType\`)`); + await queryRunner.query(`ALTER TABLE \`activity_log\` DROP COLUMN \`actorType\``); + await queryRunner.query(`ALTER TABLE \`activity_log\` ADD \`actorType\` varchar(255) NULL`); + await queryRunner.query(`CREATE INDEX \`IDX_691ba0d5b57cd5adea2c9cc285\` ON \`activity_log\` (\`actorType\`)`); + } +} diff --git a/packages/core/src/organization-project/commands/handlers/organization-project-create.handler.ts b/packages/core/src/organization-project/commands/handlers/organization-project-create.handler.ts index b3f2be5fd41..0b1cc68e4ee 100644 --- a/packages/core/src/organization-project/commands/handlers/organization-project-create.handler.ts +++ b/packages/core/src/organization-project/commands/handlers/organization-project-create.handler.ts @@ -22,9 +22,8 @@ export class OrganizationProjectCreateHandler ): Promise { try { const { input } = command; - const project = await this._organizationProjectService.create( - input - ); + + const project = await this._organizationProjectService.create(input); // 1. Create task statuses for relative organization project. this._commandBus.execute( diff --git a/packages/core/src/organization-project/commands/organization-project-create.command.ts b/packages/core/src/organization-project/commands/organization-project-create.command.ts index 905125a5f54..5090d7776dc 100644 --- a/packages/core/src/organization-project/commands/organization-project-create.command.ts +++ b/packages/core/src/organization-project/commands/organization-project-create.command.ts @@ -4,7 +4,5 @@ import { IOrganizationProjectCreateInput } from '@gauzy/contracts'; export class OrganizationProjectCreateCommand implements ICommand { static readonly type = '[Organization Project] Create'; - constructor( - public readonly input: IOrganizationProjectCreateInput - ) { } + constructor(public readonly input: IOrganizationProjectCreateInput) { } } diff --git a/packages/core/src/organization-project/organization-project.service.ts b/packages/core/src/organization-project/organization-project.service.ts index f4ab1cefc92..c9005e4d565 100644 --- a/packages/core/src/organization-project/organization-project.service.ts +++ b/packages/core/src/organization-project/organization-project.service.ts @@ -1,6 +1,10 @@ import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common'; +import { EventBus } from '@nestjs/cqrs'; import { In, IsNull, SelectQueryBuilder } from 'typeorm'; import { + ActionTypeEnum, + ActivityLogEntityEnum, + ActorTypeEnum, FavoriteEntityEnum, ID, IEmployee, @@ -16,10 +20,12 @@ import { } from '@gauzy/contracts'; import { getConfig } from '@gauzy/config'; import { CustomEmbeddedFieldConfig, isNotEmpty } from '@gauzy/common'; -import { Employee, OrganizationProjectEmployee, Role } from '../core/entities/internal'; -import { FavoriteService } from '../core/decorators'; import { PaginationParams, TenantAwareCrudService } from '../core/crud'; import { RequestContext } from '../core/context'; +import { Employee, OrganizationProjectEmployee, Role } from '../core/entities/internal'; +import { FavoriteService } from '../core/decorators'; +import { ActivityLogEvent } from '../activity-log/events'; +import { generateActivityLogDescription } from '../activity-log/activity-log.helper'; import { RoleService } from '../role/role.service'; import { OrganizationProject } from './organization-project.entity'; import { prepareSQLQuery as p } from './../database/database.helper'; @@ -39,8 +45,9 @@ export class OrganizationProjectService extends TenantAwareCrudService member.id); if (isNotEmpty(memberIds) || isNotEmpty(managerIds)) { // Find the manager role - const role = await this.roleService.findOneByWhereOptions({ + const role = await this._roleService.findOneByWhereOptions({ name: RolesEnum.MANAGER });