From f6fd92adcbf3dae4e664b5b10b8e2fb307cd9d3b Mon Sep 17 00:00:00 2001 From: Weiko Date: Tue, 27 Aug 2024 17:06:39 +0200 Subject: [PATCH] [POC] add graphql query runner (#6747) ## Context The goal is to replace pg_graphql with our own ORM wrapper (TwentyORM). This PR tries to add some parsing logic to convert graphql requests to send to the ORM to replace pg_graphql implementation. --------- Co-authored-by: Charles Bochet --- packages/twenty-front/vite.config.ts | 18 ++- packages/twenty-server/src/app.module.ts | 2 + .../src/database/typeorm/typeorm.module.ts | 2 - .../connection-max-depth.constant.ts | 1 + .../constants/query-max-records.constant.ts | 1 + .../errors/graphql-query-runner.exception.ts | 17 +++ .../graphql-query-runner.module.ts | 9 ++ .../graphql-query-runner.service.ts | 130 ++++++++++++++++++ .../graphql-query-filter-field.parser.spec.ts | 43 ++++++ ...aphql-query-filter-operator.parser.spec.ts | 130 ++++++++++++++++++ .../graphql-query-filter-condition.parser.ts | 107 ++++++++++++++ .../graphql-query-filter-field.parser.ts | 102 ++++++++++++++ .../graphql-query-filter-operator.parser.ts | 56 ++++++++ .../graphql-query-order.parser.spec.ts | 50 +++++++ .../graphql-query-order.parser.ts | 122 ++++++++++++++++ .../parsers/graphql-query.parser.ts | 70 ++++++++++ ...graphql-selected-fields-relation.parser.ts | 79 +++++++++++ .../graphql-selected-fields.parser.ts | 128 +++++++++++++++++ .../utils/apply-range-filter.util.ts | 29 ++++ .../utils/connection.util.ts | 114 +++++++++++++++ .../convert-object-metadata-to-map.util.ts | 35 +++++ .../workspace-query-runner.module.ts | 7 + .../workspace-query-runner.service.ts | 21 ++- .../api/graphql/workspace-schema.factory.ts | 1 - .../engine/core-modules/auth/auth.module.ts | 3 - .../billing/services/billing.service.ts | 4 +- .../timeline-calendar-event.module.ts | 13 +- .../enums/feature-flag-key.enum.ts | 1 + .../feature-flag/feature-flag.module.ts | 7 +- ...ags.factory.ts => feature-flag.service.ts} | 20 ++- .../services/is-feature-enabled.service.ts | 28 ---- .../user-workspace/user-workspace.module.ts | 5 +- .../log-execution-time.decorator.ts | 34 +++++ .../field-metadata/dtos/field-metadata.dto.ts | 14 +- .../field-metadata/field-metadata.module.ts | 2 - .../entity-schema-relation.factory.ts | 2 +- .../factories/workspace-datasource.factory.ts | 6 +- .../twenty-orm/twenty-orm-core.module.ts | 81 ----------- .../engine/twenty-orm/twenty-orm.module.ts | 62 ++++----- .../utils/determine-relation-details.util.ts | 17 ++- .../workspace-sync-metadata.service.ts | 11 +- .../calendar-event-import-manager.module.ts | 11 -- ...lendar-event-participant-manager.module.ts | 3 - .../query-hooks/calendar-query-hook.module.ts | 11 +- .../messaging-import-manager.module.ts | 3 - .../messaging-messages-import.service.ts | 4 +- .../message-participant-manager.module.ts | 3 - .../database-event-trigger.listener.ts | 4 +- .../workspace-member-query-hook.module.ts | 9 -- .../src/queue-worker/queue-worker.module.ts | 2 +- .../src/utils/is-plain-object.ts | 12 +- 51 files changed, 1397 insertions(+), 249 deletions(-) create mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/connection-max-depth.constant.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/__tests__/graphql-query-filter-field.parser.spec.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/__tests__/graphql-query-filter-operator.parser.spec.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/graphql-query-filter-field.parser.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/graphql-query-filter-operator.parser.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-order/__tests__/graphql-query-order.parser.spec.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-order/graphql-query-order.parser.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query.parser.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-selected-fields/graphql-selected-fields-relation.parser.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-selected-fields/graphql-selected-fields.parser.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/apply-range-filter.util.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/connection.util.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util.ts rename packages/twenty-server/src/engine/core-modules/feature-flag/services/{feature-flags.factory.ts => feature-flag.service.ts} (64%) delete mode 100644 packages/twenty-server/src/engine/core-modules/feature-flag/services/is-feature-enabled.service.ts create mode 100644 packages/twenty-server/src/engine/decorators/observability/log-execution-time.decorator.ts delete mode 100644 packages/twenty-server/src/engine/twenty-orm/twenty-orm-core.module.ts diff --git a/packages/twenty-front/vite.config.ts b/packages/twenty-front/vite.config.ts index 919f22a29..dd1f3e330 100644 --- a/packages/twenty-front/vite.config.ts +++ b/packages/twenty-front/vite.config.ts @@ -32,11 +32,19 @@ export default defineConfig(({ command, mode }) => { overlay: false, }; - console.log( - `VITE_DISABLE_TYPESCRIPT_CHECKER: ${VITE_DISABLE_TYPESCRIPT_CHECKER}`, - ); - console.log(`VITE_DISABLE_ESLINT_CHECKER: ${VITE_DISABLE_ESLINT_CHECKER}`); - console.log(`VITE_BUILD_SOURCEMAP: ${VITE_BUILD_SOURCEMAP}`); + if (VITE_DISABLE_TYPESCRIPT_CHECKER === 'true') { + console.log( + `VITE_DISABLE_TYPESCRIPT_CHECKER: ${VITE_DISABLE_TYPESCRIPT_CHECKER}`, + ); + } + + if (VITE_DISABLE_ESLINT_CHECKER === 'true') { + console.log(`VITE_DISABLE_ESLINT_CHECKER: ${VITE_DISABLE_ESLINT_CHECKER}`); + } + + if (VITE_BUILD_SOURCEMAP === 'true') { + console.log(`VITE_BUILD_SOURCEMAP: ${VITE_BUILD_SOURCEMAP}`); + } if (VITE_DISABLE_TYPESCRIPT_CHECKER !== 'true') { checkers['typescript'] = { diff --git a/packages/twenty-server/src/app.module.ts b/packages/twenty-server/src/app.module.ts index 6e7b5316e..4641f3a04 100644 --- a/packages/twenty-server/src/app.module.ts +++ b/packages/twenty-server/src/app.module.ts @@ -22,6 +22,7 @@ import { MessageQueueDriverType } from 'src/engine/integrations/message-queue/in import { MessageQueueModule } from 'src/engine/integrations/message-queue/message-queue.module'; import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; import { GraphQLHydrateRequestFromTokenMiddleware } from 'src/engine/middlewares/graphql-hydrate-request-from-token.middleware'; +import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { ModulesModule } from 'src/modules/modules.module'; import { CoreEngineModule } from './engine/core-modules/core-engine.module'; @@ -45,6 +46,7 @@ import { IntegrationsModule } from './engine/integrations/integrations.module'; imports: [CoreEngineModule, GraphQLConfigModule], useClass: GraphQLConfigService, }), + TwentyORMModule, // Integrations module, contains all the integrations with other services IntegrationsModule, // Core engine module, contains all the core modules diff --git a/packages/twenty-server/src/database/typeorm/typeorm.module.ts b/packages/twenty-server/src/database/typeorm/typeorm.module.ts index 957f6ca59..b24fe05ba 100644 --- a/packages/twenty-server/src/database/typeorm/typeorm.module.ts +++ b/packages/twenty-server/src/database/typeorm/typeorm.module.ts @@ -3,7 +3,6 @@ import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; import { typeORMCoreModuleOptions } from 'src/database/typeorm/core/core.datasource'; import { EnvironmentModule } from 'src/engine/integrations/environment/environment.module'; -import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { TypeORMService } from './typeorm.service'; @@ -29,7 +28,6 @@ const coreTypeORMFactory = async (): Promise => ({ useFactory: coreTypeORMFactory, name: 'core', }), - TwentyORMModule.register({}), EnvironmentModule, ], providers: [TypeORMService], diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/connection-max-depth.constant.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/connection-max-depth.constant.ts new file mode 100644 index 000000000..0038cb303 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/connection-max-depth.constant.ts @@ -0,0 +1 @@ +export const CONNECTION_MAX_DEPTH = 5; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant.ts new file mode 100644 index 000000000..e9e1c7191 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant.ts @@ -0,0 +1 @@ +export const QUERY_MAX_RECORDS = 60; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts new file mode 100644 index 000000000..7646b95ff --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts @@ -0,0 +1,17 @@ +import { CustomException } from 'src/utils/custom-exception'; + +export class GraphqlQueryRunnerException extends CustomException { + code: GraphqlQueryRunnerExceptionCode; + constructor(message: string, code: GraphqlQueryRunnerExceptionCode) { + super(message, code); + } +} + +export enum GraphqlQueryRunnerExceptionCode { + MAX_DEPTH_REACHED = 'MAX_DEPTH_REACHED', + INVALID_CURSOR = 'INVALID_CURSOR', + INVALID_DIRECTION = 'INVALID_DIRECTION', + UNSUPPORTED_OPERATOR = 'UNSUPPORTED_OPERATOR', + ARGS_CONFLICT = 'ARGS_CONFLICT', + FIELD_NOT_FOUND = 'FIELD_NOT_FOUND', +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module.ts new file mode 100644 index 000000000..87d6c0255 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; + +import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service'; + +@Module({ + providers: [GraphqlQueryRunnerService], + exports: [GraphqlQueryRunnerService], +}) +export class GraphqlQueryRunnerModule {} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts new file mode 100644 index 000000000..b653d8ea7 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts @@ -0,0 +1,130 @@ +import { Injectable } from '@nestjs/common'; + +import graphqlFields from 'graphql-fields'; +import { FindManyOptions, ObjectLiteral } from 'typeorm'; + +import { + Record as IRecord, + RecordFilter, + RecordOrderBy, +} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface'; +import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; +import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; + +import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant'; +import { + GraphqlQueryRunnerException, + GraphqlQueryRunnerExceptionCode, +} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; +import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-query.parser'; +import { applyRangeFilter } from 'src/engine/api/graphql/graphql-query-runner/utils/apply-range-filter.util'; +import { + createConnection, + decodeCursor, +} from 'src/engine/api/graphql/graphql-query-runner/utils/connection.util'; +import { convertObjectMetadataToMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util'; +import { LogExecutionTime } from 'src/engine/decorators/observability/log-execution-time.decorator'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; + +@Injectable() +export class GraphqlQueryRunnerService { + constructor( + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + ) {} + + @LogExecutionTime() + async findManyWithTwentyOrm< + ObjectRecord extends IRecord = IRecord, + Filter extends RecordFilter = RecordFilter, + OrderBy extends RecordOrderBy = RecordOrderBy, + >( + args: FindManyResolverArgs, + options: WorkspaceQueryRunnerOptions, + ): Promise> { + const { authContext, objectMetadataItem, info, objectMetadataCollection } = + options; + + const repository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + authContext.workspace.id, + objectMetadataItem.nameSingular, + ); + + const selectedFields = graphqlFields(info); + + const objectMetadataMap = convertObjectMetadataToMap( + objectMetadataCollection, + ); + + const objectMetadata = objectMetadataMap[objectMetadataItem.nameSingular]; + + if (!objectMetadata) { + throw new Error( + `Object metadata for ${objectMetadataItem.nameSingular} not found`, + ); + } + + const fieldMetadataMap = objectMetadata.fields; + + const graphqlQueryParser = new GraphqlQueryParser( + fieldMetadataMap, + objectMetadataMap, + ); + + const { select, relations } = graphqlQueryParser.parseSelectedFields( + objectMetadataItem, + selectedFields, + ); + + const order = args.orderBy + ? graphqlQueryParser.parseOrder(args.orderBy) + : undefined; + + const where = args.filter + ? graphqlQueryParser.parseFilter(args.filter) + : {}; + + let cursor: Record | undefined; + + if (args.after) { + cursor = decodeCursor(args.after); + } else if (args.before) { + cursor = decodeCursor(args.before); + } + + if (args.first && args.last) { + throw new GraphqlQueryRunnerException( + 'Cannot provide both first and last', + GraphqlQueryRunnerExceptionCode.ARGS_CONFLICT, + ); + } + + const take = args.first ?? args.last ?? QUERY_MAX_RECORDS; + + const findOptions: FindManyOptions = { + where, + order, + select, + relations, + take, + }; + + const totalCount = await repository.count({ + where, + }); + + if (cursor) { + applyRangeFilter(where, order, cursor); + } + + const objectRecords = await repository.find(findOptions); + + return createConnection( + (objectRecords as ObjectRecord[]) ?? [], + take, + totalCount, + order, + ); + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/__tests__/graphql-query-filter-field.parser.spec.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/__tests__/graphql-query-filter-field.parser.spec.ts new file mode 100644 index 000000000..1924cdc38 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/__tests__/graphql-query-filter-field.parser.spec.ts @@ -0,0 +1,43 @@ +import { FindOperator, Not } from 'typeorm'; + +import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; + +import { GraphqlQueryFilterFieldParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/graphql-query-filter-field.parser'; +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; + +describe('GraphqlQueryFilterFieldParser', () => { + let parser: GraphqlQueryFilterFieldParser; + let mockFieldMetadataMap: Record; + + beforeEach(() => { + mockFieldMetadataMap = { + simpleField: { + id: '1', + name: 'simpleField', + type: FieldMetadataType.TEXT, + label: 'Simple Field', + objectMetadataId: 'obj1', + }, + }; + parser = new GraphqlQueryFilterFieldParser(mockFieldMetadataMap); + }); + it('should parse simple field correctly', () => { + const result = parser.parse('simpleField', 'value', false); + + expect(result).toEqual({ simpleField: 'value' }); + }); + + it('should negate simple field correctly', () => { + const result = parser.parse('simpleField', 'value', true); + + expect(result).toEqual({ simpleField: Not('value') }); + }); + + it('should parse object value using operator parser', () => { + const result = parser.parse('simpleField', { like: '%value%' }, false); + + expect(result).toEqual({ + simpleField: new FindOperator('like', '%%value%%'), + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/__tests__/graphql-query-filter-operator.parser.spec.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/__tests__/graphql-query-filter-operator.parser.spec.ts new file mode 100644 index 000000000..9f98bff81 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/__tests__/graphql-query-filter-operator.parser.spec.ts @@ -0,0 +1,130 @@ +import { + FindOperator, + ILike, + In, + IsNull, + LessThan, + LessThanOrEqual, + Like, + MoreThan, + MoreThanOrEqual, + Not, +} from 'typeorm'; + +import { GraphqlQueryRunnerException } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; +import { GraphqlQueryFilterOperatorParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/graphql-query-filter-operator.parser'; + +describe('GraphqlQueryFilterOperatorParser', () => { + let parser: GraphqlQueryFilterOperatorParser; + + beforeEach(() => { + parser = new GraphqlQueryFilterOperatorParser(); + }); + + describe('parseOperator', () => { + it('should parse eq operator correctly', () => { + const result = parser.parseOperator({ eq: 'value' }, false); + + expect(result).toBe('value'); + }); + + it('should parse neq operator correctly', () => { + const result = parser.parseOperator({ neq: 'value' }, false); + + expect(result).toBeInstanceOf(FindOperator); + expect(result).toEqual(Not('value')); + }); + + it('should parse gt operator correctly', () => { + const result = parser.parseOperator({ gt: 5 }, false); + + expect(result).toBeInstanceOf(FindOperator); + expect(result).toEqual(MoreThan(5)); + }); + + it('should parse gte operator correctly', () => { + const result = parser.parseOperator({ gte: 5 }, false); + + expect(result).toBeInstanceOf(FindOperator); + expect(result).toEqual(MoreThanOrEqual(5)); + }); + + it('should parse lt operator correctly', () => { + const result = parser.parseOperator({ lt: 5 }, false); + + expect(result).toBeInstanceOf(FindOperator); + expect(result).toEqual(LessThan(5)); + }); + + it('should parse lte operator correctly', () => { + const result = parser.parseOperator({ lte: 5 }, false); + + expect(result).toBeInstanceOf(FindOperator); + expect(result).toEqual(LessThanOrEqual(5)); + }); + + it('should parse in operator correctly', () => { + const result = parser.parseOperator({ in: [1, 2, 3] }, false); + + expect(result).toBeInstanceOf(FindOperator); + expect(result).toEqual(In([1, 2, 3])); + }); + + it('should parse is operator with NULL correctly', () => { + const result = parser.parseOperator({ is: 'NULL' }, false); + + expect(result).toBeInstanceOf(FindOperator); + expect(result).toEqual(IsNull()); + }); + + it('should parse is operator with non-NULL value correctly', () => { + const result = parser.parseOperator({ is: 'NOT_NULL' }, false); + + expect(result).toBe('NOT_NULL'); + }); + + it('should parse like operator correctly', () => { + const result = parser.parseOperator({ like: 'test' }, false); + + expect(result).toBeInstanceOf(FindOperator); + expect(result).toEqual(Like('%test%')); + }); + + it('should parse ilike operator correctly', () => { + const result = parser.parseOperator({ ilike: 'test' }, false); + + expect(result).toBeInstanceOf(FindOperator); + expect(result).toEqual(ILike('%test%')); + }); + + it('should parse startsWith operator correctly', () => { + const result = parser.parseOperator({ startsWith: 'test' }, false); + + expect(result).toBeInstanceOf(FindOperator); + expect(result).toEqual(ILike('test%')); + }); + + it('should parse endsWith operator correctly', () => { + const result = parser.parseOperator({ endsWith: 'test' }, false); + + expect(result).toBeInstanceOf(FindOperator); + expect(result).toEqual(ILike('%test')); + }); + + it('should negate the operator when isNegated is true', () => { + const result = parser.parseOperator({ eq: 'value' }, true); + + expect(result).toBeInstanceOf(FindOperator); + expect(result).toEqual(Not('value')); + }); + + it('should throw an exception for unsupported operator', () => { + expect(() => + parser.parseOperator({ unsupported: 'value' }, false), + ).toThrow(GraphqlQueryRunnerException); + expect(() => + parser.parseOperator({ unsupported: 'value' }, false), + ).toThrow('Operator "unsupported" is not supported'); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts new file mode 100644 index 000000000..82421ed74 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts @@ -0,0 +1,107 @@ +import { FindOptionsWhere, ObjectLiteral } from 'typeorm'; + +import { RecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; + +import { FieldMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util'; + +import { GraphqlQueryFilterFieldParser } from './graphql-query-filter-field.parser'; + +export class GraphqlQueryFilterConditionParser { + private fieldMetadataMap: FieldMetadataMap; + private fieldConditionParser: GraphqlQueryFilterFieldParser; + + constructor(fieldMetadataMap: FieldMetadataMap) { + this.fieldMetadataMap = fieldMetadataMap; + this.fieldConditionParser = new GraphqlQueryFilterFieldParser( + this.fieldMetadataMap, + ); + } + + public parse( + conditions: RecordFilter, + isNegated = false, + ): FindOptionsWhere | FindOptionsWhere[] { + if (Array.isArray(conditions)) { + return this.parseAndCondition(conditions, isNegated); + } + + const result: FindOptionsWhere = {}; + + for (const [key, value] of Object.entries(conditions)) { + switch (key) { + case 'and': + return this.parseAndCondition(value, isNegated); + case 'or': + return this.parseOrCondition(value, isNegated); + case 'not': + return this.parse(value, !isNegated); + default: + Object.assign( + result, + this.fieldConditionParser.parse(key, value, isNegated), + ); + } + } + + return result; + } + + private parseAndCondition( + conditions: RecordFilter[], + isNegated: boolean, + ): FindOptionsWhere[] { + const parsedConditions = conditions.map((condition) => + this.parse(condition, isNegated), + ); + + return this.combineConditions(parsedConditions, isNegated ? 'or' : 'and'); + } + + private parseOrCondition( + conditions: RecordFilter[], + isNegated: boolean, + ): FindOptionsWhere[] { + const parsedConditions = conditions.map((condition) => + this.parse(condition, isNegated), + ); + + return this.combineConditions(parsedConditions, isNegated ? 'and' : 'or'); + } + + private combineConditions( + conditions: ( + | FindOptionsWhere + | FindOptionsWhere[] + )[], + combineType: 'and' | 'or', + ): FindOptionsWhere[] { + if (combineType === 'and') { + let result: FindOptionsWhere[] = [{}]; + + for (const condition of conditions) { + if (Array.isArray(condition)) { + const newResult: FindOptionsWhere[] = []; + + for (const existingCondition of result) { + for (const orCondition of condition) { + newResult.push({ + ...existingCondition, + ...orCondition, + }); + } + } + result = newResult; + } else { + result = result.map((existingCondition) => ({ + ...existingCondition, + ...condition, + })); + } + } + + return result; + } + + return conditions.flat(); + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/graphql-query-filter-field.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/graphql-query-filter-field.parser.ts new file mode 100644 index 000000000..a855ec6e9 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/graphql-query-filter-field.parser.ts @@ -0,0 +1,102 @@ +import { FindOptionsWhere, Not, ObjectLiteral } from 'typeorm'; + +import { RecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; + +import { FieldMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util'; +import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; +import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; +import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; +import { capitalize } from 'src/utils/capitalize'; +import { isPlainObject } from 'src/utils/is-plain-object'; + +import { GraphqlQueryFilterConditionParser } from './graphql-query-filter-condition.parser'; +import { GraphqlQueryFilterOperatorParser } from './graphql-query-filter-operator.parser'; + +export class GraphqlQueryFilterFieldParser { + private fieldMetadataMap: FieldMetadataMap; + private operatorParser: GraphqlQueryFilterOperatorParser; + + constructor(fieldMetadataMap: FieldMetadataMap) { + this.fieldMetadataMap = fieldMetadataMap; + this.operatorParser = new GraphqlQueryFilterOperatorParser(); + } + + public parse( + key: string, + value: any, + isNegated: boolean, + ): FindOptionsWhere { + const fieldMetadata = this.fieldMetadataMap[key]; + + if (!fieldMetadata) { + return { + [key]: (value: RecordFilter, isNegated: boolean) => { + const conditionParser = new GraphqlQueryFilterConditionParser( + this.fieldMetadataMap, + ); + + return conditionParser.parse(value, isNegated); + }, + }; + } + + if (isCompositeFieldMetadataType(fieldMetadata.type)) { + return this.parseCompositeFieldForFilter(fieldMetadata, value, isNegated); + } + + if (isPlainObject(value)) { + const parsedValue = this.operatorParser.parseOperator(value, isNegated); + + return { [key]: parsedValue }; + } + + return { [key]: isNegated ? Not(value) : value }; + } + + private parseCompositeFieldForFilter( + fieldMetadata: FieldMetadataInterface, + fieldValue: any, + isNegated: boolean, + ): FindOptionsWhere { + const compositeType = compositeTypeDefinitions.get( + fieldMetadata.type as CompositeFieldMetadataType, + ); + + if (!compositeType) { + throw new Error( + `Composite type definition not found for type: ${fieldMetadata.type}`, + ); + } + + return Object.entries(fieldValue).reduce( + (result, [subFieldKey, subFieldValue]) => { + const subFieldMetadata = compositeType.properties.find( + (property) => property.name === subFieldKey, + ); + + if (!subFieldMetadata) { + throw new Error( + `Sub field metadata not found for composite type: ${fieldMetadata.type}`, + ); + } + + const fullFieldName = `${fieldMetadata.name}${capitalize(subFieldKey)}`; + + if (isPlainObject(subFieldValue)) { + result[fullFieldName] = this.operatorParser.parseOperator( + subFieldValue, + isNegated, + ); + } else { + result[fullFieldName] = isNegated + ? Not(subFieldValue) + : subFieldValue; + } + + return result; + }, + {} as FindOptionsWhere, + ); + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/graphql-query-filter-operator.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/graphql-query-filter-operator.parser.ts new file mode 100644 index 000000000..3c831e424 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/graphql-query-filter-operator.parser.ts @@ -0,0 +1,56 @@ +import { + FindOperator, + ILike, + In, + IsNull, + LessThan, + LessThanOrEqual, + Like, + MoreThan, + MoreThanOrEqual, + Not, +} from 'typeorm'; + +import { + GraphqlQueryRunnerException, + GraphqlQueryRunnerExceptionCode, +} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; + +export class GraphqlQueryFilterOperatorParser { + private operatorMap: { [key: string]: (value: any) => FindOperator }; + + constructor() { + this.operatorMap = { + eq: (value: any) => value, + neq: (value: any) => Not(value), + gt: (value: any) => MoreThan(value), + gte: (value: any) => MoreThanOrEqual(value), + lt: (value: any) => LessThan(value), + lte: (value: any) => LessThanOrEqual(value), + in: (value: any) => In(value), + is: (value: any) => (value === 'NULL' ? IsNull() : value), + like: (value: string) => Like(`%${value}%`), + ilike: (value: string) => ILike(`%${value}%`), + startsWith: (value: string) => ILike(`${value}%`), + endsWith: (value: string) => ILike(`%${value}`), + }; + } + + public parseOperator( + operatorObj: Record, + isNegated: boolean, + ): FindOperator { + const [[operator, value]] = Object.entries(operatorObj); + + if (operator in this.operatorMap) { + const operatorFunction = this.operatorMap[operator]; + + return isNegated ? Not(operatorFunction(value)) : operatorFunction(value); + } + + throw new GraphqlQueryRunnerException( + `Operator "${operator}" is not supported`, + GraphqlQueryRunnerExceptionCode.UNSUPPORTED_OPERATOR, + ); + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-order/__tests__/graphql-query-order.parser.spec.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-order/__tests__/graphql-query-order.parser.spec.ts new file mode 100644 index 000000000..ab8a02dbd --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-order/__tests__/graphql-query-order.parser.spec.ts @@ -0,0 +1,50 @@ +import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; + +import { GraphqlQueryOrderFieldParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-order/graphql-query-order.parser'; +import { FieldMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util'; +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; + +describe('GraphqlQueryOrderFieldParser', () => { + let parser: GraphqlQueryOrderFieldParser; + const fieldMetadataMap: FieldMetadataMap = {}; + + beforeEach(() => { + fieldMetadataMap['name'] = { + id: 'name-id', + name: 'name', + type: FieldMetadataType.TEXT, + label: 'Name', + objectMetadataId: 'object-id', + }; + fieldMetadataMap['age'] = { + id: 'age-id', + name: 'age', + type: FieldMetadataType.NUMBER, + label: 'Age', + objectMetadataId: 'object-id', + }; + fieldMetadataMap['address'] = { + id: 'address-id', + name: 'address', + type: FieldMetadataType.ADDRESS, + label: 'Address', + objectMetadataId: 'object-id', + }; + + parser = new GraphqlQueryOrderFieldParser(fieldMetadataMap); + }); + describe('parse', () => { + it('should parse simple order by fields', () => { + const orderBy = [ + { name: OrderByDirection.AscNullsFirst }, + { age: OrderByDirection.DescNullsLast }, + ]; + const result = parser.parse(orderBy); + + expect(result).toEqual({ + name: { direction: 'ASC', nulls: 'FIRST' }, + age: { direction: 'DESC', nulls: 'LAST' }, + }); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-order/graphql-query-order.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-order/graphql-query-order.parser.ts new file mode 100644 index 000000000..d64a4b3e8 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-order/graphql-query-order.parser.ts @@ -0,0 +1,122 @@ +import { FindOptionsOrderValue } from 'typeorm'; + +import { + OrderByDirection, + RecordOrderBy, +} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; + +import { + GraphqlQueryRunnerException, + GraphqlQueryRunnerExceptionCode, +} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; +import { FieldMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util'; +import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; +import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; +import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; +import { capitalize } from 'src/utils/capitalize'; + +export class GraphqlQueryOrderFieldParser { + private fieldMetadataMap: FieldMetadataMap; + + constructor(fieldMetadataMap: FieldMetadataMap) { + this.fieldMetadataMap = fieldMetadataMap; + } + + parse(orderBy: RecordOrderBy): Record { + return orderBy.reduce( + (acc, item) => { + Object.entries(item).forEach(([key, value]) => { + const fieldMetadata = this.fieldMetadataMap[key]; + + if (!fieldMetadata || value === undefined) { + throw new GraphqlQueryRunnerException( + `Field "${key}" does not exist or is not sortable`, + GraphqlQueryRunnerExceptionCode.FIELD_NOT_FOUND, + ); + } + + if (isCompositeFieldMetadataType(fieldMetadata.type)) { + const compositeOrder = this.parseCompositeFieldForOrder( + fieldMetadata, + value, + ); + + Object.assign(acc, compositeOrder); + } else { + acc[key] = this.convertOrderByToFindOptionsOrder(value); + } + }); + + return acc; + }, + {} as Record, + ); + } + + private parseCompositeFieldForOrder( + fieldMetadata: FieldMetadataInterface, + value: any, + ): Record { + const compositeType = compositeTypeDefinitions.get( + fieldMetadata.type as CompositeFieldMetadataType, + ); + + if (!compositeType) { + throw new Error( + `Composite type definition not found for type: ${fieldMetadata.type}`, + ); + } + + return Object.entries(value).reduce( + (acc, [subFieldKey, subFieldValue]) => { + const subFieldMetadata = compositeType.properties.find( + (property) => property.name === subFieldKey, + ); + + if (!subFieldMetadata) { + throw new Error( + `Sub field metadata not found for composite type: ${fieldMetadata.type}`, + ); + } + + const fullFieldName = `${fieldMetadata.name}${capitalize(subFieldKey)}`; + + if (!this.isOrderByDirection(subFieldValue)) { + throw new Error( + `Sub field order by value must be of type OrderByDirection, but got: ${subFieldValue}`, + ); + } + acc[fullFieldName] = + this.convertOrderByToFindOptionsOrder(subFieldValue); + + return acc; + }, + {} as Record, + ); + } + + private convertOrderByToFindOptionsOrder( + direction: OrderByDirection, + ): FindOptionsOrderValue { + switch (direction) { + case OrderByDirection.AscNullsFirst: + return { direction: 'ASC', nulls: 'FIRST' }; + case OrderByDirection.AscNullsLast: + return { direction: 'ASC', nulls: 'LAST' }; + case OrderByDirection.DescNullsFirst: + return { direction: 'DESC', nulls: 'FIRST' }; + case OrderByDirection.DescNullsLast: + return { direction: 'DESC', nulls: 'LAST' }; + default: + throw new GraphqlQueryRunnerException( + `Invalid direction: ${direction}`, + GraphqlQueryRunnerExceptionCode.INVALID_DIRECTION, + ); + } + } + + private isOrderByDirection(value: unknown): value is OrderByDirection { + return Object.values(OrderByDirection).includes(value as OrderByDirection); + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query.parser.ts new file mode 100644 index 000000000..4d83a607a --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query.parser.ts @@ -0,0 +1,70 @@ +import { + FindOptionsOrderValue, + FindOptionsWhere, + ObjectLiteral, +} from 'typeorm'; + +import { + RecordFilter, + RecordOrderBy, +} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; + +import { GraphqlQueryFilterConditionParser as GraphqlQueryFilterParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/graphql-query-filter-condition.parser'; +import { GraphqlQueryOrderFieldParser as GraphqlQueryOrderParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-order/graphql-query-order.parser'; +import { GraphQLSelectedFieldsParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-selected-fields/graphql-selected-fields.parser'; +import { + FieldMetadataMap, + ObjectMetadataMap, +} from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util'; + +export class GraphqlQueryParser { + private fieldMetadataMap: FieldMetadataMap; + private objectMetadataMap: ObjectMetadataMap; + + constructor( + fieldMetadataMap: FieldMetadataMap, + objectMetadataMap: ObjectMetadataMap, + ) { + this.objectMetadataMap = objectMetadataMap; + this.fieldMetadataMap = fieldMetadataMap; + } + + parseFilter( + recordFilter: RecordFilter, + ): FindOptionsWhere | FindOptionsWhere[] { + const graphqlQueryFilterParser = new GraphqlQueryFilterParser( + this.fieldMetadataMap, + ); + + return graphqlQueryFilterParser.parse(recordFilter); + } + + parseOrder(orderBy: RecordOrderBy): Record { + const graphqlQueryOrderParser = new GraphqlQueryOrderParser( + this.fieldMetadataMap, + ); + + return graphqlQueryOrderParser.parse(orderBy); + } + + parseSelectedFields( + parentObjectMetadata: ObjectMetadataInterface, + graphqlSelectedFields: Partial>, + ): { select: Record; relations: Record } { + const parentFields = + this.objectMetadataMap[parentObjectMetadata.nameSingular]?.fields; + + if (!parentFields) { + throw new Error( + `Could not find object metadata for ${parentObjectMetadata.nameSingular}`, + ); + } + + const selectedFieldsParser = new GraphQLSelectedFieldsParser( + this.objectMetadataMap, + ); + + return selectedFieldsParser.parse(graphqlSelectedFields, parentFields); + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-selected-fields/graphql-selected-fields-relation.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-selected-fields/graphql-selected-fields-relation.parser.ts new file mode 100644 index 000000000..c4648850a --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-selected-fields/graphql-selected-fields-relation.parser.ts @@ -0,0 +1,79 @@ +import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; + +import { GraphQLSelectedFieldsParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-selected-fields/graphql-selected-fields.parser'; +import { + ObjectMetadataMap, + ObjectMetadataMapItem, +} from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util'; +import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; +import { + deduceRelationDirection, + RelationDirection, +} from 'src/engine/utils/deduce-relation-direction.util'; + +export class GraphqlSelectedFieldsRelationParser { + private objectMetadataMap: ObjectMetadataMap; + + constructor(objectMetadataMap: ObjectMetadataMap) { + this.objectMetadataMap = objectMetadataMap; + } + + parseRelationField( + fieldMetadata: FieldMetadataInterface, + fieldKey: string, + fieldValue: any, + result: { select: Record; relations: Record }, + ): void { + result.relations[fieldKey] = true; + + if (!fieldValue || typeof fieldValue !== 'object') { + return; + } + + const relationMetadata = + fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata; + + if (!relationMetadata) { + throw new Error( + `Relation metadata not found for field ${fieldMetadata.name}`, + ); + } + + const relationDirection = deduceRelationDirection( + fieldMetadata, + relationMetadata, + ); + const referencedObjectMetadata = this.getReferencedObjectMetadata( + relationMetadata, + relationDirection, + ); + + const relationFields = referencedObjectMetadata.fields; + const fieldParser = new GraphQLSelectedFieldsParser(this.objectMetadataMap); + const subResult = fieldParser.parse(fieldValue, relationFields); + + result.select[fieldKey] = { + id: true, + ...subResult.select, + }; + result.relations[fieldKey] = subResult.relations; + } + + private getReferencedObjectMetadata( + relationMetadata: RelationMetadataEntity, + relationDirection: RelationDirection, + ): ObjectMetadataMapItem { + const referencedObjectMetadata = + relationDirection === RelationDirection.TO + ? this.objectMetadataMap[relationMetadata.fromObjectMetadataId] + : this.objectMetadataMap[relationMetadata.toObjectMetadataId]; + + if (!referencedObjectMetadata) { + throw new Error( + `Referenced object metadata not found for relation ${relationMetadata.id}`, + ); + } + + return referencedObjectMetadata; + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-selected-fields/graphql-selected-fields.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-selected-fields/graphql-selected-fields.parser.ts new file mode 100644 index 000000000..bc09c12d1 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-selected-fields/graphql-selected-fields.parser.ts @@ -0,0 +1,128 @@ +import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; + +import { + GraphqlQueryRunnerException, + GraphqlQueryRunnerExceptionCode, +} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; +import { GraphqlSelectedFieldsRelationParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-selected-fields/graphql-selected-fields-relation.parser'; +import { ObjectMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util'; +import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; +import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; +import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; +import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; +import { capitalize } from 'src/utils/capitalize'; +import { isPlainObject } from 'src/utils/is-plain-object'; + +export class GraphQLSelectedFieldsParser { + private graphqlSelectedFieldsRelationParser: GraphqlSelectedFieldsRelationParser; + + constructor(objectMetadataMap: ObjectMetadataMap) { + this.graphqlSelectedFieldsRelationParser = + new GraphqlSelectedFieldsRelationParser(objectMetadataMap); + } + + parse( + graphqlSelectedFields: Partial>, + fieldMetadataMap: Record, + ): { select: Record; relations: Record } { + const result: { + select: Record; + relations: Record; + } = { + select: {}, + relations: {}, + }; + + for (const [fieldKey, fieldValue] of Object.entries( + graphqlSelectedFields, + )) { + if (this.shouldNotParseField(fieldKey)) { + continue; + } + if (this.isConnectionField(fieldKey, fieldValue)) { + const subResult = this.parse(fieldValue, fieldMetadataMap); + + Object.assign(result.select, subResult.select); + Object.assign(result.relations, subResult.relations); + continue; + } + + const fieldMetadata = fieldMetadataMap[fieldKey]; + + if (!fieldMetadata) { + throw new GraphqlQueryRunnerException( + `Field "${fieldKey}" does not exist or is not selectable`, + GraphqlQueryRunnerExceptionCode.FIELD_NOT_FOUND, + ); + } + + if (isRelationFieldMetadataType(fieldMetadata.type)) { + this.graphqlSelectedFieldsRelationParser.parseRelationField( + fieldMetadata, + fieldKey, + fieldValue, + result, + ); + } else if (isCompositeFieldMetadataType(fieldMetadata.type)) { + const compositeResult = this.parseCompositeField( + fieldMetadata, + fieldValue, + ); + + Object.assign(result.select, compositeResult); + } else { + result.select[fieldKey] = true; + } + } + + return result; + } + + private isConnectionField(fieldKey: string, fieldValue: any): boolean { + return ['edges', 'node'].includes(fieldKey) && isPlainObject(fieldValue); + } + + private shouldNotParseField(fieldKey: string): boolean { + return ['__typename', 'totalCount', 'pageInfo', 'cursor'].includes( + fieldKey, + ); + } + + private parseCompositeField( + fieldMetadata: FieldMetadataInterface, + fieldValue: any, + ): Record { + const compositeType = compositeTypeDefinitions.get( + fieldMetadata.type as CompositeFieldMetadataType, + ); + + if (!compositeType) { + throw new Error( + `Composite type definition not found for type: ${fieldMetadata.type}`, + ); + } + + return Object.keys(fieldValue) + .filter((subFieldKey) => subFieldKey !== '__typename') + .reduce( + (acc, subFieldKey) => { + const subFieldMetadata = compositeType.properties.find( + (property) => property.name === subFieldKey, + ); + + if (!subFieldMetadata) { + throw new Error( + `Sub field metadata not found for composite type: ${fieldMetadata.type}`, + ); + } + + const fullFieldName = `${fieldMetadata.name}${capitalize(subFieldKey)}`; + + acc[fullFieldName] = true; + + return acc; + }, + {} as Record, + ); + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/apply-range-filter.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/apply-range-filter.util.ts new file mode 100644 index 000000000..0af8d935c --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/apply-range-filter.util.ts @@ -0,0 +1,29 @@ +import { + FindOptionsOrderValue, + FindOptionsWhere, + LessThan, + MoreThan, + ObjectLiteral, +} from 'typeorm'; + +export const applyRangeFilter = ( + where: FindOptionsWhere, + order: Record | undefined, + cursor: Record, +): FindOptionsWhere => { + if (!order) return where; + + const orderEntries = Object.entries(order); + + orderEntries.forEach(([column, order], index) => { + if (typeof order !== 'object' || !('direction' in order)) { + return; + } + where[column] = + order.direction === 'ASC' + ? MoreThan(cursor[index]) + : LessThan(cursor[index]); + }); + + return where; +}; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/connection.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/connection.util.ts new file mode 100644 index 000000000..7adfc7944 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/connection.util.ts @@ -0,0 +1,114 @@ +import { FindOptionsOrderValue } from 'typeorm'; + +import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface'; + +import { CONNECTION_MAX_DEPTH } from 'src/engine/api/graphql/graphql-query-runner/constants/connection-max-depth.constant'; +import { + GraphqlQueryRunnerException, + GraphqlQueryRunnerExceptionCode, +} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; +import { isPlainObject } from 'src/utils/is-plain-object'; + +export const createConnection = ( + objectRecords: ObjectRecord[], + take: number, + totalCount: number, + order: Record | undefined, + depth = 0, +): IConnection => { + const edges = (objectRecords ?? []).map((objectRecord) => ({ + node: processNestedConnections( + objectRecord, + take, + totalCount, + order, + depth, + ), + cursor: encodeCursor(objectRecord, order), + })); + + return { + edges, + pageInfo: { + hasNextPage: objectRecords.length === take && totalCount > take, + hasPreviousPage: false, + startCursor: edges[0]?.cursor, + endCursor: edges[edges.length - 1]?.cursor, + }, + totalCount: totalCount, + }; +}; + +const processNestedConnections = >( + objectRecord: T, + take: number, + totalCount: number, + order: Record | undefined, + depth = 0, +): T => { + if (depth >= CONNECTION_MAX_DEPTH) { + throw new GraphqlQueryRunnerException( + `Maximum depth of ${CONNECTION_MAX_DEPTH} reached`, + GraphqlQueryRunnerExceptionCode.MAX_DEPTH_REACHED, + ); + } + + const processedObjectRecords: Record = { ...objectRecord }; + + for (const [key, value] of Object.entries(objectRecord)) { + if (Array.isArray(value)) { + if (value.length > 0 && typeof value[0] !== 'object') { + processedObjectRecords[key] = value; + } else { + processedObjectRecords[key] = createConnection( + value, + take, + value.length, + order, + depth + 1, + ); + } + } else if (value instanceof Date) { + processedObjectRecords[key] = value.toISOString(); + } else if (isPlainObject(value)) { + processedObjectRecords[key] = processNestedConnections( + value, + take, + totalCount, + order, + depth + 1, + ); + } else { + processedObjectRecords[key] = value; + } + } + + return processedObjectRecords as T; +}; + +export const decodeCursor = (cursor: string): Record => { + try { + return JSON.parse(Buffer.from(cursor, 'base64').toString()); + } catch (err) { + throw new GraphqlQueryRunnerException( + `Invalid cursor: ${cursor}`, + GraphqlQueryRunnerExceptionCode.INVALID_CURSOR, + ); + } +}; + +export const encodeCursor = ( + objectRecord: ObjectRecord, + order: Record | undefined, +): string => { + const cursor = {}; + + Object.keys(order ?? []).forEach((key) => { + cursor[key] = objectRecord[key]; + }); + + cursor['id'] = objectRecord.id; + + return Buffer.from(JSON.stringify(Object.values(cursor))).toString('base64'); +}; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util.ts new file mode 100644 index 000000000..6f470eb89 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util.ts @@ -0,0 +1,35 @@ +import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; +import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; + +export type FieldMetadataMap = Record; + +export type ObjectMetadataMapItem = Omit & { + fields: FieldMetadataMap; +}; + +export type ObjectMetadataMap = Record; + +export const convertObjectMetadataToMap = ( + objectMetadataCollection: ObjectMetadataInterface[], +): ObjectMetadataMap => { + const objectMetadataMap: ObjectMetadataMap = {}; + + for (const objectMetadata of objectMetadataCollection) { + const fieldsMap: FieldMetadataMap = {}; + + for (const fieldMetadata of objectMetadata.fields) { + fieldsMap[fieldMetadata.name] = fieldMetadata; + } + + const processedObjectMetadata: ObjectMetadataMapItem = { + ...objectMetadata, + fields: fieldsMap, + }; + + objectMetadataMap[objectMetadata.id] = processedObjectMetadata; + objectMetadataMap[objectMetadata.nameSingular] = processedObjectMetadata; + objectMetadataMap[objectMetadata.namePlural] = processedObjectMetadata; + } + + return objectMetadataMap; +}; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts index bd8153261..066886e98 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts @@ -1,5 +1,7 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { GraphqlQueryRunnerModule } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module'; import { WorkspaceQueryBuilderModule } from 'src/engine/api/graphql/workspace-query-builder/workspace-query-builder.module'; import { RecordPositionBackfillCommand } from 'src/engine/api/graphql/workspace-query-runner/commands/0-20-record-position-backfill.command'; import { workspaceQueryRunnerFactories } from 'src/engine/api/graphql/workspace-query-runner/factories'; @@ -8,6 +10,8 @@ import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; import { DuplicateModule } from 'src/engine/core-modules/duplicate/duplicate.module'; +import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { FileModule } from 'src/engine/core-modules/file/file.module'; import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; @@ -24,9 +28,12 @@ import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listen WorkspaceDataSourceModule, WorkspaceQueryHookModule, ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]), + TypeOrmModule.forFeature([FeatureFlagEntity], 'core'), AnalyticsModule, DuplicateModule, FileModule, + GraphqlQueryRunnerModule, + FeatureFlagModule, ], providers: [ WorkspaceQueryRunnerService, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts index eb91900ae..e3fd0d005 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts @@ -25,6 +25,7 @@ import { } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; +import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service'; import { WorkspaceQueryBuilderFactory } from 'src/engine/api/graphql/workspace-query-builder/workspace-query-builder.factory'; import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory'; import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory'; @@ -42,6 +43,8 @@ import { WorkspaceQueryRunnerExceptionCode, } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception'; import { DuplicateService } from 'src/engine/core-modules/duplicate/duplicate.service'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event'; import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event'; @@ -63,8 +66,8 @@ import { } from './interfaces/pg-graphql.interface'; import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-option.interface'; import { - PgGraphQLConfig, computePgGraphQLError, + PgGraphQLConfig, } from './utils/compute-pg-graphql-error.util'; @Injectable() @@ -83,6 +86,8 @@ export class WorkspaceQueryRunnerService { private readonly workspaceQueryHookService: WorkspaceQueryHookService, private readonly environmentService: EnvironmentService, private readonly duplicateService: DuplicateService, + private readonly featureFlagService: FeatureFlagService, + private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService, ) {} async findMany< @@ -96,6 +101,12 @@ export class WorkspaceQueryRunnerService { const { authContext, objectMetadataItem } = options; const start = performance.now(); + const isQueryRunnerTwentyORMEnabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsQueryRunnerTwentyORMEnabled, + authContext.workspace.id, + ); + const hookedArgs = await this.workspaceQueryHookService.executePreQueryHooks( authContext, @@ -110,6 +121,13 @@ export class WorkspaceQueryRunnerService { ResolverArgsType.FindMany, )) as FindManyResolverArgs; + if (isQueryRunnerTwentyORMEnabled) { + return this.graphqlQueryRunnerService.findManyWithTwentyOrm( + computedArgs, + options, + ); + } + const query = await this.workspaceQueryBuilderFactory.findMany( computedArgs, { @@ -119,6 +137,7 @@ export class WorkspaceQueryRunnerService { ); const result = await this.execute(query, authContext.workspace.id); + const end = performance.now(); this.logger.log( diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts index c82f3d18f..71d58e141 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts @@ -13,7 +13,6 @@ import { DataSourceService } from 'src/engine/metadata-modules/data-source/data- import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.service'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; - @Injectable() export class WorkspaceSchemaFactory { constructor( diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts index 62ca6a411..e2563b888 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts @@ -24,10 +24,8 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; -import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module'; -import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; @@ -55,7 +53,6 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy'; UserWorkspaceModule, WorkspaceModule, OnboardingModule, - TwentyORMModule.forFeature([CalendarChannelWorkspaceEntity]), WorkspaceDataSourceModule, ConnectedAccountModule, ], diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing.service.ts index e86895f9f..455b30cfe 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing.service.ts @@ -5,7 +5,7 @@ import { isDefined } from 'class-validator'; import { SubscriptionStatus } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { IsFeatureEnabledService } from 'src/engine/core-modules/feature-flag/services/is-feature-enabled.service'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; @Injectable() @@ -14,7 +14,7 @@ export class BillingService { constructor( private readonly environmentService: EnvironmentService, private readonly billingSubscriptionService: BillingSubscriptionService, - private readonly isFeatureEnabledService: IsFeatureEnabledService, + private readonly isFeatureEnabledService: FeatureFlagService, ) {} isBillingEnabled() { diff --git a/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.module.ts b/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.module.ts index 47bdee188..4bb3334b3 100644 --- a/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.module.ts +++ b/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.module.ts @@ -1,20 +1,11 @@ import { Module } from '@nestjs/common'; -import { UserModule } from 'src/engine/core-modules/user/user.module'; import { TimelineCalendarEventResolver } from 'src/engine/core-modules/calendar/timeline-calendar-event.resolver'; import { TimelineCalendarEventService } from 'src/engine/core-modules/calendar/timeline-calendar-event.service'; -import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; -import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; -import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity'; +import { UserModule } from 'src/engine/core-modules/user/user.module'; @Module({ - imports: [ - TwentyORMModule.forFeature([ - CalendarEventWorkspaceEntity, - PersonWorkspaceEntity, - ]), - UserModule, - ], + imports: [UserModule], exports: [], providers: [TimelineCalendarEventResolver, TimelineCalendarEventService], }) diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts index fd30ef5bd..ae1035253 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts @@ -11,4 +11,5 @@ export enum FeatureFlagKey { IsFunctionSettingsEnabled = 'IS_FUNCTION_SETTINGS_ENABLED', IsWorkflowEnabled = 'IS_WORKFLOW_ENABLED', IsMessageThreadSubscriberEnabled = 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED', + IsQueryRunnerTwentyORMEnabled = 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED', } diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/feature-flag.module.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/feature-flag.module.ts index 76beb418b..1394babf5 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/feature-flag.module.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/feature-flag.module.ts @@ -5,8 +5,7 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; -import { FeatureFlagFactory } from 'src/engine/core-modules/feature-flag/services/feature-flags.factory'; -import { IsFeatureEnabledService } from 'src/engine/core-modules/feature-flag/services/is-feature-enabled.service'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; @Module({ imports: [ @@ -19,7 +18,7 @@ import { IsFeatureEnabledService } from 'src/engine/core-modules/feature-flag/se resolvers: [], }), ], - exports: [IsFeatureEnabledService, FeatureFlagFactory], - providers: [IsFeatureEnabledService, FeatureFlagFactory], + exports: [FeatureFlagService], + providers: [FeatureFlagService], }) export class FeatureFlagModule {} diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/services/feature-flags.factory.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/services/feature-flag.service.ts similarity index 64% rename from packages/twenty-server/src/engine/core-modules/feature-flag/services/feature-flags.factory.ts rename to packages/twenty-server/src/engine/core-modules/feature-flag/services/feature-flag.service.ts index 856b5d11b..fc9c5038a 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/services/feature-flags.factory.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/services/feature-flag.service.ts @@ -5,16 +5,32 @@ import { Repository } from 'typeorm'; import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; @Injectable() -export class FeatureFlagFactory { +export class FeatureFlagService { constructor( @InjectRepository(FeatureFlagEntity, 'core') private readonly featureFlagRepository: Repository, ) {} - async create(workspaceId: string): Promise { + public async isFeatureEnabled( + key: FeatureFlagKey, + workspaceId: string, + ): Promise { + const featureFlag = await this.featureFlagRepository.findOneBy({ + workspaceId, + key, + value: true, + }); + + return !!featureFlag?.value; + } + + public async getWorkspaceFeatureFlags( + workspaceId: string, + ): Promise { const workspaceFeatureFlags = await this.featureFlagRepository.find({ where: { workspaceId }, }); diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/services/is-feature-enabled.service.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/services/is-feature-enabled.service.ts deleted file mode 100644 index d76020012..000000000 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/services/is-feature-enabled.service.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import { Repository } from 'typeorm'; - -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; - -@Injectable() -export class IsFeatureEnabledService { - constructor( - @InjectRepository(FeatureFlagEntity, 'core') - private readonly featureFlagRepository: Repository, - ) {} - - public async isFeatureEnabled( - key: FeatureFlagKey, - workspaceId: string, - ): Promise { - const featureFlag = await this.featureFlagRepository.findOneBy({ - workspaceId, - key, - value: true, - }); - - return !!featureFlag?.value; - } -} diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts index 1fd86d27c..570c1103f 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts @@ -6,11 +6,9 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; +import { User } from 'src/engine/core-modules/user/user.entity'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; -import { User } from 'src/engine/core-modules/user/user.entity'; -import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; -import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @Module({ imports: [ @@ -23,7 +21,6 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta ], services: [UserWorkspaceService], }), - TwentyORMModule.forFeature([WorkspaceMemberWorkspaceEntity]), ], exports: [UserWorkspaceService], providers: [UserWorkspaceService], diff --git a/packages/twenty-server/src/engine/decorators/observability/log-execution-time.decorator.ts b/packages/twenty-server/src/engine/decorators/observability/log-execution-time.decorator.ts new file mode 100644 index 000000000..3337f002b --- /dev/null +++ b/packages/twenty-server/src/engine/decorators/observability/log-execution-time.decorator.ts @@ -0,0 +1,34 @@ +import { Logger } from '@nestjs/common'; + +/** + * A decorator function that logs the execution time of the decorated method. + * + * @param target The target class of the decorated method. + * @param propertyKey The name of the decorated method. + * @param descriptor The property descriptor of the decorated method. + * @returns The modified property descriptor with the execution time logging functionality. + */ +export function LogExecutionTime() { + return function ( + target: any, + propertyKey: string, + descriptor: PropertyDescriptor, + ) { + const originalMethod = descriptor.value; + const logger = new Logger(`${target.constructor.name}:${propertyKey}`); + + descriptor.value = async function (...args: any[]) { + const start = performance.now(); + + const result = await originalMethod.apply(this, args); + const end = performance.now(); + const executionTime = end - start; + + logger.log(`Execution time: ${executionTime.toFixed(2)}ms`); + + return result; + }; + + return descriptor; + }; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts index c7e58ab8d..157110b03 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts @@ -5,7 +5,6 @@ import { registerEnumType, } from '@nestjs/graphql'; -import { GraphQLJSON } from 'graphql-type-json'; import { Authorize, FilterableField, @@ -23,17 +22,19 @@ import { IsUUID, Validate, } from 'class-validator'; +import { GraphQLJSON } from 'graphql-type-json'; -import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface'; import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; +import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface'; import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; -import { RelationMetadataDTO } from 'src/engine/metadata-modules/relation-metadata/dtos/relation-metadata.dto'; +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { IsFieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-default-value.validator'; import { IsFieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-options.validator'; -import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator'; -import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto'; +import { RelationMetadataDTO } from 'src/engine/metadata-modules/relation-metadata/dtos/relation-metadata.dto'; registerEnumType(FieldMetadataType, { name: 'FieldMetadataType', @@ -57,6 +58,9 @@ registerEnumType(FieldMetadataType, { @Relation('fromRelationMetadata', () => RelationMetadataDTO, { nullable: true, }) +@Relation('object', () => ObjectMetadataDTO, { + nullable: true, +}) export class FieldMetadataDTO< T extends FieldMetadataType | 'default' = 'default', > { diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts index 933729c43..69cde9ae7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts @@ -19,7 +19,6 @@ import { IsFieldMetadataOptions } from 'src/engine/metadata-modules/field-metada import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; -import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; import { WorkspaceStatusModule } from 'src/engine/workspace-manager/workspace-status/workspace-manager.module'; @@ -36,7 +35,6 @@ import { UpdateFieldInput } from './dtos/update-field.input'; NestjsQueryTypeOrmModule.forFeature([FieldMetadataEntity], 'metadata'), WorkspaceMigrationModule, WorkspaceStatusModule, - TwentyORMModule, WorkspaceMigrationRunnerModule, WorkspaceMetadataVersionModule, ObjectMetadataModule, diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts index 2a90e827a..fc8755ecc 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts @@ -57,7 +57,7 @@ export class EntitySchemaRelationFactory { target: relationDetails.target, inverseSide: relationDetails.inverseSide, joinColumn: relationDetails.joinColumn, - }; + } satisfies EntitySchemaRelationOptions; } return entitySchemaRelationMap; diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts index c13301020..5bf311096 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts @@ -9,11 +9,13 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.service'; import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory'; -import { workspaceDataSourceCacheInstance } from 'src/engine/twenty-orm/twenty-orm-core.module'; +import { CacheManager } from 'src/engine/twenty-orm/storage/cache-manager.storage'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; @Injectable() export class WorkspaceDatasourceFactory { + private cacheManager = new CacheManager(); + constructor( private readonly dataSourceService: DataSourceService, private readonly environmentService: EnvironmentService, @@ -48,7 +50,7 @@ export class WorkspaceDatasourceFactory { ); } - const workspaceDataSource = await workspaceDataSourceCacheInstance.execute( + const workspaceDataSource = await this.cacheManager.execute( `${workspaceId}-${latestWorkspaceMetadataVersion}`, async () => { let cachedObjectMetadataCollection = diff --git a/packages/twenty-server/src/engine/twenty-orm/twenty-orm-core.module.ts b/packages/twenty-server/src/engine/twenty-orm/twenty-orm-core.module.ts deleted file mode 100644 index 122ae0c52..000000000 --- a/packages/twenty-server/src/engine/twenty-orm/twenty-orm-core.module.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { - DynamicModule, - Global, - Logger, - Module, - OnApplicationShutdown, -} from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; - -import { - TwentyORMModuleAsyncOptions, - TwentyORMOptions, -} from 'src/engine/twenty-orm/interfaces/twenty-orm-options.interface'; - -import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; -import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; -import { entitySchemaFactories } from 'src/engine/twenty-orm/factories'; -import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory'; -import { CacheManager } from 'src/engine/twenty-orm/storage/cache-manager.storage'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; -import { ConfigurableModuleClass } from 'src/engine/twenty-orm/twenty-orm.module-definition'; -import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; - -export const workspaceDataSourceCacheInstance = - new CacheManager(); - -@Global() -@Module({ - imports: [ - TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'), - DataSourceModule, - WorkspaceCacheStorageModule, - WorkspaceMetadataVersionModule, - ], - providers: [ - ...entitySchemaFactories, - TwentyORMManager, - TwentyORMGlobalManager, - ], - exports: [EntitySchemaFactory, TwentyORMManager, TwentyORMGlobalManager], -}) -export class TwentyORMCoreModule - extends ConfigurableModuleClass - implements OnApplicationShutdown -{ - private static readonly logger = new Logger(TwentyORMCoreModule.name); - - static register(options: TwentyORMOptions): DynamicModule { - const dynamicModule = super.register(options); - - return { - ...dynamicModule, - providers: [...(dynamicModule.providers ?? [])], - exports: [...(dynamicModule.exports ?? [])], - }; - } - - static registerAsync( - asyncOptions: TwentyORMModuleAsyncOptions, - ): DynamicModule { - const dynamicModule = super.registerAsync(asyncOptions); - - return { - ...dynamicModule, - providers: [...(dynamicModule.providers ?? [])], - exports: [...(dynamicModule.exports ?? [])], - }; - } - - /** - * Destroys all data sources on application shutdown - */ - async onApplicationShutdown() { - workspaceDataSourceCacheInstance.clear((dataSource) => - dataSource.destroy(), - ); - } -} diff --git a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.module.ts b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.module.ts index ecca90357..923400c44 100644 --- a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.module.ts +++ b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.module.ts @@ -1,40 +1,28 @@ -import { DynamicModule, Global, Module } from '@nestjs/common'; -import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; +import { Global, Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; -import { - TwentyORMModuleAsyncOptions, - TwentyORMOptions, -} from 'src/engine/twenty-orm/interfaces/twenty-orm-options.interface'; +import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; +import { entitySchemaFactories } from 'src/engine/twenty-orm/factories'; +import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; +import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; -import { TwentyORMCoreModule } from 'src/engine/twenty-orm/twenty-orm-core.module'; - -// Todo: remove this file @Global() -@Module({}) -export class TwentyORMModule { - static register(options: TwentyORMOptions): DynamicModule { - return { - module: TwentyORMModule, - imports: [TwentyORMCoreModule.register(options)], - }; - } - - static forFeature(_objects: EntityClassOrSchema[] = []): DynamicModule { - const providers = []; - - return { - module: TwentyORMModule, - providers: providers, - exports: providers, - }; - } - - static registerAsync( - asyncOptions: TwentyORMModuleAsyncOptions, - ): DynamicModule { - return { - module: TwentyORMModule, - imports: [TwentyORMCoreModule.registerAsync(asyncOptions)], - }; - } -} +@Module({ + imports: [ + TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'), + DataSourceModule, + WorkspaceCacheStorageModule, + WorkspaceMetadataVersionModule, + ], + providers: [ + ...entitySchemaFactories, + TwentyORMManager, + TwentyORMGlobalManager, + ], + exports: [EntitySchemaFactory, TwentyORMManager, TwentyORMGlobalManager], +}) +export class TwentyORMModule {} diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/determine-relation-details.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/determine-relation-details.util.ts index 4a1cdb8e8..b5876783b 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/determine-relation-details.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/determine-relation-details.util.ts @@ -21,7 +21,10 @@ export async function determineRelationDetails( let fromObjectMetadata: ObjectMetadataEntity | undefined = fieldMetadata.object; let toObjectMetadata: ObjectMetadataEntity | undefined = - relationMetadata.toObjectMetadata; + objectMetadataCollection.find( + (objectMetadata) => + objectMetadata.id === relationMetadata.toObjectMetadataId, + ); // RelationMetadata always store the relation from the perspective of the `from` object, MANY_TO_ONE relations are not stored yet if (relationType === 'many-to-one') { @@ -37,6 +40,16 @@ export async function determineRelationDetails( throw new Error('Object metadata not found'); } + const toFieldMetadata = toObjectMetadata.fields.find((field) => + relationType === 'many-to-one' + ? field.id === relationMetadata.fromFieldMetadataId + : field.id === relationMetadata.toFieldMetadataId, + ); + + if (!toFieldMetadata) { + throw new Error('To field metadata not found'); + } + // TODO: Support many to many relations if (relationType === 'many-to-many') { throw new Error('Many to many relations are not supported yet'); @@ -45,7 +58,7 @@ export async function determineRelationDetails( return { relationType, target: toObjectMetadata.nameSingular, - inverseSide: fromObjectMetadata.nameSingular, + inverseSide: toFieldMetadata.name, joinColumn: // TODO: This will work for now but we need to handle this better in the future for custom names on the join column relationType === 'many-to-one' || diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts index 6dfeb5557..085316c9a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts @@ -5,7 +5,7 @@ import { DataSource, QueryFailedError } from 'typeorm'; import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; -import { FeatureFlagFactory } from 'src/engine/core-modules/feature-flag/services/feature-flags.factory'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.service'; import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; @@ -27,7 +27,7 @@ export class WorkspaceSyncMetadataService { constructor( @InjectDataSource('metadata') private readonly metadataDataSource: DataSource, - private readonly featureFlagFactory: FeatureFlagFactory, + private readonly featureFlagService: FeatureFlagService, private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, private readonly workspaceSyncObjectMetadataService: WorkspaceSyncObjectMetadataService, private readonly workspaceSyncRelationMetadataService: WorkspaceSyncRelationMetadataService, @@ -69,9 +69,10 @@ export class WorkspaceSyncMetadataService { ); // Retrieve feature flags - const workspaceFeatureFlagsMap = await this.featureFlagFactory.create( - context.workspaceId, - ); + const workspaceFeatureFlagsMap = + await this.featureFlagService.getWorkspaceFeatureFlags( + context.workspaceId, + ); this.logger.log('Syncing standard objects and fields metadata'); diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts index f23b6005e..1d3a931dd 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts @@ -6,7 +6,6 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature- import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; -import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity'; import { CalendarEventCleanerModule } from 'src/modules/calendar/calendar-event-cleaner/calendar-event-cleaner.module'; @@ -20,10 +19,6 @@ import { CalendarEventsImportService } from 'src/modules/calendar/calendar-event import { CalendarGetCalendarEventsService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service'; import { CalendarSaveEventsService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service'; import { CalendarEventParticipantManagerModule } from 'src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module'; -import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity'; -import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; -import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; -import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity'; import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module'; import { RefreshAccessTokenManagerModule } from 'src/modules/connected-account/refresh-access-token-manager/refresh-access-token-manager.module'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; @@ -31,12 +26,6 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta @Module({ imports: [ - TwentyORMModule.forFeature([ - CalendarEventWorkspaceEntity, - CalendarChannelWorkspaceEntity, - CalendarChannelEventAssociationWorkspaceEntity, - CalendarEventParticipantWorkspaceEntity, - ]), ObjectMetadataRepositoryModule.forFeature([ ConnectedAccountWorkspaceEntity, BlocklistWorkspaceEntity, diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module.ts b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module.ts index 4de8e6f5e..10f67b7cb 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module.ts @@ -7,7 +7,6 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { CalendarCreateCompanyAndContactAfterSyncJob } from 'src/modules/calendar/calendar-event-participant-manager/jobs/calendar-create-company-and-contact-after-sync.job'; import { CalendarEventParticipantMatchParticipantJob } from 'src/modules/calendar/calendar-event-participant-manager/jobs/calendar-event-participant-match-participant.job'; @@ -16,7 +15,6 @@ import { CalendarEventParticipantPersonListener } from 'src/modules/calendar/cal import { CalendarEventParticipantWorkspaceMemberListener } from 'src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-workspace-member.listener'; import { CalendarEventParticipantListener } from 'src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant.listener'; import { CalendarEventParticipantService } from 'src/modules/calendar/calendar-event-participant-manager/services/calendar-event-participant.service'; -import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; import { ContactCreationManagerModule } from 'src/modules/contact-creation-manager/contact-creation-manager.module'; import { MatchParticipantModule } from 'src/modules/match-participant/match-participant.module'; @@ -24,7 +22,6 @@ import { MatchParticipantModule } from 'src/modules/match-participant/match-part imports: [ WorkspaceDataSourceModule, WorkspaceModule, - TwentyORMModule.forFeature([CalendarEventParticipantWorkspaceEntity]), TypeOrmModule.forFeature( [ObjectMetadataEntity, FieldMetadataEntity], 'metadata', diff --git a/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-query-hook.module.ts b/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-query-hook.module.ts index e3a16adf8..ff3d178c4 100644 --- a/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-query-hook.module.ts +++ b/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-query-hook.module.ts @@ -1,21 +1,14 @@ import { Module } from '@nestjs/common'; import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; -import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; -import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; -import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { CalendarEventFindManyPreQueryHook } from 'src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook'; import { CalendarEventFindOnePreQueryHook } from 'src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook'; import { CanAccessCalendarEventService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/can-access-calendar-event.service'; -import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity'; -import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; +import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; +import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @Module({ imports: [ - TwentyORMModule.forFeature([ - CalendarChannelEventAssociationWorkspaceEntity, - CalendarChannelWorkspaceEntity, - ]), ObjectMetadataRepositoryModule.forFeature([ ConnectedAccountWorkspaceEntity, WorkspaceMemberWorkspaceEntity, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts index c908b202d..d54fe13b5 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts @@ -5,12 +5,10 @@ import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; -import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { EmailAliasManagerModule } from 'src/modules/connected-account/email-alias-manager/email-alias-manager.module'; import { RefreshAccessTokenManagerModule } from 'src/modules/connected-account/refresh-access-token-manager/refresh-access-token-manager.module'; import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module'; -import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { MessagingSingleMessageImportCommand } from 'src/modules/messaging/message-import-manager/commands/messaging-single-message-import.command'; import { MessagingMessageListFetchCronCommand } from 'src/modules/messaging/message-import-manager/crons/commands/messaging-message-list-fetch.cron.command'; import { MessagingMessagesImportCronCommand } from 'src/modules/messaging/message-import-manager/crons/commands/messaging-messages-import.cron.command'; @@ -42,7 +40,6 @@ import { MessagingMonitoringModule } from 'src/modules/messaging/monitoring/mess MessagingCommonModule, TypeOrmModule.forFeature([Workspace], 'core'), TypeOrmModule.forFeature([DataSourceEntity], 'metadata'), - TwentyORMModule.forFeature([MessageChannelWorkspaceEntity]), BillingModule, EmailAliasManagerModule, FeatureFlagModule, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-messages-import.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-messages-import.service.ts index 968140bc1..ace2f6a8c 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-messages-import.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-messages-import.service.ts @@ -1,7 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { IsFeatureEnabledService } from 'src/engine/core-modules/feature-flag/services/is-feature-enabled.service'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { CacheStorageService } from 'src/engine/integrations/cache-storage/cache-storage.service'; import { InjectCacheStorage } from 'src/engine/integrations/cache-storage/decorators/cache-storage.decorator'; import { CacheStorageNamespace } from 'src/engine/integrations/cache-storage/types/cache-storage-namespace.enum'; @@ -41,7 +41,7 @@ export class MessagingMessagesImportService { @InjectObjectMetadataRepository(BlocklistWorkspaceEntity) private readonly blocklistRepository: BlocklistRepository, private readonly emailAliasManagerService: EmailAliasManagerService, - private readonly isFeatureEnabledService: IsFeatureEnabledService, + private readonly isFeatureEnabledService: FeatureFlagService, @InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity) private readonly connectedAccountRepository: ConnectedAccountRepository, private readonly twentyORMManager: TwentyORMManager, diff --git a/packages/twenty-server/src/modules/messaging/message-participant-manager/message-participant-manager.module.ts b/packages/twenty-server/src/modules/messaging/message-participant-manager/message-participant-manager.module.ts index d31454791..d1efb3688 100644 --- a/packages/twenty-server/src/modules/messaging/message-participant-manager/message-participant-manager.module.ts +++ b/packages/twenty-server/src/modules/messaging/message-participant-manager/message-participant-manager.module.ts @@ -6,9 +6,7 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature- import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; -import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; -import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { ContactCreationManagerModule } from 'src/modules/contact-creation-manager/contact-creation-manager.module'; import { MatchParticipantModule } from 'src/modules/match-participant/match-participant.module'; import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module'; @@ -31,7 +29,6 @@ import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-o TimelineActivityWorkspaceEntity, ]), TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'), - TwentyORMModule.forFeature([CalendarChannelWorkspaceEntity]), MessagingCommonModule, MatchParticipantModule, ], diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/listeners/database-event-trigger.listener.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/listeners/database-event-trigger.listener.ts index 5291b8094..a19a99098 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/listeners/database-event-trigger.listener.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/listeners/database-event-trigger.listener.ts @@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { IsFeatureEnabledService } from 'src/engine/core-modules/feature-flag/services/is-feature-enabled.service'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event'; import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event'; import { ObjectRecordUpdateEvent } from 'src/engine/integrations/event-emitter/types/object-record-update.event'; @@ -25,7 +25,7 @@ export class DatabaseEventTriggerListener { private readonly twentyORMGlobalManager: TwentyORMGlobalManager, @InjectMessageQueue(MessageQueue.workflowQueue) private readonly messageQueueService: MessageQueueService, - private readonly isFeatureFlagEnabledService: IsFeatureEnabledService, + private readonly isFeatureFlagEnabledService: FeatureFlagService, ) {} @OnEvent('*.created') diff --git a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-query-hook.module.ts b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-query-hook.module.ts index 051354c6b..7359096dd 100644 --- a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-query-hook.module.ts +++ b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-query-hook.module.ts @@ -1,18 +1,9 @@ import { Module } from '@nestjs/common'; -import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; -import { CommentWorkspaceEntity } from 'src/modules/activity/standard-objects/comment.workspace-entity'; -import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; import { WorkspaceMemberDeleteManyPreQueryHook } from 'src/modules/workspace-member/query-hooks/workspace-member-delete-many.pre-query.hook'; import { WorkspaceMemberDeleteOnePreQueryHook } from 'src/modules/workspace-member/query-hooks/workspace-member-delete-one.pre-query.hook'; @Module({ - imports: [ - TwentyORMModule.forFeature([ - AttachmentWorkspaceEntity, - CommentWorkspaceEntity, - ]), - ], providers: [ WorkspaceMemberDeleteOnePreQueryHook, WorkspaceMemberDeleteManyPreQueryHook, diff --git a/packages/twenty-server/src/queue-worker/queue-worker.module.ts b/packages/twenty-server/src/queue-worker/queue-worker.module.ts index c6eb553d2..bef26e3a9 100644 --- a/packages/twenty-server/src/queue-worker/queue-worker.module.ts +++ b/packages/twenty-server/src/queue-worker/queue-worker.module.ts @@ -8,11 +8,11 @@ import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/ @Module({ imports: [ - TwentyORMModule.register({}), IntegrationsModule, MessageQueueModule.registerExplorer(), WorkspaceEventEmitterModule, JobsModule, + TwentyORMModule, ], }) export class QueueWorkerModule {} diff --git a/packages/twenty-server/src/utils/is-plain-object.ts b/packages/twenty-server/src/utils/is-plain-object.ts index bc9496ccb..5b7518455 100644 --- a/packages/twenty-server/src/utils/is-plain-object.ts +++ b/packages/twenty-server/src/utils/is-plain-object.ts @@ -1,9 +1,5 @@ -export const isPlainObject = (value: T): boolean => { - if (Object.prototype.toString.call(value) !== '[object Object]') { - return false; - } - - const prototype = Object.getPrototypeOf(value); - - return prototype === null || prototype === Object.prototype; +export const isPlainObject = ( + input: unknown, +): input is Record => { + return typeof input === 'object' && input !== null && !Array.isArray(input); };