Add JSON field type and Event object (#4566)

* Add JSON field type and Event object

* Simplify code

* Adress PR comments and add featureFlag
This commit is contained in:
Félix Malfait
2024-03-19 21:54:08 +01:00
committed by GitHub
parent 4ab426c52a
commit 4bfb90657f
51 changed files with 575 additions and 117 deletions

View File

@ -0,0 +1,54 @@
import { Injectable } from '@nestjs/common';
import { MessageQueueJob } from 'src/engine/integrations/message-queue/interfaces/message-queue-job.interface';
import { DataSourceService } from 'src/engine-metadata/data-source/data-source.service';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
export type SaveEventToDbJobData = {
workspaceId: string;
recordId: string;
objectName: string;
operation: string;
details: any;
};
@Injectable()
export class SaveEventToDbJob implements MessageQueueJob<SaveEventToDbJobData> {
constructor(
private readonly dataSourceService: DataSourceService,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
) {}
async handle(data: SaveEventToDbJobData): Promise<void> {
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
data.workspaceId,
);
const workspaceDataSource =
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
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"
if (
data.objectName != 'person' &&
data.objectName != 'company' &&
data.objectName != 'opportunities'
) {
return;
}
await workspaceDataSource?.query(
`INSERT INTO ${dataSourceMetadata.schema}."event"
("name", "properties", "${data.objectName}Id")
VALUES ('${eventType}', '${JSON.stringify(data.details)}', '${
data.recordId
}') RETURNING *`,
);
}
}

View File

