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:
@ -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 *`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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 ||
|
||||
|
||||
@ -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],
|
||||
})
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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 },
|
||||
},
|
||||
});
|
||||
@ -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);
|
||||
|
||||
Reference in New Issue
Block a user