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)
---------
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