Skip to content

Commit 39d9ab1

Browse files
authored
Merge pull request #8230 from ever-co/feat/activity-log-api
[Feat] Activity Log Events / APIs
2 parents 6034264 + ae85f5a commit 39d9ab1

23 files changed

+791
-41
lines changed

packages/contracts/src/activity-log.model.ts

+24-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { ActorTypeEnum, IBasePerTenantAndOrganizationEntityModel, ID } from './base-entity.model';
22
import { IUser } from './user.model';
33

4+
// Define a type for JSON data
5+
export type JsonData = Record<string, any> | string;
6+
7+
/**
8+
* Interface representing an activity log entry.
9+
*/
410
export interface IActivityLog extends IBasePerTenantAndOrganizationEntityModel {
511
entity: ActivityLogEntityEnum; // Entity / Table name concerned by activity log
612
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 {
1420
updatedEntities?: IActivityLogUpdatedValues[]; // Stores updated IDs, or other values for related entities. Eg : {members: ['member_1_ID', 'member_2_ID']},
1521
creator?: IUser;
1622
creatorId?: ID;
17-
data?: Record<string, any>;
23+
data?: JsonData;
1824
}
1925

20-
export enum ActionTypeEnum {
21-
CREATED = 'Created',
22-
UPDATED = 'Updated',
23-
DELETED = 'Deleted'
26+
export interface IActivityLogUpdatedValues {
27+
[x: string]: Record<string, any>;
2428
}
2529

26-
export interface IActivityLogUpdatedValues {
27-
[x: string]: any;
30+
/**
31+
* Enum for action types in the activity log.
32+
*/
33+
export enum ActionTypeEnum {
34+
Created = 'Created',
35+
Updated = 'Updated',
36+
Deleted = 'Deleted'
2837
}
2938

39+
/**
40+
* Enum for entities that can be involved in activity logs.
41+
*/
3042
export enum ActivityLogEntityEnum {
3143
Candidate = 'Candidate',
3244
Contact = 'Contact',
@@ -45,5 +57,9 @@ export enum ActivityLogEntityEnum {
4557
OrganizationSprint = 'OrganizationSprint',
4658
Task = 'Task',
4759
User = 'User'
48-
// Add other entities as we can to use them for activity history
4960
}
61+
62+
/**
63+
* Input type for activity log creation, excluding `creatorId` and `creator`.
64+
*/
65+
export interface IActivityLogInput extends Omit<IActivityLog, 'creatorId' | 'creator'> {}

packages/contracts/src/base-entity.model.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,6 @@ export interface IBasePerTenantAndOrganizationEntityMutationInput extends Partia
5858

5959
// Actor type defines if it's User or system performed some action
6060
export enum ActorTypeEnum {
61-
SYSTEM = 'SYSTEM',
62-
USER = 'USER'
61+
System = 0, // System performed the action
62+
User = 1 // User performed the action
6363
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
2+
import { IActivityLog, IPagination } from '@gauzy/contracts';
3+
import { Permissions } from '../shared/decorators';
4+
import { PermissionGuard, TenantPermissionGuard } from '../shared/guards';
5+
import { UseValidationPipe } from '../shared/pipes';
6+
import { GetActivityLogsDTO } from './dto/get-activity-logs.dto';
7+
import { ActivityLogService } from './activity-log.service';
8+
9+
@UseGuards(TenantPermissionGuard, PermissionGuard)
10+
@Permissions()
11+
@Controller('/activity-log')
12+
export class ActivityLogController {
13+
constructor(readonly _activityLogService: ActivityLogService) {}
14+
15+
/**
16+
* Retrieves activity logs based on query parameters.
17+
* Supports filtering, pagination, sorting, and ordering.
18+
*
19+
* @param query Query parameters for filtering, pagination, and ordering.
20+
* @returns A list of activity logs.
21+
*/
22+
@Get('/')
23+
@UseValidationPipe()
24+
async getActivityLogs(
25+
@Query() query: GetActivityLogsDTO
26+
): Promise<IPagination<IActivityLog>> {
27+
return await this._activityLogService.findActivityLogs(query);
28+
}
29+
}

packages/core/src/activity-log/activity-log.entity.ts

+9-14
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
22
import { EntityRepositoryType } from '@mikro-orm/core';
3+
import { JoinColumn, RelationId } from 'typeorm';
34
import { IsArray, IsEnum, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
45
import { isMySQL, isPostgres } from '@gauzy/config';
56
import {
67
ActivityLogEntityEnum,
78
ActionTypeEnum,
89
ActorTypeEnum,
910
IActivityLog,
10-
IActivityLogUpdatedValues,
1111
ID,
12-
IUser
12+
IUser,
13+
JsonData
1314
} from '@gauzy/contracts';
1415
import { TenantOrganizationBaseEntity, User } from '../core/entities/internal';
1516
import { ColumnIndex, MultiORMColumn, MultiORMEntity, MultiORMManyToOne } from '../core/decorators/entity';
1617
import { MikroOrmActivityLogRepository } from './repository/mikro-orm-activity-log.repository';
17-
import { JoinColumn, RelationId } from 'typeorm';
1818

1919
@MultiORMEntity('activity_log', { mikroOrmRepository: () => MikroOrmActivityLogRepository })
2020
export class ActivityLog extends TenantOrganizationBaseEntity implements IActivityLog {
@@ -32,7 +32,7 @@ export class ActivityLog extends TenantOrganizationBaseEntity implements IActivi
3232
@IsUUID()
3333
@ColumnIndex()
3434
@MultiORMColumn()
35-
entityId: string;
35+
entityId: ID;
3636

3737
@ApiProperty({ type: () => String, enum: ActionTypeEnum })
3838
@IsNotEmpty()
@@ -64,31 +64,31 @@ export class ActivityLog extends TenantOrganizationBaseEntity implements IActivi
6464
@IsOptional()
6565
@IsArray()
6666
@MultiORMColumn({ type: isPostgres() ? 'jsonb' : isMySQL() ? 'json' : 'text', nullable: true })
67-
previousValues?: IActivityLogUpdatedValues[];
67+
previousValues?: Record<string, any>[];
6868

6969
@ApiPropertyOptional({ type: () => Array })
7070
@IsOptional()
7171
@IsArray()
7272
@MultiORMColumn({ type: isPostgres() ? 'jsonb' : isMySQL() ? 'json' : 'text', nullable: true })
73-
updatedValues?: IActivityLogUpdatedValues[];
73+
updatedValues?: Record<string, any>[];
7474

7575
@ApiPropertyOptional({ type: () => Array })
7676
@IsOptional()
7777
@IsArray()
7878
@MultiORMColumn({ type: isPostgres() ? 'jsonb' : isMySQL() ? 'json' : 'text', nullable: true })
79-
previousEntities?: IActivityLogUpdatedValues[];
79+
previousEntities?: Record<string, any>[];
8080

8181
@ApiPropertyOptional({ type: () => Array })
8282
@IsOptional()
8383
@IsArray()
8484
@MultiORMColumn({ type: isPostgres() ? 'jsonb' : isMySQL() ? 'json' : 'text', nullable: true })
85-
updatedEntities?: IActivityLogUpdatedValues[];
85+
updatedEntities?: Record<string, any>[];
8686

8787
@ApiPropertyOptional({ type: () => Object })
8888
@IsOptional()
8989
@IsArray()
9090
@MultiORMColumn({ type: isPostgres() ? 'jsonb' : isMySQL() ? 'json' : 'text', nullable: true })
91-
data?: Record<string, any>;
91+
data?: JsonData;
9292

9393
/*
9494
|--------------------------------------------------------------------------
@@ -99,8 +99,6 @@ export class ActivityLog extends TenantOrganizationBaseEntity implements IActivi
9999
/**
100100
* User performed action
101101
*/
102-
@ApiPropertyOptional({ type: () => Object })
103-
@IsOptional()
104102
@MultiORMManyToOne(() => User, {
105103
/** Indicates if relation column value can be nullable or not. */
106104
nullable: true,
@@ -111,9 +109,6 @@ export class ActivityLog extends TenantOrganizationBaseEntity implements IActivi
111109
@JoinColumn()
112110
creator?: IUser;
113111

114-
@ApiPropertyOptional({ type: () => String })
115-
@IsOptional()
116-
@IsUUID()
117112
@RelationId((it: ActivityLog) => it.creator)
118113
@ColumnIndex()
119114
@MultiORMColumn({ nullable: true, relationId: true })
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { ActionTypeEnum, ActivityLogEntityEnum } from "@gauzy/contracts";
2+
3+
const ActivityTemplates = {
4+
[ActionTypeEnum.Created]: `{action} a new {entity} called "{entityName}"`,
5+
[ActionTypeEnum.Updated]: `{action} {entity} "{entityName}"`,
6+
[ActionTypeEnum.Deleted]: `{action} {entity} "{entityName}"`,
7+
};
8+
9+
/**
10+
* Generates an activity description based on the action type, entity, and entity name.
11+
* @param action - The action performed (e.g., CREATED, UPDATED, DELETED).
12+
* @param entity - The type of entity involved in the action (e.g., Project, User).
13+
* @param entityName - The name of the specific entity instance.
14+
* @returns A formatted description string.
15+
*/
16+
export function generateActivityLogDescription(
17+
action: ActionTypeEnum,
18+
entity: ActivityLogEntityEnum,
19+
entityName: string
20+
): string {
21+
// Get the template corresponding to the action
22+
const template = ActivityTemplates[action] || '{action} {entity} "{entityName}"';
23+
24+
// Replace placeholders in the template with actual values
25+
return template.replace(/\{(\w+)\}/g, (_, key) => {
26+
switch (key) {
27+
case 'action':
28+
return action;
29+
case 'entity':
30+
return entity;
31+
case 'entityName':
32+
return entityName;
33+
default:
34+
return '';
35+
}
36+
});
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { CqrsModule } from '@nestjs/cqrs';
2+
import { Module } from '@nestjs/common';
3+
import { MikroOrmModule } from '@mikro-orm/nestjs';
4+
import { TypeOrmModule } from '@nestjs/typeorm';
5+
import { RolePermissionModule } from '../role-permission/role-permission.module';
6+
import { ActivityLogController } from './activity-log.controller';
7+
import { ActivityLog } from './activity-log.entity';
8+
import { ActivityLogService } from './activity-log.service';
9+
import { EventHandlers } from './events/handlers';
10+
import { TypeOrmActivityLogRepository } from './repository/type-orm-activity-log.repository';
11+
12+
@Module({
13+
imports: [
14+
TypeOrmModule.forFeature([ActivityLog]),
15+
MikroOrmModule.forFeature([ActivityLog]),
16+
CqrsModule,
17+
RolePermissionModule
18+
],
19+
controllers: [ActivityLogController],
20+
providers: [ActivityLogService, TypeOrmActivityLogRepository, ...EventHandlers],
21+
exports: [TypeOrmModule, MikroOrmModule, ActivityLogService, TypeOrmActivityLogRepository]
22+
})
23+
export class ActivityLogModule {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { BadRequestException, Injectable } from '@nestjs/common';
2+
import { FindManyOptions, FindOptionsOrder, FindOptionsWhere } from 'typeorm';
3+
import { IActivityLog, IActivityLogInput, IPagination } from '@gauzy/contracts';
4+
import { TenantAwareCrudService } from './../core/crud';
5+
import { RequestContext } from '../core/context';
6+
import { GetActivityLogsDTO, allowedOrderDirections, allowedOrderFields } from './dto/get-activity-logs.dto';
7+
import { ActivityLog } from './activity-log.entity';
8+
import { MikroOrmActivityLogRepository, TypeOrmActivityLogRepository } from './repository';
9+
10+
@Injectable()
11+
export class ActivityLogService extends TenantAwareCrudService<ActivityLog> {
12+
constructor(
13+
readonly typeOrmActivityLogRepository: TypeOrmActivityLogRepository,
14+
readonly mikroOrmActivityLogRepository: MikroOrmActivityLogRepository
15+
) {
16+
super(typeOrmActivityLogRepository, mikroOrmActivityLogRepository);
17+
}
18+
19+
/**
20+
* Finds and retrieves activity logs based on the given filter criteria.
21+
*
22+
* @param {GetActivityLogsDTO} filter - Filter criteria to find activity logs, including entity, entityId, action, actorType, isActive, isArchived, orderBy, and order.
23+
* @returns {Promise<IPagination<IActivityLog>>} - A promise that resolves to a paginated list of activity logs.
24+
*
25+
* Example usage:
26+
* ```
27+
* const logs = await findActivityLogs({
28+
* entity: 'User',
29+
* action: 'CREATE',
30+
* orderBy: 'updatedAt',
31+
* order: 'ASC'
32+
* });
33+
* ```
34+
*/
35+
public async findActivityLogs(filter: GetActivityLogsDTO): Promise<IPagination<IActivityLog>> {
36+
const {
37+
entity,
38+
entityId,
39+
action,
40+
actorType,
41+
isActive = true,
42+
isArchived = false,
43+
orderBy = 'createdAt',
44+
order = 'DESC',
45+
relations = [],
46+
skip,
47+
take
48+
} = filter;
49+
50+
// Build the 'where' condition using concise syntax
51+
const where: FindOptionsWhere<ActivityLog> = {
52+
...(entity && { entity }),
53+
...(entityId && { entityId }),
54+
...(action && { action }),
55+
...(actorType && { actorType }),
56+
isActive,
57+
isArchived
58+
};
59+
60+
// Fallback to default if invalid orderBy/order values are provided
61+
const orderField = allowedOrderFields.includes(orderBy) ? orderBy : 'createdAt';
62+
const orderDirection = allowedOrderDirections.includes(order.toUpperCase()) ? order.toUpperCase() : 'DESC';
63+
64+
// Define order option
65+
const orderOption: FindOptionsOrder<ActivityLog> = { [orderField]: orderDirection };
66+
67+
// Define find options
68+
const findOptions: FindManyOptions<ActivityLog> = {
69+
where,
70+
order: orderOption,
71+
...(skip && { skip }),
72+
...(take && { take }),
73+
...(relations && { relations })
74+
};
75+
76+
// Retrieve activity logs using the base class method
77+
return await super.findAll(findOptions);
78+
}
79+
80+
/**
81+
* Creates a new activity log entry with the provided input, while associating it with the current user and tenant.
82+
*
83+
* @param input - The data required to create an activity log entry.
84+
* @returns The created activity log entry.
85+
* @throws BadRequestException when the log creation fails.
86+
*/
87+
async logActivity(input: IActivityLogInput): Promise<IActivityLog> {
88+
try {
89+
const creatorId = RequestContext.currentUserId(); // Retrieve the current user's ID from the request context
90+
// Create the activity log entry using the provided input along with the tenantId and creatorId
91+
return await super.create({ ...input, creatorId });
92+
} catch (error) {
93+
console.log('Error while creating activity log:', error);
94+
throw new BadRequestException('Error while creating activity log', error);
95+
}
96+
}
97+
}

0 commit comments

Comments
 (0)