@ -0,0 +1,67 @@
import { Inject, Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event';
import {
SaveEventToDbJobData,
SaveEventToDbJob,
} from 'src/engine/api/graphql/workspace-query-runner/jobs/save-event-to-db.job';
import {
FeatureFlagEntity,
FeatureFlagKeys,
} from 'src/engine/modules/feature-flag/feature-flag.entity';
@Injectable()
export class EntityEventsToDbListener {
constructor(
@Inject(MessageQueue.entityEventsToDbQueue)
private readonly messageQueueService: MessageQueueService,
@InjectRepository(FeatureFlagEntity, 'core')
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
) {}
@OnEvent('*.created')
async handleCreate(payload: ObjectRecordCreateEvent<any>) {
return this.handle(payload, 'created');
}
@OnEvent('*.updated')
async handleUpdate(payload: ObjectRecordCreateEvent<any>) {
return this.handle(payload, 'updated');
}
// @OnEvent('*.deleted') - TODO: implement when we have soft deleted
// ....
private async handle(
payload: ObjectRecordCreateEvent<any>,
operation: string,
) {
const isEventObjectEnabledFeatureFlag =
await this.featureFlagRepository.findOneBy({
workspaceId: payload.workspaceId,
key: FeatureFlagKeys.IsEventObjectEnabled,
value: true,
});
if (
!isEventObjectEnabledFeatureFlag ||
!isEventObjectEnabledFeatureFlag.value
) {
return;
}
this.messageQueueService.add<SaveEventToDbJobData>(SaveEventToDbJob.name, {
workspaceId: payload.workspaceId,
recordId: payload.recordId,
objectName: payload.objectMetadata.nameSingular,
operation: operation,
details: payload.details,
});
}
}

View File

@ -1,10 +1,9 @@
import { Inject, Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import {
CreatedObjectMetadata,
ObjectRecordCreateEvent,
} from 'src/engine/integrations/event-emitter/types/object-record-create.event';
import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface';
import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event';
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
import {
@ -21,11 +20,11 @@ export class RecordPositionListener {
@OnEvent('*.created')
async handleAllCreate(payload: ObjectRecordCreateEvent<any>) {
if (!hasPositionField(payload.createdObjectMetadata)) {
if (!hasPositionField(payload.objectMetadata)) {
return;
}
if (hasPositionSet(payload.createdRecord)) {
if (hasPositionSet(payload.details.after)) {
return;
}
@ -33,15 +32,19 @@ export class RecordPositionListener {
RecordPositionBackfillJob.name,
{
workspaceId: payload.workspaceId,
recordId: payload.createdRecord.id,
objectMetadata: payload.createdObjectMetadata,
recordId: payload.recordId,
objectMetadata: {
nameSingular: payload.objectMetadata.nameSingular,
isCustom: payload.objectMetadata.isCustom,
},
},
);
}
}
// TODO: use objectMetadata instead of hardcoded standard objects name
const hasPositionField = (
createdObjectMetadata: CreatedObjectMetadata,
createdObjectMetadata: ObjectMetadataInterface,
): boolean => {
return (
createdObjectMetadata.isCustom ||

View File

@ -1,4 +1,5 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkspaceQueryBuilderModule } from 'src/engine/api/graphql/workspace-query-builder/workspace-query-builder.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
@ -6,20 +7,26 @@ import { WorkspacePreQueryHookModule } from 'src/engine/api/graphql/workspace-qu
import { workspaceQueryRunnerFactories } from 'src/engine/api/graphql/workspace-query-runner/factories';
import { RecordPositionListener } from 'src/engine/api/graphql/workspace-query-runner/listeners/record-position.listener';
import { AuthModule } from 'src/engine/modules/auth/auth.module';
import { FeatureFlagEntity } from 'src/engine/modules/feature-flag/feature-flag.entity';
import { Workspace } from 'src/engine/modules/workspace/workspace.entity';
import { WorkspaceQueryRunnerService } from './workspace-query-runner.service';
import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listener';
@Module({
imports: [
AuthModule,
WorkspaceQueryBuilderModule,
WorkspaceDataSourceModule,
WorkspacePreQueryHookModule,
TypeOrmModule.forFeature([Workspace, FeatureFlagEntity], 'core'),
],
providers: [
WorkspaceQueryRunnerService,
...workspaceQueryRunnerFactories,
RecordPositionListener,
EntityEventsToDbListener,
],
exports: [WorkspaceQueryRunnerService],
})

View File

@ -246,10 +246,10 @@ export class WorkspaceQueryRunnerService {
parsedResults.forEach((record) => {
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.created`, {
workspaceId,
createdRecord: this.removeNestedProperties(record),
createdObjectMetadata: {
nameSingular: objectMetadataItem.nameSingular,
isCustom: objectMetadataItem.isCustom,
recordId: record.id,
objectMetadata: objectMetadataItem,
details: {
after: record,
},
} satisfies ObjectRecordCreateEvent<any>);
});
@ -300,8 +300,12 @@ export class WorkspaceQueryRunnerService {
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.updated`, {
workspaceId,
previousRecord: this.removeNestedProperties(existingRecord as Record),
updatedRecord: this.removeNestedProperties(parsedResults?.[0]),
recordId: (existingRecord as Record).id,
objectMetadata: objectMetadataItem,
details: {
before: this.removeNestedProperties(existingRecord as Record),
after: this.removeNestedProperties(parsedResults?.[0]),
},
} satisfies ObjectRecordUpdateEvent<any>);
return parsedResults?.[0];
@ -336,6 +340,12 @@ export class WorkspaceQueryRunnerService {
options,
);
// TODO: check - NO EVENT SENT?
// OK I spent 2 hours trying to implement before/after diff and
// figured out why it hasn't been implement
// Doing a findMany in that context is very hard as long as we don't
// have a proper ORM. Let's come back to this once we do (target end of April 24?)
return parsedResults;
}
@ -374,7 +384,11 @@ export class WorkspaceQueryRunnerService {
parsedResults.forEach((record) => {
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, {
workspaceId,
deletedRecord: [this.removeNestedProperties(record)],
recordId: record.id,
objectMetadata: objectMetadataItem,
details: {
before: [this.removeNestedProperties(record)],
},
} satisfies ObjectRecordDeleteEvent<any>);
});
@ -408,7 +422,11 @@ export class WorkspaceQueryRunnerService {
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, {
workspaceId,
deletedRecord: this.removeNestedProperties(parsedResults?.[0]),
recordId: args.id,
objectMetadata: objectMetadataItem,
details: {
before: this.removeNestedProperties(parsedResults?.[0]),
},
} satisfies ObjectRecordDeleteEvent<any>);
return parsedResults?.[0];

View File

@ -8,3 +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';

View File

@ -0,0 +1,10 @@
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',
fields: {
is: { type: FilterIs },
},
});

View File

@ -15,6 +15,7 @@ import {
GraphQLString,
GraphQLType,
} from 'graphql';
import GraphQLJSON from 'graphql-type-json';
import {
DateScalarMode,
@ -31,6 +32,7 @@ import {
IntFilterType,
BooleanFilterType,
BigFloatFilterType,
JsonFilterType,
} 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';
@ -68,6 +70,7 @@ export class TypeMapperService {
[FieldMetadataType.PROBABILITY, GraphQLFloat],
[FieldMetadataType.RELATION, GraphQLID],
[FieldMetadataType.POSITION, PositionScalarType],
[FieldMetadataType.JSON, GraphQLJSON],
]);
return typeScalarMapping.get(fieldMetadataType);
@ -99,6 +102,7 @@ export class TypeMapperService {
[FieldMetadataType.PROBABILITY, FloatFilterType],
[FieldMetadataType.RELATION, UUIDFilterType],
[FieldMetadataType.POSITION, FloatFilterType],
[FieldMetadataType.JSON, JsonFilterType],
]);
return typeFilterMapping.get(fieldMetadataType);
@ -122,6 +126,7 @@ export class TypeMapperService {
[FieldMetadataType.SELECT, OrderByDirectionType],
[FieldMetadataType.MULTI_SELECT, OrderByDirectionType],
[FieldMetadataType.POSITION, OrderByDirectionType],
[FieldMetadataType.JSON, OrderByDirectionType],
]);
return typeOrderByMapping.get(fieldMetadataType);