Common - Field validation (#15491)

Closes : https://github.com/twentyhq/core-team-issues/issues/1622

To do in other PR : 
- Add migration command for non nullable text, raw_json & array fields
- Add null transformation

---------

Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Etienne
2025-11-13 15:45:35 +01:00
committed by GitHub
co-authored by Weiko
parent f6ed1e8295
commit 261cba40ef
114 changed files with 3524 additions and 1189 deletions
@@ -1,224 +0,0 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { ResolverArgsType } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
describe('QueryRunnerArgsFactory', () => {
const recordPositionService = {
overridePositionOnRecords: jest
.fn()
.mockImplementation(
({ partialRecordInputs }: { partialRecordInputs: any[] }) => {
return Promise.resolve(
partialRecordInputs.map((record: any) => ({
...record,
position:
record.position === 'last' || !record.position
? 2
: record.position,
})),
);
},
),
};
const workspaceId = 'workspaceId';
const options = {
authContext: { workspace: { id: workspaceId } },
objectMetadataItemWithFieldMaps: {
isCustom: true,
nameSingular: 'testNumber',
fieldsById: {
'position-id': {
type: FieldMetadataType.POSITION,
isCustom: true,
name: 'position',
},
'testNumber-id': {
type: FieldMetadataType.NUMBER,
isCustom: true,
name: 'testNumber',
},
'otherField-id': {
type: FieldMetadataType.TEXT,
isCustom: true,
name: 'otherField',
},
} as unknown as FieldMetadataMap,
fieldIdByName: {
position: 'position-id',
testNumber: 'testNumber-id',
otherField: 'otherField-id',
},
fieldIdByJoinColumnName: {},
},
} as unknown as WorkspaceQueryRunnerOptions;
let factory: QueryRunnerArgsFactory;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
QueryRunnerArgsFactory,
RecordInputTransformerService,
{
provide: RecordPositionService,
useValue: recordPositionService,
},
],
}).compile();
factory = module.get<QueryRunnerArgsFactory>(QueryRunnerArgsFactory);
});
it('should be defined', () => {
expect(factory).toBeDefined();
});
describe('create', () => {
it('should simply return the args when data is an empty array', async () => {
const args = {
data: [],
};
const result = await factory.create(
args,
options,
ResolverArgsType.CREATE_MANY,
);
expect(result).toEqual(args);
});
it('createMany type should override data position and number', async () => {
const args = {
id: 'uuid',
data: [{ position: 'last', testNumber: 1 }],
};
const result = await factory.create(
args,
options,
ResolverArgsType.CREATE_MANY,
);
const expectedArgs = {
partialRecordInputs: [{ position: 'last', testNumber: 1 }],
objectMetadata: {
isCustom: true,
nameSingular: 'testNumber',
fieldIdByName: {
position: 'position-id',
testNumber: 'testNumber-id',
otherField: 'otherField-id',
},
},
workspaceId,
shouldBackfillPositionIfUndefined: true,
};
expect(
recordPositionService.overridePositionOnRecords,
).toHaveBeenCalledWith(expectedArgs);
expect(result).toEqual({
id: 'uuid',
data: [{ position: 2, testNumber: 1 }],
});
});
it('createMany type should override position if not present', async () => {
const args = {
id: 'uuid',
data: [{ testNumber: 1 }],
};
const result = await factory.create(
args,
options,
ResolverArgsType.CREATE_MANY,
);
const expectedArgs = {
partialRecordInputs: [{ testNumber: 1 }],
objectMetadata: {
isCustom: true,
nameSingular: 'testNumber',
fieldIdByName: {
position: 'position-id',
testNumber: 'testNumber-id',
otherField: 'otherField-id',
},
},
workspaceId,
shouldBackfillPositionIfUndefined: true,
};
expect(
recordPositionService.overridePositionOnRecords,
).toHaveBeenCalledWith(expectedArgs);
expect(result).toEqual({
id: 'uuid',
data: [{ position: 2, testNumber: 1 }],
});
});
it('findMany type should override data position and number', async () => {
const args = {
id: 'uuid',
filter: { testNumber: { eq: 1 }, otherField: { eq: 'test' } },
};
const result = await factory.create(
args,
options,
ResolverArgsType.FIND_MANY,
);
expect(result).toEqual({
id: 'uuid',
filter: { testNumber: { eq: 1 }, otherField: { eq: 'test' } },
});
});
it('findOne type should override number in filter', async () => {
const args = {
id: 'uuid',
filter: { testNumber: { eq: 1 }, otherField: { eq: 'test' } },
};
const result = await factory.create(
args,
options,
ResolverArgsType.FIND_ONE,
);
expect(result).toEqual({
id: 'uuid',
filter: { testNumber: { eq: 1 }, otherField: { eq: 'test' } },
});
});
it('findDuplicates type should override number in data and id', async () => {
const args = {
ids: [123],
data: [{ testNumber: 1, otherField: 'test' }],
};
const result = await factory.create(
args,
options,
ResolverArgsType.FIND_DUPLICATES,
);
expect(result).toEqual({
ids: [123],
data: [{ testNumber: 1, position: 2, otherField: 'test' }],
});
});
});
});
@@ -1,8 +0,0 @@
import { QueryRunnerArgsFactory } from './query-runner-args.factory';
import { QueryResultGettersFactory } from './query-result-getters/query-result-getters.factory';
export const workspaceQueryRunnerFactories = [
QueryRunnerArgsFactory,
QueryResultGettersFactory,
];
@@ -1,237 +0,0 @@
import { Injectable, Logger } from '@nestjs/common';
import { FieldMetadataType, type ObjectRecord } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value';
import { type QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface';
import { type IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
import { type IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface';
import { isQueryResultFieldValueAConnection } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-connection.guard';
import { isQueryResultFieldValueANestedRecordArray } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-nested-record-array.guard';
import { isQueryResultFieldValueARecordArray } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-record-array.guard';
import { isQueryResultFieldValueARecord } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-record.guard';
import { ActivityQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/activity-query-result-getter.handler';
import { AttachmentQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/attachment-query-result-getter.handler';
import { PersonQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/person-query-result-getter.handler';
import { WorkspaceMemberQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/workspace-member-query-result-getter.handler';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
// TODO: find a way to prevent conflict between handlers executing logic on object relations
// And this factory that is also executing logic on object relations
// Right now the factory will override any change made on relations by the handlers
@Injectable()
export class QueryResultGettersFactory {
private readonly logger = new Logger(QueryResultGettersFactory.name);
private handlers: Map<string, QueryResultGetterHandlerInterface>;
constructor(private readonly fileService: FileService) {
this.initializeHandlers();
}
private initializeHandlers() {
this.handlers = new Map<string, QueryResultGetterHandlerInterface>([
['attachment', new AttachmentQueryResultGetterHandler(this.fileService)],
['person', new PersonQueryResultGetterHandler(this.fileService)],
[
'workspaceMember',
new WorkspaceMemberQueryResultGetterHandler(this.fileService),
],
['note', new ActivityQueryResultGetterHandler(this.fileService)],
['task', new ActivityQueryResultGetterHandler(this.fileService)],
]);
}
private async processConnection(
connection: IConnection<ObjectRecord>,
objectMetadataItemId: string,
objectMetadataMaps: ObjectMetadataMaps,
workspaceId: string,
): Promise<IConnection<ObjectRecord>> {
return {
...connection,
edges: await Promise.all(
connection.edges.map(async (edge: IEdge<ObjectRecord>) => ({
...edge,
node: await this.processRecord(
edge.node,
objectMetadataItemId,
objectMetadataMaps,
workspaceId,
),
})),
),
};
}
private async processNestedRecordArray(
result: { records: ObjectRecord[] },
objectMetadataItemId: string,
objectMetadataMaps: ObjectMetadataMaps,
workspaceId: string,
) {
return {
...result,
records: await Promise.all(
result.records.map(
async (record: ObjectRecord) =>
await this.processRecord(
record,
objectMetadataItemId,
objectMetadataMaps,
workspaceId,
),
),
),
};
}
private async processRecordArray(
recordArray: ObjectRecord[],
objectMetadataItemId: string,
objectMetadataMaps: ObjectMetadataMaps,
workspaceId: string,
) {
return await Promise.all(
recordArray.map(
async (record: ObjectRecord) =>
await this.processRecord(
record,
objectMetadataItemId,
objectMetadataMaps,
workspaceId,
),
),
);
}
private async processRecord(
record: ObjectRecord,
objectMetadataItemId: string,
objectMetadataMaps: ObjectMetadataMaps,
workspaceId: string,
): Promise<ObjectRecord> {
const objectMetadataMapItem = objectMetadataMaps.byId[objectMetadataItemId];
if (!isDefined(objectMetadataMapItem)) {
throw new Error('Object metadata map item is not defined');
}
const handler = this.getHandler(objectMetadataMapItem.nameSingular);
const relationFields = Object.keys(record)
.map(
(recordFieldName) =>
objectMetadataMapItem.fieldsById[
objectMetadataMapItem.fieldIdByName[recordFieldName]
],
)
.filter(isDefined)
.filter((fieldMetadata) =>
isFieldMetadataEntityOfType(fieldMetadata, FieldMetadataType.RELATION),
);
const relationFieldsProcessedMap = {} as Record<
string,
QueryResultFieldValue
>;
for (const relationField of relationFields) {
if (!isDefined(relationField.relationTargetObjectMetadataId)) {
throw new Error('Relation target object metadata id is not defined');
}
relationFieldsProcessedMap[relationField.name] =
await this.processQueryResultField(
record[relationField.name],
relationField.relationTargetObjectMetadataId,
objectMetadataMaps,
workspaceId,
);
}
const objectRecordProcessedWithoutRelationFields = await handler.handle(
record,
workspaceId,
);
const processedRecord = {
...objectRecordProcessedWithoutRelationFields,
...relationFieldsProcessedMap,
};
return processedRecord;
}
private async processQueryResultField(
queryResultField: QueryResultFieldValue,
objectMetadataItemId: string,
objectMetadataMaps: ObjectMetadataMaps,
workspaceId: string,
) {
if (isQueryResultFieldValueAConnection(queryResultField)) {
return await this.processConnection(
queryResultField,
objectMetadataItemId,
objectMetadataMaps,
workspaceId,
);
} else if (isQueryResultFieldValueANestedRecordArray(queryResultField)) {
return await this.processNestedRecordArray(
queryResultField,
objectMetadataItemId,
objectMetadataMaps,
workspaceId,
);
} else if (isQueryResultFieldValueARecordArray(queryResultField)) {
return await this.processRecordArray(
queryResultField,
objectMetadataItemId,
objectMetadataMaps,
workspaceId,
);
} else if (isQueryResultFieldValueARecord(queryResultField)) {
return await this.processRecord(
queryResultField,
objectMetadataItemId,
objectMetadataMaps,
workspaceId,
);
} else {
this.logger.warn(
`Query result field is not a record, connection, nested record array or record array.
This is an undetected case in query result getter that should be implemented !!`,
);
return queryResultField;
}
}
async create(
result: QueryResultFieldValue,
objectMetadataItem: ObjectMetadataItemWithFieldMaps,
workspaceId: string,
objectMetadataMaps: ObjectMetadataMaps,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): Promise<any> {
return await this.processQueryResultField(
result,
objectMetadataItem.id,
objectMetadataMaps,
workspaceId,
);
}
private getHandler(objectType: string): QueryResultGetterHandlerInterface {
return (
this.handlers.get(objectType) || {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handle: (result: any) => result,
}
);
}
}
@@ -1,323 +0,0 @@
import { Injectable } from '@nestjs/common';
import { FieldMetadataType, ObjectRecord } from 'twenty-shared/types';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import {
type CreateManyResolverArgs,
type CreateOneResolverArgs,
type FindDuplicatesResolverArgs,
type FindManyResolverArgs,
type FindOneResolverArgs,
GroupByResolverArgs,
type MergeManyResolverArgs,
type ResolverArgs,
ResolverArgsType,
type UpdateManyResolverArgs,
type UpdateOneResolverArgs,
type WorkspaceResolverBuilderMethodNames,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
@Injectable()
export class QueryRunnerArgsFactory {
constructor(
private readonly recordPositionService: RecordPositionService,
private readonly recordInputTransformerService: RecordInputTransformerService,
) {}
async create(
args: ResolverArgs,
options: WorkspaceQueryRunnerOptions,
resolverArgsType: WorkspaceResolverBuilderMethodNames,
) {
const fieldMetadataMapByNameByName =
options.objectMetadataItemWithFieldMaps.fieldsById;
const { objectMetadataItemWithFieldMaps, authContext } = options;
switch (resolverArgsType) {
case ResolverArgsType.CREATE_ONE:
return {
...args,
data: (
await this.overrideDataByFieldMetadata({
partialRecordInputs: [(args as CreateOneResolverArgs).data],
authContext,
objectMetadataItemWithFieldMaps,
})
)[0],
} satisfies CreateOneResolverArgs;
case ResolverArgsType.CREATE_MANY:
return {
...args,
data: await this.overrideDataByFieldMetadata({
partialRecordInputs: (args as CreateManyResolverArgs).data,
authContext,
objectMetadataItemWithFieldMaps,
}),
} satisfies CreateManyResolverArgs;
case ResolverArgsType.UPDATE_ONE:
return {
...args,
id: (args as UpdateOneResolverArgs).id,
data: (
await this.overrideDataByFieldMetadata({
partialRecordInputs: [(args as UpdateOneResolverArgs).data],
authContext,
objectMetadataItemWithFieldMaps,
shouldBackfillPositionIfUndefined: false,
})
)[0],
} satisfies UpdateOneResolverArgs;
case ResolverArgsType.UPDATE_MANY:
return {
...args,
filter: this.overrideFilterByFieldMetadata(
(args as UpdateManyResolverArgs).filter,
options.objectMetadataItemWithFieldMaps,
),
data: (
await this.overrideDataByFieldMetadata({
partialRecordInputs: [(args as UpdateManyResolverArgs).data],
authContext,
objectMetadataItemWithFieldMaps,
shouldBackfillPositionIfUndefined: false,
})
)[0],
} satisfies UpdateManyResolverArgs;
case ResolverArgsType.FIND_ONE:
return {
...args,
filter: this.overrideFilterByFieldMetadata(
(args as FindOneResolverArgs).filter,
options.objectMetadataItemWithFieldMaps,
),
};
case ResolverArgsType.FIND_MANY:
return {
...args,
filter: this.overrideFilterByFieldMetadata(
(args as FindManyResolverArgs).filter,
options.objectMetadataItemWithFieldMaps,
),
};
case ResolverArgsType.FIND_DUPLICATES:
return {
...args,
ids: (await Promise.all(
(args as FindDuplicatesResolverArgs).ids?.map((id) =>
this.overrideValueByFieldMetadata(
'id',
id,
fieldMetadataMapByNameByName,
options.objectMetadataItemWithFieldMaps,
),
) ?? [],
)) as string[],
data: await this.overrideDataByFieldMetadata({
partialRecordInputs: (args as FindDuplicatesResolverArgs).data,
authContext,
objectMetadataItemWithFieldMaps,
shouldBackfillPositionIfUndefined: false,
}),
} satisfies FindDuplicatesResolverArgs;
case ResolverArgsType.MERGE_MANY:
return {
...args,
ids: (await Promise.all(
(args as MergeManyResolverArgs).ids?.map((id) =>
this.overrideValueByFieldMetadata(
'id',
id,
fieldMetadataMapByNameByName,
options.objectMetadataItemWithFieldMaps,
),
) ?? [],
)) as string[],
conflictPriorityIndex: (args as MergeManyResolverArgs)
.conflictPriorityIndex,
dryRun: (args as MergeManyResolverArgs).dryRun,
} satisfies MergeManyResolverArgs;
case ResolverArgsType.GROUP_BY:
return {
...args,
filter: this.overrideFilterByFieldMetadata(
(args as GroupByResolverArgs).filter,
options.objectMetadataItemWithFieldMaps,
),
};
default:
return args;
}
}
async overrideDataByFieldMetadata({
partialRecordInputs,
authContext,
objectMetadataItemWithFieldMaps,
shouldBackfillPositionIfUndefined = true,
}: {
partialRecordInputs: Partial<ObjectRecord>[] | undefined;
authContext: AuthContext;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
shouldBackfillPositionIfUndefined?: boolean;
}): Promise<Partial<ObjectRecord>[]> {
if (!isDefined(partialRecordInputs)) {
return [];
}
const allOverriddenRecords: Partial<ObjectRecord>[] = [];
const workspace = authContext.workspace;
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
const overriddenPositionRecords =
await this.recordPositionService.overridePositionOnRecords({
partialRecordInputs,
workspaceId: workspace.id,
objectMetadata: {
isCustom: objectMetadataItemWithFieldMaps.isCustom,
nameSingular: objectMetadataItemWithFieldMaps.nameSingular,
fieldIdByName: objectMetadataItemWithFieldMaps.fieldIdByName,
},
shouldBackfillPositionIfUndefined,
});
for (const record of overriddenPositionRecords) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const createArgByArgKey: [string, any][] = await Promise.all(
Object.entries(record).map(async ([key, value]) => {
const fieldMetadataId =
objectMetadataItemWithFieldMaps.fieldIdByName[key];
const fieldMetadata =
objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId];
if (!fieldMetadata) {
return [key, value];
}
switch (fieldMetadata.type) {
case FieldMetadataType.NUMBER:
case FieldMetadataType.RICH_TEXT:
case FieldMetadataType.PHONES:
case FieldMetadataType.RICH_TEXT_V2:
case FieldMetadataType.LINKS:
case FieldMetadataType.EMAILS: {
const transformedRecord =
await this.recordInputTransformerService.process({
recordInput: { [key]: value },
objectMetadataMapItem: objectMetadataItemWithFieldMaps,
});
return [key, transformedRecord[key]];
}
default:
return [key, value];
}
}),
);
allOverriddenRecords.push(Object.fromEntries(createArgByArgKey));
}
return allOverriddenRecords;
}
public overrideFilterByFieldMetadata<
T extends ObjectRecordFilter | undefined,
>(
filter: T,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
): T {
if (!isDefined(filter)) {
return filter;
}
const overrideFilter = (filterObject: ObjectRecordFilter) => {
return Object.entries(filterObject).reduce((acc, [key, value]) => {
if (key === 'and' || key === 'or') {
// @ts-expect-error legacy noImplicitAny
acc[key] = value.map((nestedFilter: ObjectRecordFilter) =>
overrideFilter(nestedFilter),
);
} else if (key === 'not') {
// @ts-expect-error legacy noImplicitAny
acc[key] = overrideFilter(value);
} else {
// @ts-expect-error legacy noImplicitAny
acc[key] = this.transformFilterValueByType(
key,
value,
objectMetadataItemWithFieldMaps,
);
}
return acc;
}, {});
};
return overrideFilter(filter) as T;
}
private transformFilterValueByType(
key: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
) {
const fieldMetadataId = objectMetadataItemWithFieldMaps.fieldIdByName[key];
const fieldMetadata =
objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId];
if (!fieldMetadata) {
return value;
}
// Special handling for filter values, which have a specific structure
switch (fieldMetadata.type) {
case FieldMetadataType.NUMBER: {
if (value?.is === 'NULL') {
return value;
} else {
return Object.fromEntries(
Object.entries(value).map(([filterKey, filterValue]) => [
filterKey,
Number(filterValue),
]),
);
}
}
default:
return value;
}
}
async overrideValueByFieldMetadata(
key: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any,
fieldMetadataMapByName: FieldMetadataMap,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
) {
const fieldMetadata = fieldMetadataMapByName[key];
if (!fieldMetadata) {
return value;
}
return this.recordInputTransformerService.process({
recordInput: { [key]: value },
objectMetadataMapItem: objectMetadataItemWithFieldMaps,
});
}
}
@@ -22,7 +22,6 @@ import { twentyORMGraphqlApiExceptionHandler } from 'src/engine/twenty-orm/utils
interface QueryFailedErrorWithCode extends QueryFailedError {
code: string;
}
//TODO : Refacto-common - Should be handle first in common api layer
export const workspaceQueryRunnerGraphqlApiExceptionHandler = (
error: QueryFailedErrorWithCode,
@@ -2,7 +2,6 @@ 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 { workspaceQueryRunnerFactories } from 'src/engine/api/graphql/workspace-query-runner/factories';
import { TelemetryListener } from 'src/engine/api/graphql/workspace-query-runner/listeners/telemetry.listener';
import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
@@ -29,11 +28,6 @@ import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listen
RecordPositionModule,
SubscriptionsModule,
],
providers: [
...workspaceQueryRunnerFactories,
EntityEventsToDbListener,
TelemetryListener,
],
exports: [...workspaceQueryRunnerFactories],
providers: [EntityEventsToDbListener, TelemetryListener],
})
export class WorkspaceQueryRunnerModule {}