From a79937048383d4911d6d47b8d16e76cd0e855614 Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Thu, 14 Nov 2024 18:05:05 +0100 Subject: [PATCH] Aggregated queries #1 (#8345) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First step of https://github.com/twentyhq/twenty/issues/6868 Adds min.., max.. queries for DATETIME fields adds min.., max.., avg.., sum.. queries for NUMBER fields (count distinct operation and composite fields such as CURRENCY handling will be dealt with in a future PR) Capture d’écran 2024-11-06 à 15 48 46 --------- Co-authored-by: Charles Bochet Co-authored-by: Weiko --- .github/workflows/ci-tinybird.yaml | 31 +- .../components/MainNavigationDrawerItems.tsx | 18 +- .../modules/workspace/types/FeatureFlagKey.ts | 5 +- .../typeorm-seeds/core/feature-flags.ts | 10 +- .../graphql-query-filter-condition.parser.ts | 18 +- .../graphql-query-filter-field.parser.ts | 10 +- .../graphql-query-order.parser.ts | 16 +- ...raphql-selected-fields-aggregate.parser.ts | 30 ++ ...graphql-selected-fields-relation.parser.ts | 32 +- .../graphql-selected-fields.parser.ts | 104 ++-- .../graphql-query.parser.ts | 48 +- .../graphql-query-runner.service.ts | 168 ++++--- ...ct-records-to-graphql-connection.helper.ts | 96 +++- .../helpers/process-aggregate.helper.ts | 37 ++ .../process-nested-relations.helper.ts | 455 +++++++++++++----- ...phql-query-create-many-resolver.service.ts | 55 ++- ...hql-query-destroy-many-resolver.service.ts | 51 +- ...phql-query-destroy-one-resolver.service.ts | 49 +- ...-query-find-duplicates-resolver.service.ts | 74 +-- ...raphql-query-find-many-resolver.service.ts | 140 +++--- ...graphql-query-find-one-resolver.service.ts | 58 +-- .../graphql-query-search-resolver.service.ts | 37 +- ...phql-query-update-many-resolver.service.ts | 62 +-- ...aphql-query-update-one-resolver.service.ts | 60 +-- .../services/api-event-emitter.service.ts | 18 +- .../utils/compute-cursor-arg-filter.ts | 24 +- .../utils/cursors.util.ts | 12 +- .../get-object-metadata-or-throw.util.ts | 21 - .../get-relation-object-metadata.util.ts | 8 +- ...nterface.ts => object-record.interface.ts} | 12 +- .../query-runner-args.factory.spec.ts | 59 ++- .../factories/query-runner-args.factory.ts | 97 ++-- .../interfaces/pg-graphql.interface.ts | 15 - .../query-runner-option.interface.ts | 16 +- .../listeners/telemetry.listener.ts | 4 +- .../utils/with-soft-deleted.util.ts | 4 +- ...nner-graphql-api-exception-handler.util.ts | 29 +- .../workspace-query-hook.service.ts | 6 +- .../factories/create-many-resolver.factory.ts | 8 +- .../factories/create-one-resolver.factory.ts | 8 +- .../factories/delete-many-resolver.factory.ts | 8 +- .../factories/delete-one-resolver.factory.ts | 8 +- .../destroy-many-resolver.factory.ts | 8 +- .../factories/destroy-one-resolver.factory.ts | 8 +- .../find-duplicates-resolver.factory.ts | 8 +- .../factories/find-many-resolver.factory.ts | 8 +- .../factories/find-one-resolver.factory.ts | 8 +- .../restore-many-resolver.factory.ts | 8 +- .../factories/search-resolver-factory.ts | 8 +- .../factories/update-many-resolver.factory.ts | 8 +- .../factories/update-one-resolver.factory.ts | 8 +- .../interfaces/pg-graphql.interface.ts | 14 - .../workspace-resolvers-builder.interface.ts | 24 +- .../workspace-resolver.factory.ts | 23 +- .../factories/aggregation-type.factory.ts | 32 ++ .../connection-type-definition.factory.ts | 19 +- .../factories/factories.ts | 26 +- .../graphql-types/scalars/date-time.scalar.ts | 38 -- .../graphql-types/scalars/index.ts | 9 +- ...kspace-schema-builder-context.interface.ts | 16 +- ...le-aggregations-from-object-fields.util.ts | 84 ++++ .../workspace-graphql-schema.factory.ts | 4 +- .../api/graphql/workspace-schema.factory.ts | 13 +- .../utils/check-order-by.utils.ts | 4 +- .../__tests__/order-by-input.factory.spec.ts | 2 +- .../input-factories/order-by-input.factory.ts | 6 +- .../constants/duplicate-criteria.constants.ts | 4 +- .../object-record-changed-properties.util.ts | 6 +- .../utils/object-record-changed-values.ts | 14 +- .../enums/feature-flag-key.enum.ts | 3 +- .../utils/__tests__/parameters.utils.spec.ts | 2 +- .../open-api/utils/parameters.utils.ts | 2 +- .../relation-metadata.service.ts | 18 +- .../types/field-metadata-map.ts | 3 + .../object-metadata-item-with-field-maps.ts | 8 + .../types/object-metadata-maps.ts | 7 + .../generate-object-metadata-map.util.ts | 36 -- .../generate-object-metadata-maps.util.ts | 39 ++ .../workspace-metadata-cache.service.ts | 12 +- .../factories/entity-schema-column.factory.ts | 6 +- .../entity-schema-relation.factory.ts | 14 +- .../factories/entity-schema.factory.ts | 18 +- .../factories/workspace-datasource.factory.ts | 23 +- .../workspace-internal-context.interface.ts | 4 +- .../repository/workspace.repository.ts | 14 +- .../utils/determine-relation-details.util.ts | 15 +- .../twenty-orm/utils/format-data.util.ts | 10 +- .../twenty-orm/utils/format-result.util.ts | 39 +- ...get-composite-field-metadata-collection.ts | 10 +- .../workspace-cache-storage.service.ts | 24 +- .../timeline-activity.repository.ts | 12 +- .../generate-fake-object-record-event.ts | 2 +- .../folder-architecture-server.mdx | 4 - 93 files changed, 1584 insertions(+), 1172 deletions(-) create mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-aggregate.parser.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-object-metadata-or-throw.util.ts rename packages/twenty-server/src/engine/api/graphql/workspace-query-builder/interfaces/{record.interface.ts => object-record.interface.ts} (57%) delete mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-query-runner/interfaces/pg-graphql.interface.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/pg-graphql.interface.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/aggregation-type.factory.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/date-time.scalar.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/types/field-metadata-map.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/types/object-metadata-item-with-field-maps.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/types/object-metadata-maps.ts delete mode 100644 packages/twenty-server/src/engine/metadata-modules/utils/generate-object-metadata-map.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/utils/generate-object-metadata-maps.util.ts diff --git a/.github/workflows/ci-tinybird.yaml b/.github/workflows/ci-tinybird.yaml index 44328556408c..46849e3150e0 100644 --- a/.github/workflows/ci-tinybird.yaml +++ b/.github/workflows/ci-tinybird.yaml @@ -3,8 +3,14 @@ on: push: branches: - main + paths: + - 'package.json' + - 'packages/twenty-tinybird/**' pull_request: + paths: + - 'package.json' + - 'packages/twenty-tinybird/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -12,24 +18,9 @@ concurrency: jobs: ci: - timeout-minutes: 10 - runs-on: ubuntu-latest uses: tinybirdco/ci/.github/workflows/ci.yml@main - steps: - - name: Check for changed files - id: changed-files - uses: tj-actions/changed-files@v11 - with: - files: | - package.json - packages/twenty-tinybird/** - - - name: Skip if no relevant changes - if: steps.changed-files.outputs.any_changed == 'false' - run: echo "No relevant changes. Skipping CI." - - - name: Check twenty-tinybird package - with: - data_project_dir: packages/twenty-tinybird - tb_admin_token: ${{ secrets.TB_ADMIN_TOKEN }} - tb_host: https://api.eu-central-1.aws.tinybird.co + with: + data_project_dir: packages/twenty-tinybird + secrets: + tb_admin_token: ${{ secrets.TB_ADMIN_TOKEN }} + tb_host: https://api.eu-central-1.aws.tinybird.co diff --git a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx index 0169ac150c4c..fa3c813981e0 100644 --- a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx +++ b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx @@ -13,7 +13,6 @@ import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNaviga import { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import styled from '@emotion/styled'; const StyledMainSection = styled(NavigationDrawerSection)` @@ -27,9 +26,7 @@ export const MainNavigationDrawerItems = () => { const setNavigationMemorizedUrl = useSetRecoilState( navigationMemorizedUrlState, ); - const isWorkspaceFavoriteEnabled = useIsFeatureEnabled( - 'IS_WORKSPACE_FAVORITE_ENABLED', - ); + const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] = useRecoilState(isNavigationDrawerExpandedState); const setNavigationDrawerExpandedMemorized = useSetRecoilState( @@ -58,18 +55,9 @@ export const MainNavigationDrawerItems = () => { /> )} - - {isWorkspaceFavoriteEnabled && } - + - - {isWorkspaceFavoriteEnabled ? ( - - ) : ( - - )} + ); diff --git a/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts b/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts index c8f427d8cff6..337f252be7de 100644 --- a/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts +++ b/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts @@ -9,12 +9,11 @@ export type FeatureFlagKey = | 'IS_FREE_ACCESS_ENABLED' | 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED' | 'IS_WORKFLOW_ENABLED' - | 'IS_WORKSPACE_FAVORITE_ENABLED' - | 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED' | 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED' | 'IS_ANALYTICS_V2_ENABLED' | 'IS_SSO_ENABLED' | 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_ARRAY_AND_JSON_FILTER_ENABLED' | 'IS_MICROSOFT_SYNC_ENABLED' - | 'IS_ADVANCED_FILTERS_ENABLED'; + | 'IS_ADVANCED_FILTERS_ENABLED' + | 'IS_AGGREGATE_QUERY_ENABLED'; diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts index 0c75053e1560..064d1794600a 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts @@ -50,11 +50,6 @@ export const seedFeatureFlags = async ( workspaceId: workspaceId, value: false, }, - { - key: FeatureFlagKey.IsWorkspaceFavoriteEnabled, - workspaceId: workspaceId, - value: true, - }, { key: FeatureFlagKey.IsAnalyticsV2Enabled, workspaceId: workspaceId, @@ -85,6 +80,11 @@ export const seedFeatureFlags = async ( workspaceId: workspaceId, value: false, }, + { + key: FeatureFlagKey.IsAggregateQueryEnabled, + workspaceId: workspaceId, + value: false, + }, ]) .execute(); }; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts index 21f9bdbdccb0..2b7949613f1e 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts @@ -5,27 +5,27 @@ import { WhereExpressionBuilder, } from 'typeorm'; -import { RecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; -import { FieldMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; import { GraphqlQueryFilterFieldParser } from './graphql-query-filter-field.parser'; export class GraphqlQueryFilterConditionParser { - private fieldMetadataMap: FieldMetadataMap; + private fieldMetadataMapByName: FieldMetadataMap; private queryFilterFieldParser: GraphqlQueryFilterFieldParser; - constructor(fieldMetadataMap: FieldMetadataMap) { - this.fieldMetadataMap = fieldMetadataMap; + constructor(fieldMetadataMapByName: FieldMetadataMap) { + this.fieldMetadataMapByName = fieldMetadataMapByName; this.queryFilterFieldParser = new GraphqlQueryFilterFieldParser( - this.fieldMetadataMap, + this.fieldMetadataMapByName, ); } public parse( queryBuilder: SelectQueryBuilder, objectNameSingular: string, - filter: Partial, + filter: Partial, ): SelectQueryBuilder { if (!filter || Object.keys(filter).length === 0) { return queryBuilder; @@ -50,7 +50,7 @@ export class GraphqlQueryFilterConditionParser { switch (key) { case 'and': { const andWhereCondition = new Brackets((qb) => { - value.forEach((filter: RecordFilter, index: number) => { + value.forEach((filter: ObjectRecordFilter, index: number) => { const whereCondition = new Brackets((qb2) => { Object.entries(filter).forEach( ([subFilterkey, subFilterValue], index) => { @@ -82,7 +82,7 @@ export class GraphqlQueryFilterConditionParser { } case 'or': { const orWhereCondition = new Brackets((qb) => { - value.forEach((filter: RecordFilter, index: number) => { + value.forEach((filter: ObjectRecordFilter, index: number) => { const whereCondition = new Brackets((qb2) => { Object.entries(filter).forEach( ([subFilterkey, subFilterValue], index) => { diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts index 5d35ebf5ecba..95fb650826a4 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts @@ -9,17 +9,17 @@ import { import { computeWhereConditionParts } from 'src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts'; 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 { FieldMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; import { capitalize } from 'src/utils/capitalize'; const ARRAY_OPERATORS = ['in', 'contains', 'not_contains']; export class GraphqlQueryFilterFieldParser { - private fieldMetadataMap: FieldMetadataMap; + private fieldMetadataMapByName: FieldMetadataMap; - constructor(fieldMetadataMap: FieldMetadataMap) { - this.fieldMetadataMap = fieldMetadataMap; + constructor(fieldMetadataMapByName: FieldMetadataMap) { + this.fieldMetadataMapByName = fieldMetadataMapByName; } public parse( @@ -29,7 +29,7 @@ export class GraphqlQueryFilterFieldParser { filterValue: any, isFirst = false, ): void { - const fieldMetadata = this.fieldMetadataMap[`${key}`]; + const fieldMetadata = this.fieldMetadataMapByName[`${key}`]; if (!fieldMetadata) { throw new Error(`Field metadata not found for field: ${key}`); diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts index aaa242d804aa..a16a9c0c149c 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts @@ -1,7 +1,7 @@ import { + ObjectRecordOrderBy, OrderByDirection, - RecordOrderBy, -} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { @@ -10,25 +10,25 @@ import { } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; 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 { FieldMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; 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; + private fieldMetadataMapByName: FieldMetadataMap; - constructor(fieldMetadataMap: FieldMetadataMap) { - this.fieldMetadataMap = fieldMetadataMap; + constructor(fieldMetadataMapByName: FieldMetadataMap) { + this.fieldMetadataMapByName = fieldMetadataMapByName; } parse( - orderBy: RecordOrderBy, + orderBy: ObjectRecordOrderBy, objectNameSingular: string, isForwardPagination = true, ): Record { return orderBy.reduce( (acc, item) => { Object.entries(item).forEach(([key, value]) => { - const fieldMetadata = this.fieldMetadataMap[key]; + const fieldMetadata = this.fieldMetadataMapByName[key]; if (!fieldMetadata || value === undefined) { throw new GraphqlQueryRunnerException( diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-aggregate.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-aggregate.parser.ts new file mode 100644 index 000000000000..fcd3ed6a1458 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-aggregate.parser.ts @@ -0,0 +1,30 @@ +import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; + +import { GraphqlQuerySelectedFieldsResult } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser'; +import { + AggregationField, + getAvailableAggregationsFromObjectFields, +} from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; + +export class GraphqlQuerySelectedFieldsAggregateParser { + parse( + graphqlSelectedFields: Partial>, + fieldMetadataMapByName: Record, + accumulator: GraphqlQuerySelectedFieldsResult, + ): void { + const availableAggregations: Record = + getAvailableAggregationsFromObjectFields( + Object.values(fieldMetadataMapByName), + ); + + for (const selectedField of Object.keys(graphqlSelectedFields)) { + const selectedAggregation = availableAggregations[selectedField]; + + if (!selectedAggregation) { + continue; + } + + accumulator.aggregate[selectedField] = selectedAggregation; + } + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser.ts index 19308a44989c..54f294acb455 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser.ts @@ -1,43 +1,47 @@ import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; -import { GraphqlQuerySelectedFieldsParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser'; +import { + GraphqlQuerySelectedFieldsParser, + GraphqlQuerySelectedFieldsResult, +} from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser'; import { getRelationObjectMetadata } from 'src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util'; -import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; export class GraphqlQuerySelectedFieldsRelationParser { - private objectMetadataMap: ObjectMetadataMap; + private objectMetadataMaps: ObjectMetadataMaps; - constructor(objectMetadataMap: ObjectMetadataMap) { - this.objectMetadataMap = objectMetadataMap; + constructor(objectMetadataMaps: ObjectMetadataMaps) { + this.objectMetadataMaps = objectMetadataMaps; } parseRelationField( fieldMetadata: FieldMetadataInterface, fieldKey: string, fieldValue: any, - result: { select: Record; relations: Record }, + accumulator: GraphqlQuerySelectedFieldsResult, ): void { if (!fieldValue || typeof fieldValue !== 'object') { return; } - result.relations[fieldKey] = true; + accumulator.relations[fieldKey] = true; const referencedObjectMetadata = getRelationObjectMetadata( fieldMetadata, - this.objectMetadataMap, + this.objectMetadataMaps, ); - const relationFields = referencedObjectMetadata.fields; + const relationFields = referencedObjectMetadata.fieldsByName; const fieldParser = new GraphqlQuerySelectedFieldsParser( - this.objectMetadataMap, + this.objectMetadataMaps, ); - const subResult = fieldParser.parse(fieldValue, relationFields); + const relationAccumulator = fieldParser.parse(fieldValue, relationFields); - result.select[fieldKey] = { + accumulator.select[fieldKey] = { id: true, - ...subResult.select, + ...relationAccumulator.select, }; - result.relations[fieldKey] = subResult.relations; + accumulator.relations[fieldKey] = relationAccumulator.relations; + accumulator.aggregate[fieldKey] = relationAccumulator.aggregate; } } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser.ts index d1b69345fee2..5c35b1eff0c4 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser.ts @@ -1,59 +1,71 @@ 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 { GraphqlQuerySelectedFieldsAggregateParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-aggregate.parser'; import { GraphqlQuerySelectedFieldsRelationParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser'; 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 { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; 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 type GraphqlQuerySelectedFieldsResult = { + select: Record; + relations: Record; + aggregate: Record; +}; export class GraphqlQuerySelectedFieldsParser { private graphqlQuerySelectedFieldsRelationParser: GraphqlQuerySelectedFieldsRelationParser; + private aggregateParser: GraphqlQuerySelectedFieldsAggregateParser; - constructor(objectMetadataMap: ObjectMetadataMap) { + constructor(objectMetadataMaps: ObjectMetadataMaps) { this.graphqlQuerySelectedFieldsRelationParser = - new GraphqlQuerySelectedFieldsRelationParser(objectMetadataMap); + new GraphqlQuerySelectedFieldsRelationParser(objectMetadataMaps); + this.aggregateParser = new GraphqlQuerySelectedFieldsAggregateParser(); } parse( graphqlSelectedFields: Partial>, - fieldMetadataMap: Record, - ): { select: Record; relations: Record } { - const result: { - select: Record; - relations: Record; - } = { + fieldMetadataMapByName: Record, + ): GraphqlQuerySelectedFieldsResult { + const accumulator: GraphqlQuerySelectedFieldsResult = { select: {}, relations: {}, + aggregate: {}, }; - for (const [fieldKey, fieldValue] of Object.entries( + if (this.isRootConnection(graphqlSelectedFields)) { + this.parseConnectionField( + graphqlSelectedFields, + fieldMetadataMapByName, + accumulator, + ); + + return accumulator; + } + + this.parseRecordField( graphqlSelectedFields, - )) { - if (this.shouldNotParseField(fieldKey)) { - continue; - } - if (this.isConnectionField(fieldKey, fieldValue)) { - const subResult = this.parse(fieldValue, fieldMetadataMap); + fieldMetadataMapByName, + accumulator, + ); - Object.assign(result.select, subResult.select); - Object.assign(result.relations, subResult.relations); - continue; - } + return accumulator; + } - const fieldMetadata = fieldMetadataMap[fieldKey]; + private parseRecordField( + graphqlSelectedFields: Partial>, + fieldMetadataMapByName: Record, + accumulator: GraphqlQuerySelectedFieldsResult, + ): void { + for (const [fieldKey, fieldValue] of Object.entries( + graphqlSelectedFields, + )) { + const fieldMetadata = fieldMetadataMapByName[fieldKey]; if (!fieldMetadata) { - throw new GraphqlQueryRunnerException( - `Field "${fieldKey}" does not exist or is not selectable`, - GraphqlQueryRunnerExceptionCode.FIELD_NOT_FOUND, - ); + continue; } if (isRelationFieldMetadataType(fieldMetadata.type)) { @@ -61,7 +73,7 @@ export class GraphqlQuerySelectedFieldsParser { fieldMetadata, fieldKey, fieldValue, - result, + accumulator, ); } else if (isCompositeFieldMetadataType(fieldMetadata.type)) { const compositeResult = this.parseCompositeField( @@ -69,23 +81,33 @@ export class GraphqlQuerySelectedFieldsParser { fieldValue, ); - Object.assign(result.select, compositeResult); + Object.assign(accumulator.select, compositeResult); } else { - result.select[fieldKey] = true; + accumulator.select[fieldKey] = true; } } - - return result; } - private isConnectionField(fieldKey: string, fieldValue: any): boolean { - return ['edges', 'node'].includes(fieldKey) && isPlainObject(fieldValue); + private parseConnectionField( + graphqlSelectedFields: Partial>, + fieldMetadataMapByName: Record, + accumulator: GraphqlQuerySelectedFieldsResult, + ): void { + this.aggregateParser.parse( + graphqlSelectedFields, + fieldMetadataMapByName, + accumulator, + ); + + const node = graphqlSelectedFields.edges.node; + + this.parseRecordField(node, fieldMetadataMapByName, accumulator); } - private shouldNotParseField(fieldKey: string): boolean { - return ['__typename', 'totalCount', 'pageInfo', 'cursor'].includes( - fieldKey, - ); + private isRootConnection( + graphqlSelectedFields: Partial>, + ): boolean { + return Object.keys(graphqlSelectedFields).includes('edges'); } private parseCompositeField( diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts index 0aa047fc31f7..aa7700c5678d 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts @@ -6,43 +6,44 @@ import { } from 'typeorm'; import { - RecordFilter, - RecordOrderBy, -} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; + ObjectRecordFilter, + ObjectRecordOrderBy, +} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { GraphqlQueryFilterConditionParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser'; import { GraphqlQueryOrderFieldParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser'; -import { GraphqlQuerySelectedFieldsParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser'; import { - FieldMetadataMap, - ObjectMetadataMap, - ObjectMetadataMapItem, -} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; + GraphqlQuerySelectedFieldsParser, + GraphqlQuerySelectedFieldsResult, +} from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser'; +import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; export class GraphqlQueryParser { - private fieldMetadataMap: FieldMetadataMap; - private objectMetadataMap: ObjectMetadataMap; + private fieldMetadataMapByName: FieldMetadataMap; + private objectMetadataMaps: ObjectMetadataMaps; private filterConditionParser: GraphqlQueryFilterConditionParser; private orderFieldParser: GraphqlQueryOrderFieldParser; constructor( - fieldMetadataMap: FieldMetadataMap, - objectMetadataMap: ObjectMetadataMap, + fieldMetadataMapByName: FieldMetadataMap, + objectMetadataMaps: ObjectMetadataMaps, ) { - this.objectMetadataMap = objectMetadataMap; - this.fieldMetadataMap = fieldMetadataMap; + this.objectMetadataMaps = objectMetadataMaps; + this.fieldMetadataMapByName = fieldMetadataMapByName; this.filterConditionParser = new GraphqlQueryFilterConditionParser( - this.fieldMetadataMap, + this.fieldMetadataMapByName, ); this.orderFieldParser = new GraphqlQueryOrderFieldParser( - this.fieldMetadataMap, + this.fieldMetadataMapByName, ); } public applyFilterToBuilder( queryBuilder: SelectQueryBuilder, objectNameSingular: string, - recordFilter: Partial, + recordFilter: Partial, ): SelectQueryBuilder { return this.filterConditionParser.parse( queryBuilder, @@ -53,7 +54,7 @@ export class GraphqlQueryParser { public applyDeletedAtToBuilder( queryBuilder: SelectQueryBuilder, - recordFilter: RecordFilter, + recordFilter: ObjectRecordFilter, ): SelectQueryBuilder { if (this.checkForDeletedAtFilter(recordFilter)) { queryBuilder.withDeleted(); @@ -90,7 +91,7 @@ export class GraphqlQueryParser { public applyOrderToBuilder( queryBuilder: SelectQueryBuilder, - orderBy: RecordOrderBy, + orderBy: ObjectRecordOrderBy, objectNameSingular: string, isForwardPagination = true, ): SelectQueryBuilder { @@ -104,11 +105,12 @@ export class GraphqlQueryParser { } public parseSelectedFields( - parentObjectMetadata: ObjectMetadataMapItem, + parentObjectMetadata: ObjectMetadataItemWithFieldMaps, graphqlSelectedFields: Partial>, - ): { select: Record; relations: Record } { + ): GraphqlQuerySelectedFieldsResult { const parentFields = - this.objectMetadataMap[parentObjectMetadata.nameSingular]?.fields; + this.objectMetadataMaps.byNameSingular[parentObjectMetadata.nameSingular] + ?.fieldsByName; if (!parentFields) { throw new Error( @@ -117,7 +119,7 @@ export class GraphqlQueryParser { } const selectedFieldsParser = new GraphqlQuerySelectedFieldsParser( - this.objectMetadataMap, + this.objectMetadataMaps, ); return selectedFieldsParser.parse(graphqlSelectedFields, parentFields); 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 index c3fe76e2e07b..0e02201065ad 100644 --- 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 @@ -1,10 +1,10 @@ import { Injectable } from '@nestjs/common'; import { - Record as IRecord, - RecordFilter, - RecordOrderBy, -} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; + ObjectRecord, + ObjectRecordFilter, + ObjectRecordOrderBy, +} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface'; import { IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface'; import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; @@ -48,11 +48,11 @@ export class GraphqlQueryRunnerService { /** QUERIES */ @LogExecutionTime() - async findOne( + async findOne( args: FindOneResolverArgs, options: WorkspaceQueryRunnerOptions, - ): Promise { - return this.executeQuery, ObjectRecord>( + ): Promise { + return this.executeQuery, T>( 'findOne', args, options, @@ -61,36 +61,36 @@ export class GraphqlQueryRunnerService { @LogExecutionTime() async findMany< - ObjectRecord extends IRecord, - Filter extends RecordFilter, - OrderBy extends RecordOrderBy, + T extends ObjectRecord, + Filter extends ObjectRecordFilter, + OrderBy extends ObjectRecordOrderBy, >( args: FindManyResolverArgs, options: WorkspaceQueryRunnerOptions, - ): Promise>> { + ): Promise>> { return this.executeQuery< FindManyResolverArgs, - IConnection> + IConnection> >('findMany', args, options); } @LogExecutionTime() - async findDuplicates( - args: FindDuplicatesResolverArgs>, + async findDuplicates( + args: FindDuplicatesResolverArgs>, options: WorkspaceQueryRunnerOptions, - ): Promise[]> { + ): Promise[]> { return this.executeQuery< - FindDuplicatesResolverArgs>, - IConnection[] + FindDuplicatesResolverArgs>, + IConnection[] >('findDuplicates', args, options); } @LogExecutionTime() - async search( + async search( args: SearchResolverArgs, options: WorkspaceQueryRunnerOptions, - ): Promise> { - return this.executeQuery>( + ): Promise> { + return this.executeQuery>( 'search', args, options, @@ -100,13 +100,13 @@ export class GraphqlQueryRunnerService { /** MUTATIONS */ @LogExecutionTime() - async createOne( - args: CreateOneResolverArgs>, + async createOne( + args: CreateOneResolverArgs>, options: WorkspaceQueryRunnerOptions, - ): Promise { + ): Promise { const results = await this.executeQuery< - CreateManyResolverArgs>, - ObjectRecord[] + CreateManyResolverArgs>, + T[] >('createMany', { data: [args.data], upsert: args.upsert }, options); // TODO: emitCreateEvents should be moved to the ORM layer @@ -114,7 +114,7 @@ export class GraphqlQueryRunnerService { this.apiEventEmitterService.emitCreateEvents( results, options.authContext, - options.objectMetadataItem, + options.objectMetadataItemWithFieldMaps, ); } @@ -122,20 +122,20 @@ export class GraphqlQueryRunnerService { } @LogExecutionTime() - async createMany( - args: CreateManyResolverArgs>, + async createMany( + args: CreateManyResolverArgs>, options: WorkspaceQueryRunnerOptions, - ): Promise { + ): Promise { const results = await this.executeQuery< - CreateManyResolverArgs>, - ObjectRecord[] + CreateManyResolverArgs>, + T[] >('createMany', args, options); if (results) { this.apiEventEmitterService.emitCreateEvents( results, options.authContext, - options.objectMetadataItem, + options.objectMetadataItemWithFieldMaps, ); } @@ -143,14 +143,11 @@ export class GraphqlQueryRunnerService { } @LogExecutionTime() - public async updateOne( - args: UpdateOneResolverArgs>, + public async updateOne( + args: UpdateOneResolverArgs>, options: WorkspaceQueryRunnerOptions, - ): Promise { - const existingRecord = await this.executeQuery< - FindOneResolverArgs, - ObjectRecord - >( + ): Promise { + const existingRecord = await this.executeQuery( 'findOne', { filter: { id: { eq: args.id } }, @@ -159,8 +156,8 @@ export class GraphqlQueryRunnerService { ); const result = await this.executeQuery< - UpdateOneResolverArgs>, - ObjectRecord + UpdateOneResolverArgs>, + T >('updateOne', args, options); this.apiEventEmitterService.emitUpdateEvents( @@ -168,20 +165,20 @@ export class GraphqlQueryRunnerService { [result], Object.keys(args.data), options.authContext, - options.objectMetadataItem, + options.objectMetadataItemWithFieldMaps, ); return result; } @LogExecutionTime() - public async updateMany( - args: UpdateManyResolverArgs>, + public async updateMany( + args: UpdateManyResolverArgs>, options: WorkspaceQueryRunnerOptions, - ): Promise { + ): Promise { const existingRecords = await this.executeQuery< FindManyResolverArgs, - IConnection> + IConnection> >( 'findMany', { @@ -191,8 +188,8 @@ export class GraphqlQueryRunnerService { ); const result = await this.executeQuery< - UpdateManyResolverArgs>, - ObjectRecord[] + UpdateManyResolverArgs>, + T[] >('updateMany', args, options); this.apiEventEmitterService.emitUpdateEvents( @@ -200,25 +197,25 @@ export class GraphqlQueryRunnerService { result, Object.keys(args.data), options.authContext, - options.objectMetadataItem, + options.objectMetadataItemWithFieldMaps, ); return result; } @LogExecutionTime() - public async deleteOne( + public async deleteOne( args: DeleteOneResolverArgs, options: WorkspaceQueryRunnerOptions, - ): Promise { + ): Promise { const result = await this.executeQuery< - UpdateOneResolverArgs>, - ObjectRecord + UpdateOneResolverArgs>, + T >( 'deleteOne', { id: args.id, - data: { deletedAt: new Date() } as Partial, + data: { deletedAt: new Date() } as Partial, }, options, ); @@ -226,26 +223,26 @@ export class GraphqlQueryRunnerService { this.apiEventEmitterService.emitDeletedEvents( [result], options.authContext, - options.objectMetadataItem, + options.objectMetadataItemWithFieldMaps, ); return result; } @LogExecutionTime() - public async deleteMany( + public async deleteMany( args: DeleteManyResolverArgs, options: WorkspaceQueryRunnerOptions, - ): Promise { + ): Promise { const result = await this.executeQuery< - UpdateManyResolverArgs>, - ObjectRecord[] + UpdateManyResolverArgs>, + T[] >( 'deleteMany', { filter: args.filter, - data: { deletedAt: new Date() } as Partial, + data: { deletedAt: new Date() } as Partial, }, options, ); @@ -253,63 +250,62 @@ export class GraphqlQueryRunnerService { this.apiEventEmitterService.emitDeletedEvents( result, options.authContext, - options.objectMetadataItem, + options.objectMetadataItemWithFieldMaps, ); return result; } @LogExecutionTime() - async destroyOne( + async destroyOne( args: DestroyOneResolverArgs, options: WorkspaceQueryRunnerOptions, - ): Promise { - const result = await this.executeQuery< - DestroyOneResolverArgs, - ObjectRecord - >('destroyOne', args, options); + ): Promise { + const result = await this.executeQuery( + 'destroyOne', + args, + options, + ); this.apiEventEmitterService.emitDestroyEvents( [result], options.authContext, - options.objectMetadataItem, + options.objectMetadataItemWithFieldMaps, ); return result; } @LogExecutionTime() - async destroyMany( + async destroyMany( args: DestroyManyResolverArgs, options: WorkspaceQueryRunnerOptions, - ): Promise { - const result = await this.executeQuery< - DestroyManyResolverArgs, - ObjectRecord[] - >('destroyMany', args, options); + ): Promise { + const result = await this.executeQuery( + 'destroyMany', + args, + options, + ); this.apiEventEmitterService.emitDestroyEvents( result, options.authContext, - options.objectMetadataItem, + options.objectMetadataItemWithFieldMaps, ); return result; } @LogExecutionTime() - public async restoreMany( + public async restoreMany( args: RestoreManyResolverArgs, options: WorkspaceQueryRunnerOptions, - ): Promise { - return await this.executeQuery< - UpdateManyResolverArgs>, - ObjectRecord - >( + ): Promise { + return await this.executeQuery>, T>( 'restoreMany', { filter: args.filter, - data: { deletedAt: null } as Partial, + data: { deletedAt: null } as Partial, }, options, ); @@ -320,7 +316,7 @@ export class GraphqlQueryRunnerService { args: Input, options: WorkspaceQueryRunnerOptions, ): Promise { - const { authContext, objectMetadataItem } = options; + const { authContext, objectMetadataItemWithFieldMaps } = options; const resolver = this.graphqlQueryResolverFactory.getResolver(operationName); @@ -330,7 +326,7 @@ export class GraphqlQueryRunnerService { const hookedArgs = await this.workspaceQueryHookService.executePreQueryHooks( authContext, - objectMetadataItem.nameSingular, + objectMetadataItemWithFieldMaps.nameSingular, operationName, args, ); @@ -345,7 +341,7 @@ export class GraphqlQueryRunnerService { const resultWithGetters = await this.queryResultGettersFactory.create( results, - objectMetadataItem, + objectMetadataItemWithFieldMaps, authContext.workspace.id, ); @@ -355,7 +351,7 @@ export class GraphqlQueryRunnerService { await this.workspaceQueryHookService.executePostQueryHooks( authContext, - objectMetadataItem.nameSingular, + objectMetadataItemWithFieldMaps.nameSingular, operationName, resultWithGettersArray, ); diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts index 54220315345a..0553c2e73666 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts @@ -1,7 +1,7 @@ import { - Record as IRecord, - RecordOrderBy, -} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; + ObjectRecord, + ObjectRecordOrderBy, +} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; @@ -12,23 +12,27 @@ import { } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { encodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util'; import { getRelationObjectMetadata } from 'src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util'; +import { AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; 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 { isPlainObject } from 'src/utils/is-plain-object'; export class ObjectRecordsToGraphqlConnectionHelper { - private objectMetadataMap: ObjectMetadataMap; + private objectMetadataMaps: ObjectMetadataMaps; - constructor(objectMetadataMap: ObjectMetadataMap) { - this.objectMetadataMap = objectMetadataMap; + constructor(objectMetadataMaps: ObjectMetadataMaps) { + this.objectMetadataMaps = objectMetadataMaps; } - public createConnection({ + public createConnection({ objectRecords, + parentObjectRecord, + objectRecordsAggregatedValues = {}, + selectedAggregatedFields = [], objectName, take, totalCount, @@ -37,19 +41,24 @@ export class ObjectRecordsToGraphqlConnectionHelper { hasPreviousPage, depth = 0, }: { - objectRecords: ObjectRecord[]; + objectRecords: T[]; + parentObjectRecord?: T; + objectRecordsAggregatedValues?: Record; + selectedAggregatedFields?: Record; objectName: string; take: number; totalCount: number; - order?: RecordOrderBy; + order?: ObjectRecordOrderBy; hasNextPage: boolean; hasPreviousPage: boolean; depth?: number; - }): IConnection { + }): IConnection { const edges = (objectRecords ?? []).map((objectRecord) => ({ node: this.processRecord({ objectRecord, objectName, + objectRecordsAggregatedValues, + selectedAggregatedFields, take, totalCount, order, @@ -58,7 +67,15 @@ export class ObjectRecordsToGraphqlConnectionHelper { cursor: encodeCursor(objectRecord, order), })); + const aggregatedFieldsValues = this.extractAggregatedFieldsValues({ + selectedAggregatedFields, + objectRecordsAggregatedValues: parentObjectRecord + ? objectRecordsAggregatedValues[parentObjectRecord.id] + : objectRecordsAggregatedValues, + }); + return { + ...aggregatedFieldsValues, edges, pageInfo: { hasNextPage, @@ -70,9 +87,41 @@ export class ObjectRecordsToGraphqlConnectionHelper { }; } + private extractAggregatedFieldsValues = ({ + selectedAggregatedFields, + objectRecordsAggregatedValues, + }: { + selectedAggregatedFields: Record; + objectRecordsAggregatedValues: Record; + }) => { + if (!objectRecordsAggregatedValues) { + return {}; + } + + return Object.entries(selectedAggregatedFields).reduce( + (acc, [aggregatedFieldName]) => { + const aggregatedFieldValue = + objectRecordsAggregatedValues[aggregatedFieldName]; + + if (!aggregatedFieldValue) { + return acc; + } + + return { + ...acc, + [aggregatedFieldName]: + objectRecordsAggregatedValues[aggregatedFieldName], + }; + }, + {}, + ); + }; + public processRecord>({ objectRecord, objectName, + objectRecordsAggregatedValues = {}, + selectedAggregatedFields = [], take, totalCount, order, @@ -80,9 +129,11 @@ export class ObjectRecordsToGraphqlConnectionHelper { }: { objectRecord: T; objectName: string; + objectRecordsAggregatedValues?: Record; + selectedAggregatedFields?: Record; take: number; totalCount: number; - order?: RecordOrderBy; + order?: ObjectRecordOrderBy; depth?: number; }): T { if (depth >= CONNECTION_MAX_DEPTH) { @@ -92,7 +143,7 @@ export class ObjectRecordsToGraphqlConnectionHelper { ); } - const objectMetadata = this.objectMetadataMap[objectName]; + const objectMetadata = this.objectMetadataMaps.byNameSingular[objectName]; if (!objectMetadata) { throw new GraphqlQueryRunnerException( @@ -104,7 +155,7 @@ export class ObjectRecordsToGraphqlConnectionHelper { const processedObjectRecord: Record = {}; for (const [key, value] of Object.entries(objectRecord)) { - const fieldMetadata = objectMetadata.fields[key]; + const fieldMetadata = objectMetadata.fieldsByName[key]; if (!fieldMetadata) { processedObjectRecord[key] = value; @@ -115,12 +166,19 @@ export class ObjectRecordsToGraphqlConnectionHelper { if (Array.isArray(value)) { processedObjectRecord[key] = this.createConnection({ objectRecords: value, + parentObjectRecord: objectRecord, + objectRecordsAggregatedValues: + objectRecordsAggregatedValues[fieldMetadata.name], + selectedAggregatedFields: + selectedAggregatedFields[fieldMetadata.name], objectName: getRelationObjectMetadata( fieldMetadata, - this.objectMetadataMap, + this.objectMetadataMaps, ).nameSingular, take, - totalCount: value.length, + totalCount: + objectRecordsAggregatedValues[fieldMetadata.name]?.totalCount ?? + value.length, order, hasNextPage: false, hasPreviousPage: false, @@ -129,9 +187,13 @@ export class ObjectRecordsToGraphqlConnectionHelper { } else if (isPlainObject(value)) { processedObjectRecord[key] = this.processRecord({ objectRecord: value, + objectRecordsAggregatedValues: + objectRecordsAggregatedValues[fieldMetadata.name], + selectedAggregatedFields: + selectedAggregatedFields[fieldMetadata.name], objectName: getRelationObjectMetadata( fieldMetadata, - this.objectMetadataMap, + this.objectMetadataMaps, ).nameSingular, take, totalCount, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper.ts new file mode 100644 index 000000000000..3ac1b554d0d4 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper.ts @@ -0,0 +1,37 @@ +import { SelectQueryBuilder } from 'typeorm'; + +import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; + +import { AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; + +export class ProcessAggregateHelper { + public addSelectedAggregatedFieldsQueriesToQueryBuilder = ({ + fieldMetadataMapByName, + selectedAggregatedFields, + queryBuilder, + }: { + fieldMetadataMapByName: Record; + selectedAggregatedFields: Record; + queryBuilder: SelectQueryBuilder; + }) => { + queryBuilder.select([]); + + for (const [aggregatedFieldName, aggregatedField] of Object.entries( + selectedAggregatedFields, + )) { + const fieldMetadata = fieldMetadataMapByName[aggregatedField.fromField]; + + if (!fieldMetadata) { + continue; + } + + const fieldName = fieldMetadata.name; + const operation = aggregatedField.aggregationOperation; + + queryBuilder.addSelect( + `${operation}("${fieldName}")`, + `${aggregatedFieldName}`, + ); + } + }; +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts index dd3e5abd4020..1a4e7896d1b9 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts @@ -1,64 +1,95 @@ import { DataSource, - FindManyOptions, FindOptionsRelations, - In, ObjectLiteral, - Repository, + SelectQueryBuilder, } from 'typeorm'; -import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; +import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper'; import { getRelationMetadata, getRelationObjectMetadata, } from 'src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util'; -import { - ObjectMetadataMap, - ObjectMetadataMapItem, -} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; import { deduceRelationDirection } from 'src/engine/utils/deduce-relation-direction.util'; export class ProcessNestedRelationsHelper { - constructor() {} - - public async processNestedRelations( - objectMetadataMap: ObjectMetadataMap, - parentObjectMetadataItem: ObjectMetadataMapItem, - parentObjectRecords: ObjectRecord[], - relations: Record>, - limit: number, - authContext: any, - dataSource: DataSource, - ): Promise { + private processAggregateHelper: ProcessAggregateHelper; + + constructor() { + this.processAggregateHelper = new ProcessAggregateHelper(); + } + + public async processNestedRelations({ + objectMetadataMaps, + parentObjectMetadataItem, + parentObjectRecords, + parentObjectRecordsAggregatedValues = {}, + relations, + aggregate = {}, + limit, + authContext, + dataSource, + }: { + objectMetadataMaps: ObjectMetadataMaps; + parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps; + parentObjectRecords: T[]; + parentObjectRecordsAggregatedValues?: Record; + relations: Record>; + aggregate?: Record; + limit: number; + authContext: any; + dataSource: DataSource; + }): Promise { const processRelationTasks = Object.entries(relations).map( ([relationName, nestedRelations]) => - this.processRelation( - objectMetadataMap, + this.processRelation({ + objectMetadataMaps, parentObjectMetadataItem, parentObjectRecords, + parentObjectRecordsAggregatedValues, relationName, nestedRelations, + aggregate, limit, authContext, dataSource, - ), + }), ); await Promise.all(processRelationTasks); } - private async processRelation( - objectMetadataMap: ObjectMetadataMap, - parentObjectMetadataItem: ObjectMetadataMapItem, - parentObjectRecords: ObjectRecord[], - relationName: string, - nestedRelations: any, - limit: number, - authContext: any, - dataSource: DataSource, - ): Promise { - const relationFieldMetadata = parentObjectMetadataItem.fields[relationName]; + private async processRelation({ + objectMetadataMaps, + parentObjectMetadataItem, + parentObjectRecords, + parentObjectRecordsAggregatedValues, + relationName, + nestedRelations, + aggregate, + limit, + authContext, + dataSource, + }: { + objectMetadataMaps: ObjectMetadataMaps; + parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps; + parentObjectRecords: T[]; + parentObjectRecordsAggregatedValues: Record; + relationName: string; + nestedRelations: any; + aggregate: Record; + limit: number; + authContext: any; + dataSource: DataSource; + }): Promise { + const relationFieldMetadata = + parentObjectMetadataItem.fieldsByName[relationName]; const relationMetadata = getRelationMetadata(relationFieldMetadata); const relationDirection = deduceRelationDirection( relationFieldMetadata, @@ -70,181 +101,341 @@ export class ProcessNestedRelationsHelper { ? this.processToRelation : this.processFromRelation; - await processor.call( - this, - objectMetadataMap, + await processor.call(this, { + objectMetadataMaps, parentObjectMetadataItem, parentObjectRecords, + parentObjectRecordsAggregatedValues, relationName, nestedRelations, + aggregate, limit, authContext, dataSource, - ); + }); } - private async processFromRelation( - objectMetadataMap: ObjectMetadataMap, - parentObjectMetadataItem: ObjectMetadataMapItem, - parentObjectRecords: ObjectRecord[], - relationName: string, - nestedRelations: any, - limit: number, - authContext: any, - dataSource: DataSource, - ): Promise { + private async processFromRelation({ + objectMetadataMaps, + parentObjectMetadataItem, + parentObjectRecords, + parentObjectRecordsAggregatedValues, + relationName, + nestedRelations, + aggregate, + limit, + authContext, + dataSource, + }: { + objectMetadataMaps: ObjectMetadataMaps; + parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps; + parentObjectRecords: T[]; + parentObjectRecordsAggregatedValues: Record; + relationName: string; + nestedRelations: any; + aggregate: Record; + limit: number; + authContext: any; + dataSource: DataSource; + }): Promise { const { inverseRelationName, referenceObjectMetadata } = - this.getRelationMetadata( - objectMetadataMap, + this.getRelationMetadata({ + objectMetadataMaps, parentObjectMetadataItem, relationName, - ); + }); const relationRepository = dataSource.getRepository( referenceObjectMetadata.nameSingular, ); - const relationIds = this.getUniqueIds(parentObjectRecords, 'id'); - const relationResults = await this.findRelations( - relationRepository, - inverseRelationName, - relationIds, - limit * parentObjectRecords.length, + const referenceQueryBuilder = relationRepository.createQueryBuilder( + referenceObjectMetadata.nameSingular, ); - this.assignRelationResults( - parentObjectRecords, + const relationIds = this.getUniqueIds({ + records: parentObjectRecords, + idField: 'id', + }); + const { relationResults, relationAggregatedFieldsResult } = + await this.findRelations({ + referenceQueryBuilder, + column: `"${inverseRelationName}Id"`, + ids: relationIds, + limit: limit * parentObjectRecords.length, + objectMetadataMaps, + referenceObjectMetadata, + aggregate, + relationName, + }); + + this.assignFromRelationResults({ + parentRecords: parentObjectRecords, + parentObjectRecordsAggregatedValues, relationResults, + relationAggregatedFieldsResult, relationName, - `${inverseRelationName}Id`, - ); + joinField: `${inverseRelationName}Id`, + }); if (Object.keys(nestedRelations).length > 0) { - await this.processNestedRelations( - objectMetadataMap, - objectMetadataMap[referenceObjectMetadata.nameSingular], - relationResults as ObjectRecord[], - nestedRelations as Record>, + await this.processNestedRelations({ + objectMetadataMaps, + parentObjectMetadataItem: + objectMetadataMaps.byNameSingular[ + referenceObjectMetadata.nameSingular + ], + parentObjectRecords: relationResults as ObjectRecord[], + parentObjectRecordsAggregatedValues: relationAggregatedFieldsResult, + relations: nestedRelations as Record< + string, + FindOptionsRelations + >, + aggregate, limit, authContext, dataSource, - ); + }); } } - private async processToRelation( - objectMetadataMap: ObjectMetadataMap, - parentObjectMetadataItem: ObjectMetadataMapItem, - parentObjectRecords: ObjectRecord[], - relationName: string, - nestedRelations: any, - limit: number, - authContext: any, - dataSource: DataSource, - ): Promise { - const { referenceObjectMetadata } = this.getRelationMetadata( - objectMetadataMap, + private async processToRelation({ + objectMetadataMaps, + parentObjectMetadataItem, + parentObjectRecords, + parentObjectRecordsAggregatedValues, + relationName, + nestedRelations, + aggregate, + limit, + authContext, + dataSource, + }: { + objectMetadataMaps: ObjectMetadataMaps; + parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps; + parentObjectRecords: T[]; + parentObjectRecordsAggregatedValues: Record; + relationName: string; + nestedRelations: any; + aggregate: Record; + limit: number; + authContext: any; + dataSource: DataSource; + }): Promise { + const { referenceObjectMetadata } = this.getRelationMetadata({ + objectMetadataMaps, parentObjectMetadataItem, relationName, - ); + }); const relationRepository = dataSource.getRepository( referenceObjectMetadata.nameSingular, ); - const relationIds = this.getUniqueIds( - parentObjectRecords, - `${relationName}Id`, - ); - const relationResults = await this.findRelations( - relationRepository, - 'id', - relationIds, - limit, + const referenceQueryBuilder = relationRepository.createQueryBuilder( + referenceObjectMetadata.nameSingular, ); - this.assignToRelationResults( - parentObjectRecords, + const relationIds = this.getUniqueIds({ + records: parentObjectRecords, + idField: `${relationName}Id`, + }); + const { relationResults, relationAggregatedFieldsResult } = + await this.findRelations({ + referenceQueryBuilder, + column: 'id', + ids: relationIds, + limit, + objectMetadataMaps, + referenceObjectMetadata, + aggregate, + relationName, + }); + + this.assignToRelationResults({ + parentRecords: parentObjectRecords, + parentObjectRecordsAggregatedValues: parentObjectRecordsAggregatedValues, relationResults, + relationAggregatedFieldsResult, relationName, - ); + }); if (Object.keys(nestedRelations).length > 0) { - await this.processNestedRelations( - objectMetadataMap, - objectMetadataMap[referenceObjectMetadata.nameSingular], - relationResults as ObjectRecord[], - nestedRelations as Record>, + await this.processNestedRelations({ + objectMetadataMaps, + parentObjectMetadataItem: + objectMetadataMaps.byNameSingular[ + referenceObjectMetadata.nameSingular + ], + parentObjectRecords: relationResults as ObjectRecord[], + parentObjectRecordsAggregatedValues: relationAggregatedFieldsResult, + relations: nestedRelations as Record< + string, + FindOptionsRelations + >, + aggregate, limit, authContext, dataSource, - ); + }); } } - private getRelationMetadata( - objectMetadataMap: ObjectMetadataMap, - parentObjectMetadataItem: ObjectMetadataMapItem, - relationName: string, - ) { - const relationFieldMetadata = parentObjectMetadataItem.fields[relationName]; + private getRelationMetadata({ + objectMetadataMaps, + parentObjectMetadataItem, + relationName, + }: { + objectMetadataMaps: ObjectMetadataMaps; + parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps; + relationName: string; + }) { + const relationFieldMetadata = + parentObjectMetadataItem.fieldsByName[relationName]; const relationMetadata = getRelationMetadata(relationFieldMetadata); const referenceObjectMetadata = getRelationObjectMetadata( relationFieldMetadata, - objectMetadataMap, + objectMetadataMaps, ); const inverseRelationName = - objectMetadataMap[relationMetadata.toObjectMetadataId]?.fields[ + objectMetadataMaps.byId[relationMetadata.toObjectMetadataId]?.fieldsById[ relationMetadata.toFieldMetadataId ]?.name; return { inverseRelationName, referenceObjectMetadata }; } - private getUniqueIds(records: IRecord[], idField: string): any[] { + private getUniqueIds({ + records, + idField, + }: { + records: ObjectRecord[]; + idField: string; + }): any[] { return [...new Set(records.map((item) => item[idField]))]; } - private async findRelations( - repository: Repository, - field: string, - ids: any[], - limit: number, - ): Promise { + private async findRelations({ + referenceQueryBuilder, + column, + ids, + limit, + objectMetadataMaps, + referenceObjectMetadata, + aggregate, + relationName, + }: { + referenceQueryBuilder: SelectQueryBuilder; + column: string; + ids: any[]; + limit: number; + objectMetadataMaps: ObjectMetadataMaps; + referenceObjectMetadata: ObjectMetadataItemWithFieldMaps; + aggregate: Record; + relationName: string; + }): Promise<{ relationResults: any[]; relationAggregatedFieldsResult: any }> { if (ids.length === 0) { - return []; + return { relationResults: [], relationAggregatedFieldsResult: {} }; + } + + const aggregateForRelation = aggregate[relationName]; + let relationAggregatedFieldsResult: Record = {}; + + if (aggregateForRelation) { + const aggregateQueryBuilder = referenceQueryBuilder.clone(); + + this.processAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder( + { + fieldMetadataMapByName: referenceObjectMetadata.fieldsByName, + selectedAggregatedFields: aggregateForRelation, + queryBuilder: aggregateQueryBuilder, + }, + ); + + const aggregatedFieldsValues = await aggregateQueryBuilder + .addSelect(column) + .where(`${column} IN (:...ids)`, { + ids, + }) + .groupBy(column) + .getRawMany(); + + relationAggregatedFieldsResult = aggregatedFieldsValues.reduce( + (acc, item) => { + const columnWithoutQuotes = column.replace(/["']/g, ''); + const key = item[columnWithoutQuotes]; + const { [column]: _, ...itemWithoutColumn } = item; + + acc[key] = itemWithoutColumn; + + return acc; + }, + {}, + ); } - const findOptions: FindManyOptions = { - where: { [field]: In(ids) }, - take: limit, - }; - return repository.find(findOptions); + const result = await referenceQueryBuilder + .where(`${column} IN (:...ids)`, { + ids, + }) + .take(limit) + .getMany(); + + const relationResults = formatResult( + result, + referenceObjectMetadata, + objectMetadataMaps, + ); + + return { relationResults, relationAggregatedFieldsResult }; } - private assignRelationResults( - parentRecords: IRecord[], - relationResults: any[], - relationName: string, - joinField: string, - ): void { + private assignFromRelationResults({ + parentRecords, + parentObjectRecordsAggregatedValues, + relationResults, + relationAggregatedFieldsResult, + relationName, + joinField, + }: { + parentRecords: ObjectRecord[]; + parentObjectRecordsAggregatedValues: Record; + relationResults: any[]; + relationAggregatedFieldsResult: Record; + relationName: string; + joinField: string; + }): void { parentRecords.forEach((item) => { - (item as any)[relationName] = relationResults.filter( + item[relationName] = relationResults.filter( (rel) => rel[joinField] === item.id, ); }); + + parentObjectRecordsAggregatedValues[relationName] = + relationAggregatedFieldsResult; } - private assignToRelationResults( - parentRecords: IRecord[], - relationResults: any[], - relationName: string, - ): void { + private assignToRelationResults({ + parentRecords, + parentObjectRecordsAggregatedValues, + relationResults, + relationAggregatedFieldsResult, + relationName, + }: { + parentRecords: ObjectRecord[]; + parentObjectRecordsAggregatedValues: Record; + relationResults: any[]; + relationAggregatedFieldsResult: Record; + relationName: string; + }): void { parentRecords.forEach((item) => { if (relationResults.length === 0) { - (item as any)[`${relationName}Id`] = null; + item[`${relationName}Id`] = null; } - (item as any)[relationName] = + item[relationName] = relationResults.find((rel) => rel.id === item[`${relationName}Id`]) ?? null; }); + + parentObjectRecordsAggregatedValues[relationName] = + relationAggregatedFieldsResult; } } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts index 6cd7a111138e..19bc28cf0ddd 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts @@ -4,7 +4,7 @@ import graphqlFields from 'graphql-fields'; import { In, InsertResult } from 'typeorm'; import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; -import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { CreateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; @@ -19,35 +19,40 @@ import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; @Injectable() export class GraphqlQueryCreateManyResolverService - implements ResolverService + implements ResolverService { constructor( private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} - async resolve( + async resolve( args: CreateManyResolverArgs>, options: WorkspaceQueryRunnerOptions, - ): Promise { - const { authContext, info, objectMetadataMap, objectMetadataMapItem } = - options; + ): Promise { + const { + authContext, + info, + objectMetadataMaps, + objectMetadataItemWithFieldMaps, + } = options; const dataSource = await this.twentyORMGlobalManager.getDataSourceForWorkspace( authContext.workspace.id, ); + const repository = dataSource.getRepository( - objectMetadataMapItem.nameSingular, + objectMetadataItemWithFieldMaps.nameSingular, ); const graphqlQueryParser = new GraphqlQueryParser( - objectMetadataMapItem.fields, - objectMetadataMap, + objectMetadataItemWithFieldMaps.fieldsByName, + objectMetadataMaps, ); const selectedFields = graphqlFields(info); const { relations } = graphqlQueryParser.parseSelectedFields( - objectMetadataMapItem, + objectMetadataItemWithFieldMaps, selectedFields, ); @@ -59,7 +64,7 @@ export class GraphqlQueryCreateManyResolverService }); const queryBuilder = repository.createQueryBuilder( - objectMetadataMapItem.nameSingular, + objectMetadataItemWithFieldMaps.nameSingular, ); const nonFormattedUpsertedRecords = (await queryBuilder @@ -71,42 +76,42 @@ export class GraphqlQueryCreateManyResolverService const upsertedRecords = formatResult( nonFormattedUpsertedRecords, - objectMetadataMapItem, - objectMetadataMap, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, ); const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); if (relations) { - await processNestedRelationsHelper.processNestedRelations( - objectMetadataMap, - objectMetadataMapItem, - upsertedRecords, + await processNestedRelationsHelper.processNestedRelations({ + objectMetadataMaps, + parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + parentObjectRecords: upsertedRecords, relations, - QUERY_MAX_RECORDS, + limit: QUERY_MAX_RECORDS, authContext, dataSource, - ); + }); } const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); + new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); - return upsertedRecords.map((record: ObjectRecord) => + return upsertedRecords.map((record: T) => typeORMObjectRecordsParser.processRecord({ objectRecord: record, - objectName: objectMetadataMapItem.nameSingular, + objectName: objectMetadataItemWithFieldMaps.nameSingular, take: 1, totalCount: 1, }), ); } - async validate( - args: CreateManyResolverArgs>, + async validate( + args: CreateManyResolverArgs>, options: WorkspaceQueryRunnerOptions, ): Promise { - assertMutationNotOnRemoteObject(options.objectMetadataItem); + assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps); args.data.forEach((record) => { if (record?.id) { diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts index 04ceddf9ac9d..8b4176d267f2 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common'; import graphqlFields from 'graphql-fields'; import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; -import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { DestroyManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; @@ -16,46 +16,51 @@ import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; @Injectable() export class GraphqlQueryDestroyManyResolverService - implements ResolverService + implements ResolverService { constructor( private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} - async resolve( + async resolve( args: DestroyManyResolverArgs, options: WorkspaceQueryRunnerOptions, - ): Promise { - const { authContext, objectMetadataMapItem, objectMetadataMap, info } = - options; + ): Promise { + const { + authContext, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + info, + } = options; + const dataSource = await this.twentyORMGlobalManager.getDataSourceForWorkspace( authContext.workspace.id, ); const repository = dataSource.getRepository( - objectMetadataMapItem.nameSingular, + objectMetadataItemWithFieldMaps.nameSingular, ); const graphqlQueryParser = new GraphqlQueryParser( - objectMetadataMapItem.fields, - objectMetadataMap, + objectMetadataItemWithFieldMaps.fieldsByName, + objectMetadataMaps, ); const selectedFields = graphqlFields(info); const { relations } = graphqlQueryParser.parseSelectedFields( - objectMetadataMapItem, + objectMetadataItemWithFieldMaps, selectedFields, ); const queryBuilder = repository.createQueryBuilder( - objectMetadataMapItem.nameSingular, + objectMetadataItemWithFieldMaps.nameSingular, ); const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder( queryBuilder, - objectMetadataMapItem.nameSingular, + objectMetadataItemWithFieldMaps.nameSingular, args.filter, ); @@ -66,31 +71,31 @@ export class GraphqlQueryDestroyManyResolverService const deletedRecords = formatResult( nonFormattedDeletedObjectRecords.raw, - objectMetadataMapItem, - objectMetadataMap, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, ); const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); if (relations) { - await processNestedRelationsHelper.processNestedRelations( - objectMetadataMap, - objectMetadataMapItem, - deletedRecords, + await processNestedRelationsHelper.processNestedRelations({ + objectMetadataMaps, + parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + parentObjectRecords: deletedRecords, relations, - QUERY_MAX_RECORDS, + limit: QUERY_MAX_RECORDS, authContext, dataSource, - ); + }); } const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); + new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); - return deletedRecords.map((record: ObjectRecord) => + return deletedRecords.map((record: T) => typeORMObjectRecordsParser.processRecord({ objectRecord: record, - objectName: objectMetadataMapItem.nameSingular, + objectName: objectMetadataItemWithFieldMaps.nameSingular, take: 1, totalCount: 1, }), diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts index 5467d6c0ff0a..044370a0730a 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common'; import graphqlFields from 'graphql-fields'; import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; -import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { DestroyOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; @@ -20,45 +20,50 @@ import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; @Injectable() export class GraphqlQueryDestroyOneResolverService - implements ResolverService + implements ResolverService { constructor( private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} - async resolve( + async resolve( args: DestroyOneResolverArgs, options: WorkspaceQueryRunnerOptions, - ): Promise { - const { authContext, objectMetadataMapItem, objectMetadataMap, info } = - options; + ): Promise { + const { + authContext, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + info, + } = options; + const dataSource = await this.twentyORMGlobalManager.getDataSourceForWorkspace( authContext.workspace.id, ); const repository = dataSource.getRepository( - objectMetadataMapItem.nameSingular, + objectMetadataItemWithFieldMaps.nameSingular, ); const graphqlQueryParser = new GraphqlQueryParser( - objectMetadataMapItem.fields, - objectMetadataMap, + objectMetadataItemWithFieldMaps.fieldsByName, + objectMetadataMaps, ); const selectedFields = graphqlFields(info); const { relations } = graphqlQueryParser.parseSelectedFields( - objectMetadataMapItem, + objectMetadataItemWithFieldMaps, selectedFields, ); const queryBuilder = repository.createQueryBuilder( - objectMetadataMapItem.nameSingular, + objectMetadataItemWithFieldMaps.nameSingular, ); const nonFormattedDeletedObjectRecords = await queryBuilder - .where(`"${objectMetadataMapItem.nameSingular}".id = :id`, { + .where(`"${objectMetadataItemWithFieldMaps.nameSingular}".id = :id`, { id: args.id, }) .take(1) @@ -75,30 +80,30 @@ export class GraphqlQueryDestroyOneResolverService const recordBeforeDeletion = formatResult( nonFormattedDeletedObjectRecords.raw, - objectMetadataMapItem, - objectMetadataMap, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, )[0]; const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); if (relations) { - await processNestedRelationsHelper.processNestedRelations( - objectMetadataMap, - objectMetadataMapItem, - [recordBeforeDeletion], + await processNestedRelationsHelper.processNestedRelations({ + objectMetadataMaps, + parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + parentObjectRecords: [recordBeforeDeletion], relations, - QUERY_MAX_RECORDS, + limit: QUERY_MAX_RECORDS, authContext, dataSource, - ); + }); } const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); + new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); return typeORMObjectRecordsParser.processRecord({ objectRecord: recordBeforeDeletion, - objectName: objectMetadataMapItem.nameSingular, + objectName: objectMetadataItemWithFieldMaps.nameSingular, take: 1, totalCount: 1, }); diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts index d3bc72fa8220..ebc2200c1cc1 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts @@ -5,10 +5,10 @@ import { In } from 'typeorm'; import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; import { - Record as IRecord, + ObjectRecord, + ObjectRecordFilter, OrderByDirection, - RecordFilter, -} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-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 { FindDuplicatesResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; @@ -21,7 +21,7 @@ import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/ import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; import { settings } from 'src/engine/constants/settings'; import { DUPLICATE_CRITERIA_COLLECTION } from 'src/engine/core-modules/duplicate/constants/duplicate-criteria.constants'; -import { ObjectMetadataMapItem } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; @@ -29,60 +29,63 @@ import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; @Injectable() export class GraphqlQueryFindDuplicatesResolverService implements - ResolverService[]> + ResolverService[]> { constructor( private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} - async resolve( - args: FindDuplicatesResolverArgs>, + async resolve( + args: FindDuplicatesResolverArgs>, options: WorkspaceQueryRunnerOptions, - ): Promise[]> { - const { authContext, objectMetadataMapItem, objectMetadataMap } = options; + ): Promise[]> { + const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = + options; const dataSource = await this.twentyORMGlobalManager.getDataSourceForWorkspace( authContext.workspace.id, ); const repository = dataSource.getRepository( - objectMetadataMapItem.nameSingular, + objectMetadataItemWithFieldMaps.nameSingular, ); const existingRecordsQueryBuilder = repository.createQueryBuilder( - objectMetadataMapItem.nameSingular, + objectMetadataItemWithFieldMaps.nameSingular, ); const duplicateRecordsQueryBuilder = repository.createQueryBuilder( - objectMetadataMapItem.nameSingular, + objectMetadataItemWithFieldMaps.nameSingular, ); const graphqlQueryParser = new GraphqlQueryParser( - objectMetadataMap[objectMetadataMapItem.nameSingular].fields, - objectMetadataMap, + objectMetadataMaps.byNameSingular[ + objectMetadataItemWithFieldMaps.nameSingular + ].fieldsByName, + objectMetadataMaps, ); const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); + new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); - let objectRecords: Partial[] = []; + let objectRecords: Partial[] = []; if (args.ids) { const nonFormattedObjectRecords = (await existingRecordsQueryBuilder .where({ id: In(args.ids) }) - .getMany()) as ObjectRecord[]; + .getMany()) as T[]; objectRecords = formatResult( nonFormattedObjectRecords, - objectMetadataMapItem, - objectMetadataMap, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, ); } else if (args.data && !isEmpty(args.data)) { - objectRecords = formatData(args.data, objectMetadataMapItem); + objectRecords = formatData(args.data, objectMetadataItemWithFieldMaps); } - const duplicateConnections: IConnection[] = await Promise.all( + const duplicateConnections: IConnection[] = await Promise.all( objectRecords.map(async (record) => { const duplicateConditions = this.buildDuplicateConditions( - objectMetadataMapItem, + objectMetadataItemWithFieldMaps, [record], record.id, ); @@ -90,7 +93,7 @@ export class GraphqlQueryFindDuplicatesResolverService if (isEmpty(duplicateConditions)) { return typeORMObjectRecordsParser.createConnection({ objectRecords: [], - objectName: objectMetadataMapItem.nameSingular, + objectName: objectMetadataItemWithFieldMaps.nameSingular, take: 0, totalCount: 0, order: [{ id: OrderByDirection.AscNullsFirst }], @@ -101,22 +104,22 @@ export class GraphqlQueryFindDuplicatesResolverService const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder( duplicateRecordsQueryBuilder, - objectMetadataMapItem.nameSingular, + objectMetadataItemWithFieldMaps.nameSingular, duplicateConditions, ); const nonFormattedDuplicates = - (await withFilterQueryBuilder.getMany()) as ObjectRecord[]; + (await withFilterQueryBuilder.getMany()) as T[]; const duplicates = formatResult( nonFormattedDuplicates, - objectMetadataMapItem, - objectMetadataMap, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, ); return typeORMObjectRecordsParser.createConnection({ objectRecords: duplicates, - objectName: objectMetadataMapItem.nameSingular, + objectName: objectMetadataItemWithFieldMaps.nameSingular, take: duplicates.length, totalCount: duplicates.length, order: [{ id: OrderByDirection.AscNullsFirst }], @@ -130,16 +133,16 @@ export class GraphqlQueryFindDuplicatesResolverService } private buildDuplicateConditions( - objectMetadataMapItem: ObjectMetadataMapItem, - records?: Partial[] | undefined, + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + records?: Partial[] | undefined, filteringByExistingRecordId?: string, - ): Partial { + ): Partial { if (!records || records.length === 0) { return {}; } const criteriaCollection = this.getApplicableDuplicateCriteriaCollection( - objectMetadataMapItem, + objectMetadataItemWithFieldMaps, ); const conditions = records.flatMap((record) => { @@ -164,7 +167,7 @@ export class GraphqlQueryFindDuplicatesResolverService }); }); - const filter: Partial = {}; + const filter: Partial = {}; if (conditions && !isEmpty(conditions)) { filter.or = conditions; @@ -178,11 +181,12 @@ export class GraphqlQueryFindDuplicatesResolverService } private getApplicableDuplicateCriteriaCollection( - objectMetadataMapItem: ObjectMetadataMapItem, + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, ) { return DUPLICATE_CRITERIA_COLLECTION.filter( (duplicateCriteria) => - duplicateCriteria.objectName === objectMetadataMapItem.nameSingular, + duplicateCriteria.objectName === + objectMetadataItemWithFieldMaps.nameSingular, ); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts index 9411c5502103..9b4c850de096 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts @@ -1,15 +1,14 @@ import { Injectable } from '@nestjs/common'; -import { isDefined } from 'class-validator'; import graphqlFields from 'graphql-fields'; import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; import { - Record as IRecord, + ObjectRecord, + ObjectRecordFilter, + ObjectRecordOrderBy, OrderByDirection, - RecordFilter, - RecordOrderBy, -} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-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'; @@ -19,35 +18,45 @@ import { GraphqlQueryRunnerException, GraphqlQueryRunnerExceptionCode, } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; +import { GraphqlQuerySelectedFieldsResult } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser'; import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; +import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper'; import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; import { computeCursorArgFilter } from 'src/engine/api/graphql/graphql-query-runner/utils/compute-cursor-arg-filter'; import { getCursor, getPaginationInfo, } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util'; +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 { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; +import { isDefined } from 'src/utils/is-defined'; @Injectable() export class GraphqlQueryFindManyResolverService - implements ResolverService> + implements ResolverService> { constructor( private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly featureFlagService: FeatureFlagService, ) {} async resolve< - ObjectRecord extends IRecord = IRecord, - Filter extends RecordFilter = RecordFilter, - OrderBy extends RecordOrderBy = RecordOrderBy, + T extends ObjectRecord = ObjectRecord, + Filter extends ObjectRecordFilter = ObjectRecordFilter, + OrderBy extends ObjectRecordOrderBy = ObjectRecordOrderBy, >( args: FindManyResolverArgs, options: WorkspaceQueryRunnerOptions, - ): Promise> { - const { authContext, objectMetadataMapItem, info, objectMetadataMap } = - options; + ): Promise> { + const { + authContext, + objectMetadataItemWithFieldMaps, + info, + objectMetadataMaps, + } = options; const dataSource = await this.twentyORMGlobalManager.getDataSourceForWorkspace( @@ -55,48 +64,44 @@ export class GraphqlQueryFindManyResolverService ); const repository = dataSource.getRepository( - objectMetadataMapItem.nameSingular, + objectMetadataItemWithFieldMaps.nameSingular, ); const queryBuilder = repository.createQueryBuilder( - objectMetadataMapItem.nameSingular, + objectMetadataItemWithFieldMaps.nameSingular, ); - const countQueryBuilder = repository.createQueryBuilder( - objectMetadataMapItem.nameSingular, + const aggregateQueryBuilder = repository.createQueryBuilder( + objectMetadataItemWithFieldMaps.nameSingular, ); const graphqlQueryParser = new GraphqlQueryParser( - objectMetadataMapItem.fields, - objectMetadataMap, + objectMetadataItemWithFieldMaps.fieldsByName, + objectMetadataMaps, ); - const withFilterCountQueryBuilder = graphqlQueryParser.applyFilterToBuilder( - countQueryBuilder, - objectMetadataMapItem.nameSingular, - args.filter ?? ({} as Filter), - ); + const withFilterAggregateQueryBuilder = + graphqlQueryParser.applyFilterToBuilder( + aggregateQueryBuilder, + objectMetadataItemWithFieldMaps.nameSingular, + args.filter ?? ({} as Filter), + ); const selectedFields = graphqlFields(info); - const { relations } = graphqlQueryParser.parseSelectedFields( - objectMetadataMapItem, - selectedFields, - ); + const graphqlQuerySelectedFieldsResult: GraphqlQuerySelectedFieldsResult = + graphqlQueryParser.parseSelectedFields( + objectMetadataItemWithFieldMaps, + selectedFields, + ); const isForwardPagination = !isDefined(args.before); - const limit = args.first ?? args.last ?? QUERY_MAX_RECORDS; - - const withDeletedCountQueryBuilder = + const withDeletedAggregateQueryBuilder = graphqlQueryParser.applyDeletedAtToBuilder( - withFilterCountQueryBuilder, + withFilterAggregateQueryBuilder, args.filter ?? ({} as Filter), ); - const totalCount = isDefined(selectedFields.totalCount) - ? await withDeletedCountQueryBuilder.getCount() - : 0; - const cursor = getCursor(args); let appliedFilters = args.filter ?? ({} as Filter); @@ -110,7 +115,7 @@ export class GraphqlQueryFindManyResolverService const cursorArgFilter = computeCursorArgFilter( cursor, orderByWithIdCondition, - objectMetadataMapItem.fields, + objectMetadataItemWithFieldMaps.fieldsByName, isForwardPagination, ); @@ -123,14 +128,14 @@ export class GraphqlQueryFindManyResolverService const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder( queryBuilder, - objectMetadataMapItem.nameSingular, + objectMetadataItemWithFieldMaps.nameSingular, appliedFilters, ); const withOrderByQueryBuilder = graphqlQueryParser.applyOrderToBuilder( withFilterQueryBuilder, orderByWithIdCondition, - objectMetadataMapItem.nameSingular, + objectMetadataItemWithFieldMaps.nameSingular, isForwardPagination, ); @@ -139,14 +144,36 @@ export class GraphqlQueryFindManyResolverService args.filter ?? ({} as Filter), ); + const isAggregationsEnabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsAggregateQueryEnabled, + authContext.workspace.id, + ); + + if (!isAggregationsEnabled) { + graphqlQuerySelectedFieldsResult.aggregate = { + totalCount: graphqlQuerySelectedFieldsResult.aggregate.totalCount, + }; + } + + const processAggregateHelper = new ProcessAggregateHelper(); + + processAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({ + fieldMetadataMapByName: objectMetadataItemWithFieldMaps.fieldsByName, + selectedAggregatedFields: graphqlQuerySelectedFieldsResult.aggregate, + queryBuilder: withDeletedAggregateQueryBuilder, + }); + + const limit = args.first ?? args.last ?? QUERY_MAX_RECORDS; + const nonFormattedObjectRecords = await withDeletedQueryBuilder .take(limit + 1) .getMany(); const objectRecords = formatResult( nonFormattedObjectRecords, - objectMetadataMapItem, - objectMetadataMap, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, ); const { hasNextPage, hasPreviousPage } = getPaginationInfo( @@ -159,37 +186,42 @@ export class GraphqlQueryFindManyResolverService objectRecords.pop(); } + const parentObjectRecordsAggregatedValues = + await withDeletedAggregateQueryBuilder.getRawOne(); + const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); - if (relations) { - await processNestedRelationsHelper.processNestedRelations( - objectMetadataMap, - objectMetadataMapItem, - objectRecords, - relations, + if (graphqlQuerySelectedFieldsResult.relations) { + await processNestedRelationsHelper.processNestedRelations({ + objectMetadataMaps, + parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + parentObjectRecords: objectRecords, + parentObjectRecordsAggregatedValues, + relations: graphqlQuerySelectedFieldsResult.relations, + aggregate: graphqlQuerySelectedFieldsResult.aggregate, limit, authContext, dataSource, - ); + }); } const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); + new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); - const result = typeORMObjectRecordsParser.createConnection({ + return typeORMObjectRecordsParser.createConnection({ objectRecords, - objectName: objectMetadataMapItem.nameSingular, + objectRecordsAggregatedValues: parentObjectRecordsAggregatedValues, + selectedAggregatedFields: graphqlQuerySelectedFieldsResult.aggregate, + objectName: objectMetadataItemWithFieldMaps.nameSingular, take: limit, - totalCount, + totalCount: parentObjectRecordsAggregatedValues.totalCount, order: orderByWithIdCondition, hasNextPage, hasPreviousPage, }); - - return result; } - async validate( + async validate( args: FindManyResolverArgs, _options: WorkspaceQueryRunnerOptions, ): Promise { diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts index 42c8daae8079..bcd076a1729d 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts @@ -4,9 +4,9 @@ import graphqlFields from 'graphql-fields'; import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; import { - Record as IRecord, - RecordFilter, -} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; + ObjectRecord, + ObjectRecordFilter, +} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; @@ -27,21 +27,25 @@ import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; @Injectable() export class GraphqlQueryFindOneResolverService - implements ResolverService + implements ResolverService { constructor( private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} async resolve< - ObjectRecord extends IRecord = IRecord, - Filter extends RecordFilter = RecordFilter, + T extends ObjectRecord = ObjectRecord, + Filter extends ObjectRecordFilter = ObjectRecordFilter, >( args: FindOneResolverArgs, options: WorkspaceQueryRunnerOptions, - ): Promise { - const { authContext, objectMetadataMapItem, info, objectMetadataMap } = - options; + ): Promise { + const { + authContext, + objectMetadataItemWithFieldMaps, + info, + objectMetadataMaps, + } = options; const dataSource = await this.twentyORMGlobalManager.getDataSourceForWorkspace( @@ -49,28 +53,28 @@ export class GraphqlQueryFindOneResolverService ); const repository = dataSource.getRepository( - objectMetadataMapItem.nameSingular, + objectMetadataItemWithFieldMaps.nameSingular, ); const queryBuilder = repository.createQueryBuilder( - objectMetadataMapItem.nameSingular, + objectMetadataItemWithFieldMaps.nameSingular, ); const graphqlQueryParser = new GraphqlQueryParser( - objectMetadataMapItem.fields, - objectMetadataMap, + objectMetadataItemWithFieldMaps.fieldsByName, + objectMetadataMaps, ); const selectedFields = graphqlFields(info); const { relations } = graphqlQueryParser.parseSelectedFields( - objectMetadataMapItem, + objectMetadataItemWithFieldMaps, selectedFields, ); const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder( queryBuilder, - objectMetadataMapItem.nameSingular, + objectMetadataItemWithFieldMaps.nameSingular, args.filter ?? ({} as Filter), ); @@ -83,8 +87,8 @@ export class GraphqlQueryFindOneResolverService const objectRecord = formatResult( nonFormattedObjectRecord, - objectMetadataMapItem, - objectMetadataMap, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, ); if (!objectRecord) { @@ -99,29 +103,29 @@ export class GraphqlQueryFindOneResolverService const objectRecords = [objectRecord]; if (relations) { - await processNestedRelationsHelper.processNestedRelations( - objectMetadataMap, - objectMetadataMapItem, - objectRecords, + await processNestedRelationsHelper.processNestedRelations({ + objectMetadataMaps, + parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + parentObjectRecords: objectRecords, relations, - QUERY_MAX_RECORDS, + limit: QUERY_MAX_RECORDS, authContext, dataSource, - ); + }); } const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); + new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); return typeORMObjectRecordsParser.processRecord({ objectRecord: objectRecords[0], - objectName: objectMetadataMapItem.nameSingular, + objectName: objectMetadataItemWithFieldMaps.nameSingular, take: 1, totalCount: 1, - }) as ObjectRecord; + }) as T; } - async validate( + async validate( args: FindOneResolverArgs, _options: WorkspaceQueryRunnerOptions, ): Promise { diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts index 4cde38cfe6b4..c9e7455a37b1 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts @@ -5,10 +5,10 @@ import { Brackets } from 'typeorm'; import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; import { - Record as IRecord, + ObjectRecord, + ObjectRecordFilter, OrderByDirection, - RecordFilter, -} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-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 { SearchResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; @@ -22,40 +22,39 @@ import { isDefined } from 'src/utils/is-defined'; @Injectable() export class GraphqlQuerySearchResolverService - implements ResolverService> + implements ResolverService> { constructor( private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} async resolve< - ObjectRecord extends IRecord = IRecord, - Filter extends RecordFilter = RecordFilter, + T extends ObjectRecord = ObjectRecord, + Filter extends ObjectRecordFilter = ObjectRecordFilter, >( args: SearchResolverArgs, options: WorkspaceQueryRunnerOptions, - ): Promise> { + ): Promise> { const { authContext, - objectMetadataItem, - objectMetadataMapItem, - objectMetadataMap, + objectMetadataMaps, + objectMetadataItemWithFieldMaps, info, } = options; const repository = await this.twentyORMGlobalManager.getRepositoryForWorkspace( authContext.workspace.id, - objectMetadataItem.nameSingular, + objectMetadataItemWithFieldMaps.nameSingular, ); const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); + new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); if (!isDefined(args.searchInput)) { return typeORMObjectRecordsParser.createConnection({ objectRecords: [], - objectName: objectMetadataItem.nameSingular, + objectName: objectMetadataItemWithFieldMaps.nameSingular, take: 0, totalCount: 0, order: [{ id: OrderByDirection.AscNullsFirst }], @@ -69,16 +68,16 @@ export class GraphqlQuerySearchResolverService const limit = args?.limit ?? QUERY_MAX_RECORDS; const queryBuilder = repository.createQueryBuilder( - objectMetadataItem.nameSingular, + objectMetadataItemWithFieldMaps.nameSingular, ); const graphqlQueryParser = new GraphqlQueryParser( - objectMetadataMapItem.fields, - objectMetadataMap, + objectMetadataItemWithFieldMaps.fieldsByName, + objectMetadataMaps, ); const queryBuilderWithFilter = graphqlQueryParser.applyFilterToBuilder( queryBuilder, - objectMetadataMapItem.nameSingular, + objectMetadataItemWithFieldMaps.nameSingular, args.filter ?? ({} as Filter), ); @@ -109,7 +108,7 @@ export class GraphqlQuerySearchResolverService .setParameter('searchTerms', searchTerms) .setParameter('searchTermsOr', searchTermsOr) .take(limit) - .getMany()) as ObjectRecord[]; + .getMany()) as T[]; const objectRecords = await repository.formatResult(resultsWithTsVector); @@ -122,7 +121,7 @@ export class GraphqlQuerySearchResolverService return typeORMObjectRecordsParser.createConnection({ objectRecords: objectRecords ?? [], - objectName: objectMetadataItem.nameSingular, + objectName: objectMetadataItemWithFieldMaps.nameSingular, take: limit, totalCount, order, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts index 020bf08fa722..461940be3f3f 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common'; import graphqlFields from 'graphql-fields'; import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; -import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { UpdateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; @@ -20,18 +20,22 @@ import { computeTableName } from 'src/engine/utils/compute-table-name.util'; @Injectable() export class GraphqlQueryUpdateManyResolverService - implements ResolverService + implements ResolverService { constructor( private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} - async resolve( - args: UpdateManyResolverArgs>, + async resolve( + args: UpdateManyResolverArgs>, options: WorkspaceQueryRunnerOptions, - ): Promise { - const { authContext, objectMetadataMapItem, objectMetadataMap, info } = - options; + ): Promise { + const { + authContext, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + info, + } = options; const dataSource = await this.twentyORMGlobalManager.getDataSourceForWorkspace( @@ -39,28 +43,28 @@ export class GraphqlQueryUpdateManyResolverService ); const repository = dataSource.getRepository( - objectMetadataMapItem.nameSingular, + objectMetadataItemWithFieldMaps.nameSingular, ); const graphqlQueryParser = new GraphqlQueryParser( - objectMetadataMapItem.fields, - objectMetadataMap, + objectMetadataItemWithFieldMaps.fieldsByName, + objectMetadataMaps, ); const selectedFields = graphqlFields(info); const { relations } = graphqlQueryParser.parseSelectedFields( - objectMetadataMapItem, + objectMetadataItemWithFieldMaps, selectedFields, ); const queryBuilder = repository.createQueryBuilder( - objectMetadataMapItem.nameSingular, + objectMetadataItemWithFieldMaps.nameSingular, ); const tableName = computeTableName( - objectMetadataMapItem.nameSingular, - objectMetadataMapItem.isCustom, + objectMetadataItemWithFieldMaps.nameSingular, + objectMetadataItemWithFieldMaps.isCustom, ); const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder( @@ -69,7 +73,7 @@ export class GraphqlQueryUpdateManyResolverService args.filter, ); - const data = formatData(args.data, objectMetadataMapItem); + const data = formatData(args.data, objectMetadataItemWithFieldMaps); const nonFormattedUpdatedObjectRecords = await withFilterQueryBuilder .update(data) @@ -78,42 +82,42 @@ export class GraphqlQueryUpdateManyResolverService const updatedRecords = formatResult( nonFormattedUpdatedObjectRecords.raw, - objectMetadataMapItem, - objectMetadataMap, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, ); const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); if (relations) { - await processNestedRelationsHelper.processNestedRelations( - objectMetadataMap, - objectMetadataMapItem, - updatedRecords, + await processNestedRelationsHelper.processNestedRelations({ + objectMetadataMaps, + parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + parentObjectRecords: updatedRecords, relations, - QUERY_MAX_RECORDS, + limit: QUERY_MAX_RECORDS, authContext, dataSource, - ); + }); } const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); + new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); - return updatedRecords.map((record: ObjectRecord) => + return updatedRecords.map((record: T) => typeORMObjectRecordsParser.processRecord({ objectRecord: record, - objectName: objectMetadataMapItem.nameSingular, + objectName: objectMetadataItemWithFieldMaps.nameSingular, take: 1, totalCount: 1, }), ); } - async validate( - args: UpdateManyResolverArgs>, + async validate( + args: UpdateManyResolverArgs>, options: WorkspaceQueryRunnerOptions, ): Promise { - assertMutationNotOnRemoteObject(options.objectMetadataMapItem); + assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps); if (!args.filter) { throw new Error('Filter is required'); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts index 8fe4396d2413..6475e6488d50 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common'; import graphqlFields from 'graphql-fields'; import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; -import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { UpdateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; @@ -23,18 +23,22 @@ import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; @Injectable() export class GraphqlQueryUpdateOneResolverService - implements ResolverService + implements ResolverService { constructor( private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} - async resolve( - args: UpdateOneResolverArgs>, + async resolve( + args: UpdateOneResolverArgs>, options: WorkspaceQueryRunnerOptions, - ): Promise { - const { authContext, objectMetadataMapItem, objectMetadataMap, info } = - options; + ): Promise { + const { + authContext, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + info, + } = options; const dataSource = await this.twentyORMGlobalManager.getDataSourceForWorkspace( @@ -42,26 +46,26 @@ export class GraphqlQueryUpdateOneResolverService ); const repository = dataSource.getRepository( - objectMetadataMapItem.nameSingular, + objectMetadataItemWithFieldMaps.nameSingular, ); const graphqlQueryParser = new GraphqlQueryParser( - objectMetadataMapItem.fields, - objectMetadataMap, + objectMetadataItemWithFieldMaps.fieldsByName, + objectMetadataMaps, ); const selectedFields = graphqlFields(info); const { relations } = graphqlQueryParser.parseSelectedFields( - objectMetadataMapItem, + objectMetadataItemWithFieldMaps, selectedFields, ); const queryBuilder = repository.createQueryBuilder( - objectMetadataMapItem.nameSingular, + objectMetadataItemWithFieldMaps.nameSingular, ); - const data = formatData(args.data, objectMetadataMapItem); + const data = formatData(args.data, objectMetadataItemWithFieldMaps); const result = await queryBuilder .update(data) @@ -73,8 +77,8 @@ export class GraphqlQueryUpdateOneResolverService const updatedRecords = formatResult( nonFormattedUpdatedObjectRecords, - objectMetadataMapItem, - objectMetadataMap, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, ); if (updatedRecords.length === 0) { @@ -84,38 +88,38 @@ export class GraphqlQueryUpdateOneResolverService ); } - const updatedRecord = updatedRecords[0] as ObjectRecord; + const updatedRecord = updatedRecords[0] as T; const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); if (relations) { - await processNestedRelationsHelper.processNestedRelations( - objectMetadataMap, - objectMetadataMapItem, - [updatedRecord], + await processNestedRelationsHelper.processNestedRelations({ + objectMetadataMaps, + parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + parentObjectRecords: [updatedRecord], relations, - QUERY_MAX_RECORDS, + limit: QUERY_MAX_RECORDS, authContext, dataSource, - ); + }); } const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); + new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); - return typeORMObjectRecordsParser.processRecord({ + return typeORMObjectRecordsParser.processRecord({ objectRecord: updatedRecord, - objectName: objectMetadataMapItem.nameSingular, + objectName: objectMetadataItemWithFieldMaps.nameSingular, take: 1, totalCount: 1, }); } - async validate( - args: UpdateOneResolverArgs>, + async validate( + args: UpdateOneResolverArgs>, options: WorkspaceQueryRunnerOptions, ): Promise { - assertMutationNotOnRemoteObject(options.objectMetadataMapItem); + assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps); assertIsValidUuid(args.id); } } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service.ts index a96a52d99a3b..1f5c96fd91ca 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service.ts @@ -1,18 +1,18 @@ import { Injectable } from '@nestjs/common'; -import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-values'; -import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; +import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; @Injectable() export class ApiEventEmitterService { constructor(private readonly workspaceEventEmitter: WorkspaceEventEmitter) {} - public emitCreateEvents( + public emitCreateEvents( records: T[], authContext: AuthContext, objectMetadataItem: ObjectMetadataInterface, @@ -32,7 +32,7 @@ export class ApiEventEmitterService { ); } - public emitUpdateEvents( + public emitUpdateEvents( existingRecords: T[], records: T[], updatedFields: string[], @@ -77,7 +77,7 @@ export class ApiEventEmitterService { ); } - public emitDeletedEvents( + public emitDeletedEvents( records: T[], authContext: AuthContext, objectMetadataItem: ObjectMetadataInterface, @@ -99,7 +99,7 @@ export class ApiEventEmitterService { ); } - public emitDestroyEvents( + public emitDestroyEvents( records: T[], authContext: AuthContext, objectMetadataItem: ObjectMetadataInterface, @@ -121,9 +121,7 @@ export class ApiEventEmitterService { ); } - private removeGraphQLAndNestedProperties( - record: ObjectRecord, - ) { + private removeGraphQLAndNestedProperties(record: T) { if (!record) { return {}; } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-cursor-arg-filter.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-cursor-arg-filter.ts index c602aef7fcba..02cd804447db 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-cursor-arg-filter.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-cursor-arg-filter.ts @@ -1,8 +1,8 @@ import { + ObjectRecordFilter, + ObjectRecordOrderBy, OrderByDirection, - RecordFilter, - RecordOrderBy, -} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { GraphqlQueryRunnerException, @@ -11,14 +11,14 @@ import { import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { FieldMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; export const computeCursorArgFilter = ( cursor: Record, - orderBy: RecordOrderBy, - fieldMetadataMap: FieldMetadataMap, + orderBy: ObjectRecordOrderBy, + fieldMetadataMapByName: FieldMetadataMap, isForwardPagination = true, -): RecordFilter[] => { +): ObjectRecordFilter[] => { const cursorKeys = Object.keys(cursor ?? {}); const cursorValues = Object.values(cursor ?? {}); @@ -39,7 +39,7 @@ export const computeCursorArgFilter = ( ...buildWhereCondition( cursorKeys[subConditionIndex], cursorValues[subConditionIndex], - fieldMetadataMap, + fieldMetadataMapByName, 'eq', ), }; @@ -68,18 +68,18 @@ export const computeCursorArgFilter = ( return { ...whereCondition, - ...buildWhereCondition(key, value, fieldMetadataMap, operator), - } as RecordFilter; + ...buildWhereCondition(key, value, fieldMetadataMapByName, operator), + } as ObjectRecordFilter; }); }; const buildWhereCondition = ( key: string, value: any, - fieldMetadataMap: FieldMetadataMap, + fieldMetadataMapByName: FieldMetadataMap, operator: string, ): Record => { - const fieldMetadata = fieldMetadataMap[key]; + const fieldMetadata = fieldMetadataMapByName[key]; if (!fieldMetadata) { throw new GraphqlQueryRunnerException( diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts index bd27522ce1b2..8ae1486c08ff 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts @@ -1,7 +1,7 @@ import { - Record as IRecord, - RecordOrderBy, -} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; + ObjectRecord, + ObjectRecordOrderBy, +} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { @@ -24,9 +24,9 @@ export const decodeCursor = (cursor: string): CursorData => { } }; -export const encodeCursor = ( - objectRecord: ObjectRecord, - order: RecordOrderBy | undefined, +export const encodeCursor = ( + objectRecord: T, + order: ObjectRecordOrderBy | undefined, ): string => { const orderByValues: Record = {}; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-object-metadata-or-throw.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-object-metadata-or-throw.util.ts deleted file mode 100644 index 00ef040204e0..000000000000 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-object-metadata-or-throw.util.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { - GraphqlQueryRunnerException, - GraphqlQueryRunnerExceptionCode, -} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; -import { ObjectMetadataMapItem } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; - -export const getObjectMetadataOrThrow = ( - objectMetadataMap: Record, - objectName: string, -): ObjectMetadataMapItem => { - const objectMetadata = objectMetadataMap[objectName]; - - if (!objectMetadata) { - throw new GraphqlQueryRunnerException( - `Object metadata not found for ${objectName}`, - GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND, - ); - } - - return objectMetadata; -}; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util.ts index d05bdcced27a..2e26962403cc 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util.ts @@ -1,7 +1,7 @@ import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; -import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { deduceRelationDirection, RelationDirection, @@ -9,7 +9,7 @@ import { export const getRelationObjectMetadata = ( fieldMetadata: FieldMetadataInterface, - objectMetadataMap: ObjectMetadataMap, + objectMetadataMaps: ObjectMetadataMaps, ) => { const relationMetadata = getRelationMetadata(fieldMetadata); @@ -20,8 +20,8 @@ export const getRelationObjectMetadata = ( const referencedObjectMetadata = relationDirection === RelationDirection.TO - ? objectMetadataMap[relationMetadata.fromObjectMetadataId] - : objectMetadataMap[relationMetadata.toObjectMetadataId]; + ? objectMetadataMaps.byId[relationMetadata.fromObjectMetadataId] + : objectMetadataMaps.byId[relationMetadata.toObjectMetadataId]; if (!referencedObjectMetadata) { throw new Error( diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/interfaces/record.interface.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface.ts similarity index 57% rename from packages/twenty-server/src/engine/api/graphql/workspace-query-builder/interfaces/record.interface.ts rename to packages/twenty-server/src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface.ts index 54d2c373b642..a93e752e2267 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/interfaces/record.interface.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface.ts @@ -1,4 +1,4 @@ -export interface Record { +export interface ObjectRecord { id: string; [key: string]: any; createdAt: string; @@ -6,8 +6,8 @@ export interface Record { deletedAt: string | null; } -export type RecordFilter = { - [Property in keyof Record]: any; +export type ObjectRecordFilter = { + [Property in keyof ObjectRecord]: any; }; export enum OrderByDirection { @@ -17,11 +17,11 @@ export enum OrderByDirection { DescNullsLast = 'DescNullsLast', } -export type RecordOrderBy = Array<{ - [Property in keyof Record]?: OrderByDirection; +export type ObjectRecordOrderBy = Array<{ + [Property in keyof ObjectRecord]?: OrderByDirection; }>; -export interface RecordDuplicateCriteria { +export interface ObjectRecordDuplicateCriteria { objectName: string; columnNames: string[]; } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/__tests__/query-runner-args.factory.spec.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/__tests__/query-runner-args.factory.spec.ts index 5c5650538b09..f814e04f7c97 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/__tests__/query-runner-args.factory.spec.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/__tests__/query-runner-args.factory.spec.ts @@ -1,12 +1,12 @@ import { Test, TestingModule } from '@nestjs/testing'; import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { ResolverArgsType } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RecordPositionFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/record-position.factory'; +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; describe('QueryRunnerArgsFactory', () => { const recordPositionFactory = { @@ -14,13 +14,29 @@ describe('QueryRunnerArgsFactory', () => { }; const workspaceId = 'workspaceId'; const options = { - fieldMetadataCollection: [ - { name: 'position', type: FieldMetadataType.POSITION }, - { name: 'testNumber', type: FieldMetadataType.NUMBER }, - ] as FieldMetadataInterface[], - objectMetadataItem: { isCustom: true, nameSingular: 'test' }, authContext: { workspace: { id: workspaceId } }, - } as WorkspaceQueryRunnerOptions; + objectMetadataItemWithFieldMaps: { + isCustom: true, + nameSingular: 'testNumber', + fieldsByName: { + position: { + type: FieldMetadataType.POSITION, + isCustom: true, + nameSingular: 'position', + }, + testNumber: { + type: FieldMetadataType.NUMBER, + isCustom: true, + nameSingular: 'testNumber', + }, + otherField: { + type: FieldMetadataType.TEXT, + isCustom: true, + nameSingular: 'otherField', + }, + } as unknown as FieldMetadataMap, + }, + } as unknown as WorkspaceQueryRunnerOptions; let factory: QueryRunnerArgsFactory; @@ -61,7 +77,7 @@ describe('QueryRunnerArgsFactory', () => { it('createMany type should override data position and number', async () => { const args = { id: 'uuid', - data: [{ position: 'last', testNumber: '1' }], + data: [{ position: 'last', testNumber: 1 }], }; const result = await factory.create( @@ -72,7 +88,7 @@ describe('QueryRunnerArgsFactory', () => { expect(recordPositionFactory.create).toHaveBeenCalledWith( 'last', - { isCustom: true, nameSingular: 'test' }, + { isCustom: true, nameSingular: 'testNumber' }, workspaceId, 0, ); @@ -85,7 +101,7 @@ describe('QueryRunnerArgsFactory', () => { it('createMany type should override position if not present', async () => { const args = { id: 'uuid', - data: [{ testNumber: '1' }], + data: [{ testNumber: 1 }], }; const result = await factory.create( @@ -96,7 +112,7 @@ describe('QueryRunnerArgsFactory', () => { expect(recordPositionFactory.create).toHaveBeenCalledWith( 'first', - { isCustom: true, nameSingular: 'test' }, + { isCustom: true, nameSingular: 'testNumber' }, workspaceId, 0, ); @@ -109,7 +125,7 @@ describe('QueryRunnerArgsFactory', () => { it('findMany type should override data position and number', async () => { const args = { id: 'uuid', - filter: { testNumber: { eq: '1' }, otherField: { eq: 'test' } }, + filter: { testNumber: { eq: 1 }, otherField: { eq: 'test' } }, }; const result = await factory.create( @@ -127,7 +143,7 @@ describe('QueryRunnerArgsFactory', () => { it('findOne type should override number in filter', async () => { const args = { id: 'uuid', - filter: { testNumber: { eq: '1' }, otherField: { eq: 'test' } }, + filter: { testNumber: { eq: 1 }, otherField: { eq: 'test' } }, }; const result = await factory.create( @@ -143,23 +159,14 @@ describe('QueryRunnerArgsFactory', () => { }); it('findDuplicates type should override number in data and id', async () => { - const optionsDuplicate = { - fieldMetadataCollection: [ - { name: 'id', type: FieldMetadataType.NUMBER }, - { name: 'testNumber', type: FieldMetadataType.NUMBER }, - ] as FieldMetadataInterface[], - objectMetadataItem: { isCustom: true, nameSingular: 'test' }, - authContext: { workspace: { id: workspaceId } }, - } as WorkspaceQueryRunnerOptions; - const args = { - ids: ['123'], - data: [{ testNumber: '1', otherField: 'test' }], + ids: [123], + data: [{ testNumber: 1, otherField: 'test' }], }; const result = await factory.create( args, - optionsDuplicate, + options, ResolverArgsType.FindDuplicates, ); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts index bdde719563b4..ddcf2ff282c6 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts @@ -1,6 +1,9 @@ import { Injectable } from '@nestjs/common'; -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; +import { + ObjectRecord, + ObjectRecordFilter, +} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { CreateManyResolverArgs, @@ -10,13 +13,11 @@ import { ResolverArgs, ResolverArgsType, } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; -import { - Record, - 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 { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { hasPositionField } from 'src/engine/metadata-modules/object-metadata/utils/has-position-field.util'; +import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; import { RecordPositionFactory } from './record-position.factory'; @@ -34,27 +35,28 @@ export class QueryRunnerArgsFactory { options: WorkspaceQueryRunnerOptions, resolverArgsType: ResolverArgsType, ) { - const fieldMetadataCollection = options.fieldMetadataCollection; + const fieldMetadataMapByNameByName = + options.objectMetadataItemWithFieldMaps.fieldsByName; - const fieldMetadataMap = new Map( - fieldMetadataCollection.map((fieldMetadata) => [ - fieldMetadata.name, - fieldMetadata, - ]), + const shouldBackfillPosition = hasPositionField( + options.objectMetadataItemWithFieldMaps, ); - const shouldBackfillPosition = hasPositionField(options.objectMetadataItem); - switch (resolverArgsType) { case ResolverArgsType.CreateMany: return { ...args, data: await Promise.all( (args as CreateManyResolverArgs).data?.map((arg, index) => - this.overrideDataByFieldMetadata(arg, options, fieldMetadataMap, { - argIndex: index, - shouldBackfillPosition, - }), + this.overrideDataByFieldMetadata( + arg, + options, + fieldMetadataMapByNameByName, + { + argIndex: index, + shouldBackfillPosition, + }, + ), ) ?? [], ), } satisfies CreateManyResolverArgs; @@ -63,7 +65,7 @@ export class QueryRunnerArgsFactory { ...args, filter: await this.overrideFilterByFieldMetadata( (args as FindOneResolverArgs).filter, - fieldMetadataMap, + fieldMetadataMapByNameByName, ), }; case ResolverArgsType.FindMany: @@ -71,7 +73,7 @@ export class QueryRunnerArgsFactory { ...args, filter: await this.overrideFilterByFieldMetadata( (args as FindManyResolverArgs).filter, - fieldMetadataMap, + fieldMetadataMapByNameByName, ), }; @@ -80,15 +82,24 @@ export class QueryRunnerArgsFactory { ...args, ids: (await Promise.all( (args as FindDuplicatesResolverArgs).ids?.map((id) => - this.overrideValueByFieldMetadata('id', id, fieldMetadataMap), + this.overrideValueByFieldMetadata( + 'id', + id, + fieldMetadataMapByNameByName, + ), ) ?? [], )) as string[], data: await Promise.all( (args as FindDuplicatesResolverArgs).data?.map((arg, index) => - this.overrideDataByFieldMetadata(arg, options, fieldMetadataMap, { - argIndex: index, - shouldBackfillPosition, - }), + this.overrideDataByFieldMetadata( + arg, + options, + fieldMetadataMapByNameByName, + { + argIndex: index, + shouldBackfillPosition, + }, + ), ) ?? [], ), } satisfies FindDuplicatesResolverArgs; @@ -98,9 +109,9 @@ export class QueryRunnerArgsFactory { } private async overrideDataByFieldMetadata( - data: Partial | undefined, + data: Partial | undefined, options: WorkspaceQueryRunnerOptions, - fieldMetadataMap: Map, + fieldMetadataMapByNameByName: Record, argPositionBackfillInput: ArgPositionBackfillInput, ) { if (!data) { @@ -111,7 +122,7 @@ export class QueryRunnerArgsFactory { const createArgPromiseByArgKey = Object.entries(data).map( async ([key, value]) => { - const fieldMetadata = fieldMetadataMap.get(key); + const fieldMetadata = fieldMetadataMapByNameByName[key]; if (!fieldMetadata) { return [key, await Promise.resolve(value)]; @@ -126,8 +137,9 @@ export class QueryRunnerArgsFactory { await this.recordPositionFactory.create( value, { - isCustom: options.objectMetadataItem.isCustom, - nameSingular: options.objectMetadataItem.nameSingular, + isCustom: options.objectMetadataItemWithFieldMaps.isCustom, + nameSingular: + options.objectMetadataItemWithFieldMaps.nameSingular, }, options.authContext.workspace.id, argPositionBackfillInput.argIndex, @@ -154,8 +166,9 @@ export class QueryRunnerArgsFactory { await this.recordPositionFactory.create( 'first', { - isCustom: options.objectMetadataItem.isCustom, - nameSingular: options.objectMetadataItem.nameSingular, + isCustom: options.objectMetadataItemWithFieldMaps.isCustom, + nameSingular: + options.objectMetadataItemWithFieldMaps.nameSingular, }, options.authContext.workspace.id, argPositionBackfillInput.argIndex, @@ -168,23 +181,27 @@ export class QueryRunnerArgsFactory { } private overrideFilterByFieldMetadata( - filter: RecordFilter | undefined, - fieldMetadataMap: Map, + filter: ObjectRecordFilter | undefined, + fieldMetadataMapByName: Record, ) { if (!filter) { return; } - const overrideFilter = (filterObject: RecordFilter) => { + const overrideFilter = (filterObject: ObjectRecordFilter) => { return Object.entries(filterObject).reduce((acc, [key, value]) => { if (key === 'and' || key === 'or') { - acc[key] = value.map((nestedFilter: RecordFilter) => + acc[key] = value.map((nestedFilter: ObjectRecordFilter) => overrideFilter(nestedFilter), ); } else if (key === 'not') { acc[key] = overrideFilter(value); } else { - acc[key] = this.transformValueByType(key, value, fieldMetadataMap); + acc[key] = this.transformValueByType( + key, + value, + fieldMetadataMapByName, + ); } return acc; @@ -197,9 +214,9 @@ export class QueryRunnerArgsFactory { private transformValueByType( key: string, value: any, - fieldMetadataMap: Map, + fieldMetadataMapByName: FieldMetadataMap, ) { - const fieldMetadata = fieldMetadataMap.get(key); + const fieldMetadata = fieldMetadataMapByName[key]; if (!fieldMetadata) { return value; @@ -226,9 +243,9 @@ export class QueryRunnerArgsFactory { private async overrideValueByFieldMetadata( key: string, value: any, - fieldMetadataMap: Map, + fieldMetadataMapByName: FieldMetadataMap, ) { - const fieldMetadata = fieldMetadataMap.get(key); + const fieldMetadata = fieldMetadataMapByName[key]; if (!fieldMetadata) { return value; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/interfaces/pg-graphql.interface.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/interfaces/pg-graphql.interface.ts deleted file mode 100644 index c015f34a3b86..000000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/interfaces/pg-graphql.interface.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; - -export interface PGGraphQLResponse { - resolve: { - data: Data; - errors: any[]; - }; -} - -export type PGGraphQLResult = [PGGraphQLResponse]; - -export interface PGGraphQLMutation { - affectedRows: number; - records: Record[]; -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface.ts index d960c3d45a7f..5f8268c7102c 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface.ts @@ -1,20 +1,12 @@ import { GraphQLResolveInfo } from 'graphql'; -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'; - import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { - ObjectMetadataMap, - ObjectMetadataMapItem, -} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; export interface WorkspaceQueryRunnerOptions { authContext: AuthContext; info: GraphQLResolveInfo; - objectMetadataItem: ObjectMetadataInterface; - fieldMetadataCollection: FieldMetadataInterface[]; - objectMetadataCollection: ObjectMetadataInterface[]; - objectMetadataMap: ObjectMetadataMap; - objectMetadataMapItem: ObjectMetadataMapItem; + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; + objectMetadataMaps: ObjectMetadataMaps; } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/telemetry.listener.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/telemetry.listener.ts index 67c2f321f97d..df7d196711d1 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/telemetry.listener.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/telemetry.listener.ts @@ -1,12 +1,12 @@ import { Injectable } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; +import { OnDatabaseEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-event.decorator'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { AnalyticsService } from 'src/engine/core-modules/analytics/analytics.service'; import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; import { TelemetryService } from 'src/engine/core-modules/telemetry/telemetry.service'; import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/workspace-event.type'; -import { OnDatabaseEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-event.decorator'; -import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @Injectable() export class TelemetryListener { diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/with-soft-deleted.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/with-soft-deleted.util.ts index a972782a6ad1..0ed2d7391821 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/with-soft-deleted.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/with-soft-deleted.util.ts @@ -1,8 +1,8 @@ -import { RecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { isDefined } from 'src/utils/is-defined'; -export const withSoftDeleted = ( +export const withSoftDeleted = ( filter: T | undefined | null, ): boolean => { if (!isDefined(filter)) { diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts index fc2f0fbbaef9..7afd8133eb17 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts @@ -31,22 +31,23 @@ export const workspaceQueryRunnerGraphqlApiExceptionHandler = ( if (indexNameMatch) { const indexName = indexNameMatch[1]; - const deletedAtFieldMetadata = context.objectMetadataItem.fields.find( - (field) => field.name === 'deletedAt', - ); + const deletedAtFieldMetadata = + context.objectMetadataItemWithFieldMaps.fieldsByName['deletedAt']; - const affectedColumns = context.objectMetadataItem.indexMetadatas - .find((index) => index.name === indexName) - ?.indexFieldMetadatas?.filter( - (field) => field.fieldMetadataId !== deletedAtFieldMetadata?.id, - ) - .map((indexField) => { - const fieldMetadata = context.objectMetadataItem.fields.find( - (objectField) => indexField.fieldMetadataId === objectField.id, - ); + const affectedColumns = + context.objectMetadataItemWithFieldMaps.indexMetadatas + .find((index) => index.name === indexName) + ?.indexFieldMetadatas?.filter( + (field) => field.fieldMetadataId !== deletedAtFieldMetadata?.id, + ) + .map((indexField) => { + const fieldMetadata = + context.objectMetadataItemWithFieldMaps.fieldsById[ + indexField.fieldMetadataId + ]; - return fieldMetadata?.label; - }); + return fieldMetadata?.label; + }); const columnNames = affectedColumns?.join(', '); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service.ts index f1cc8f6e93c8..5d180984699f 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import merge from 'lodash.merge'; -import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { WorkspaceQueryHookKey } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; @@ -53,13 +53,13 @@ export class WorkspaceQueryHookService { public async executePostQueryHooks< T extends WorkspaceResolverBuilderMethodNames, - Record extends IRecord = IRecord, + U extends ObjectRecord = ObjectRecord, >( authContext: AuthContext, // TODO: We should allow wildcard for object name objectName: string, methodName: T, - payload: Record[], + payload: U[], ): Promise { const key: WorkspaceQueryHookKey = `${objectName}.${methodName}`; const postHookInstances = diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts index c6d3303fb42f..84e16ce844c4 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts @@ -30,12 +30,10 @@ export class CreateManyResolverFactory try { const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, - objectMetadataItem: internalContext.objectMetadataItem, info, - fieldMetadataCollection: internalContext.fieldMetadataCollection, - objectMetadataCollection: internalContext.objectMetadataCollection, - objectMetadataMap: internalContext.objectMetadataMap, - objectMetadataMapItem: internalContext.objectMetadataMapItem, + objectMetadataMaps: internalContext.objectMetadataMaps, + objectMetadataItemWithFieldMaps: + internalContext.objectMetadataItemWithFieldMaps, }; return await this.graphqlQueryRunnerService.createMany(args, options); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts index 3c2d9095e62c..650b0bda1f4b 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts @@ -30,12 +30,10 @@ export class CreateOneResolverFactory try { const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, - objectMetadataItem: internalContext.objectMetadataItem, info, - fieldMetadataCollection: internalContext.fieldMetadataCollection, - objectMetadataCollection: internalContext.objectMetadataCollection, - objectMetadataMap: internalContext.objectMetadataMap, - objectMetadataMapItem: internalContext.objectMetadataMapItem, + objectMetadataMaps: internalContext.objectMetadataMaps, + objectMetadataItemWithFieldMaps: + internalContext.objectMetadataItemWithFieldMaps, }; return await this.graphqlQueryRunnerService.createOne(args, options); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts index 191514f86309..3be04fdd7fdb 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts @@ -30,12 +30,10 @@ export class DeleteManyResolverFactory try { const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, - objectMetadataItem: internalContext.objectMetadataItem, info, - fieldMetadataCollection: internalContext.fieldMetadataCollection, - objectMetadataCollection: internalContext.objectMetadataCollection, - objectMetadataMap: internalContext.objectMetadataMap, - objectMetadataMapItem: internalContext.objectMetadataMapItem, + objectMetadataMaps: internalContext.objectMetadataMaps, + objectMetadataItemWithFieldMaps: + internalContext.objectMetadataItemWithFieldMaps, }; return await this.graphqlQueryRunnerService.deleteMany(args, options); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts index 7cbd7bf3bddd..596a1d4db260 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts @@ -30,12 +30,10 @@ export class DeleteOneResolverFactory try { const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, - objectMetadataItem: internalContext.objectMetadataItem, info, - fieldMetadataCollection: internalContext.fieldMetadataCollection, - objectMetadataCollection: internalContext.objectMetadataCollection, - objectMetadataMap: internalContext.objectMetadataMap, - objectMetadataMapItem: internalContext.objectMetadataMapItem, + objectMetadataMaps: internalContext.objectMetadataMaps, + objectMetadataItemWithFieldMaps: + internalContext.objectMetadataItemWithFieldMaps, }; return await this.graphqlQueryRunnerService.deleteOne(args, options); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts index 80da084e630c..3eacd530b062 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts @@ -30,12 +30,10 @@ export class DestroyManyResolverFactory try { const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, - objectMetadataItem: internalContext.objectMetadataItem, info, - fieldMetadataCollection: internalContext.fieldMetadataCollection, - objectMetadataCollection: internalContext.objectMetadataCollection, - objectMetadataMap: internalContext.objectMetadataMap, - objectMetadataMapItem: internalContext.objectMetadataMapItem, + objectMetadataMaps: internalContext.objectMetadataMaps, + objectMetadataItemWithFieldMaps: + internalContext.objectMetadataItemWithFieldMaps, }; return await this.graphqlQueryRunnerService.destroyMany(args, options); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory.ts index c3dd4416918b..7dbf01a8307f 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory.ts @@ -30,12 +30,10 @@ export class DestroyOneResolverFactory try { const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, - objectMetadataItem: internalContext.objectMetadataItem, info, - fieldMetadataCollection: internalContext.fieldMetadataCollection, - objectMetadataCollection: internalContext.objectMetadataCollection, - objectMetadataMap: internalContext.objectMetadataMap, - objectMetadataMapItem: internalContext.objectMetadataMapItem, + objectMetadataMaps: internalContext.objectMetadataMaps, + objectMetadataItemWithFieldMaps: + internalContext.objectMetadataItemWithFieldMaps, }; return await this.graphQLQueryRunnerService.destroyOne(args, options); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts index 154c2c88646e..9fe6fc822135 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts @@ -30,12 +30,10 @@ export class FindDuplicatesResolverFactory try { const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, - objectMetadataItem: internalContext.objectMetadataItem, info, - fieldMetadataCollection: internalContext.fieldMetadataCollection, - objectMetadataCollection: internalContext.objectMetadataCollection, - objectMetadataMap: internalContext.objectMetadataMap, - objectMetadataMapItem: internalContext.objectMetadataMapItem, + objectMetadataMaps: internalContext.objectMetadataMaps, + objectMetadataItemWithFieldMaps: + internalContext.objectMetadataItemWithFieldMaps, }; return await this.graphqlQueryRunnerService.findDuplicates( diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-many-resolver.factory.ts index d46db50962b8..064927d211e5 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-many-resolver.factory.ts @@ -30,12 +30,10 @@ export class FindManyResolverFactory try { const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, - objectMetadataItem: internalContext.objectMetadataItem, info, - fieldMetadataCollection: internalContext.fieldMetadataCollection, - objectMetadataCollection: internalContext.objectMetadataCollection, - objectMetadataMap: internalContext.objectMetadataMap, - objectMetadataMapItem: internalContext.objectMetadataMapItem, + objectMetadataMaps: internalContext.objectMetadataMaps, + objectMetadataItemWithFieldMaps: + internalContext.objectMetadataItemWithFieldMaps, }; return await this.graphqlQueryRunnerService.findMany(args, options); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts index 7543d59eccd3..a6ce32c33b63 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts @@ -30,12 +30,10 @@ export class FindOneResolverFactory try { const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, - objectMetadataItem: internalContext.objectMetadataItem, info, - fieldMetadataCollection: internalContext.fieldMetadataCollection, - objectMetadataCollection: internalContext.objectMetadataCollection, - objectMetadataMap: internalContext.objectMetadataMap, - objectMetadataMapItem: internalContext.objectMetadataMapItem, + objectMetadataMaps: internalContext.objectMetadataMaps, + objectMetadataItemWithFieldMaps: + internalContext.objectMetadataItemWithFieldMaps, }; return await this.graphqlQueryRunnerService.findOne(args, options); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts index 709dcc40d312..dbbe7eb31b16 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts @@ -30,12 +30,10 @@ export class RestoreManyResolverFactory try { const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, - objectMetadataItem: internalContext.objectMetadataItem, info, - fieldMetadataCollection: internalContext.fieldMetadataCollection, - objectMetadataCollection: internalContext.objectMetadataCollection, - objectMetadataMap: internalContext.objectMetadataMap, - objectMetadataMapItem: internalContext.objectMetadataMapItem, + objectMetadataMaps: internalContext.objectMetadataMaps, + objectMetadataItemWithFieldMaps: + internalContext.objectMetadataItemWithFieldMaps, }; return await this.graphqlQueryRunnerService.restoreMany(args, options); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/search-resolver-factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/search-resolver-factory.ts index 35520538b0f1..d2510862cf5a 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/search-resolver-factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/search-resolver-factory.ts @@ -28,12 +28,10 @@ export class SearchResolverFactory try { const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, - objectMetadataItem: internalContext.objectMetadataItem, info, - fieldMetadataCollection: internalContext.fieldMetadataCollection, - objectMetadataCollection: internalContext.objectMetadataCollection, - objectMetadataMap: internalContext.objectMetadataMap, - objectMetadataMapItem: internalContext.objectMetadataMapItem, + objectMetadataMaps: internalContext.objectMetadataMaps, + objectMetadataItemWithFieldMaps: + internalContext.objectMetadataItemWithFieldMaps, }; return await this.graphqlQueryRunnerService.search(args, options); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts index af9f0935eeb4..dce0a58a2c4d 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts @@ -30,12 +30,10 @@ export class UpdateManyResolverFactory try { const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, - objectMetadataItem: internalContext.objectMetadataItem, info, - fieldMetadataCollection: internalContext.fieldMetadataCollection, - objectMetadataCollection: internalContext.objectMetadataCollection, - objectMetadataMap: internalContext.objectMetadataMap, - objectMetadataMapItem: internalContext.objectMetadataMapItem, + objectMetadataMaps: internalContext.objectMetadataMaps, + objectMetadataItemWithFieldMaps: + internalContext.objectMetadataItemWithFieldMaps, }; return await this.graphqlQueryRunnerService.updateMany(args, options); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts index b1198cf1a361..258b51b4a5aa 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts @@ -30,12 +30,10 @@ export class UpdateOneResolverFactory try { const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, - objectMetadataItem: internalContext.objectMetadataItem, info, - fieldMetadataCollection: internalContext.fieldMetadataCollection, - objectMetadataCollection: internalContext.objectMetadataCollection, - objectMetadataMap: internalContext.objectMetadataMap, - objectMetadataMapItem: internalContext.objectMetadataMapItem, + objectMetadataMaps: internalContext.objectMetadataMaps, + objectMetadataItemWithFieldMaps: + internalContext.objectMetadataItemWithFieldMaps, }; return await this.graphqlQueryRunnerService.updateOne(args, options); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/pg-graphql.interface.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/pg-graphql.interface.ts deleted file mode 100644 index 4a3f3e13fa13..000000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/pg-graphql.interface.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; - -export interface PGGraphQLResponse { - resolve: { - data: Data; - }; -} - -export type PGGraphQLResult = [PGGraphQLResponse]; - -export interface PGGraphQLMutation { - affectedRows: number; - records: Record[]; -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface.ts index 69bc97777b10..ea40e9c8cfcf 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface.ts @@ -1,10 +1,10 @@ import { GraphQLFieldResolver } from 'graphql'; import { - Record, - RecordFilter, - RecordOrderBy, -} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; + ObjectRecord, + ObjectRecordFilter, + ObjectRecordOrderBy, +} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/factories/factories'; @@ -26,8 +26,8 @@ export enum ResolverArgsType { } export interface FindManyResolverArgs< - Filter extends RecordFilter = RecordFilter, - OrderBy extends RecordOrderBy = RecordOrderBy, + Filter extends ObjectRecordFilter = ObjectRecordFilter, + OrderBy extends ObjectRecordOrderBy = ObjectRecordOrderBy, > { first?: number; last?: number; @@ -42,14 +42,14 @@ export interface FindOneResolverArgs { } export interface FindDuplicatesResolverArgs< - Data extends Partial = Partial, + Data extends Partial = Partial, > { ids?: string[]; data?: Data[]; } export interface SearchResolverArgs< - Filter extends RecordFilter = RecordFilter, + Filter extends ObjectRecordFilter = ObjectRecordFilter, > { searchInput?: string; filter?: Filter; @@ -57,28 +57,28 @@ export interface SearchResolverArgs< } export interface CreateOneResolverArgs< - Data extends Partial = Partial, + Data extends Partial = Partial, > { data: Data; upsert?: boolean; } export interface CreateManyResolverArgs< - Data extends Partial = Partial, + Data extends Partial = Partial, > { data: Data[]; upsert?: boolean; } export interface UpdateOneResolverArgs< - Data extends Partial = Partial, + Data extends Partial = Partial, > { id: string; data: Data; } export interface UpdateManyResolverArgs< - Data extends Partial = Partial, + Data extends Partial = Partial, Filter = any, > { filter: Filter; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts index a652e3065c81..9d5370546cc2 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts @@ -2,8 +2,6 @@ import { Injectable, Logger } from '@nestjs/common'; import { IResolvers } from '@graphql-tools/utils'; -import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; - import { DeleteManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory'; import { DestroyManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory'; import { DestroyOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory'; @@ -11,7 +9,7 @@ import { RestoreManyResolverFactory } from 'src/engine/api/graphql/workspace-res import { SearchResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/search-resolver-factory'; import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { getResolverName } from 'src/engine/utils/get-resolver-name.util'; import { CreateManyResolverFactory } from './factories/create-many-resolver.factory'; @@ -49,8 +47,7 @@ export class WorkspaceResolverFactory { async create( authContext: AuthContext, - objectMetadataCollection: ObjectMetadataInterface[], - objectMetadataMap: ObjectMetadataMap, + objectMetadataMaps: ObjectMetadataMaps, workspaceResolverBuilderMethods: WorkspaceResolverBuilderMethods, ): Promise { const factories = new Map< @@ -76,7 +73,7 @@ export class WorkspaceResolverFactory { Mutation: {}, }; - for (const objectMetadata of objectMetadataCollection) { + for (const objectMetadata of Object.values(objectMetadataMaps.byId)) { // Generate query resolvers for (const methodName of workspaceResolverBuilderMethods.queries) { const resolverName = getResolverName(objectMetadata, methodName); @@ -94,11 +91,8 @@ export class WorkspaceResolverFactory { resolvers.Query[resolverName] = resolverFactory.create({ authContext, - objectMetadataItem: objectMetadata, - fieldMetadataCollection: objectMetadata.fields, - objectMetadataCollection, - objectMetadataMap, - objectMetadataMapItem: objectMetadataMap[objectMetadata.nameSingular], + objectMetadataMaps, + objectMetadataItemWithFieldMaps: objectMetadata, }); } @@ -119,11 +113,8 @@ export class WorkspaceResolverFactory { resolvers.Mutation[resolverName] = resolverFactory.create({ authContext, - objectMetadataItem: objectMetadata, - fieldMetadataCollection: objectMetadata.fields, - objectMetadataCollection, - objectMetadataMap, - objectMetadataMapItem: objectMetadataMap[objectMetadata.nameSingular], + objectMetadataMaps, + objectMetadataItemWithFieldMaps: objectMetadata, }); } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/aggregation-type.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/aggregation-type.factory.ts new file mode 100644 index 000000000000..dec6bd49adb3 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/aggregation-type.factory.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@nestjs/common'; + +import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; + +import { + AggregationField, + getAvailableAggregationsFromObjectFields, +} from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; + +type AggregationGraphQLType = Pick; + +@Injectable() +export class AggregationTypeFactory { + public create( + objectMetadata: ObjectMetadataInterface, + ): Record { + const availableAggregations = getAvailableAggregationsFromObjectFields( + objectMetadata.fields, + ); + + return Object.entries(availableAggregations).reduce< + Record + >((acc, [key, agg]) => { + acc[key] = { + type: agg.type, + description: agg.description, + }; + + return acc; + }, {}); + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/connection-type-definition.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/connection-type-definition.factory.ts index dfaf2f37e99e..e691609b01d2 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/connection-type-definition.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/connection-type-definition.factory.ts @@ -1,17 +1,18 @@ import { Injectable } from '@nestjs/common'; -import { GraphQLFieldConfigMap, GraphQLInt, GraphQLObjectType } from 'graphql'; +import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql'; import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; +import { AggregationTypeFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/aggregation-type.factory'; import { pascalCase } from 'src/utils/pascal-case'; +import { ConnectionTypeFactory } from './connection-type.factory'; import { ObjectTypeDefinition, ObjectTypeDefinitionKind, } from './object-type-definition.factory'; -import { ConnectionTypeFactory } from './connection-type.factory'; export enum ConnectionTypeDefinitionKind { Edge = 'Edge', @@ -20,7 +21,10 @@ export enum ConnectionTypeDefinitionKind { @Injectable() export class ConnectionTypeDefinitionFactory { - constructor(private readonly connectionTypeFactory: ConnectionTypeFactory) {} + constructor( + private readonly connectionTypeFactory: ConnectionTypeFactory, + private readonly aggregationTypeFactory: AggregationTypeFactory, + ) {} public create( objectMetadata: ObjectMetadataInterface, @@ -45,6 +49,10 @@ export class ConnectionTypeDefinitionFactory { ): GraphQLFieldConfigMap { const fields: GraphQLFieldConfigMap = {}; + const aggregatedFields = this.aggregationTypeFactory.create(objectMetadata); + + Object.assign(fields, aggregatedFields); + fields.edges = { type: this.connectionTypeFactory.create( objectMetadata, @@ -69,11 +77,6 @@ export class ConnectionTypeDefinitionFactory { ), }; - fields.totalCount = { - type: GraphQLInt, - description: 'Total number of records in the connection', - }; - return fields; } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/factories.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/factories.ts index b28c497092aa..1e1219192d5c 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/factories.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/factories.ts @@ -1,23 +1,24 @@ -import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory'; -import { CompositeObjectTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory'; -import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory'; +import { AggregationTypeFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/aggregation-type.factory'; import { CompositeEnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-enum-type-definition.factory'; +import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory'; +import { CompositeObjectTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory'; +import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory'; import { ArgsFactory } from './args.factory'; -import { InputTypeFactory } from './input-type.factory'; +import { ConnectionTypeDefinitionFactory } from './connection-type-definition.factory'; +import { ConnectionTypeFactory } from './connection-type.factory'; +import { EdgeTypeDefinitionFactory } from './edge-type-definition.factory'; +import { EdgeTypeFactory } from './edge-type.factory'; +import { ExtendObjectTypeDefinitionFactory } from './extend-object-type-definition.factory'; import { InputTypeDefinitionFactory } from './input-type-definition.factory'; +import { InputTypeFactory } from './input-type.factory'; +import { MutationTypeFactory } from './mutation-type.factory'; import { ObjectTypeDefinitionFactory } from './object-type-definition.factory'; +import { OrphanedTypesFactory } from './orphaned-types.factory'; import { OutputTypeFactory } from './output-type.factory'; import { QueryTypeFactory } from './query-type.factory'; -import { RootTypeFactory } from './root-type.factory'; -import { ConnectionTypeFactory } from './connection-type.factory'; -import { ConnectionTypeDefinitionFactory } from './connection-type-definition.factory'; -import { EdgeTypeFactory } from './edge-type.factory'; -import { EdgeTypeDefinitionFactory } from './edge-type-definition.factory'; -import { MutationTypeFactory } from './mutation-type.factory'; import { RelationTypeFactory } from './relation-type.factory'; -import { ExtendObjectTypeDefinitionFactory } from './extend-object-type-definition.factory'; -import { OrphanedTypesFactory } from './orphaned-types.factory'; +import { RootTypeFactory } from './root-type.factory'; export const workspaceSchemaBuilderFactories = [ ArgsFactory, @@ -39,4 +40,5 @@ export const workspaceSchemaBuilderFactories = [ QueryTypeFactory, MutationTypeFactory, OrphanedTypesFactory, + AggregationTypeFactory, ]; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/date-time.scalar.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/date-time.scalar.ts deleted file mode 100644 index c907b4dd4baf..000000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/date-time.scalar.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { GraphQLScalarType } from 'graphql'; -import { Kind } from 'graphql/language'; - -export const DateTimeScalarType = new GraphQLScalarType({ - name: 'DateTime', - description: 'A custom scalar that represents a datetime in ISO format', - serialize(value: string): string { - const date = new Date(value); - - if (isNaN(date.getTime())) { - throw new Error('Invalid date format, expected ISO date string'); - } - - return date.toISOString(); - }, - parseValue(value: string): Date { - const date = new Date(value); - - if (isNaN(date.getTime())) { - throw new Error('Invalid date format, expected ISO date string'); - } - - return date; - }, - parseLiteral(ast): Date { - if (ast.kind !== Kind.STRING) { - throw new Error('Invalid date format, expected ISO date string'); - } - - const date = new Date(ast.value); - - if (isNaN(date.getTime())) { - throw new Error('Invalid date format, expected ISO date string'); - } - - return date; - }, -}); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/index.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/index.ts index 071e99b1dba3..e5aec2019868 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/index.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/index.ts @@ -1,10 +1,9 @@ -import { RawJSONScalar } from './raw-json.scalar'; -import { PositionScalarType } from './position.scalar'; -import { CursorScalarType } from './cursor.scalar'; import { BigFloatScalarType } from './big-float.scalar'; import { BigIntScalarType } from './big-int.scalar'; +import { CursorScalarType } from './cursor.scalar'; import { DateScalarType } from './date.scalar'; -import { DateTimeScalarType } from './date-time.scalar'; +import { PositionScalarType } from './position.scalar'; +import { RawJSONScalar } from './raw-json.scalar'; import { TimeScalarType } from './time.scalar'; import { UUIDScalarType } from './uuid.scalar'; @@ -12,7 +11,6 @@ export * from './big-float.scalar'; export * from './big-int.scalar'; export * from './cursor.scalar'; export * from './date.scalar'; -export * from './date-time.scalar'; export * from './time.scalar'; export * from './uuid.scalar'; @@ -20,7 +18,6 @@ export const scalars = [ BigFloatScalarType, BigIntScalarType, DateScalarType, - DateTimeScalarType, TimeScalarType, UUIDScalarType, CursorScalarType, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface.ts index d0ab66983309..e95e27b13d4a 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface.ts @@ -1,17 +1,9 @@ -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'; - import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { - ObjectMetadataMap, - ObjectMetadataMapItem, -} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; export interface WorkspaceSchemaBuilderContext { authContext: AuthContext; - fieldMetadataCollection: FieldMetadataInterface[]; - objectMetadataCollection: ObjectMetadataInterface[]; - objectMetadataItem: ObjectMetadataInterface; - objectMetadataMap: ObjectMetadataMap; - objectMetadataMapItem: ObjectMetadataMapItem; + objectMetadataMaps: ObjectMetadataMaps; + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util.ts new file mode 100644 index 000000000000..20ea94644538 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util.ts @@ -0,0 +1,84 @@ +import { GraphQLISODateTime } from '@nestjs/graphql'; + +import { GraphQLFloat, GraphQLInt, GraphQLScalarType } from 'graphql'; + +import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; + +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { capitalize } from 'src/utils/capitalize'; + +enum AGGREGATION_OPERATIONS { + min = 'MIN', + max = 'MAX', + avg = 'AVG', + sum = 'SUM', + count = 'COUNT', +} + +export type AggregationField = { + type: GraphQLScalarType; + description: string; + fromField: string; + aggregationOperation: AGGREGATION_OPERATIONS; +}; + +export const getAvailableAggregationsFromObjectFields = ( + fields: FieldMetadataInterface[], +): Record => { + return fields.reduce>((acc, field) => { + acc['totalCount'] = { + type: GraphQLInt, + description: `Total number of records in the connection`, + fromField: 'id', + aggregationOperation: AGGREGATION_OPERATIONS.count, + }; + + if (field.type === FieldMetadataType.DATE_TIME) { + acc[`min${capitalize(field.name)}`] = { + type: GraphQLISODateTime, + description: `Oldest date contained in the field ${field.name}`, + fromField: field.name, + aggregationOperation: AGGREGATION_OPERATIONS.min, + }; + + acc[`max${capitalize(field.name)}`] = { + type: GraphQLISODateTime, + description: `Most recent date contained in the field ${field.name}`, + fromField: field.name, + aggregationOperation: AGGREGATION_OPERATIONS.max, + }; + } + + if (field.type === FieldMetadataType.NUMBER) { + acc[`min${capitalize(field.name)}`] = { + type: GraphQLFloat, + description: `Minimum amount contained in the field ${field.name}`, + fromField: field.name, + aggregationOperation: AGGREGATION_OPERATIONS.min, + }; + + acc[`max${capitalize(field.name)}`] = { + type: GraphQLFloat, + description: `Maximum amount contained in the field ${field.name}`, + fromField: field.name, + aggregationOperation: AGGREGATION_OPERATIONS.max, + }; + + acc[`avg${capitalize(field.name)}`] = { + type: GraphQLFloat, + description: `Average amount contained in the field ${field.name}`, + fromField: field.name, + aggregationOperation: AGGREGATION_OPERATIONS.avg, + }; + + acc[`sum${capitalize(field.name)}`] = { + type: GraphQLFloat, + description: `Sum of amounts contained in the field ${field.name}`, + fromField: field.name, + aggregationOperation: AGGREGATION_OPERATIONS.sum, + }; + } + + return acc; + }, {}); +}; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory.ts index 4f905032f8b7..d39f1c59e675 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory.ts @@ -7,10 +7,10 @@ import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metad import { TypeDefinitionsGenerator } from './type-definitions.generator'; -import { WorkspaceBuildSchemaOptions } from './interfaces/workspace-build-schema-optionts.interface'; -import { QueryTypeFactory } from './factories/query-type.factory'; import { MutationTypeFactory } from './factories/mutation-type.factory'; import { OrphanedTypesFactory } from './factories/orphaned-types.factory'; +import { QueryTypeFactory } from './factories/query-type.factory'; +import { WorkspaceBuildSchemaOptions } from './interfaces/workspace-build-schema-optionts.interface'; @Injectable() export class WorkspaceGraphQLSchemaFactory { 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 558ece2b4768..c80bb0c2e6a3 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 @@ -56,13 +56,13 @@ export class WorkspaceSchemaFactory { ); } - const objectMetadataMap = - await this.workspaceCacheStorageService.getObjectMetadataMap( + const objectMetadataMaps = + await this.workspaceCacheStorageService.getObjectMetadataMaps( authContext.workspace.id, currentCacheVersion, ); - if (!objectMetadataMap) { + if (!objectMetadataMaps) { await this.workspaceMetadataCacheService.recomputeMetadataCache({ workspaceId: authContext.workspace.id, }); @@ -72,10 +72,10 @@ export class WorkspaceSchemaFactory { ); } - const objectMetadataCollection = Object.values(objectMetadataMap).map( + const objectMetadataCollection = Object.values(objectMetadataMaps.byId).map( (objectMetadataItem) => ({ ...objectMetadataItem, - fields: Object.values(objectMetadataItem.fields), + fields: objectMetadataItem.fields, indexes: objectMetadataItem.indexMetadatas, }), ); @@ -117,8 +117,7 @@ export class WorkspaceSchemaFactory { const autoGeneratedResolvers = await this.workspaceResolverFactory.create( authContext, - objectMetadataCollection, - objectMetadataMap, + objectMetadataMaps, workspaceResolverBuilderMethodNames, ); const scalarsResolvers = diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/check-order-by.utils.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/check-order-by.utils.ts index 851282b6727b..55f92979116d 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/check-order-by.utils.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/check-order-by.utils.ts @@ -1,6 +1,6 @@ import { BadRequestException } from '@nestjs/common'; -import { Record } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; @@ -9,7 +9,7 @@ import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target export const checkArrayFields = ( objectMetadata: ObjectMetadataInterface, - fields: Array>, + fields: Array>, ): void => { const fieldMetadataNames = objectMetadata.fields .map((field) => { diff --git a/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/order-by-input.factory.spec.ts b/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/order-by-input.factory.spec.ts index 9f6923c23802..d61fc5bcb542 100644 --- a/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/order-by-input.factory.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/order-by-input.factory.spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { objectMetadataItemMock } from 'src/engine/api/__mocks__/object-metadata-item.mock'; import { OrderByInputFactory } from 'src/engine/api/rest/input-factories/order-by-input.factory'; diff --git a/packages/twenty-server/src/engine/api/rest/input-factories/order-by-input.factory.ts b/packages/twenty-server/src/engine/api/rest/input-factories/order-by-input.factory.ts index 832de44c52f9..25a3700e0ba8 100644 --- a/packages/twenty-server/src/engine/api/rest/input-factories/order-by-input.factory.ts +++ b/packages/twenty-server/src/engine/api/rest/input-factories/order-by-input.factory.ts @@ -3,9 +3,9 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { Request } from 'express'; import { + ObjectRecordOrderBy, OrderByDirection, - RecordOrderBy, -} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { checkArrayFields } from 'src/engine/api/rest/core/query-builder/utils/check-order-by.utils'; @@ -13,7 +13,7 @@ export const DEFAULT_ORDER_DIRECTION = OrderByDirection.AscNullsFirst; @Injectable() export class OrderByInputFactory { - create(request: Request, objectMetadata): RecordOrderBy { + create(request: Request, objectMetadata): ObjectRecordOrderBy { const orderByQuery = request.query.order_by; if (typeof orderByQuery !== 'string') { diff --git a/packages/twenty-server/src/engine/core-modules/duplicate/constants/duplicate-criteria.constants.ts b/packages/twenty-server/src/engine/core-modules/duplicate/constants/duplicate-criteria.constants.ts index eac026e84e0e..e79a55d37dd4 100644 --- a/packages/twenty-server/src/engine/core-modules/duplicate/constants/duplicate-criteria.constants.ts +++ b/packages/twenty-server/src/engine/core-modules/duplicate/constants/duplicate-criteria.constants.ts @@ -1,4 +1,4 @@ -import { RecordDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { ObjectRecordDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; /** * objectName: directly reference the name of the object from the metadata tables. @@ -6,7 +6,7 @@ import { RecordDuplicateCriteria } from 'src/engine/api/graphql/workspace-query- * So if we need to reference a custom field, we should directly add the column name like `_customColumn`. * If we need to terence a composite field, we should add all children of the composite like `nameFirstName` and `nameLastName` */ -export const DUPLICATE_CRITERIA_COLLECTION: RecordDuplicateCriteria[] = [ +export const DUPLICATE_CRITERIA_COLLECTION: ObjectRecordDuplicateCriteria[] = [ { objectName: 'company', columnNames: ['domainName'], diff --git a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-properties.util.ts b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-properties.util.ts index 5d77c207623c..60bc708a1542 100644 --- a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-properties.util.ts +++ b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-properties.util.ts @@ -1,11 +1,13 @@ import deepEqual from 'deep-equal'; -import { Record } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; export const objectRecordChangedProperties = < - PRecord extends Partial = Partial, + PRecord extends Partial< + ObjectRecord | BaseWorkspaceEntity + > = Partial, >( oldRecord: PRecord, newRecord: PRecord, diff --git a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts index 363c4059ef5e..c8439f7c0b28 100644 --- a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts +++ b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts @@ -1,23 +1,19 @@ import deepEqual from 'deep-equal'; -import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; export const objectRecordChangedValues = ( - oldRecord: Partial, - newRecord: Partial, + oldRecord: Partial, + newRecord: Partial, updatedKeys: string[] | undefined, - objectMetadata: ObjectMetadataInterface, + objectMetadataItem: ObjectMetadataInterface, ) => { - const fieldsByKey = new Map( - objectMetadata.fields.map((field) => [field.name, field]), - ); - return Object.keys(newRecord).reduce( (acc, key) => { - const field = fieldsByKey.get(key); + const field = objectMetadataItem.fields.find((f) => f.name === key); const oldRecordValue = oldRecord[key]; const newRecordValue = newRecord[key]; 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 9bf22bc2a18e..099ee18a2c54 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 @@ -8,12 +8,11 @@ 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', - IsWorkspaceFavoriteEnabled = 'IS_WORKSPACE_FAVORITE_ENABLED', IsSSOEnabled = 'IS_SSO_ENABLED', IsGmailSendEmailScopeEnabled = 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED', IsAnalyticsV2Enabled = 'IS_ANALYTICS_V2_ENABLED', IsUniqueIndexesEnabled = 'IS_UNIQUE_INDEXES_ENABLED', IsMicrosoftSyncEnabled = 'IS_MICROSOFT_SYNC_ENABLED', IsAdvancedFiltersEnabled = 'IS_ADVANCED_FILTERS_ENABLED', + IsAggregateQueryEnabled = 'IS_AGGREGATE_QUERY_ENABLED', } diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/parameters.utils.spec.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/parameters.utils.spec.ts index 6d5ab0751d5a..0f3198ce624b 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/parameters.utils.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/parameters.utils.spec.ts @@ -1,4 +1,4 @@ -import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { DEFAULT_CONJUNCTION } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/add-default-conjunction.utils'; import { FilterComparators } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-base-filter.utils'; diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts index 26679a18fc34..6924deae86ed 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts @@ -1,6 +1,6 @@ import { OpenAPIV3_1 } from 'openapi-types'; -import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { DEFAULT_CONJUNCTION } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/add-default-conjunction.utils'; import { FilterComparators } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-base-filter.utils'; diff --git a/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts index d9a1850a5df6..19daa9c46bf9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts @@ -467,13 +467,13 @@ export class RelationMetadataService extends TypeOrmQueryService { const objectMetadata = - objectMetadataMap[fieldMetadataItem.objectMetadataId]; + objectMetadataMaps.byId[fieldMetadataItem.objectMetadataId]; - const fieldMetadata = objectMetadata.fields[fieldMetadataItem.id]; + const fieldMetadata = objectMetadata.fieldsById[fieldMetadataItem.id]; const relationMetadata = fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata; @@ -495,18 +495,18 @@ export class RelationMetadataService extends TypeOrmQueryService; diff --git a/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-item-with-field-maps.ts b/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-item-with-field-maps.ts new file mode 100644 index 000000000000..b46cbe3cc2cc --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-item-with-field-maps.ts @@ -0,0 +1,8 @@ +import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; + +import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; + +export type ObjectMetadataItemWithFieldMaps = ObjectMetadataInterface & { + fieldsById: FieldMetadataMap; + fieldsByName: FieldMetadataMap; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-maps.ts b/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-maps.ts new file mode 100644 index 000000000000..a10603c36f6b --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-maps.ts @@ -0,0 +1,7 @@ +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; + +export type ObjectMetadataMaps = { + byId: Record; + byNameSingular: Record; + byNamePlural: Record; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/generate-object-metadata-map.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/generate-object-metadata-map.util.ts deleted file mode 100644 index 3455efa5ff13..000000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/generate-object-metadata-map.util.ts +++ /dev/null @@ -1,36 +0,0 @@ -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 generateObjectMetadataMap = ( - objectMetadataCollection: ObjectMetadataInterface[], -): ObjectMetadataMap => { - const objectMetadataMap: ObjectMetadataMap = {}; - - for (const objectMetadata of objectMetadataCollection) { - const fieldsMap: FieldMetadataMap = {}; - - for (const fieldMetadata of objectMetadata.fields) { - fieldsMap[fieldMetadata.name] = fieldMetadata; - fieldsMap[fieldMetadata.id] = 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/metadata-modules/utils/generate-object-metadata-maps.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/generate-object-metadata-maps.util.ts new file mode 100644 index 000000000000..abaea68e1007 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/utils/generate-object-metadata-maps.util.ts @@ -0,0 +1,39 @@ +import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; + +import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; + +export const generateObjectMetadataMaps = ( + objectMetadataCollection: ObjectMetadataInterface[], +): ObjectMetadataMaps => { + const objectMetadataMaps: ObjectMetadataMaps = { + byId: {}, + byNameSingular: {}, + byNamePlural: {}, + }; + + for (const objectMetadata of objectMetadataCollection) { + const fieldsByIdMap: FieldMetadataMap = {}; + const fieldsByNameMap: FieldMetadataMap = {}; + + for (const fieldMetadata of objectMetadata.fields) { + fieldsByNameMap[fieldMetadata.name] = fieldMetadata; + fieldsByIdMap[fieldMetadata.id] = fieldMetadata; + } + + const processedObjectMetadata: ObjectMetadataItemWithFieldMaps = { + ...objectMetadata, + fieldsById: fieldsByIdMap, + fieldsByName: fieldsByNameMap, + }; + + objectMetadataMaps.byId[objectMetadata.id] = processedObjectMetadata; + objectMetadataMaps.byNameSingular[objectMetadata.nameSingular] = + processedObjectMetadata; + objectMetadataMaps.byNamePlural[objectMetadata.namePlural] = + processedObjectMetadata; + } + + return objectMetadataMaps; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service.ts index c3d454c9450b..97070a741870 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service.ts @@ -1,12 +1,14 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import console from 'console'; + import { Repository } from 'typeorm'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { LogExecutionTime } from 'src/engine/decorators/observability/log-execution-time.decorator'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { generateObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { generateObjectMetadataMaps } from 'src/engine/metadata-modules/utils/generate-object-metadata-maps.util'; import { WorkspaceMetadataCacheException, WorkspaceMetadataCacheExceptionCode, @@ -85,15 +87,15 @@ export class WorkspaceMetadataCacheService { console.timeEnd('fetching object metadata'); console.time('generating object metadata map'); - const freshObjectMetadataMap = - generateObjectMetadataMap(objectMetadataItems); + const freshObjectMetadataMaps = + generateObjectMetadataMaps(objectMetadataItems); console.timeEnd('generating object metadata map'); - await this.workspaceCacheStorageService.setObjectMetadataMap( + await this.workspaceCacheStorageService.setObjectMetadataMaps( workspaceId, currentDatabaseVersion, - freshObjectMetadataMap, + freshObjectMetadataMaps, ); await this.workspaceCacheStorageService.removeObjectMetadataOngoingCachingLock( diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts index db67298f1e95..323a02befdb3 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts @@ -10,7 +10,7 @@ import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-me import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value'; -import { FieldMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util'; import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; @@ -20,10 +20,10 @@ type EntitySchemaColumnMap = { @Injectable() export class EntitySchemaColumnFactory { - create(fieldMetadataMap: FieldMetadataMap): EntitySchemaColumnMap { + create(fieldMetadataMapByName: FieldMetadataMap): EntitySchemaColumnMap { let entitySchemaColumnMap: EntitySchemaColumnMap = {}; - const fieldMetadataCollection = Object.values(fieldMetadataMap); + const fieldMetadataCollection = Object.values(fieldMetadataMapByName); for (const fieldMetadata of fieldMetadataCollection) { const key = fieldMetadata.name; 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 f6fc88de34f2..4d86fce6f0a3 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 @@ -2,10 +2,8 @@ import { Injectable } from '@nestjs/common'; import { EntitySchemaRelationOptions } from 'typeorm'; -import { - FieldMetadataMap, - ObjectMetadataMap, -} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { determineRelationDetails } from 'src/engine/twenty-orm/utils/determine-relation-details.util'; import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; @@ -18,12 +16,12 @@ export class EntitySchemaRelationFactory { constructor() {} async create( - fieldMetadataMap: FieldMetadataMap, - objectMetadataMap: ObjectMetadataMap, + fieldMetadataMapByName: FieldMetadataMap, + objectMetadataMaps: ObjectMetadataMaps, ): Promise { const entitySchemaRelationMap: EntitySchemaRelationMap = {}; - const fieldMetadataCollection = Object.values(fieldMetadataMap); + const fieldMetadataCollection = Object.values(fieldMetadataMapByName); for (const fieldMetadata of fieldMetadataCollection) { if (!isRelationFieldMetadataType(fieldMetadata.type)) { @@ -42,7 +40,7 @@ export class EntitySchemaRelationFactory { const relationDetails = await determineRelationDetails( fieldMetadata, relationMetadata, - objectMetadataMap, + objectMetadataMaps, ); entitySchemaRelationMap[fieldMetadata.name] = { diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema.factory.ts index eeb697e8867e..2d71c065efb9 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema.factory.ts @@ -2,10 +2,8 @@ import { Injectable } from '@nestjs/common'; import { EntitySchema } from 'typeorm'; -import { - ObjectMetadataMap, - ObjectMetadataMapItem, -} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { EntitySchemaColumnFactory } from 'src/engine/twenty-orm/factories/entity-schema-column.factory'; import { EntitySchemaRelationFactory } from 'src/engine/twenty-orm/factories/entity-schema-relation.factory'; import { WorkspaceEntitiesStorage } from 'src/engine/twenty-orm/storage/workspace-entities.storage'; @@ -20,17 +18,17 @@ export class EntitySchemaFactory { async create( workspaceId: string, - metadataVersion: number, - objectMetadata: ObjectMetadataMapItem, - objectMetadataMap: ObjectMetadataMap, + _metadataVersion: number, + objectMetadata: ObjectMetadataItemWithFieldMaps, + objectMetadataMaps: ObjectMetadataMaps, ): Promise { const columns = this.entitySchemaColumnFactory.create( - objectMetadata.fields, + objectMetadata.fieldsByName, ); const relations = await this.entitySchemaRelationFactory.create( - objectMetadata.fields, - objectMetadataMap, + objectMetadata.fieldsByName, + objectMetadataMaps, ); const entitySchema = new EntitySchema({ 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 60aecca27029..7a2dbbfa47b3 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 @@ -86,13 +86,13 @@ export class WorkspaceDatasourceFactory { let cachedEntitySchemas: EntitySchema[]; - const cachedObjectMetadataMap = - await this.workspaceCacheStorageService.getObjectMetadataMap( + const cachedObjectMetadataMaps = + await this.workspaceCacheStorageService.getObjectMetadataMaps( workspaceId, cachedWorkspaceMetadataVersion, ); - if (!cachedObjectMetadataMap) { + if (!cachedObjectMetadataMaps) { throw new TwentyORMException( `Workspace Schema not found for workspace ${workspaceId}`, TwentyORMExceptionCode.METADATA_COLLECTION_NOT_FOUND, @@ -105,13 +105,14 @@ export class WorkspaceDatasourceFactory { ); } else { const entitySchemas = await Promise.all( - Object.values(cachedObjectMetadataMap).map((objectMetadata) => - this.entitySchemaFactory.create( - workspaceId, - cachedWorkspaceMetadataVersion, - objectMetadata, - cachedObjectMetadataMap, - ), + Object.values(cachedObjectMetadataMaps.byId).map( + (objectMetadata) => + this.entitySchemaFactory.create( + workspaceId, + cachedWorkspaceMetadataVersion, + objectMetadata, + cachedObjectMetadataMaps, + ), ), ); @@ -127,7 +128,7 @@ export class WorkspaceDatasourceFactory { const workspaceDataSource = new WorkspaceDataSource( { workspaceId, - objectMetadataMap: cachedObjectMetadataMap, + objectMetadataMaps: cachedObjectMetadataMaps, }, { url: diff --git a/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-internal-context.interface.ts b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-internal-context.interface.ts index f68611f678db..be7d9c712c8b 100644 --- a/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-internal-context.interface.ts +++ b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-internal-context.interface.ts @@ -1,6 +1,6 @@ -import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; export interface WorkspaceInternalContext { workspaceId: string; - objectMetadataMap: ObjectMetadataMap; + objectMetadataMaps: ObjectMetadataMaps; } diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts index bb4327cc8b1b..b81eaf1c0d72 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts @@ -22,7 +22,7 @@ import { UpsertOptions } from 'typeorm/repository/UpsertOptions'; import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface'; -import { ObjectMetadataMapItem } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { WorkspaceEntitiesStorage } from 'src/engine/twenty-orm/storage/workspace-entities.storage'; import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; @@ -631,13 +631,15 @@ export class WorkspaceRepository< } const objectMetadata = - this.internalContext.objectMetadataMap[objectMetadataName]; + this.internalContext.objectMetadataMaps.byNameSingular[ + objectMetadataName + ]; if (!objectMetadata) { throw new Error( `Object metadata for object "${objectMetadataName}" is missing ` + `in workspace "${this.internalContext.workspaceId}" ` + - `with object metadata collection length: ${this.internalContext.objectMetadataMap.length}`, + `with object metadata collection length: ${this.internalContext.objectMetadataMaps.byNameSingular.length}`, ); } @@ -666,12 +668,12 @@ export class WorkspaceRepository< async formatResult( data: T, - objectMetadata?: ObjectMetadataMapItem, + objectMetadata?: ObjectMetadataItemWithFieldMaps, ): Promise { objectMetadata ??= await this.getObjectMetadataFromTarget(); - const objectMetadataMap = this.internalContext.objectMetadataMap; + const objectMetadataMaps = this.internalContext.objectMetadataMaps; - return formatResult(data, objectMetadata, objectMetadataMap) as T; + return formatResult(data, objectMetadata, objectMetadataMaps) as T; } } 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 783995afb0a9..3e8dd64e1a0e 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 @@ -3,7 +3,7 @@ import { RelationType } from 'typeorm/metadata/types/RelationTypes'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; -import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { computeRelationType } from 'src/engine/twenty-orm/utils/compute-relation-type.util'; interface RelationDetails { @@ -16,22 +16,25 @@ interface RelationDetails { export async function determineRelationDetails( fieldMetadata: FieldMetadataInterface, relationMetadata: RelationMetadataEntity, - objectMetadataMap: ObjectMetadataMap, + objectMetadataMaps: ObjectMetadataMaps, ): Promise { const relationType = computeRelationType(fieldMetadata, relationMetadata); - const fromObjectMetadata = objectMetadataMap[fieldMetadata.objectMetadataId]; - let toObjectMetadata = objectMetadataMap[relationMetadata.toObjectMetadataId]; + const fromObjectMetadata = + objectMetadataMaps.byId[fieldMetadata.objectMetadataId]; + let toObjectMetadata = + objectMetadataMaps.byId[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') { - toObjectMetadata = objectMetadataMap[relationMetadata.fromObjectMetadataId]; + toObjectMetadata = + objectMetadataMaps.byId[relationMetadata.fromObjectMetadataId]; } if (!fromObjectMetadata || !toObjectMetadata) { throw new Error('Object metadata not found'); } - const toFieldMetadata = Object.values(toObjectMetadata.fields).find( + const toFieldMetadata = Object.values(toObjectMetadata.fieldsById).find( (field) => relationType === 'many-to-one' ? field.id === relationMetadata.fromFieldMetadataId diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts index cd3851393678..d856a47b9381 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts @@ -3,26 +3,28 @@ import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metada import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { ObjectMetadataMapItem } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; import { capitalize } from 'src/utils/capitalize'; export function formatData( data: T, - objectMetadata: ObjectMetadataMapItem, + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, ): T { if (!data) { return data; } if (Array.isArray(data)) { - return data.map((item) => formatData(item, objectMetadata)) as T; + return data.map((item) => + formatData(item, objectMetadataItemWithFieldMaps), + ) as T; } const newData: Record = {}; for (const [key, value] of Object.entries(data)) { - const fieldMetadata = objectMetadata.fields[key]; + const fieldMetadata = objectMetadataItemWithFieldMaps.fieldsByName[key]; if (!fieldMetadata) { throw new Error( diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts index 0780db58831e..2cba94d43678 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts @@ -6,18 +6,16 @@ import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-meta import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; -import { - ObjectMetadataMap, - ObjectMetadataMapItem, -} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { computeRelationType } from 'src/engine/twenty-orm/utils/compute-relation-type.util'; import { getCompositeFieldMetadataCollection } from 'src/engine/twenty-orm/utils/get-composite-field-metadata-collection'; import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; export function formatResult( data: T, - objectMetadata: ObjectMetadataMapItem, - objectMetadataMap: ObjectMetadataMap, + ObjectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + objectMetadataMaps: ObjectMetadataMaps, ): T { if (!data) { return data; @@ -25,7 +23,7 @@ export function formatResult( if (Array.isArray(data)) { return data.map((item) => - formatResult(item, objectMetadata, objectMetadataMap), + formatResult(item, ObjectMetadataItemWithFieldMaps, objectMetadataMaps), ) as T; } @@ -33,12 +31,13 @@ export function formatResult( return data; } - if (!objectMetadata) { + if (!ObjectMetadataItemWithFieldMaps) { throw new Error('Object metadata is missing'); } - const compositeFieldMetadataCollection = - getCompositeFieldMetadataCollection(objectMetadata); + const compositeFieldMetadataCollection = getCompositeFieldMetadataCollection( + ObjectMetadataItemWithFieldMaps, + ); const compositeFieldMetadataMap = new Map( compositeFieldMetadataCollection.flatMap((fieldMetadata) => { @@ -58,7 +57,7 @@ export function formatResult( ); const relationMetadataMap = new Map( - Object.values(objectMetadata.fields) + Object.values(ObjectMetadataItemWithFieldMaps.fieldsById) .filter(({ type }) => isRelationFieldMetadataType(type)) .map((fieldMetadata) => [ fieldMetadata.name, @@ -75,6 +74,8 @@ export function formatResult( ]), ); const newData: object = {}; + const objectMetadaItemFieldsByName = + objectMetadataMaps.byId[ObjectMetadataItemWithFieldMaps.id]?.fieldsByName; for (const [key, value] of Object.entries(data)) { const compositePropertyArgs = compositeFieldMetadataMap.get(key); @@ -83,11 +84,15 @@ export function formatResult( if (!compositePropertyArgs && !relationMetadata) { if (isPlainObject(value)) { - newData[key] = formatResult(value, objectMetadata, objectMetadataMap); - } else if (objectMetadata.fields[key]) { + newData[key] = formatResult( + value, + ObjectMetadataItemWithFieldMaps, + objectMetadataMaps, + ); + } else if (objectMetadaItemFieldsByName[key]) { newData[key] = formatFieldMetadataValue( value, - objectMetadata.fields[key], + objectMetadaItemFieldsByName[key], ); } else { newData[key] = value; @@ -98,10 +103,10 @@ export function formatResult( if (relationMetadata) { const toObjectMetadata = - objectMetadataMap[relationMetadata.toObjectMetadataId]; + objectMetadataMaps.byId[relationMetadata.toObjectMetadataId]; const fromObjectMetadata = - objectMetadataMap[relationMetadata.fromObjectMetadataId]; + objectMetadataMaps.byId[relationMetadata.fromObjectMetadataId]; if (!toObjectMetadata) { throw new Error( @@ -118,7 +123,7 @@ export function formatResult( newData[key] = formatResult( value, relationType === 'one-to-many' ? toObjectMetadata : fromObjectMetadata, - objectMetadataMap, + objectMetadataMaps, ); continue; } diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/get-composite-field-metadata-collection.ts b/packages/twenty-server/src/engine/twenty-orm/utils/get-composite-field-metadata-collection.ts index 88ac2820c1b1..de829cb19a3a 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/get-composite-field-metadata-collection.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/get-composite-field-metadata-collection.ts @@ -1,12 +1,14 @@ import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { ObjectMetadataMapItem } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; export function getCompositeFieldMetadataCollection( - objectMetadata: ObjectMetadataMapItem, + ObjectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, ) { const compositeFieldMetadataCollection = Object.values( - objectMetadata.fields, - ).filter((fieldMetadata) => isCompositeFieldMetadataType(fieldMetadata.type)); + ObjectMetadataItemWithFieldMaps.fieldsById, + ).filter((fieldMetadataItem) => + isCompositeFieldMetadataType(fieldMetadataItem.type), + ); return compositeFieldMetadataCollection; } diff --git a/packages/twenty-server/src/engine/workspace-cache-storage/workspace-cache-storage.service.ts b/packages/twenty-server/src/engine/workspace-cache-storage/workspace-cache-storage.service.ts index 9d3da728566f..d549f14c171f 100644 --- a/packages/twenty-server/src/engine/workspace-cache-storage/workspace-cache-storage.service.ts +++ b/packages/twenty-server/src/engine/workspace-cache-storage/workspace-cache-storage.service.ts @@ -5,14 +5,14 @@ import { EntitySchemaOptions } from 'typeorm'; import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; -import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; export enum WorkspaceCacheKeys { GraphQLTypeDefs = 'graphql:type-defs', GraphQLUsedScalarNames = 'graphql:used-scalar-names', GraphQLOperations = 'graphql:operations', ORMEntitySchemas = 'orm:entity-schemas', - MetadataObjectMetadataMap = 'metadata:object-metadata-map', + MetadataObjectMetadataMaps = 'metadata:object-metadata-maps', MetadataObjectMetadataOngoingCachingLock = 'metadata:object-metadata-ongoing-caching-lock', MetadataVersion = 'metadata:workspace-metadata-version', } @@ -88,23 +88,23 @@ export class WorkspaceCacheStorageService { ); } - setObjectMetadataMap( + setObjectMetadataMaps( workspaceId: string, metadataVersion: number, - objectMetadataMap: ObjectMetadataMap, + objectMetadataMaps: ObjectMetadataMaps, ) { - return this.cacheStorageService.set( - `${WorkspaceCacheKeys.MetadataObjectMetadataMap}:${workspaceId}:${metadataVersion}`, - objectMetadataMap, + return this.cacheStorageService.set( + `${WorkspaceCacheKeys.MetadataObjectMetadataMaps}:${workspaceId}:${metadataVersion}`, + objectMetadataMaps, ); } - getObjectMetadataMap( + getObjectMetadataMaps( workspaceId: string, metadataVersion: number, - ): Promise { - return this.cacheStorageService.get( - `${WorkspaceCacheKeys.MetadataObjectMetadataMap}:${workspaceId}:${metadataVersion}`, + ): Promise { + return this.cacheStorageService.get( + `${WorkspaceCacheKeys.MetadataObjectMetadataMaps}:${workspaceId}:${metadataVersion}`, ); } @@ -150,7 +150,7 @@ export class WorkspaceCacheStorageService { async flush(workspaceId: string, metadataVersion: number): Promise { await this.cacheStorageService.del( - `${WorkspaceCacheKeys.MetadataObjectMetadataMap}:${workspaceId}:${metadataVersion}`, + `${WorkspaceCacheKeys.MetadataObjectMetadataMaps}:${workspaceId}:${metadataVersion}`, ); await this.cacheStorageService.del( `${WorkspaceCacheKeys.MetadataVersion}:${workspaceId}:${metadataVersion}`, diff --git a/packages/twenty-server/src/modules/timeline/repositiories/timeline-activity.repository.ts b/packages/twenty-server/src/modules/timeline/repositiories/timeline-activity.repository.ts index ec858797e536..cae34c1d9b92 100644 --- a/packages/twenty-server/src/modules/timeline/repositiories/timeline-activity.repository.ts +++ b/packages/twenty-server/src/modules/timeline/repositiories/timeline-activity.repository.ts @@ -2,10 +2,10 @@ import { Injectable } from '@nestjs/common'; import { EntityManager } from 'typeorm'; -import { Record } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; -import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; import { objectRecordDiffMerge } from 'src/engine/core-modules/event-emitter/utils/object-record-diff-merge'; +import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; @Injectable() export class TimelineActivityRepository { @@ -15,7 +15,7 @@ export class TimelineActivityRepository { async upsertOne( name: string, - properties: Partial, + properties: Partial, objectName: string, recordId: string, workspaceId: string, @@ -105,7 +105,7 @@ export class TimelineActivityRepository { private async updateTimelineActivity( dataSourceSchema: string, id: string, - properties: Partial, + properties: Partial, workspaceMemberId: string | undefined, workspaceId: string, ) { @@ -121,7 +121,7 @@ export class TimelineActivityRepository { private async insertTimelineActivity( dataSourceSchema: string, name: string, - properties: Partial, + properties: Partial, objectName: string, recordId: string, workspaceMemberId: string | undefined, @@ -151,7 +151,7 @@ export class TimelineActivityRepository { objectName: string, activities: { name: string; - properties: Partial | null; + properties: Partial | null; workspaceMemberId: string | undefined; recordId: string | null; linkedRecordCachedName: string; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/utils/generate-fake-object-record-event.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/utils/generate-fake-object-record-event.ts index 84025618a8dd..ac2d065e6f99 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/utils/generate-fake-object-record-event.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/utils/generate-fake-object-record-event.ts @@ -1,12 +1,12 @@ import { v4 } from 'uuid'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event'; import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { generateFakeObjectRecord } from 'src/modules/workflow/workflow-builder/utils/generate-fake-object-record'; -import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; export const generateFakeObjectRecordEvent = ( objectMetadataEntity: ObjectMetadataEntity, diff --git a/packages/twenty-website/src/content/developers/backend-development/folder-architecture-server.mdx b/packages/twenty-website/src/content/developers/backend-development/folder-architecture-server.mdx index ac34f8c69547..2d47d1fd57da 100644 --- a/packages/twenty-website/src/content/developers/backend-development/folder-architecture-server.mdx +++ b/packages/twenty-website/src/content/developers/backend-development/folder-architecture-server.mdx @@ -123,10 +123,6 @@ Creates resolver functions for querying and mutating the GraphQL schema. Each factory in this directory is responsible for producing a distinct resolver type, such as the `FindManyResolverFactory`, designed for adaptable application across various tables. -### Workspace Query Builder - -Includes factories that generate `pg_graphql` queries. - ### Workspace Query Runner Runs the generated queries on the database and parses the result.