From 5a521e20a15a2edf7f0de246f47b8b4143355301 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Sat, 14 Sep 2024 12:03:20 +0200 Subject: [PATCH 1/5] feat: initialize activity history model --- .../contracts/src/activity-history.model.ts | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 packages/contracts/src/activity-history.model.ts diff --git a/packages/contracts/src/activity-history.model.ts b/packages/contracts/src/activity-history.model.ts new file mode 100644 index 00000000000..c7e6d1ef0eb --- /dev/null +++ b/packages/contracts/src/activity-history.model.ts @@ -0,0 +1,46 @@ +import { IBasePerTenantAndOrganizationEntityModel, ID } from './base-entity.model'; +import { IUser } from './user.model'; + +export interface IActivityHistory extends IBasePerTenantAndOrganizationEntityModel { + entity: ActivityEntityEnum; // Entity / Table name concerned by activity log + entityId: ID; // The ID of the element we are interacting with (a task, an organization, an employee, ...) + action: ActivityHistoryAction; + description?: string; // A short sentence describing the action performed + updatedProperties?: string[]; // In case of update actions, which entity properties was modified simultaneously. Avoid multiple records. E.g For task : ['name', 'members', 'projectId'] + previousValues?: IActivityHistoryUpdatedValues[]; // E.g For task : {title: ' First Task', members: ['Member1Name', 'Member2Name'], projectId: 'project1UUId'} + updatedValues?: IActivityHistoryUpdatedValues[]; // E.g For task : {title: ' First Task Updated', members: ['Member4Name', 'Member3Name'], projectId: 'project2UUId'} + previousEntities: IActivityHistoryUpdatedValues[]; // Stores previous IDs or other values for related entities. Eg : {members: ['member_1_ID', 'member_2_ID']} + updatedEntities: IActivityHistoryUpdatedValues[]; // Stores updated IDs, or other values for related entities. Eg : {members: ['member_1_ID', 'member_2_ID']}, + creator?: IUser; + creatorId?: ID; + details: ActivityHistoryDeails; +} + +export type ActivityHistoryAction = 'creation' | 'modification' | 'deletion'; // User, Organization,... action + +export interface IActivityHistoryUpdatedValues { + [x: string]: any; +} + +export type ActivityHistoryDeails = IActivityHistoryUpdatedValues; + +export enum ActivityEntityEnum { + Candidate = 'Candidate', + Contact = 'Contact', + Employee = 'Employee', + Expense = 'Expense', + DailyPlan = 'DailyPlan', + Invoice = 'Invoice', + Income = 'Income', + Organization = 'Organization', + OrganizationContact = 'OrganizationContact', + OrganizationDepartment = 'OrganizationDepartment', + OrganizationDocument = 'OrganizationDocument', + OrganizationProject = 'OrganizationProject', + OrganizationTeam = 'OrganizationTeam', + OrganizationProjectModule = 'OrganizationProjectModule', + OrganizationSprint = 'OrganizationSprint', + Task = 'Task', + User = 'User' + // Add other entities as we can to use them for activity history +} From d94f7cbb8f52dfbfa29b0a467a5f3083316626cf Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Sat, 14 Sep 2024 12:06:50 +0200 Subject: [PATCH 2/5] fix(typos): typo errors --- packages/contracts/src/activity-history.model.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/activity-history.model.ts b/packages/contracts/src/activity-history.model.ts index c7e6d1ef0eb..15b846d1c9d 100644 --- a/packages/contracts/src/activity-history.model.ts +++ b/packages/contracts/src/activity-history.model.ts @@ -13,7 +13,7 @@ export interface IActivityHistory extends IBasePerTenantAndOrganizationEntityMod updatedEntities: IActivityHistoryUpdatedValues[]; // Stores updated IDs, or other values for related entities. Eg : {members: ['member_1_ID', 'member_2_ID']}, creator?: IUser; creatorId?: ID; - details: ActivityHistoryDeails; + details: ActivityHistoryDetails; } export type ActivityHistoryAction = 'creation' | 'modification' | 'deletion'; // User, Organization,... action @@ -22,7 +22,7 @@ export interface IActivityHistoryUpdatedValues { [x: string]: any; } -export type ActivityHistoryDeails = IActivityHistoryUpdatedValues; +export type ActivityHistoryDetails = IActivityHistoryUpdatedValues; export enum ActivityEntityEnum { Candidate = 'Candidate', From e9b15095068d0aef4f349ea00ad3d6c6e6f690f6 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Sun, 15 Sep 2024 07:44:17 +0200 Subject: [PATCH 3/5] fix: missing optional properties --- packages/contracts/src/activity-history.model.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/contracts/src/activity-history.model.ts b/packages/contracts/src/activity-history.model.ts index 15b846d1c9d..7b67f30fe65 100644 --- a/packages/contracts/src/activity-history.model.ts +++ b/packages/contracts/src/activity-history.model.ts @@ -9,11 +9,11 @@ export interface IActivityHistory extends IBasePerTenantAndOrganizationEntityMod updatedProperties?: string[]; // In case of update actions, which entity properties was modified simultaneously. Avoid multiple records. E.g For task : ['name', 'members', 'projectId'] previousValues?: IActivityHistoryUpdatedValues[]; // E.g For task : {title: ' First Task', members: ['Member1Name', 'Member2Name'], projectId: 'project1UUId'} updatedValues?: IActivityHistoryUpdatedValues[]; // E.g For task : {title: ' First Task Updated', members: ['Member4Name', 'Member3Name'], projectId: 'project2UUId'} - previousEntities: IActivityHistoryUpdatedValues[]; // Stores previous IDs or other values for related entities. Eg : {members: ['member_1_ID', 'member_2_ID']} - updatedEntities: IActivityHistoryUpdatedValues[]; // Stores updated IDs, or other values for related entities. Eg : {members: ['member_1_ID', 'member_2_ID']}, + previousEntities?: IActivityHistoryUpdatedValues[]; // Stores previous IDs or other values for related entities. Eg : {members: ['member_1_ID', 'member_2_ID']} + updatedEntities?: IActivityHistoryUpdatedValues[]; // Stores updated IDs, or other values for related entities. Eg : {members: ['member_1_ID', 'member_2_ID']}, creator?: IUser; creatorId?: ID; - details: ActivityHistoryDetails; + details?: ActivityHistoryDetails; } export type ActivityHistoryAction = 'creation' | 'modification' | 'deletion'; // User, Organization,... action From 35bad66e6bab066b2c44cc07b1c9c37b4b21b713 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Sun, 15 Sep 2024 18:43:45 +0200 Subject: [PATCH 4/5] feat: initialize activity log entity --- .../contracts/src/activity-history.model.ts | 46 ------- packages/contracts/src/activity-log.model.ts | 57 +++++++++ packages/contracts/src/index.ts | 1 + .../src/activity-log/activity-log.entity.ts | 121 ++++++++++++++++++ .../core/src/activity-log/repository/index.ts | 1 + .../mikro-orm-activity-log.repository.ts | 4 + .../type-orm-activity-log.repository.ts | 11 ++ 7 files changed, 195 insertions(+), 46 deletions(-) delete mode 100644 packages/contracts/src/activity-history.model.ts create mode 100644 packages/contracts/src/activity-log.model.ts create mode 100644 packages/core/src/activity-log/activity-log.entity.ts create mode 100644 packages/core/src/activity-log/repository/index.ts create mode 100644 packages/core/src/activity-log/repository/mikro-orm-activity-log.repository.ts create mode 100644 packages/core/src/activity-log/repository/type-orm-activity-log.repository.ts diff --git a/packages/contracts/src/activity-history.model.ts b/packages/contracts/src/activity-history.model.ts deleted file mode 100644 index 7b67f30fe65..00000000000 --- a/packages/contracts/src/activity-history.model.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { IBasePerTenantAndOrganizationEntityModel, ID } from './base-entity.model'; -import { IUser } from './user.model'; - -export interface IActivityHistory extends IBasePerTenantAndOrganizationEntityModel { - entity: ActivityEntityEnum; // Entity / Table name concerned by activity log - entityId: ID; // The ID of the element we are interacting with (a task, an organization, an employee, ...) - action: ActivityHistoryAction; - description?: string; // A short sentence describing the action performed - updatedProperties?: string[]; // In case of update actions, which entity properties was modified simultaneously. Avoid multiple records. E.g For task : ['name', 'members', 'projectId'] - previousValues?: IActivityHistoryUpdatedValues[]; // E.g For task : {title: ' First Task', members: ['Member1Name', 'Member2Name'], projectId: 'project1UUId'} - updatedValues?: IActivityHistoryUpdatedValues[]; // E.g For task : {title: ' First Task Updated', members: ['Member4Name', 'Member3Name'], projectId: 'project2UUId'} - previousEntities?: IActivityHistoryUpdatedValues[]; // Stores previous IDs or other values for related entities. Eg : {members: ['member_1_ID', 'member_2_ID']} - updatedEntities?: IActivityHistoryUpdatedValues[]; // Stores updated IDs, or other values for related entities. Eg : {members: ['member_1_ID', 'member_2_ID']}, - creator?: IUser; - creatorId?: ID; - details?: ActivityHistoryDetails; -} - -export type ActivityHistoryAction = 'creation' | 'modification' | 'deletion'; // User, Organization,... action - -export interface IActivityHistoryUpdatedValues { - [x: string]: any; -} - -export type ActivityHistoryDetails = IActivityHistoryUpdatedValues; - -export enum ActivityEntityEnum { - Candidate = 'Candidate', - Contact = 'Contact', - Employee = 'Employee', - Expense = 'Expense', - DailyPlan = 'DailyPlan', - Invoice = 'Invoice', - Income = 'Income', - Organization = 'Organization', - OrganizationContact = 'OrganizationContact', - OrganizationDepartment = 'OrganizationDepartment', - OrganizationDocument = 'OrganizationDocument', - OrganizationProject = 'OrganizationProject', - OrganizationTeam = 'OrganizationTeam', - OrganizationProjectModule = 'OrganizationProjectModule', - OrganizationSprint = 'OrganizationSprint', - Task = 'Task', - User = 'User' - // Add other entities as we can to use them for activity history -} diff --git a/packages/contracts/src/activity-log.model.ts b/packages/contracts/src/activity-log.model.ts new file mode 100644 index 00000000000..954c57e0196 --- /dev/null +++ b/packages/contracts/src/activity-log.model.ts @@ -0,0 +1,57 @@ +import { ActorTypeEnum, IBasePerTenantAndOrganizationEntityModel, ID } from './base-entity.model'; +import { IUser } from './user.model'; + +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, ...) + action: ActivityLogActionEnum; + actorType?: ActorTypeEnum; + description?: string; // A short sentence describing the action performed. (E.g John Doe created this on 22.09.2024) + updatedFields?: string[]; // In case of update actions, which entity fields was modified simultaneously. Avoid multiple records. (E.g For task : ['name', 'members', 'projectId']) + previousValues?: IActivityLogUpdatedValues[]; // Values before update (E.g For task : {title: ' First Task', members: ['Member1Name', 'Member2Name'], projectId: 'project1UUId'}) + updatedValues?: IActivityLogUpdatedValues[]; // Values after update (E.g For task : {title: ' First Task Updated', members: ['Member4Name', 'Member3Name'], projectId: 'project2UUId'}) + previousEntities?: IActivityLogUpdatedValues[]; // Stores previous IDs or other values for related entities. Eg : {members: ['member_1_ID', 'member_2_ID']} + 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; +} + +export enum ActivityLogActionEnum { + CREATED = 'Created', + UPDATED = 'Updated', + DELETED = 'Deleted', + APPLIED = 'Applied', + HIRED = 'Hired', + OFFERED = 'Offered', + REJECTED = 'Rejected', + RATING_UPDATED = 'Rating-updated', + INTERVIEW_SCHEDULED = 'Interview-scheduled', + INTERVIEW_COMPLETED = 'Interview-completed', + PAID = 'Paid' +} + +export interface IActivityLogUpdatedValues { + [x: string]: any; +} + +export enum ActivityLogEntityEnum { + Candidate = 'Candidate', + Contact = 'Contact', + Employee = 'Employee', + Expense = 'Expense', + DailyPlan = 'DailyPlan', + Invoice = 'Invoice', + Income = 'Income', + Organization = 'Organization', + OrganizationContact = 'OrganizationContact', + OrganizationDepartment = 'OrganizationDepartment', + OrganizationDocument = 'OrganizationDocument', + OrganizationProject = 'OrganizationProject', + OrganizationTeam = 'OrganizationTeam', + OrganizationProjectModule = 'OrganizationProjectModule', + OrganizationSprint = 'OrganizationSprint', + Task = 'Task', + User = 'User' + // Add other entities as we can to use them for activity history +} diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index b0204aefc5d..6e24c420870 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -1,5 +1,6 @@ export * from './accounting-template.model'; /** App Setting Model */ +export * from './activity-log.model'; export * from './app.model'; export * from './appointment-employees.model'; export * from './approval-policy.model'; diff --git a/packages/core/src/activity-log/activity-log.entity.ts b/packages/core/src/activity-log/activity-log.entity.ts new file mode 100644 index 00000000000..b78664fbc48 --- /dev/null +++ b/packages/core/src/activity-log/activity-log.entity.ts @@ -0,0 +1,121 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { EntityRepositoryType } from '@mikro-orm/core'; +import { IsArray, IsEnum, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator'; +import { isMySQL, isPostgres } from '@gauzy/config'; +import { + ActivityLogActionEnum, + ActivityLogEntityEnum, + ActorTypeEnum, + IActivityLog, + IActivityLogUpdatedValues, + ID, + IUser +} 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 { + [EntityRepositoryType]?: MikroOrmActivityLogRepository; + + @ApiProperty({ type: () => String, enum: ActivityLogEntityEnum }) + @IsNotEmpty() + @IsEnum(ActivityLogEntityEnum) + @ColumnIndex() + @MultiORMColumn() + entity: ActivityLogEntityEnum; + + @ApiProperty({ type: () => String }) + @IsNotEmpty() + @IsUUID() + @ColumnIndex() + @MultiORMColumn() + entityId: string; + + @ApiProperty({ type: () => String, enum: ActivityLogActionEnum }) + @IsNotEmpty() + @IsEnum(ActivityLogActionEnum) + @ColumnIndex() + @MultiORMColumn() + action: ActivityLogActionEnum; + + @ApiPropertyOptional({ type: () => String, enum: ActorTypeEnum }) + @IsOptional() + @IsEnum(ActorTypeEnum) + @ColumnIndex() + @MultiORMColumn({ nullable: true }) + actorType?: ActorTypeEnum; + + @ApiPropertyOptional({ type: () => String }) + @IsOptional() + @IsString() + @MultiORMColumn({ type: 'text', nullable: true }) + description?: string; + + @ApiPropertyOptional({ type: () => Array }) + @IsOptional() + @IsArray() + @MultiORMColumn({ type: isPostgres() ? 'jsonb' : isMySQL() ? 'json' : 'text', nullable: true }) + updatedFields?: string[]; + + @ApiPropertyOptional({ type: () => Array }) + @IsOptional() + @IsArray() + @MultiORMColumn({ type: isPostgres() ? 'jsonb' : isMySQL() ? 'json' : 'text', nullable: true }) + previousValues?: IActivityLogUpdatedValues[]; + + @ApiPropertyOptional({ type: () => Array }) + @IsOptional() + @IsArray() + @MultiORMColumn({ type: isPostgres() ? 'jsonb' : isMySQL() ? 'json' : 'text', nullable: true }) + updatedValues?: IActivityLogUpdatedValues[]; + + @ApiPropertyOptional({ type: () => Array }) + @IsOptional() + @IsArray() + @MultiORMColumn({ type: isPostgres() ? 'jsonb' : isMySQL() ? 'json' : 'text', nullable: true }) + previousEntities?: IActivityLogUpdatedValues[]; + + @ApiPropertyOptional({ type: () => Array }) + @IsOptional() + @IsArray() + @MultiORMColumn({ type: isPostgres() ? 'jsonb' : isMySQL() ? 'json' : 'text', nullable: true }) + updatedEntities?: IActivityLogUpdatedValues[]; + + @ApiPropertyOptional({ type: () => Object }) + @IsOptional() + @IsArray() + @MultiORMColumn({ type: isPostgres() ? 'jsonb' : isMySQL() ? 'json' : 'text', nullable: true }) + data?: Record; + + /* + |-------------------------------------------------------------------------- + | @ManyToOne + |-------------------------------------------------------------------------- + */ + + /** + * User performed action + */ + @ApiPropertyOptional({ type: () => Object }) + @IsOptional() + @MultiORMManyToOne(() => User, { + /** Indicates if relation column value can be nullable or not. */ + nullable: true, + + /** Database cascade action on delete. */ + onDelete: 'CASCADE' + }) + @JoinColumn() + creator?: IUser; + + @ApiPropertyOptional({ type: () => String }) + @IsOptional() + @IsUUID() + @RelationId((it: ActivityLog) => it.creator) + @ColumnIndex() + @MultiORMColumn({ nullable: true, relationId: true }) + creatorId?: ID; +} diff --git a/packages/core/src/activity-log/repository/index.ts b/packages/core/src/activity-log/repository/index.ts new file mode 100644 index 00000000000..9b525616a1b --- /dev/null +++ b/packages/core/src/activity-log/repository/index.ts @@ -0,0 +1 @@ +export * from './mikro-orm-activity-log.repository'; diff --git a/packages/core/src/activity-log/repository/mikro-orm-activity-log.repository.ts b/packages/core/src/activity-log/repository/mikro-orm-activity-log.repository.ts new file mode 100644 index 00000000000..244ff923c80 --- /dev/null +++ b/packages/core/src/activity-log/repository/mikro-orm-activity-log.repository.ts @@ -0,0 +1,4 @@ +import { MikroOrmBaseEntityRepository } from '../../core/repository/mikro-orm-base-entity.repository'; +import { ActivityLog } from '../activity-log.entity'; + +export class MikroOrmActivityLogRepository extends MikroOrmBaseEntityRepository {} diff --git a/packages/core/src/activity-log/repository/type-orm-activity-log.repository.ts b/packages/core/src/activity-log/repository/type-orm-activity-log.repository.ts new file mode 100644 index 00000000000..b0bd36189f2 --- /dev/null +++ b/packages/core/src/activity-log/repository/type-orm-activity-log.repository.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { ActivityLog } from '../activity-log.entity'; + +@Injectable() +export class TypeOrmActivityLogRepository extends Repository { + constructor(@InjectRepository(ActivityLog) readonly repository: Repository) { + super(repository.target, repository.manager, repository.queryRunner); + } +} From e1bfa0de9b44838f87c7411978a9df34289fc752 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Wed, 18 Sep 2024 11:32:27 +0200 Subject: [PATCH 5/5] feat: activity logs migrations --- packages/contracts/src/activity-log.model.ts | 14 +- .../src/activity-log/activity-log.entity.ts | 8 +- packages/core/src/core/entities/index.ts | 2 + packages/core/src/core/entities/internal.ts | 1 + .../1726648418130-CreateActivityLogTable.ts | 234 ++++++++++++++++++ 5 files changed, 244 insertions(+), 15 deletions(-) create mode 100644 packages/core/src/database/migrations/1726648418130-CreateActivityLogTable.ts diff --git a/packages/contracts/src/activity-log.model.ts b/packages/contracts/src/activity-log.model.ts index 954c57e0196..80283fd2d2e 100644 --- a/packages/contracts/src/activity-log.model.ts +++ b/packages/contracts/src/activity-log.model.ts @@ -4,7 +4,7 @@ import { IUser } from './user.model'; 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, ...) - action: ActivityLogActionEnum; + action: ActionTypeEnum; actorType?: ActorTypeEnum; description?: string; // A short sentence describing the action performed. (E.g John Doe created this on 22.09.2024) updatedFields?: string[]; // In case of update actions, which entity fields was modified simultaneously. Avoid multiple records. (E.g For task : ['name', 'members', 'projectId']) @@ -17,18 +17,10 @@ export interface IActivityLog extends IBasePerTenantAndOrganizationEntityModel { data?: Record; } -export enum ActivityLogActionEnum { +export enum ActionTypeEnum { CREATED = 'Created', UPDATED = 'Updated', - DELETED = 'Deleted', - APPLIED = 'Applied', - HIRED = 'Hired', - OFFERED = 'Offered', - REJECTED = 'Rejected', - RATING_UPDATED = 'Rating-updated', - INTERVIEW_SCHEDULED = 'Interview-scheduled', - INTERVIEW_COMPLETED = 'Interview-completed', - PAID = 'Paid' + DELETED = 'Deleted' } export interface IActivityLogUpdatedValues { diff --git a/packages/core/src/activity-log/activity-log.entity.ts b/packages/core/src/activity-log/activity-log.entity.ts index b78664fbc48..def5a624831 100644 --- a/packages/core/src/activity-log/activity-log.entity.ts +++ b/packages/core/src/activity-log/activity-log.entity.ts @@ -3,8 +3,8 @@ import { EntityRepositoryType } from '@mikro-orm/core'; import { IsArray, IsEnum, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator'; import { isMySQL, isPostgres } from '@gauzy/config'; import { - ActivityLogActionEnum, ActivityLogEntityEnum, + ActionTypeEnum, ActorTypeEnum, IActivityLog, IActivityLogUpdatedValues, @@ -34,12 +34,12 @@ export class ActivityLog extends TenantOrganizationBaseEntity implements IActivi @MultiORMColumn() entityId: string; - @ApiProperty({ type: () => String, enum: ActivityLogActionEnum }) + @ApiProperty({ type: () => String, enum: ActionTypeEnum }) @IsNotEmpty() - @IsEnum(ActivityLogActionEnum) + @IsEnum(ActionTypeEnum) @ColumnIndex() @MultiORMColumn() - action: ActivityLogActionEnum; + action: ActionTypeEnum; @ApiPropertyOptional({ type: () => String, enum: ActorTypeEnum }) @IsOptional() diff --git a/packages/core/src/core/entities/index.ts b/packages/core/src/core/entities/index.ts index 228c08821da..97e432c0c4a 100644 --- a/packages/core/src/core/entities/index.ts +++ b/packages/core/src/core/entities/index.ts @@ -1,6 +1,7 @@ import { AccountingTemplate, Activity, + ActivityLog, AppointmentEmployee, ApprovalPolicy, AvailabilitySlot, @@ -143,6 +144,7 @@ import { export const coreEntities = [ AccountingTemplate, Activity, + ActivityLog, AppointmentEmployee, ApprovalPolicy, AvailabilitySlot, diff --git a/packages/core/src/core/entities/internal.ts b/packages/core/src/core/entities/internal.ts index 7b731965f73..ae9a28af94c 100644 --- a/packages/core/src/core/entities/internal.ts +++ b/packages/core/src/core/entities/internal.ts @@ -6,6 +6,7 @@ export * from './translate-base'; //core entities export * from '../../accounting-template/accounting-template.entity'; +export * from '../../activity-log/activity-log.entity'; export * from '../../appointment-employees/appointment-employees.entity'; export * from '../../approval-policy/approval-policy.entity'; export * from '../../availability-slots/availability-slots.entity'; diff --git a/packages/core/src/database/migrations/1726648418130-CreateActivityLogTable.ts b/packages/core/src/database/migrations/1726648418130-CreateActivityLogTable.ts new file mode 100644 index 00000000000..a135a01e437 --- /dev/null +++ b/packages/core/src/database/migrations/1726648418130-CreateActivityLogTable.ts @@ -0,0 +1,234 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { yellow } from 'chalk'; +import { DatabaseTypeEnum } from '@gauzy/config'; + +export class CreateActivityLogTable1726648418130 implements MigrationInterface { + name = 'CreateActivityLogTable1726648418130'; + + /** + * 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( + `CREATE TABLE "activity_log" ("deletedAt" TIMESTAMP, "id" uuid NOT NULL DEFAULT gen_random_uuid(), "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "isActive" boolean DEFAULT true, "isArchived" boolean DEFAULT false, "archivedAt" TIMESTAMP, "tenantId" uuid, "organizationId" uuid, "entity" character varying NOT NULL, "entityId" character varying NOT NULL, "action" character varying NOT NULL, "actorType" character varying, "description" text, "updatedFields" jsonb, "previousValues" jsonb, "updatedValues" jsonb, "previousEntities" jsonb, "updatedEntities" jsonb, "data" jsonb, "creatorId" uuid, CONSTRAINT "PK_067d761e2956b77b14e534fd6f1" PRIMARY KEY ("id"))` + ); + 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( + `ALTER TABLE "activity_log" ADD CONSTRAINT "FK_d42f36e39404cb6455254deb360" FOREIGN KEY ("tenantId") REFERENCES "tenant"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "activity_log" ADD CONSTRAINT "FK_3e7ec906ac1026a6c9779e82a21" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE CASCADE` + ); + await queryRunner.query( + `ALTER TABLE "activity_log" ADD CONSTRAINT "FK_b6e9a5c3e1ee65a3bcb8a00de2b" FOREIGN KEY ("creatorId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + } + + /** + * PostgresDB Down Migration + * + * @param queryRunner + */ + public async postgresDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "activity_log" DROP CONSTRAINT "FK_b6e9a5c3e1ee65a3bcb8a00de2b"`); + await queryRunner.query(`ALTER TABLE "activity_log" DROP CONSTRAINT "FK_3e7ec906ac1026a6c9779e82a21"`); + await queryRunner.query(`ALTER TABLE "activity_log" DROP CONSTRAINT "FK_d42f36e39404cb6455254deb360"`); + await queryRunner.query(`DROP INDEX "public"."IDX_b6e9a5c3e1ee65a3bcb8a00de2"`); + await queryRunner.query(`DROP INDEX "public"."IDX_691ba0d5b57cd5adea2c9cc285"`); + await queryRunner.query(`DROP INDEX "public"."IDX_695624cb02a5da0e86cd4489c0"`); + await queryRunner.query(`DROP INDEX "public"."IDX_ef0a3bcee9c0305f755d5add13"`); + await queryRunner.query(`DROP INDEX "public"."IDX_c60ac1ac95c2d901afd2f68909"`); + await queryRunner.query(`DROP INDEX "public"."IDX_3e7ec906ac1026a6c9779e82a2"`); + await queryRunner.query(`DROP INDEX "public"."IDX_d42f36e39404cb6455254deb36"`); + await queryRunner.query(`DROP INDEX "public"."IDX_eb63f18992743f35225ae4e77c"`); + await queryRunner.query(`DROP INDEX "public"."IDX_4a88f1b97dd306d919f844828d"`); + await queryRunner.query(`DROP TABLE "activity_log"`); + } + + /** + * SqliteDB and BetterSQlite3DB Up Migration + * + * @param queryRunner + */ + public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise { + 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)` + ); + 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_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( + `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_d42f36e39404cb6455254deb360" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("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_b6e9a5c3e1ee65a3bcb8a00de2b" FOREIGN KEY ("creatorId") REFERENCES "user" ("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_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") `); + } + + /** + * SqliteDB and BetterSQlite3DB Down Migration + * + * @param queryRunner + */ + public async sqliteDownQueryRunner(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(`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)` + ); + 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_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_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(`DROP TABLE "activity_log"`); + } + + /** + * MySQL Up Migration + * + * @param queryRunner + */ + public async mysqlUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE \`activity_log\` (\`deletedAt\` datetime(6) NULL, \`id\` varchar(36) NOT NULL, \`createdAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updatedAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), \`isActive\` tinyint NULL DEFAULT 1, \`isArchived\` tinyint NULL DEFAULT 0, \`archivedAt\` datetime NULL, \`tenantId\` varchar(255) NULL, \`organizationId\` varchar(255) NULL, \`entity\` varchar(255) NOT NULL, \`entityId\` varchar(255) NOT NULL, \`action\` varchar(255) NOT NULL, \`actorType\` varchar(255) NULL, \`description\` text NULL, \`updatedFields\` json NULL, \`previousValues\` json NULL, \`updatedValues\` json NULL, \`previousEntities\` json NULL, \`updatedEntities\` json NULL, \`data\` json NULL, \`creatorId\` varchar(255) NULL, INDEX \`IDX_4a88f1b97dd306d919f844828d\` (\`isActive\`), INDEX \`IDX_eb63f18992743f35225ae4e77c\` (\`isArchived\`), INDEX \`IDX_d42f36e39404cb6455254deb36\` (\`tenantId\`), INDEX \`IDX_3e7ec906ac1026a6c9779e82a2\` (\`organizationId\`), INDEX \`IDX_c60ac1ac95c2d901afd2f68909\` (\`entity\`), INDEX \`IDX_ef0a3bcee9c0305f755d5add13\` (\`entityId\`), INDEX \`IDX_695624cb02a5da0e86cd4489c0\` (\`action\`), INDEX \`IDX_691ba0d5b57cd5adea2c9cc285\` (\`actorType\`), INDEX \`IDX_b6e9a5c3e1ee65a3bcb8a00de2\` (\`creatorId\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB` + ); + await queryRunner.query( + `ALTER TABLE \`activity_log\` ADD CONSTRAINT \`FK_d42f36e39404cb6455254deb360\` FOREIGN KEY (\`tenantId\`) REFERENCES \`tenant\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE \`activity_log\` ADD CONSTRAINT \`FK_3e7ec906ac1026a6c9779e82a21\` FOREIGN KEY (\`organizationId\`) REFERENCES \`organization\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE` + ); + await queryRunner.query( + `ALTER TABLE \`activity_log\` ADD CONSTRAINT \`FK_b6e9a5c3e1ee65a3bcb8a00de2b\` FOREIGN KEY (\`creatorId\`) REFERENCES \`user\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` + ); + } + + /** + * MySQL Down Migration + * + * @param queryRunner + */ + public async mysqlDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`activity_log\` DROP FOREIGN KEY \`FK_b6e9a5c3e1ee65a3bcb8a00de2b\``); + await queryRunner.query(`ALTER TABLE \`activity_log\` DROP FOREIGN KEY \`FK_3e7ec906ac1026a6c9779e82a21\``); + await queryRunner.query(`ALTER TABLE \`activity_log\` DROP FOREIGN KEY \`FK_d42f36e39404cb6455254deb360\``); + await queryRunner.query(`DROP INDEX \`IDX_b6e9a5c3e1ee65a3bcb8a00de2\` ON \`activity_log\``); + await queryRunner.query(`DROP INDEX \`IDX_691ba0d5b57cd5adea2c9cc285\` ON \`activity_log\``); + await queryRunner.query(`DROP INDEX \`IDX_695624cb02a5da0e86cd4489c0\` ON \`activity_log\``); + await queryRunner.query(`DROP INDEX \`IDX_ef0a3bcee9c0305f755d5add13\` ON \`activity_log\``); + await queryRunner.query(`DROP INDEX \`IDX_c60ac1ac95c2d901afd2f68909\` ON \`activity_log\``); + await queryRunner.query(`DROP INDEX \`IDX_3e7ec906ac1026a6c9779e82a2\` ON \`activity_log\``); + await queryRunner.query(`DROP INDEX \`IDX_d42f36e39404cb6455254deb36\` ON \`activity_log\``); + await queryRunner.query(`DROP INDEX \`IDX_eb63f18992743f35225ae4e77c\` ON \`activity_log\``); + await queryRunner.query(`DROP INDEX \`IDX_4a88f1b97dd306d919f844828d\` ON \`activity_log\``); + await queryRunner.query(`DROP TABLE \`activity_log\``); + } +}