Logs show page (#4611)

* Being implementing events on the frontend

* Rename JSON to RAW JSON

* Fix handling of json field on frontend

* Log user id

* Add frontend tests

* Update packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/save-event-to-db.job.ts

Co-authored-by: Weiko <corentin@twenty.com>

* Move db calls to a dedicated repository

* Add server-side tests

---------

Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Félix Malfait
2024-03-22 14:01:16 +01:00
committed by GitHub
parent aee6d49ea9
commit d876b40056
38 changed files with 488 additions and 95 deletions

View File

@ -2,12 +2,16 @@ import { Injectable } from '@nestjs/common';
import { MessageQueueJob } from 'src/engine/integrations/message-queue/interfaces/message-queue-job.interface';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { EventRepository } from 'src/modules/event/repositiories/event.repository';
import { EventObjectMetadata } from 'src/modules/event/standard-objects/event.object-metadata';
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
import { WorkspaceMemberObjectMetadata } from 'src/modules/workspace-member/standard-objects/workspace-member.object-metadata';
export type SaveEventToDbJobData = {
workspaceId: string;
recordId: string;
userId: string | undefined;
objectName: string;
operation: string;
details: any;
@ -16,39 +20,47 @@ export type SaveEventToDbJobData = {
@Injectable()
export class SaveEventToDbJob implements MessageQueueJob<SaveEventToDbJobData> {
constructor(
private readonly dataSourceService: DataSourceService,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
@InjectObjectMetadataRepository(WorkspaceMemberObjectMetadata)
private readonly workspaceMemberService: WorkspaceMemberRepository,
@InjectObjectMetadataRepository(EventObjectMetadata)
private readonly eventService: EventRepository,
) {}
// TODO: need to support objects others than "person", "company", "opportunity"
async handle(data: SaveEventToDbJobData): Promise<void> {
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
data.workspaceId,
);
const workspaceDataSource =
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
let workspaceMemberId: string | null = null;
if (data.userId) {
const workspaceMember = await this.workspaceMemberService.getByIdOrFail(
data.userId,
data.workspaceId,
);
const eventType = `${data.operation}.${data.objectName}`;
// TODO: add "workspaceMember" (who performed the action, need to send it in the event)
// TODO: need to support objects others than "person", "company", "opportunities"
workspaceMemberId = workspaceMember.id;
}
if (
data.objectName != 'person' &&
data.objectName != 'company' &&
data.objectName != 'opportunities'
data.objectName != 'opportunity'
) {
return;
}
await workspaceDataSource?.query(
`INSERT INTO ${dataSourceMetadata.schema}."event"
("name", "properties", "${data.objectName}Id")
VALUES ('${eventType}', '${JSON.stringify(data.details)}', '${
data.recordId
}') RETURNING *`,
if (data.details.diff) {
// we remove "before" and "after" property for a cleaner/slimmer event payload
data.details = {
diff: data.details.diff,
};
}
await this.eventService.insert(
`${data.operation}.${data.objectName}`,
data.details,
workspaceMemberId,
data.objectName,
data.recordId,
data.workspaceId,
);
}
}

View File

@ -15,6 +15,8 @@ import {
FeatureFlagEntity,
FeatureFlagKeys,
} from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { objectRecordChangedValues } from 'src/engine/integrations/event-emitter/utils/object-record-changed-values';
import { ObjectRecordUpdateEvent } from 'src/engine/integrations/event-emitter/types/object-record-update.event';
@Injectable()
export class EntityEventsToDbListener {
@ -31,7 +33,12 @@ export class EntityEventsToDbListener {
}
@OnEvent('*.updated')
async handleUpdate(payload: ObjectRecordCreateEvent<any>) {
async handleUpdate(payload: ObjectRecordUpdateEvent<any>) {
payload.details.diff = objectRecordChangedValues(
payload.details.before,
payload.details.after,
);
return this.handle(payload, 'updated');
}
@ -58,6 +65,7 @@ export class EntityEventsToDbListener {
this.messageQueueService.add<SaveEventToDbJobData>(SaveEventToDbJob.name, {
workspaceId: payload.workspaceId,
userId: payload.userId,
recordId: payload.recordId,
objectName: payload.objectMetadata.nameSingular,
operation: operation,

View File

@ -9,6 +9,9 @@ import { RecordPositionListener } from 'src/engine/api/graphql/workspace-query-r
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { EventObjectMetadata } from 'src/modules/event/standard-objects/event.object-metadata';
import { WorkspaceMemberObjectMetadata } from 'src/modules/workspace-member/standard-objects/workspace-member.object-metadata';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { WorkspaceQueryRunnerService } from './workspace-query-runner.service';
@ -21,6 +24,10 @@ import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listen
WorkspaceDataSourceModule,
WorkspacePreQueryHookModule,
TypeOrmModule.forFeature([Workspace, FeatureFlagEntity], 'core'),
ObjectMetadataRepositoryModule.forFeature([
WorkspaceMemberObjectMetadata,
EventObjectMetadata,
]),
],
providers: [
WorkspaceQueryRunnerService,

View File

@ -216,7 +216,7 @@ export class WorkspaceQueryRunnerService {
args: CreateManyResolverArgs<Record>,
options: WorkspaceQueryRunnerOptions,
): Promise<Record[] | undefined> {
const { workspaceId, objectMetadataItem } = options;
const { workspaceId, userId, objectMetadataItem } = options;
const computedArgs = await this.queryRunnerArgsFactory.create(
args,
options,
@ -246,6 +246,7 @@ export class WorkspaceQueryRunnerService {
parsedResults.forEach((record) => {
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.created`, {
workspaceId,
userId,
recordId: record.id,
objectMetadata: objectMetadataItem,
details: {
@ -270,7 +271,7 @@ export class WorkspaceQueryRunnerService {
args: UpdateOneResolverArgs<Record>,
options: WorkspaceQueryRunnerOptions,
): Promise<Record | undefined> {
const { workspaceId, objectMetadataItem } = options;
const { workspaceId, userId, objectMetadataItem } = options;
const existingRecord = await this.findOne(
{ filter: { id: { eq: args.id } } } as FindOneResolverArgs,
@ -300,6 +301,7 @@ export class WorkspaceQueryRunnerService {
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.updated`, {
workspaceId,
userId,
recordId: (existingRecord as Record).id,
objectMetadata: objectMetadataItem,
details: {
@ -356,7 +358,7 @@ export class WorkspaceQueryRunnerService {
args: DeleteManyResolverArgs<Filter>,
options: WorkspaceQueryRunnerOptions,
): Promise<Record[] | undefined> {
const { workspaceId, objectMetadataItem } = options;
const { workspaceId, userId, objectMetadataItem } = options;
const maximumRecordAffected = this.environmentService.get(
'MUTATION_MAXIMUM_RECORD_AFFECTED',
);
@ -384,6 +386,7 @@ export class WorkspaceQueryRunnerService {
parsedResults.forEach((record) => {
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, {
workspaceId,
userId,
recordId: record.id,
objectMetadata: objectMetadataItem,
details: {
@ -399,7 +402,7 @@ export class WorkspaceQueryRunnerService {
args: DeleteOneResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<Record | undefined> {
const { workspaceId, objectMetadataItem } = options;
const { workspaceId, userId, objectMetadataItem } = options;
const query = await this.workspaceQueryBuilderFactory.deleteOne(
args,
options,
@ -422,6 +425,7 @@ export class WorkspaceQueryRunnerService {
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, {
workspaceId,
userId,
recordId: args.id,
objectMetadata: objectMetadataItem,
details: {

View File

@ -8,4 +8,4 @@ export * from './string-filter.input-type';
export * from './time-filter.input-type';
export * from './uuid-filter.input-type';
export * from './boolean-filter.input-type';
export * from './json-filter.input-type';
export * from './raw-json-filter.input-type';

View File

@ -2,8 +2,8 @@ import { GraphQLInputObjectType } from 'graphql';
import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type';
export const JsonFilterType = new GraphQLInputObjectType({
name: 'JsonFilter',
export const RawJsonFilterType = new GraphQLInputObjectType({
name: 'RawJsonFilter',
fields: {
is: { type: FilterIs },
},

View File

@ -32,7 +32,7 @@ import {
IntFilterType,
BooleanFilterType,
BigFloatFilterType,
JsonFilterType,
RawJsonFilterType,
} from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input';
import { OrderByDirectionType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/enum';
import { BigFloatScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@ -70,7 +70,7 @@ export class TypeMapperService {
[FieldMetadataType.PROBABILITY, GraphQLFloat],
[FieldMetadataType.RELATION, GraphQLID],
[FieldMetadataType.POSITION, PositionScalarType],
[FieldMetadataType.JSON, GraphQLJSON],
[FieldMetadataType.RAW_JSON, GraphQLJSON],
]);
return typeScalarMapping.get(fieldMetadataType);
@ -102,7 +102,7 @@ export class TypeMapperService {
[FieldMetadataType.PROBABILITY, FloatFilterType],
[FieldMetadataType.RELATION, UUIDFilterType],
[FieldMetadataType.POSITION, FloatFilterType],
[FieldMetadataType.JSON, JsonFilterType],
[FieldMetadataType.RAW_JSON, RawJsonFilterType],
]);
return typeFilterMapping.get(fieldMetadataType);
@ -126,7 +126,7 @@ export class TypeMapperService {
[FieldMetadataType.SELECT, OrderByDirectionType],
[FieldMetadataType.MULTI_SELECT, OrderByDirectionType],
[FieldMetadataType.POSITION, OrderByDirectionType],
[FieldMetadataType.JSON, OrderByDirectionType],
[FieldMetadataType.RAW_JSON, OrderByDirectionType],
]);
return typeOrderByMapping.get(fieldMetadataType);