diff --git a/.github/workflows/ci-tinybird.yaml b/.github/workflows/ci-tinybird.yaml
index 44328556408c..46849e3150e0 100644
--- a/.github/workflows/ci-tinybird.yaml
+++ b/.github/workflows/ci-tinybird.yaml
@@ -3,8 +3,14 @@ on:
push:
branches:
- main
+ paths:
+ - 'package.json'
+ - 'packages/twenty-tinybird/**'
pull_request:
+ paths:
+ - 'package.json'
+ - 'packages/twenty-tinybird/**'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -12,24 +18,9 @@ concurrency:
jobs:
ci:
- timeout-minutes: 10
- runs-on: ubuntu-latest
uses: tinybirdco/ci/.github/workflows/ci.yml@main
- steps:
- - name: Check for changed files
- id: changed-files
- uses: tj-actions/changed-files@v11
- with:
- files: |
- package.json
- packages/twenty-tinybird/**
-
- - name: Skip if no relevant changes
- if: steps.changed-files.outputs.any_changed == 'false'
- run: echo "No relevant changes. Skipping CI."
-
- - name: Check twenty-tinybird package
- with:
- data_project_dir: packages/twenty-tinybird
- tb_admin_token: ${{ secrets.TB_ADMIN_TOKEN }}
- tb_host: https://api.eu-central-1.aws.tinybird.co
+ with:
+ data_project_dir: packages/twenty-tinybird
+ secrets:
+ tb_admin_token: ${{ secrets.TB_ADMIN_TOKEN }}
+ tb_host: https://api.eu-central-1.aws.tinybird.co
diff --git a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx
index 0169ac150c4c..fa3c813981e0 100644
--- a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx
+++ b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx
@@ -13,7 +13,6 @@ import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNaviga
import { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
-import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import styled from '@emotion/styled';
const StyledMainSection = styled(NavigationDrawerSection)`
@@ -27,9 +26,7 @@ export const MainNavigationDrawerItems = () => {
const setNavigationMemorizedUrl = useSetRecoilState(
navigationMemorizedUrlState,
);
- const isWorkspaceFavoriteEnabled = useIsFeatureEnabled(
- 'IS_WORKSPACE_FAVORITE_ENABLED',
- );
+
const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] =
useRecoilState(isNavigationDrawerExpandedState);
const setNavigationDrawerExpandedMemorized = useSetRecoilState(
@@ -58,18 +55,9 @@ export const MainNavigationDrawerItems = () => {
/>
)}
-
- {isWorkspaceFavoriteEnabled && }
-
+
-
- {isWorkspaceFavoriteEnabled ? (
-
- ) : (
-
- )}
+
>
);
diff --git a/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts b/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts
index c8f427d8cff6..337f252be7de 100644
--- a/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts
+++ b/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts
@@ -9,12 +9,11 @@ export type FeatureFlagKey =
| 'IS_FREE_ACCESS_ENABLED'
| 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED'
| 'IS_WORKFLOW_ENABLED'
- | 'IS_WORKSPACE_FAVORITE_ENABLED'
- | 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED'
| 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED'
| 'IS_ANALYTICS_V2_ENABLED'
| 'IS_SSO_ENABLED'
| 'IS_UNIQUE_INDEXES_ENABLED'
| 'IS_ARRAY_AND_JSON_FILTER_ENABLED'
| 'IS_MICROSOFT_SYNC_ENABLED'
- | 'IS_ADVANCED_FILTERS_ENABLED';
+ | 'IS_ADVANCED_FILTERS_ENABLED'
+ | 'IS_AGGREGATE_QUERY_ENABLED';
diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts
index 0c75053e1560..064d1794600a 100644
--- a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts
+++ b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts
@@ -50,11 +50,6 @@ export const seedFeatureFlags = async (
workspaceId: workspaceId,
value: false,
},
- {
- key: FeatureFlagKey.IsWorkspaceFavoriteEnabled,
- workspaceId: workspaceId,
- value: true,
- },
{
key: FeatureFlagKey.IsAnalyticsV2Enabled,
workspaceId: workspaceId,
@@ -85,6 +80,11 @@ export const seedFeatureFlags = async (
workspaceId: workspaceId,
value: false,
},
+ {
+ key: FeatureFlagKey.IsAggregateQueryEnabled,
+ workspaceId: workspaceId,
+ value: false,
+ },
])
.execute();
};
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts
index 21f9bdbdccb0..2b7949613f1e 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts
@@ -5,27 +5,27 @@ import {
WhereExpressionBuilder,
} from 'typeorm';
-import { RecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
+import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
-import { FieldMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
+import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { GraphqlQueryFilterFieldParser } from './graphql-query-filter-field.parser';
export class GraphqlQueryFilterConditionParser {
- private fieldMetadataMap: FieldMetadataMap;
+ private fieldMetadataMapByName: FieldMetadataMap;
private queryFilterFieldParser: GraphqlQueryFilterFieldParser;
- constructor(fieldMetadataMap: FieldMetadataMap) {
- this.fieldMetadataMap = fieldMetadataMap;
+ constructor(fieldMetadataMapByName: FieldMetadataMap) {
+ this.fieldMetadataMapByName = fieldMetadataMapByName;
this.queryFilterFieldParser = new GraphqlQueryFilterFieldParser(
- this.fieldMetadataMap,
+ this.fieldMetadataMapByName,
);
}
public parse(
queryBuilder: SelectQueryBuilder,
objectNameSingular: string,
- filter: Partial,
+ filter: Partial,
): SelectQueryBuilder {
if (!filter || Object.keys(filter).length === 0) {
return queryBuilder;
@@ -50,7 +50,7 @@ export class GraphqlQueryFilterConditionParser {
switch (key) {
case 'and': {
const andWhereCondition = new Brackets((qb) => {
- value.forEach((filter: RecordFilter, index: number) => {
+ value.forEach((filter: ObjectRecordFilter, index: number) => {
const whereCondition = new Brackets((qb2) => {
Object.entries(filter).forEach(
([subFilterkey, subFilterValue], index) => {
@@ -82,7 +82,7 @@ export class GraphqlQueryFilterConditionParser {
}
case 'or': {
const orWhereCondition = new Brackets((qb) => {
- value.forEach((filter: RecordFilter, index: number) => {
+ value.forEach((filter: ObjectRecordFilter, index: number) => {
const whereCondition = new Brackets((qb2) => {
Object.entries(filter).forEach(
([subFilterkey, subFilterValue], index) => {
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts
index 5d35ebf5ecba..95fb650826a4 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts
@@ -9,17 +9,17 @@ import {
import { computeWhereConditionParts } from 'src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
-import { FieldMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
+import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
import { capitalize } from 'src/utils/capitalize';
const ARRAY_OPERATORS = ['in', 'contains', 'not_contains'];
export class GraphqlQueryFilterFieldParser {
- private fieldMetadataMap: FieldMetadataMap;
+ private fieldMetadataMapByName: FieldMetadataMap;
- constructor(fieldMetadataMap: FieldMetadataMap) {
- this.fieldMetadataMap = fieldMetadataMap;
+ constructor(fieldMetadataMapByName: FieldMetadataMap) {
+ this.fieldMetadataMapByName = fieldMetadataMapByName;
}
public parse(
@@ -29,7 +29,7 @@ export class GraphqlQueryFilterFieldParser {
filterValue: any,
isFirst = false,
): void {
- const fieldMetadata = this.fieldMetadataMap[`${key}`];
+ const fieldMetadata = this.fieldMetadataMapByName[`${key}`];
if (!fieldMetadata) {
throw new Error(`Field metadata not found for field: ${key}`);
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts
index aaa242d804aa..a16a9c0c149c 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts
@@ -1,7 +1,7 @@
import {
+ ObjectRecordOrderBy,
OrderByDirection,
- RecordOrderBy,
-} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
+} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import {
@@ -10,25 +10,25 @@ import {
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
-import { FieldMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
+import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
import { capitalize } from 'src/utils/capitalize';
export class GraphqlQueryOrderFieldParser {
- private fieldMetadataMap: FieldMetadataMap;
+ private fieldMetadataMapByName: FieldMetadataMap;
- constructor(fieldMetadataMap: FieldMetadataMap) {
- this.fieldMetadataMap = fieldMetadataMap;
+ constructor(fieldMetadataMapByName: FieldMetadataMap) {
+ this.fieldMetadataMapByName = fieldMetadataMapByName;
}
parse(
- orderBy: RecordOrderBy,
+ orderBy: ObjectRecordOrderBy,
objectNameSingular: string,
isForwardPagination = true,
): Record {
return orderBy.reduce(
(acc, item) => {
Object.entries(item).forEach(([key, value]) => {
- const fieldMetadata = this.fieldMetadataMap[key];
+ const fieldMetadata = this.fieldMetadataMapByName[key];
if (!fieldMetadata || value === undefined) {
throw new GraphqlQueryRunnerException(
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-aggregate.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-aggregate.parser.ts
new file mode 100644
index 000000000000..fcd3ed6a1458
--- /dev/null
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-aggregate.parser.ts
@@ -0,0 +1,30 @@
+import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
+
+import { GraphqlQuerySelectedFieldsResult } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
+import {
+ AggregationField,
+ getAvailableAggregationsFromObjectFields,
+} from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util';
+
+export class GraphqlQuerySelectedFieldsAggregateParser {
+ parse(
+ graphqlSelectedFields: Partial>,
+ fieldMetadataMapByName: Record,
+ accumulator: GraphqlQuerySelectedFieldsResult,
+ ): void {
+ const availableAggregations: Record =
+ getAvailableAggregationsFromObjectFields(
+ Object.values(fieldMetadataMapByName),
+ );
+
+ for (const selectedField of Object.keys(graphqlSelectedFields)) {
+ const selectedAggregation = availableAggregations[selectedField];
+
+ if (!selectedAggregation) {
+ continue;
+ }
+
+ accumulator.aggregate[selectedField] = selectedAggregation;
+ }
+ }
+}
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser.ts
index 19308a44989c..54f294acb455 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser.ts
@@ -1,43 +1,47 @@
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
-import { GraphqlQuerySelectedFieldsParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
+import {
+ GraphqlQuerySelectedFieldsParser,
+ GraphqlQuerySelectedFieldsResult,
+} from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
import { getRelationObjectMetadata } from 'src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util';
-import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
+import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
export class GraphqlQuerySelectedFieldsRelationParser {
- private objectMetadataMap: ObjectMetadataMap;
+ private objectMetadataMaps: ObjectMetadataMaps;
- constructor(objectMetadataMap: ObjectMetadataMap) {
- this.objectMetadataMap = objectMetadataMap;
+ constructor(objectMetadataMaps: ObjectMetadataMaps) {
+ this.objectMetadataMaps = objectMetadataMaps;
}
parseRelationField(
fieldMetadata: FieldMetadataInterface,
fieldKey: string,
fieldValue: any,
- result: { select: Record; relations: Record },
+ accumulator: GraphqlQuerySelectedFieldsResult,
): void {
if (!fieldValue || typeof fieldValue !== 'object') {
return;
}
- result.relations[fieldKey] = true;
+ accumulator.relations[fieldKey] = true;
const referencedObjectMetadata = getRelationObjectMetadata(
fieldMetadata,
- this.objectMetadataMap,
+ this.objectMetadataMaps,
);
- const relationFields = referencedObjectMetadata.fields;
+ const relationFields = referencedObjectMetadata.fieldsByName;
const fieldParser = new GraphqlQuerySelectedFieldsParser(
- this.objectMetadataMap,
+ this.objectMetadataMaps,
);
- const subResult = fieldParser.parse(fieldValue, relationFields);
+ const relationAccumulator = fieldParser.parse(fieldValue, relationFields);
- result.select[fieldKey] = {
+ accumulator.select[fieldKey] = {
id: true,
- ...subResult.select,
+ ...relationAccumulator.select,
};
- result.relations[fieldKey] = subResult.relations;
+ accumulator.relations[fieldKey] = relationAccumulator.relations;
+ accumulator.aggregate[fieldKey] = relationAccumulator.aggregate;
}
}
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser.ts
index d1b69345fee2..5c35b1eff0c4 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser.ts
@@ -1,59 +1,71 @@
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
-import {
- GraphqlQueryRunnerException,
- GraphqlQueryRunnerExceptionCode,
-} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
+import { GraphqlQuerySelectedFieldsAggregateParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-aggregate.parser';
import { GraphqlQuerySelectedFieldsRelationParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
-import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
+import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { capitalize } from 'src/utils/capitalize';
-import { isPlainObject } from 'src/utils/is-plain-object';
+
+export type GraphqlQuerySelectedFieldsResult = {
+ select: Record;
+ relations: Record;
+ aggregate: Record;
+};
export class GraphqlQuerySelectedFieldsParser {
private graphqlQuerySelectedFieldsRelationParser: GraphqlQuerySelectedFieldsRelationParser;
+ private aggregateParser: GraphqlQuerySelectedFieldsAggregateParser;
- constructor(objectMetadataMap: ObjectMetadataMap) {
+ constructor(objectMetadataMaps: ObjectMetadataMaps) {
this.graphqlQuerySelectedFieldsRelationParser =
- new GraphqlQuerySelectedFieldsRelationParser(objectMetadataMap);
+ new GraphqlQuerySelectedFieldsRelationParser(objectMetadataMaps);
+ this.aggregateParser = new GraphqlQuerySelectedFieldsAggregateParser();
}
parse(
graphqlSelectedFields: Partial>,
- fieldMetadataMap: Record,
- ): { select: Record; relations: Record } {
- const result: {
- select: Record;
- relations: Record;
- } = {
+ fieldMetadataMapByName: Record,
+ ): GraphqlQuerySelectedFieldsResult {
+ const accumulator: GraphqlQuerySelectedFieldsResult = {
select: {},
relations: {},
+ aggregate: {},
};
- for (const [fieldKey, fieldValue] of Object.entries(
+ if (this.isRootConnection(graphqlSelectedFields)) {
+ this.parseConnectionField(
+ graphqlSelectedFields,
+ fieldMetadataMapByName,
+ accumulator,
+ );
+
+ return accumulator;
+ }
+
+ this.parseRecordField(
graphqlSelectedFields,
- )) {
- if (this.shouldNotParseField(fieldKey)) {
- continue;
- }
- if (this.isConnectionField(fieldKey, fieldValue)) {
- const subResult = this.parse(fieldValue, fieldMetadataMap);
+ fieldMetadataMapByName,
+ accumulator,
+ );
- Object.assign(result.select, subResult.select);
- Object.assign(result.relations, subResult.relations);
- continue;
- }
+ return accumulator;
+ }
- const fieldMetadata = fieldMetadataMap[fieldKey];
+ private parseRecordField(
+ graphqlSelectedFields: Partial>,
+ fieldMetadataMapByName: Record,
+ accumulator: GraphqlQuerySelectedFieldsResult,
+ ): void {
+ for (const [fieldKey, fieldValue] of Object.entries(
+ graphqlSelectedFields,
+ )) {
+ const fieldMetadata = fieldMetadataMapByName[fieldKey];
if (!fieldMetadata) {
- throw new GraphqlQueryRunnerException(
- `Field "${fieldKey}" does not exist or is not selectable`,
- GraphqlQueryRunnerExceptionCode.FIELD_NOT_FOUND,
- );
+ continue;
}
if (isRelationFieldMetadataType(fieldMetadata.type)) {
@@ -61,7 +73,7 @@ export class GraphqlQuerySelectedFieldsParser {
fieldMetadata,
fieldKey,
fieldValue,
- result,
+ accumulator,
);
} else if (isCompositeFieldMetadataType(fieldMetadata.type)) {
const compositeResult = this.parseCompositeField(
@@ -69,23 +81,33 @@ export class GraphqlQuerySelectedFieldsParser {
fieldValue,
);
- Object.assign(result.select, compositeResult);
+ Object.assign(accumulator.select, compositeResult);
} else {
- result.select[fieldKey] = true;
+ accumulator.select[fieldKey] = true;
}
}
-
- return result;
}
- private isConnectionField(fieldKey: string, fieldValue: any): boolean {
- return ['edges', 'node'].includes(fieldKey) && isPlainObject(fieldValue);
+ private parseConnectionField(
+ graphqlSelectedFields: Partial>,
+ fieldMetadataMapByName: Record,
+ accumulator: GraphqlQuerySelectedFieldsResult,
+ ): void {
+ this.aggregateParser.parse(
+ graphqlSelectedFields,
+ fieldMetadataMapByName,
+ accumulator,
+ );
+
+ const node = graphqlSelectedFields.edges.node;
+
+ this.parseRecordField(node, fieldMetadataMapByName, accumulator);
}
- private shouldNotParseField(fieldKey: string): boolean {
- return ['__typename', 'totalCount', 'pageInfo', 'cursor'].includes(
- fieldKey,
- );
+ private isRootConnection(
+ graphqlSelectedFields: Partial>,
+ ): boolean {
+ return Object.keys(graphqlSelectedFields).includes('edges');
}
private parseCompositeField(
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts
index 0aa047fc31f7..aa7700c5678d 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts
@@ -6,43 +6,44 @@ import {
} from 'typeorm';
import {
- RecordFilter,
- RecordOrderBy,
-} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
+ ObjectRecordFilter,
+ ObjectRecordOrderBy,
+} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { GraphqlQueryFilterConditionParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser';
import { GraphqlQueryOrderFieldParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser';
-import { GraphqlQuerySelectedFieldsParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
import {
- FieldMetadataMap,
- ObjectMetadataMap,
- ObjectMetadataMapItem,
-} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
+ GraphqlQuerySelectedFieldsParser,
+ GraphqlQuerySelectedFieldsResult,
+} from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
+import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
+import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
+import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
export class GraphqlQueryParser {
- private fieldMetadataMap: FieldMetadataMap;
- private objectMetadataMap: ObjectMetadataMap;
+ private fieldMetadataMapByName: FieldMetadataMap;
+ private objectMetadataMaps: ObjectMetadataMaps;
private filterConditionParser: GraphqlQueryFilterConditionParser;
private orderFieldParser: GraphqlQueryOrderFieldParser;
constructor(
- fieldMetadataMap: FieldMetadataMap,
- objectMetadataMap: ObjectMetadataMap,
+ fieldMetadataMapByName: FieldMetadataMap,
+ objectMetadataMaps: ObjectMetadataMaps,
) {
- this.objectMetadataMap = objectMetadataMap;
- this.fieldMetadataMap = fieldMetadataMap;
+ this.objectMetadataMaps = objectMetadataMaps;
+ this.fieldMetadataMapByName = fieldMetadataMapByName;
this.filterConditionParser = new GraphqlQueryFilterConditionParser(
- this.fieldMetadataMap,
+ this.fieldMetadataMapByName,
);
this.orderFieldParser = new GraphqlQueryOrderFieldParser(
- this.fieldMetadataMap,
+ this.fieldMetadataMapByName,
);
}
public applyFilterToBuilder(
queryBuilder: SelectQueryBuilder,
objectNameSingular: string,
- recordFilter: Partial,
+ recordFilter: Partial,
): SelectQueryBuilder {
return this.filterConditionParser.parse(
queryBuilder,
@@ -53,7 +54,7 @@ export class GraphqlQueryParser {
public applyDeletedAtToBuilder(
queryBuilder: SelectQueryBuilder,
- recordFilter: RecordFilter,
+ recordFilter: ObjectRecordFilter,
): SelectQueryBuilder {
if (this.checkForDeletedAtFilter(recordFilter)) {
queryBuilder.withDeleted();
@@ -90,7 +91,7 @@ export class GraphqlQueryParser {
public applyOrderToBuilder(
queryBuilder: SelectQueryBuilder,
- orderBy: RecordOrderBy,
+ orderBy: ObjectRecordOrderBy,
objectNameSingular: string,
isForwardPagination = true,
): SelectQueryBuilder {
@@ -104,11 +105,12 @@ export class GraphqlQueryParser {
}
public parseSelectedFields(
- parentObjectMetadata: ObjectMetadataMapItem,
+ parentObjectMetadata: ObjectMetadataItemWithFieldMaps,
graphqlSelectedFields: Partial>,
- ): { select: Record; relations: Record } {
+ ): GraphqlQuerySelectedFieldsResult {
const parentFields =
- this.objectMetadataMap[parentObjectMetadata.nameSingular]?.fields;
+ this.objectMetadataMaps.byNameSingular[parentObjectMetadata.nameSingular]
+ ?.fieldsByName;
if (!parentFields) {
throw new Error(
@@ -117,7 +119,7 @@ export class GraphqlQueryParser {
}
const selectedFieldsParser = new GraphqlQuerySelectedFieldsParser(
- this.objectMetadataMap,
+ this.objectMetadataMaps,
);
return selectedFieldsParser.parse(graphqlSelectedFields, parentFields);
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts
index c3fe76e2e07b..0e02201065ad 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts
@@ -1,10 +1,10 @@
import { Injectable } from '@nestjs/common';
import {
- Record as IRecord,
- RecordFilter,
- RecordOrderBy,
-} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
+ ObjectRecord,
+ ObjectRecordFilter,
+ ObjectRecordOrderBy,
+} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
import { IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
@@ -48,11 +48,11 @@ export class GraphqlQueryRunnerService {
/** QUERIES */
@LogExecutionTime()
- async findOne(
+ async findOne(
args: FindOneResolverArgs,
options: WorkspaceQueryRunnerOptions,
- ): Promise {
- return this.executeQuery, ObjectRecord>(
+ ): Promise {
+ return this.executeQuery, T>(
'findOne',
args,
options,
@@ -61,36 +61,36 @@ export class GraphqlQueryRunnerService {
@LogExecutionTime()
async findMany<
- ObjectRecord extends IRecord,
- Filter extends RecordFilter,
- OrderBy extends RecordOrderBy,
+ T extends ObjectRecord,
+ Filter extends ObjectRecordFilter,
+ OrderBy extends ObjectRecordOrderBy,
>(
args: FindManyResolverArgs,
options: WorkspaceQueryRunnerOptions,
- ): Promise>> {
+ ): Promise>> {
return this.executeQuery<
FindManyResolverArgs,
- IConnection>
+ IConnection>
>('findMany', args, options);
}
@LogExecutionTime()
- async findDuplicates(
- args: FindDuplicatesResolverArgs>,
+ async findDuplicates(
+ args: FindDuplicatesResolverArgs>,
options: WorkspaceQueryRunnerOptions,
- ): Promise[]> {
+ ): Promise[]> {
return this.executeQuery<
- FindDuplicatesResolverArgs>,
- IConnection[]
+ FindDuplicatesResolverArgs>,
+ IConnection[]
>('findDuplicates', args, options);
}
@LogExecutionTime()
- async search(
+ async search(
args: SearchResolverArgs,
options: WorkspaceQueryRunnerOptions,
- ): Promise> {
- return this.executeQuery>(
+ ): Promise> {
+ return this.executeQuery>(
'search',
args,
options,
@@ -100,13 +100,13 @@ export class GraphqlQueryRunnerService {
/** MUTATIONS */
@LogExecutionTime()
- async createOne(
- args: CreateOneResolverArgs>,
+ async createOne(
+ args: CreateOneResolverArgs>,
options: WorkspaceQueryRunnerOptions,
- ): Promise {
+ ): Promise {
const results = await this.executeQuery<
- CreateManyResolverArgs>,
- ObjectRecord[]
+ CreateManyResolverArgs>,
+ T[]
>('createMany', { data: [args.data], upsert: args.upsert }, options);
// TODO: emitCreateEvents should be moved to the ORM layer
@@ -114,7 +114,7 @@ export class GraphqlQueryRunnerService {
this.apiEventEmitterService.emitCreateEvents(
results,
options.authContext,
- options.objectMetadataItem,
+ options.objectMetadataItemWithFieldMaps,
);
}
@@ -122,20 +122,20 @@ export class GraphqlQueryRunnerService {
}
@LogExecutionTime()
- async createMany(
- args: CreateManyResolverArgs>,
+ async createMany(
+ args: CreateManyResolverArgs>,
options: WorkspaceQueryRunnerOptions,
- ): Promise {
+ ): Promise {
const results = await this.executeQuery<
- CreateManyResolverArgs>,
- ObjectRecord[]
+ CreateManyResolverArgs>,
+ T[]
>('createMany', args, options);
if (results) {
this.apiEventEmitterService.emitCreateEvents(
results,
options.authContext,
- options.objectMetadataItem,
+ options.objectMetadataItemWithFieldMaps,
);
}
@@ -143,14 +143,11 @@ export class GraphqlQueryRunnerService {
}
@LogExecutionTime()
- public async updateOne(
- args: UpdateOneResolverArgs>,
+ public async updateOne(
+ args: UpdateOneResolverArgs>,
options: WorkspaceQueryRunnerOptions,
- ): Promise {
- const existingRecord = await this.executeQuery<
- FindOneResolverArgs,
- ObjectRecord
- >(
+ ): Promise {
+ const existingRecord = await this.executeQuery(
'findOne',
{
filter: { id: { eq: args.id } },
@@ -159,8 +156,8 @@ export class GraphqlQueryRunnerService {
);
const result = await this.executeQuery<
- UpdateOneResolverArgs>,
- ObjectRecord
+ UpdateOneResolverArgs>,
+ T
>('updateOne', args, options);
this.apiEventEmitterService.emitUpdateEvents(
@@ -168,20 +165,20 @@ export class GraphqlQueryRunnerService {
[result],
Object.keys(args.data),
options.authContext,
- options.objectMetadataItem,
+ options.objectMetadataItemWithFieldMaps,
);
return result;
}
@LogExecutionTime()
- public async updateMany(
- args: UpdateManyResolverArgs>,
+ public async updateMany(
+ args: UpdateManyResolverArgs>,
options: WorkspaceQueryRunnerOptions,
- ): Promise {
+ ): Promise {
const existingRecords = await this.executeQuery<
FindManyResolverArgs,
- IConnection>
+ IConnection>
>(
'findMany',
{
@@ -191,8 +188,8 @@ export class GraphqlQueryRunnerService {
);
const result = await this.executeQuery<
- UpdateManyResolverArgs>,
- ObjectRecord[]
+ UpdateManyResolverArgs>,
+ T[]
>('updateMany', args, options);
this.apiEventEmitterService.emitUpdateEvents(
@@ -200,25 +197,25 @@ export class GraphqlQueryRunnerService {
result,
Object.keys(args.data),
options.authContext,
- options.objectMetadataItem,
+ options.objectMetadataItemWithFieldMaps,
);
return result;
}
@LogExecutionTime()
- public async deleteOne(
+ public async deleteOne(
args: DeleteOneResolverArgs,
options: WorkspaceQueryRunnerOptions,
- ): Promise {
+ ): Promise {
const result = await this.executeQuery<
- UpdateOneResolverArgs>,
- ObjectRecord
+ UpdateOneResolverArgs>,
+ T
>(
'deleteOne',
{
id: args.id,
- data: { deletedAt: new Date() } as Partial,
+ data: { deletedAt: new Date() } as Partial,
},
options,
);
@@ -226,26 +223,26 @@ export class GraphqlQueryRunnerService {
this.apiEventEmitterService.emitDeletedEvents(
[result],
options.authContext,
- options.objectMetadataItem,
+ options.objectMetadataItemWithFieldMaps,
);
return result;
}
@LogExecutionTime()
- public async deleteMany(
+ public async deleteMany(
args: DeleteManyResolverArgs,
options: WorkspaceQueryRunnerOptions,
- ): Promise {
+ ): Promise {
const result = await this.executeQuery<
- UpdateManyResolverArgs>,
- ObjectRecord[]
+ UpdateManyResolverArgs>,
+ T[]
>(
'deleteMany',
{
filter: args.filter,
- data: { deletedAt: new Date() } as Partial,
+ data: { deletedAt: new Date() } as Partial,
},
options,
);
@@ -253,63 +250,62 @@ export class GraphqlQueryRunnerService {
this.apiEventEmitterService.emitDeletedEvents(
result,
options.authContext,
- options.objectMetadataItem,
+ options.objectMetadataItemWithFieldMaps,
);
return result;
}
@LogExecutionTime()
- async destroyOne(
+ async destroyOne(
args: DestroyOneResolverArgs,
options: WorkspaceQueryRunnerOptions,
- ): Promise {
- const result = await this.executeQuery<
- DestroyOneResolverArgs,
- ObjectRecord
- >('destroyOne', args, options);
+ ): Promise {
+ const result = await this.executeQuery(
+ 'destroyOne',
+ args,
+ options,
+ );
this.apiEventEmitterService.emitDestroyEvents(
[result],
options.authContext,
- options.objectMetadataItem,
+ options.objectMetadataItemWithFieldMaps,
);
return result;
}
@LogExecutionTime()
- async destroyMany(
+ async destroyMany(
args: DestroyManyResolverArgs,
options: WorkspaceQueryRunnerOptions,
- ): Promise {
- const result = await this.executeQuery<
- DestroyManyResolverArgs,
- ObjectRecord[]
- >('destroyMany', args, options);
+ ): Promise {
+ const result = await this.executeQuery(
+ 'destroyMany',
+ args,
+ options,
+ );
this.apiEventEmitterService.emitDestroyEvents(
result,
options.authContext,
- options.objectMetadataItem,
+ options.objectMetadataItemWithFieldMaps,
);
return result;
}
@LogExecutionTime()
- public async restoreMany(
+ public async restoreMany(
args: RestoreManyResolverArgs,
options: WorkspaceQueryRunnerOptions,
- ): Promise {
- return await this.executeQuery<
- UpdateManyResolverArgs>,
- ObjectRecord
- >(
+ ): Promise {
+ return await this.executeQuery>, T>(
'restoreMany',
{
filter: args.filter,
- data: { deletedAt: null } as Partial,
+ data: { deletedAt: null } as Partial,
},
options,
);
@@ -320,7 +316,7 @@ export class GraphqlQueryRunnerService {
args: Input,
options: WorkspaceQueryRunnerOptions,
): Promise {
- const { authContext, objectMetadataItem } = options;
+ const { authContext, objectMetadataItemWithFieldMaps } = options;
const resolver =
this.graphqlQueryResolverFactory.getResolver(operationName);
@@ -330,7 +326,7 @@ export class GraphqlQueryRunnerService {
const hookedArgs =
await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
- objectMetadataItem.nameSingular,
+ objectMetadataItemWithFieldMaps.nameSingular,
operationName,
args,
);
@@ -345,7 +341,7 @@ export class GraphqlQueryRunnerService {
const resultWithGetters = await this.queryResultGettersFactory.create(
results,
- objectMetadataItem,
+ objectMetadataItemWithFieldMaps,
authContext.workspace.id,
);
@@ -355,7 +351,7 @@ export class GraphqlQueryRunnerService {
await this.workspaceQueryHookService.executePostQueryHooks(
authContext,
- objectMetadataItem.nameSingular,
+ objectMetadataItemWithFieldMaps.nameSingular,
operationName,
resultWithGettersArray,
);
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts
index 54220315345a..0553c2e73666 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts
@@ -1,7 +1,7 @@
import {
- Record as IRecord,
- RecordOrderBy,
-} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
+ ObjectRecord,
+ ObjectRecordOrderBy,
+} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
@@ -12,23 +12,27 @@ import {
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { encodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
import { getRelationObjectMetadata } from 'src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util';
+import { AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
-import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
+import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { isPlainObject } from 'src/utils/is-plain-object';
export class ObjectRecordsToGraphqlConnectionHelper {
- private objectMetadataMap: ObjectMetadataMap;
+ private objectMetadataMaps: ObjectMetadataMaps;
- constructor(objectMetadataMap: ObjectMetadataMap) {
- this.objectMetadataMap = objectMetadataMap;
+ constructor(objectMetadataMaps: ObjectMetadataMaps) {
+ this.objectMetadataMaps = objectMetadataMaps;
}
- public createConnection({
+ public createConnection({
objectRecords,
+ parentObjectRecord,
+ objectRecordsAggregatedValues = {},
+ selectedAggregatedFields = [],
objectName,
take,
totalCount,
@@ -37,19 +41,24 @@ export class ObjectRecordsToGraphqlConnectionHelper {
hasPreviousPage,
depth = 0,
}: {
- objectRecords: ObjectRecord[];
+ objectRecords: T[];
+ parentObjectRecord?: T;
+ objectRecordsAggregatedValues?: Record;
+ selectedAggregatedFields?: Record;
objectName: string;
take: number;
totalCount: number;
- order?: RecordOrderBy;
+ order?: ObjectRecordOrderBy;
hasNextPage: boolean;
hasPreviousPage: boolean;
depth?: number;
- }): IConnection {
+ }): IConnection {
const edges = (objectRecords ?? []).map((objectRecord) => ({
node: this.processRecord({
objectRecord,
objectName,
+ objectRecordsAggregatedValues,
+ selectedAggregatedFields,
take,
totalCount,
order,
@@ -58,7 +67,15 @@ export class ObjectRecordsToGraphqlConnectionHelper {
cursor: encodeCursor(objectRecord, order),
}));
+ const aggregatedFieldsValues = this.extractAggregatedFieldsValues({
+ selectedAggregatedFields,
+ objectRecordsAggregatedValues: parentObjectRecord
+ ? objectRecordsAggregatedValues[parentObjectRecord.id]
+ : objectRecordsAggregatedValues,
+ });
+
return {
+ ...aggregatedFieldsValues,
edges,
pageInfo: {
hasNextPage,
@@ -70,9 +87,41 @@ export class ObjectRecordsToGraphqlConnectionHelper {
};
}
+ private extractAggregatedFieldsValues = ({
+ selectedAggregatedFields,
+ objectRecordsAggregatedValues,
+ }: {
+ selectedAggregatedFields: Record;
+ objectRecordsAggregatedValues: Record;
+ }) => {
+ if (!objectRecordsAggregatedValues) {
+ return {};
+ }
+
+ return Object.entries(selectedAggregatedFields).reduce(
+ (acc, [aggregatedFieldName]) => {
+ const aggregatedFieldValue =
+ objectRecordsAggregatedValues[aggregatedFieldName];
+
+ if (!aggregatedFieldValue) {
+ return acc;
+ }
+
+ return {
+ ...acc,
+ [aggregatedFieldName]:
+ objectRecordsAggregatedValues[aggregatedFieldName],
+ };
+ },
+ {},
+ );
+ };
+
public processRecord>({
objectRecord,
objectName,
+ objectRecordsAggregatedValues = {},
+ selectedAggregatedFields = [],
take,
totalCount,
order,
@@ -80,9 +129,11 @@ export class ObjectRecordsToGraphqlConnectionHelper {
}: {
objectRecord: T;
objectName: string;
+ objectRecordsAggregatedValues?: Record;
+ selectedAggregatedFields?: Record;
take: number;
totalCount: number;
- order?: RecordOrderBy;
+ order?: ObjectRecordOrderBy;
depth?: number;
}): T {
if (depth >= CONNECTION_MAX_DEPTH) {
@@ -92,7 +143,7 @@ export class ObjectRecordsToGraphqlConnectionHelper {
);
}
- const objectMetadata = this.objectMetadataMap[objectName];
+ const objectMetadata = this.objectMetadataMaps.byNameSingular[objectName];
if (!objectMetadata) {
throw new GraphqlQueryRunnerException(
@@ -104,7 +155,7 @@ export class ObjectRecordsToGraphqlConnectionHelper {
const processedObjectRecord: Record = {};
for (const [key, value] of Object.entries(objectRecord)) {
- const fieldMetadata = objectMetadata.fields[key];
+ const fieldMetadata = objectMetadata.fieldsByName[key];
if (!fieldMetadata) {
processedObjectRecord[key] = value;
@@ -115,12 +166,19 @@ export class ObjectRecordsToGraphqlConnectionHelper {
if (Array.isArray(value)) {
processedObjectRecord[key] = this.createConnection({
objectRecords: value,
+ parentObjectRecord: objectRecord,
+ objectRecordsAggregatedValues:
+ objectRecordsAggregatedValues[fieldMetadata.name],
+ selectedAggregatedFields:
+ selectedAggregatedFields[fieldMetadata.name],
objectName: getRelationObjectMetadata(
fieldMetadata,
- this.objectMetadataMap,
+ this.objectMetadataMaps,
).nameSingular,
take,
- totalCount: value.length,
+ totalCount:
+ objectRecordsAggregatedValues[fieldMetadata.name]?.totalCount ??
+ value.length,
order,
hasNextPage: false,
hasPreviousPage: false,
@@ -129,9 +187,13 @@ export class ObjectRecordsToGraphqlConnectionHelper {
} else if (isPlainObject(value)) {
processedObjectRecord[key] = this.processRecord({
objectRecord: value,
+ objectRecordsAggregatedValues:
+ objectRecordsAggregatedValues[fieldMetadata.name],
+ selectedAggregatedFields:
+ selectedAggregatedFields[fieldMetadata.name],
objectName: getRelationObjectMetadata(
fieldMetadata,
- this.objectMetadataMap,
+ this.objectMetadataMaps,
).nameSingular,
take,
totalCount,
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper.ts
new file mode 100644
index 000000000000..3ac1b554d0d4
--- /dev/null
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper.ts
@@ -0,0 +1,37 @@
+import { SelectQueryBuilder } from 'typeorm';
+
+import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
+
+import { AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util';
+
+export class ProcessAggregateHelper {
+ public addSelectedAggregatedFieldsQueriesToQueryBuilder = ({
+ fieldMetadataMapByName,
+ selectedAggregatedFields,
+ queryBuilder,
+ }: {
+ fieldMetadataMapByName: Record;
+ selectedAggregatedFields: Record;
+ queryBuilder: SelectQueryBuilder;
+ }) => {
+ queryBuilder.select([]);
+
+ for (const [aggregatedFieldName, aggregatedField] of Object.entries(
+ selectedAggregatedFields,
+ )) {
+ const fieldMetadata = fieldMetadataMapByName[aggregatedField.fromField];
+
+ if (!fieldMetadata) {
+ continue;
+ }
+
+ const fieldName = fieldMetadata.name;
+ const operation = aggregatedField.aggregationOperation;
+
+ queryBuilder.addSelect(
+ `${operation}("${fieldName}")`,
+ `${aggregatedFieldName}`,
+ );
+ }
+ };
+}
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts
index dd3e5abd4020..1a4e7896d1b9 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts
@@ -1,64 +1,95 @@
import {
DataSource,
- FindManyOptions,
FindOptionsRelations,
- In,
ObjectLiteral,
- Repository,
+ SelectQueryBuilder,
} from 'typeorm';
-import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
+import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
+import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
import {
getRelationMetadata,
getRelationObjectMetadata,
} from 'src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util';
-import {
- ObjectMetadataMap,
- ObjectMetadataMapItem,
-} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
+import { AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util';
+import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
+import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
+import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { deduceRelationDirection } from 'src/engine/utils/deduce-relation-direction.util';
export class ProcessNestedRelationsHelper {
- constructor() {}
-
- public async processNestedRelations(
- objectMetadataMap: ObjectMetadataMap,
- parentObjectMetadataItem: ObjectMetadataMapItem,
- parentObjectRecords: ObjectRecord[],
- relations: Record>,
- limit: number,
- authContext: any,
- dataSource: DataSource,
- ): Promise {
+ private processAggregateHelper: ProcessAggregateHelper;
+
+ constructor() {
+ this.processAggregateHelper = new ProcessAggregateHelper();
+ }
+
+ public async processNestedRelations({
+ objectMetadataMaps,
+ parentObjectMetadataItem,
+ parentObjectRecords,
+ parentObjectRecordsAggregatedValues = {},
+ relations,
+ aggregate = {},
+ limit,
+ authContext,
+ dataSource,
+ }: {
+ objectMetadataMaps: ObjectMetadataMaps;
+ parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps;
+ parentObjectRecords: T[];
+ parentObjectRecordsAggregatedValues?: Record;
+ relations: Record>;
+ aggregate?: Record;
+ limit: number;
+ authContext: any;
+ dataSource: DataSource;
+ }): Promise {
const processRelationTasks = Object.entries(relations).map(
([relationName, nestedRelations]) =>
- this.processRelation(
- objectMetadataMap,
+ this.processRelation({
+ objectMetadataMaps,
parentObjectMetadataItem,
parentObjectRecords,
+ parentObjectRecordsAggregatedValues,
relationName,
nestedRelations,
+ aggregate,
limit,
authContext,
dataSource,
- ),
+ }),
);
await Promise.all(processRelationTasks);
}
- private async processRelation(
- objectMetadataMap: ObjectMetadataMap,
- parentObjectMetadataItem: ObjectMetadataMapItem,
- parentObjectRecords: ObjectRecord[],
- relationName: string,
- nestedRelations: any,
- limit: number,
- authContext: any,
- dataSource: DataSource,
- ): Promise {
- const relationFieldMetadata = parentObjectMetadataItem.fields[relationName];
+ private async processRelation({
+ objectMetadataMaps,
+ parentObjectMetadataItem,
+ parentObjectRecords,
+ parentObjectRecordsAggregatedValues,
+ relationName,
+ nestedRelations,
+ aggregate,
+ limit,
+ authContext,
+ dataSource,
+ }: {
+ objectMetadataMaps: ObjectMetadataMaps;
+ parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps;
+ parentObjectRecords: T[];
+ parentObjectRecordsAggregatedValues: Record;
+ relationName: string;
+ nestedRelations: any;
+ aggregate: Record;
+ limit: number;
+ authContext: any;
+ dataSource: DataSource;
+ }): Promise {
+ const relationFieldMetadata =
+ parentObjectMetadataItem.fieldsByName[relationName];
const relationMetadata = getRelationMetadata(relationFieldMetadata);
const relationDirection = deduceRelationDirection(
relationFieldMetadata,
@@ -70,181 +101,341 @@ export class ProcessNestedRelationsHelper {
? this.processToRelation
: this.processFromRelation;
- await processor.call(
- this,
- objectMetadataMap,
+ await processor.call(this, {
+ objectMetadataMaps,
parentObjectMetadataItem,
parentObjectRecords,
+ parentObjectRecordsAggregatedValues,
relationName,
nestedRelations,
+ aggregate,
limit,
authContext,
dataSource,
- );
+ });
}
- private async processFromRelation(
- objectMetadataMap: ObjectMetadataMap,
- parentObjectMetadataItem: ObjectMetadataMapItem,
- parentObjectRecords: ObjectRecord[],
- relationName: string,
- nestedRelations: any,
- limit: number,
- authContext: any,
- dataSource: DataSource,
- ): Promise {
+ private async processFromRelation({
+ objectMetadataMaps,
+ parentObjectMetadataItem,
+ parentObjectRecords,
+ parentObjectRecordsAggregatedValues,
+ relationName,
+ nestedRelations,
+ aggregate,
+ limit,
+ authContext,
+ dataSource,
+ }: {
+ objectMetadataMaps: ObjectMetadataMaps;
+ parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps;
+ parentObjectRecords: T[];
+ parentObjectRecordsAggregatedValues: Record;
+ relationName: string;
+ nestedRelations: any;
+ aggregate: Record;
+ limit: number;
+ authContext: any;
+ dataSource: DataSource;
+ }): Promise {
const { inverseRelationName, referenceObjectMetadata } =
- this.getRelationMetadata(
- objectMetadataMap,
+ this.getRelationMetadata({
+ objectMetadataMaps,
parentObjectMetadataItem,
relationName,
- );
+ });
const relationRepository = dataSource.getRepository(
referenceObjectMetadata.nameSingular,
);
- const relationIds = this.getUniqueIds(parentObjectRecords, 'id');
- const relationResults = await this.findRelations(
- relationRepository,
- inverseRelationName,
- relationIds,
- limit * parentObjectRecords.length,
+ const referenceQueryBuilder = relationRepository.createQueryBuilder(
+ referenceObjectMetadata.nameSingular,
);
- this.assignRelationResults(
- parentObjectRecords,
+ const relationIds = this.getUniqueIds({
+ records: parentObjectRecords,
+ idField: 'id',
+ });
+ const { relationResults, relationAggregatedFieldsResult } =
+ await this.findRelations({
+ referenceQueryBuilder,
+ column: `"${inverseRelationName}Id"`,
+ ids: relationIds,
+ limit: limit * parentObjectRecords.length,
+ objectMetadataMaps,
+ referenceObjectMetadata,
+ aggregate,
+ relationName,
+ });
+
+ this.assignFromRelationResults({
+ parentRecords: parentObjectRecords,
+ parentObjectRecordsAggregatedValues,
relationResults,
+ relationAggregatedFieldsResult,
relationName,
- `${inverseRelationName}Id`,
- );
+ joinField: `${inverseRelationName}Id`,
+ });
if (Object.keys(nestedRelations).length > 0) {
- await this.processNestedRelations(
- objectMetadataMap,
- objectMetadataMap[referenceObjectMetadata.nameSingular],
- relationResults as ObjectRecord[],
- nestedRelations as Record>,
+ await this.processNestedRelations({
+ objectMetadataMaps,
+ parentObjectMetadataItem:
+ objectMetadataMaps.byNameSingular[
+ referenceObjectMetadata.nameSingular
+ ],
+ parentObjectRecords: relationResults as ObjectRecord[],
+ parentObjectRecordsAggregatedValues: relationAggregatedFieldsResult,
+ relations: nestedRelations as Record<
+ string,
+ FindOptionsRelations
+ >,
+ aggregate,
limit,
authContext,
dataSource,
- );
+ });
}
}
- private async processToRelation(
- objectMetadataMap: ObjectMetadataMap,
- parentObjectMetadataItem: ObjectMetadataMapItem,
- parentObjectRecords: ObjectRecord[],
- relationName: string,
- nestedRelations: any,
- limit: number,
- authContext: any,
- dataSource: DataSource,
- ): Promise {
- const { referenceObjectMetadata } = this.getRelationMetadata(
- objectMetadataMap,
+ private async processToRelation({
+ objectMetadataMaps,
+ parentObjectMetadataItem,
+ parentObjectRecords,
+ parentObjectRecordsAggregatedValues,
+ relationName,
+ nestedRelations,
+ aggregate,
+ limit,
+ authContext,
+ dataSource,
+ }: {
+ objectMetadataMaps: ObjectMetadataMaps;
+ parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps;
+ parentObjectRecords: T[];
+ parentObjectRecordsAggregatedValues: Record;
+ relationName: string;
+ nestedRelations: any;
+ aggregate: Record;
+ limit: number;
+ authContext: any;
+ dataSource: DataSource;
+ }): Promise {
+ const { referenceObjectMetadata } = this.getRelationMetadata({
+ objectMetadataMaps,
parentObjectMetadataItem,
relationName,
- );
+ });
const relationRepository = dataSource.getRepository(
referenceObjectMetadata.nameSingular,
);
- const relationIds = this.getUniqueIds(
- parentObjectRecords,
- `${relationName}Id`,
- );
- const relationResults = await this.findRelations(
- relationRepository,
- 'id',
- relationIds,
- limit,
+ const referenceQueryBuilder = relationRepository.createQueryBuilder(
+ referenceObjectMetadata.nameSingular,
);
- this.assignToRelationResults(
- parentObjectRecords,
+ const relationIds = this.getUniqueIds({
+ records: parentObjectRecords,
+ idField: `${relationName}Id`,
+ });
+ const { relationResults, relationAggregatedFieldsResult } =
+ await this.findRelations({
+ referenceQueryBuilder,
+ column: 'id',
+ ids: relationIds,
+ limit,
+ objectMetadataMaps,
+ referenceObjectMetadata,
+ aggregate,
+ relationName,
+ });
+
+ this.assignToRelationResults({
+ parentRecords: parentObjectRecords,
+ parentObjectRecordsAggregatedValues: parentObjectRecordsAggregatedValues,
relationResults,
+ relationAggregatedFieldsResult,
relationName,
- );
+ });
if (Object.keys(nestedRelations).length > 0) {
- await this.processNestedRelations(
- objectMetadataMap,
- objectMetadataMap[referenceObjectMetadata.nameSingular],
- relationResults as ObjectRecord[],
- nestedRelations as Record>,
+ await this.processNestedRelations({
+ objectMetadataMaps,
+ parentObjectMetadataItem:
+ objectMetadataMaps.byNameSingular[
+ referenceObjectMetadata.nameSingular
+ ],
+ parentObjectRecords: relationResults as ObjectRecord[],
+ parentObjectRecordsAggregatedValues: relationAggregatedFieldsResult,
+ relations: nestedRelations as Record<
+ string,
+ FindOptionsRelations
+ >,
+ aggregate,
limit,
authContext,
dataSource,
- );
+ });
}
}
- private getRelationMetadata(
- objectMetadataMap: ObjectMetadataMap,
- parentObjectMetadataItem: ObjectMetadataMapItem,
- relationName: string,
- ) {
- const relationFieldMetadata = parentObjectMetadataItem.fields[relationName];
+ private getRelationMetadata({
+ objectMetadataMaps,
+ parentObjectMetadataItem,
+ relationName,
+ }: {
+ objectMetadataMaps: ObjectMetadataMaps;
+ parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps;
+ relationName: string;
+ }) {
+ const relationFieldMetadata =
+ parentObjectMetadataItem.fieldsByName[relationName];
const relationMetadata = getRelationMetadata(relationFieldMetadata);
const referenceObjectMetadata = getRelationObjectMetadata(
relationFieldMetadata,
- objectMetadataMap,
+ objectMetadataMaps,
);
const inverseRelationName =
- objectMetadataMap[relationMetadata.toObjectMetadataId]?.fields[
+ objectMetadataMaps.byId[relationMetadata.toObjectMetadataId]?.fieldsById[
relationMetadata.toFieldMetadataId
]?.name;
return { inverseRelationName, referenceObjectMetadata };
}
- private getUniqueIds(records: IRecord[], idField: string): any[] {
+ private getUniqueIds({
+ records,
+ idField,
+ }: {
+ records: ObjectRecord[];
+ idField: string;
+ }): any[] {
return [...new Set(records.map((item) => item[idField]))];
}
- private async findRelations(
- repository: Repository,
- field: string,
- ids: any[],
- limit: number,
- ): Promise {
+ private async findRelations({
+ referenceQueryBuilder,
+ column,
+ ids,
+ limit,
+ objectMetadataMaps,
+ referenceObjectMetadata,
+ aggregate,
+ relationName,
+ }: {
+ referenceQueryBuilder: SelectQueryBuilder;
+ column: string;
+ ids: any[];
+ limit: number;
+ objectMetadataMaps: ObjectMetadataMaps;
+ referenceObjectMetadata: ObjectMetadataItemWithFieldMaps;
+ aggregate: Record;
+ relationName: string;
+ }): Promise<{ relationResults: any[]; relationAggregatedFieldsResult: any }> {
if (ids.length === 0) {
- return [];
+ return { relationResults: [], relationAggregatedFieldsResult: {} };
+ }
+
+ const aggregateForRelation = aggregate[relationName];
+ let relationAggregatedFieldsResult: Record = {};
+
+ if (aggregateForRelation) {
+ const aggregateQueryBuilder = referenceQueryBuilder.clone();
+
+ this.processAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder(
+ {
+ fieldMetadataMapByName: referenceObjectMetadata.fieldsByName,
+ selectedAggregatedFields: aggregateForRelation,
+ queryBuilder: aggregateQueryBuilder,
+ },
+ );
+
+ const aggregatedFieldsValues = await aggregateQueryBuilder
+ .addSelect(column)
+ .where(`${column} IN (:...ids)`, {
+ ids,
+ })
+ .groupBy(column)
+ .getRawMany();
+
+ relationAggregatedFieldsResult = aggregatedFieldsValues.reduce(
+ (acc, item) => {
+ const columnWithoutQuotes = column.replace(/["']/g, '');
+ const key = item[columnWithoutQuotes];
+ const { [column]: _, ...itemWithoutColumn } = item;
+
+ acc[key] = itemWithoutColumn;
+
+ return acc;
+ },
+ {},
+ );
}
- const findOptions: FindManyOptions = {
- where: { [field]: In(ids) },
- take: limit,
- };
- return repository.find(findOptions);
+ const result = await referenceQueryBuilder
+ .where(`${column} IN (:...ids)`, {
+ ids,
+ })
+ .take(limit)
+ .getMany();
+
+ const relationResults = formatResult(
+ result,
+ referenceObjectMetadata,
+ objectMetadataMaps,
+ );
+
+ return { relationResults, relationAggregatedFieldsResult };
}
- private assignRelationResults(
- parentRecords: IRecord[],
- relationResults: any[],
- relationName: string,
- joinField: string,
- ): void {
+ private assignFromRelationResults({
+ parentRecords,
+ parentObjectRecordsAggregatedValues,
+ relationResults,
+ relationAggregatedFieldsResult,
+ relationName,
+ joinField,
+ }: {
+ parentRecords: ObjectRecord[];
+ parentObjectRecordsAggregatedValues: Record;
+ relationResults: any[];
+ relationAggregatedFieldsResult: Record;
+ relationName: string;
+ joinField: string;
+ }): void {
parentRecords.forEach((item) => {
- (item as any)[relationName] = relationResults.filter(
+ item[relationName] = relationResults.filter(
(rel) => rel[joinField] === item.id,
);
});
+
+ parentObjectRecordsAggregatedValues[relationName] =
+ relationAggregatedFieldsResult;
}
- private assignToRelationResults(
- parentRecords: IRecord[],
- relationResults: any[],
- relationName: string,
- ): void {
+ private assignToRelationResults({
+ parentRecords,
+ parentObjectRecordsAggregatedValues,
+ relationResults,
+ relationAggregatedFieldsResult,
+ relationName,
+ }: {
+ parentRecords: ObjectRecord[];
+ parentObjectRecordsAggregatedValues: Record;
+ relationResults: any[];
+ relationAggregatedFieldsResult: Record;
+ relationName: string;
+ }): void {
parentRecords.forEach((item) => {
if (relationResults.length === 0) {
- (item as any)[`${relationName}Id`] = null;
+ item[`${relationName}Id`] = null;
}
- (item as any)[relationName] =
+ item[relationName] =
relationResults.find((rel) => rel.id === item[`${relationName}Id`]) ??
null;
});
+
+ parentObjectRecordsAggregatedValues[relationName] =
+ relationAggregatedFieldsResult;
}
}
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts
index 6cd7a111138e..19bc28cf0ddd 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts
@@ -4,7 +4,7 @@ import graphqlFields from 'graphql-fields';
import { In, InsertResult } from 'typeorm';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
-import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
+import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { CreateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
@@ -19,35 +19,40 @@ import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable()
export class GraphqlQueryCreateManyResolverService
- implements ResolverService
+ implements ResolverService
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
- async resolve(
+ async resolve(
args: CreateManyResolverArgs>,
options: WorkspaceQueryRunnerOptions,
- ): Promise {
- const { authContext, info, objectMetadataMap, objectMetadataMapItem } =
- options;
+ ): Promise {
+ const {
+ authContext,
+ info,
+ objectMetadataMaps,
+ objectMetadataItemWithFieldMaps,
+ } = options;
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
authContext.workspace.id,
);
+
const repository = dataSource.getRepository(
- objectMetadataMapItem.nameSingular,
+ objectMetadataItemWithFieldMaps.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
- objectMetadataMapItem.fields,
- objectMetadataMap,
+ objectMetadataItemWithFieldMaps.fieldsByName,
+ objectMetadataMaps,
);
const selectedFields = graphqlFields(info);
const { relations } = graphqlQueryParser.parseSelectedFields(
- objectMetadataMapItem,
+ objectMetadataItemWithFieldMaps,
selectedFields,
);
@@ -59,7 +64,7 @@ export class GraphqlQueryCreateManyResolverService
});
const queryBuilder = repository.createQueryBuilder(
- objectMetadataMapItem.nameSingular,
+ objectMetadataItemWithFieldMaps.nameSingular,
);
const nonFormattedUpsertedRecords = (await queryBuilder
@@ -71,42 +76,42 @@ export class GraphqlQueryCreateManyResolverService
const upsertedRecords = formatResult(
nonFormattedUpsertedRecords,
- objectMetadataMapItem,
- objectMetadataMap,
+ objectMetadataItemWithFieldMaps,
+ objectMetadataMaps,
);
const processNestedRelationsHelper = new ProcessNestedRelationsHelper();
if (relations) {
- await processNestedRelationsHelper.processNestedRelations(
- objectMetadataMap,
- objectMetadataMapItem,
- upsertedRecords,
+ await processNestedRelationsHelper.processNestedRelations({
+ objectMetadataMaps,
+ parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
+ parentObjectRecords: upsertedRecords,
relations,
- QUERY_MAX_RECORDS,
+ limit: QUERY_MAX_RECORDS,
authContext,
dataSource,
- );
+ });
}
const typeORMObjectRecordsParser =
- new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
+ new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
- return upsertedRecords.map((record: ObjectRecord) =>
+ return upsertedRecords.map((record: T) =>
typeORMObjectRecordsParser.processRecord({
objectRecord: record,
- objectName: objectMetadataMapItem.nameSingular,
+ objectName: objectMetadataItemWithFieldMaps.nameSingular,
take: 1,
totalCount: 1,
}),
);
}
- async validate(
- args: CreateManyResolverArgs>,
+ async validate(
+ args: CreateManyResolverArgs>,
options: WorkspaceQueryRunnerOptions,
): Promise {
- assertMutationNotOnRemoteObject(options.objectMetadataItem);
+ assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps);
args.data.forEach((record) => {
if (record?.id) {
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts
index 04ceddf9ac9d..8b4176d267f2 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts
@@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
import graphqlFields from 'graphql-fields';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
-import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
+import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { DestroyManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
@@ -16,46 +16,51 @@ import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable()
export class GraphqlQueryDestroyManyResolverService
- implements ResolverService
+ implements ResolverService
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
- async resolve(
+ async resolve(
args: DestroyManyResolverArgs,
options: WorkspaceQueryRunnerOptions,
- ): Promise {
- const { authContext, objectMetadataMapItem, objectMetadataMap, info } =
- options;
+ ): Promise {
+ const {
+ authContext,
+ objectMetadataItemWithFieldMaps,
+ objectMetadataMaps,
+ info,
+ } = options;
+
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
authContext.workspace.id,
);
const repository = dataSource.getRepository(
- objectMetadataMapItem.nameSingular,
+ objectMetadataItemWithFieldMaps.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
- objectMetadataMapItem.fields,
- objectMetadataMap,
+ objectMetadataItemWithFieldMaps.fieldsByName,
+ objectMetadataMaps,
);
const selectedFields = graphqlFields(info);
const { relations } = graphqlQueryParser.parseSelectedFields(
- objectMetadataMapItem,
+ objectMetadataItemWithFieldMaps,
selectedFields,
);
const queryBuilder = repository.createQueryBuilder(
- objectMetadataMapItem.nameSingular,
+ objectMetadataItemWithFieldMaps.nameSingular,
);
const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
queryBuilder,
- objectMetadataMapItem.nameSingular,
+ objectMetadataItemWithFieldMaps.nameSingular,
args.filter,
);
@@ -66,31 +71,31 @@ export class GraphqlQueryDestroyManyResolverService
const deletedRecords = formatResult(
nonFormattedDeletedObjectRecords.raw,
- objectMetadataMapItem,
- objectMetadataMap,
+ objectMetadataItemWithFieldMaps,
+ objectMetadataMaps,
);
const processNestedRelationsHelper = new ProcessNestedRelationsHelper();
if (relations) {
- await processNestedRelationsHelper.processNestedRelations(
- objectMetadataMap,
- objectMetadataMapItem,
- deletedRecords,
+ await processNestedRelationsHelper.processNestedRelations({
+ objectMetadataMaps,
+ parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
+ parentObjectRecords: deletedRecords,
relations,
- QUERY_MAX_RECORDS,
+ limit: QUERY_MAX_RECORDS,
authContext,
dataSource,
- );
+ });
}
const typeORMObjectRecordsParser =
- new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
+ new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
- return deletedRecords.map((record: ObjectRecord) =>
+ return deletedRecords.map((record: T) =>
typeORMObjectRecordsParser.processRecord({
objectRecord: record,
- objectName: objectMetadataMapItem.nameSingular,
+ objectName: objectMetadataItemWithFieldMaps.nameSingular,
take: 1,
totalCount: 1,
}),
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts
index 5467d6c0ff0a..044370a0730a 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts
@@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
import graphqlFields from 'graphql-fields';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
-import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
+import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { DestroyOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
@@ -20,45 +20,50 @@ import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable()
export class GraphqlQueryDestroyOneResolverService
- implements ResolverService
+ implements ResolverService
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
- async resolve(
+ async resolve(
args: DestroyOneResolverArgs,
options: WorkspaceQueryRunnerOptions,
- ): Promise {
- const { authContext, objectMetadataMapItem, objectMetadataMap, info } =
- options;
+ ): Promise {
+ const {
+ authContext,
+ objectMetadataItemWithFieldMaps,
+ objectMetadataMaps,
+ info,
+ } = options;
+
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
authContext.workspace.id,
);
const repository = dataSource.getRepository(
- objectMetadataMapItem.nameSingular,
+ objectMetadataItemWithFieldMaps.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
- objectMetadataMapItem.fields,
- objectMetadataMap,
+ objectMetadataItemWithFieldMaps.fieldsByName,
+ objectMetadataMaps,
);
const selectedFields = graphqlFields(info);
const { relations } = graphqlQueryParser.parseSelectedFields(
- objectMetadataMapItem,
+ objectMetadataItemWithFieldMaps,
selectedFields,
);
const queryBuilder = repository.createQueryBuilder(
- objectMetadataMapItem.nameSingular,
+ objectMetadataItemWithFieldMaps.nameSingular,
);
const nonFormattedDeletedObjectRecords = await queryBuilder
- .where(`"${objectMetadataMapItem.nameSingular}".id = :id`, {
+ .where(`"${objectMetadataItemWithFieldMaps.nameSingular}".id = :id`, {
id: args.id,
})
.take(1)
@@ -75,30 +80,30 @@ export class GraphqlQueryDestroyOneResolverService
const recordBeforeDeletion = formatResult(
nonFormattedDeletedObjectRecords.raw,
- objectMetadataMapItem,
- objectMetadataMap,
+ objectMetadataItemWithFieldMaps,
+ objectMetadataMaps,
)[0];
const processNestedRelationsHelper = new ProcessNestedRelationsHelper();
if (relations) {
- await processNestedRelationsHelper.processNestedRelations(
- objectMetadataMap,
- objectMetadataMapItem,
- [recordBeforeDeletion],
+ await processNestedRelationsHelper.processNestedRelations({
+ objectMetadataMaps,
+ parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
+ parentObjectRecords: [recordBeforeDeletion],
relations,
- QUERY_MAX_RECORDS,
+ limit: QUERY_MAX_RECORDS,
authContext,
dataSource,
- );
+ });
}
const typeORMObjectRecordsParser =
- new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
+ new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
return typeORMObjectRecordsParser.processRecord({
objectRecord: recordBeforeDeletion,
- objectName: objectMetadataMapItem.nameSingular,
+ objectName: objectMetadataItemWithFieldMaps.nameSingular,
take: 1,
totalCount: 1,
});
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts
index d3bc72fa8220..ebc2200c1cc1 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts
@@ -5,10 +5,10 @@ import { In } from 'typeorm';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
import {
- Record as IRecord,
+ ObjectRecord,
+ ObjectRecordFilter,
OrderByDirection,
- RecordFilter,
-} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
+} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { FindDuplicatesResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
@@ -21,7 +21,7 @@ import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
import { settings } from 'src/engine/constants/settings';
import { DUPLICATE_CRITERIA_COLLECTION } from 'src/engine/core-modules/duplicate/constants/duplicate-criteria.constants';
-import { ObjectMetadataMapItem } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
+import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@@ -29,60 +29,63 @@ import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable()
export class GraphqlQueryFindDuplicatesResolverService
implements
- ResolverService[]>
+ ResolverService[]>
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
- async resolve(
- args: FindDuplicatesResolverArgs>,
+ async resolve(
+ args: FindDuplicatesResolverArgs>,
options: WorkspaceQueryRunnerOptions,
- ): Promise[]> {
- const { authContext, objectMetadataMapItem, objectMetadataMap } = options;
+ ): Promise[]> {
+ const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } =
+ options;
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
authContext.workspace.id,
);
const repository = dataSource.getRepository(
- objectMetadataMapItem.nameSingular,
+ objectMetadataItemWithFieldMaps.nameSingular,
);
const existingRecordsQueryBuilder = repository.createQueryBuilder(
- objectMetadataMapItem.nameSingular,
+ objectMetadataItemWithFieldMaps.nameSingular,
);
const duplicateRecordsQueryBuilder = repository.createQueryBuilder(
- objectMetadataMapItem.nameSingular,
+ objectMetadataItemWithFieldMaps.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
- objectMetadataMap[objectMetadataMapItem.nameSingular].fields,
- objectMetadataMap,
+ objectMetadataMaps.byNameSingular[
+ objectMetadataItemWithFieldMaps.nameSingular
+ ].fieldsByName,
+ objectMetadataMaps,
);
const typeORMObjectRecordsParser =
- new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
+ new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
- let objectRecords: Partial[] = [];
+ let objectRecords: Partial[] = [];
if (args.ids) {
const nonFormattedObjectRecords = (await existingRecordsQueryBuilder
.where({ id: In(args.ids) })
- .getMany()) as ObjectRecord[];
+ .getMany()) as T[];
objectRecords = formatResult(
nonFormattedObjectRecords,
- objectMetadataMapItem,
- objectMetadataMap,
+ objectMetadataItemWithFieldMaps,
+ objectMetadataMaps,
);
} else if (args.data && !isEmpty(args.data)) {
- objectRecords = formatData(args.data, objectMetadataMapItem);
+ objectRecords = formatData(args.data, objectMetadataItemWithFieldMaps);
}
- const duplicateConnections: IConnection[] = await Promise.all(
+ const duplicateConnections: IConnection[] = await Promise.all(
objectRecords.map(async (record) => {
const duplicateConditions = this.buildDuplicateConditions(
- objectMetadataMapItem,
+ objectMetadataItemWithFieldMaps,
[record],
record.id,
);
@@ -90,7 +93,7 @@ export class GraphqlQueryFindDuplicatesResolverService
if (isEmpty(duplicateConditions)) {
return typeORMObjectRecordsParser.createConnection({
objectRecords: [],
- objectName: objectMetadataMapItem.nameSingular,
+ objectName: objectMetadataItemWithFieldMaps.nameSingular,
take: 0,
totalCount: 0,
order: [{ id: OrderByDirection.AscNullsFirst }],
@@ -101,22 +104,22 @@ export class GraphqlQueryFindDuplicatesResolverService
const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
duplicateRecordsQueryBuilder,
- objectMetadataMapItem.nameSingular,
+ objectMetadataItemWithFieldMaps.nameSingular,
duplicateConditions,
);
const nonFormattedDuplicates =
- (await withFilterQueryBuilder.getMany()) as ObjectRecord[];
+ (await withFilterQueryBuilder.getMany()) as T[];
const duplicates = formatResult(
nonFormattedDuplicates,
- objectMetadataMapItem,
- objectMetadataMap,
+ objectMetadataItemWithFieldMaps,
+ objectMetadataMaps,
);
return typeORMObjectRecordsParser.createConnection({
objectRecords: duplicates,
- objectName: objectMetadataMapItem.nameSingular,
+ objectName: objectMetadataItemWithFieldMaps.nameSingular,
take: duplicates.length,
totalCount: duplicates.length,
order: [{ id: OrderByDirection.AscNullsFirst }],
@@ -130,16 +133,16 @@ export class GraphqlQueryFindDuplicatesResolverService
}
private buildDuplicateConditions(
- objectMetadataMapItem: ObjectMetadataMapItem,
- records?: Partial[] | undefined,
+ objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
+ records?: Partial[] | undefined,
filteringByExistingRecordId?: string,
- ): Partial {
+ ): Partial {
if (!records || records.length === 0) {
return {};
}
const criteriaCollection = this.getApplicableDuplicateCriteriaCollection(
- objectMetadataMapItem,
+ objectMetadataItemWithFieldMaps,
);
const conditions = records.flatMap((record) => {
@@ -164,7 +167,7 @@ export class GraphqlQueryFindDuplicatesResolverService
});
});
- const filter: Partial = {};
+ const filter: Partial = {};
if (conditions && !isEmpty(conditions)) {
filter.or = conditions;
@@ -178,11 +181,12 @@ export class GraphqlQueryFindDuplicatesResolverService
}
private getApplicableDuplicateCriteriaCollection(
- objectMetadataMapItem: ObjectMetadataMapItem,
+ objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
) {
return DUPLICATE_CRITERIA_COLLECTION.filter(
(duplicateCriteria) =>
- duplicateCriteria.objectName === objectMetadataMapItem.nameSingular,
+ duplicateCriteria.objectName ===
+ objectMetadataItemWithFieldMaps.nameSingular,
);
}
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts
index 9411c5502103..9b4c850de096 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts
@@ -1,15 +1,14 @@
import { Injectable } from '@nestjs/common';
-import { isDefined } from 'class-validator';
import graphqlFields from 'graphql-fields';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
import {
- Record as IRecord,
+ ObjectRecord,
+ ObjectRecordFilter,
+ ObjectRecordOrderBy,
OrderByDirection,
- RecordFilter,
- RecordOrderBy,
-} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
+} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
@@ -19,35 +18,45 @@ import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
+import { GraphqlQuerySelectedFieldsResult } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
+import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
import { computeCursorArgFilter } from 'src/engine/api/graphql/graphql-query-runner/utils/compute-cursor-arg-filter';
import {
getCursor,
getPaginationInfo,
} from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
+import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
+import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
+import { isDefined } from 'src/utils/is-defined';
@Injectable()
export class GraphqlQueryFindManyResolverService
- implements ResolverService>
+ implements ResolverService>
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
+ private readonly featureFlagService: FeatureFlagService,
) {}
async resolve<
- ObjectRecord extends IRecord = IRecord,
- Filter extends RecordFilter = RecordFilter,
- OrderBy extends RecordOrderBy = RecordOrderBy,
+ T extends ObjectRecord = ObjectRecord,
+ Filter extends ObjectRecordFilter = ObjectRecordFilter,
+ OrderBy extends ObjectRecordOrderBy = ObjectRecordOrderBy,
>(
args: FindManyResolverArgs,
options: WorkspaceQueryRunnerOptions,
- ): Promise> {
- const { authContext, objectMetadataMapItem, info, objectMetadataMap } =
- options;
+ ): Promise> {
+ const {
+ authContext,
+ objectMetadataItemWithFieldMaps,
+ info,
+ objectMetadataMaps,
+ } = options;
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
@@ -55,48 +64,44 @@ export class GraphqlQueryFindManyResolverService
);
const repository = dataSource.getRepository(
- objectMetadataMapItem.nameSingular,
+ objectMetadataItemWithFieldMaps.nameSingular,
);
const queryBuilder = repository.createQueryBuilder(
- objectMetadataMapItem.nameSingular,
+ objectMetadataItemWithFieldMaps.nameSingular,
);
- const countQueryBuilder = repository.createQueryBuilder(
- objectMetadataMapItem.nameSingular,
+ const aggregateQueryBuilder = repository.createQueryBuilder(
+ objectMetadataItemWithFieldMaps.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
- objectMetadataMapItem.fields,
- objectMetadataMap,
+ objectMetadataItemWithFieldMaps.fieldsByName,
+ objectMetadataMaps,
);
- const withFilterCountQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
- countQueryBuilder,
- objectMetadataMapItem.nameSingular,
- args.filter ?? ({} as Filter),
- );
+ const withFilterAggregateQueryBuilder =
+ graphqlQueryParser.applyFilterToBuilder(
+ aggregateQueryBuilder,
+ objectMetadataItemWithFieldMaps.nameSingular,
+ args.filter ?? ({} as Filter),
+ );
const selectedFields = graphqlFields(info);
- const { relations } = graphqlQueryParser.parseSelectedFields(
- objectMetadataMapItem,
- selectedFields,
- );
+ const graphqlQuerySelectedFieldsResult: GraphqlQuerySelectedFieldsResult =
+ graphqlQueryParser.parseSelectedFields(
+ objectMetadataItemWithFieldMaps,
+ selectedFields,
+ );
const isForwardPagination = !isDefined(args.before);
- const limit = args.first ?? args.last ?? QUERY_MAX_RECORDS;
-
- const withDeletedCountQueryBuilder =
+ const withDeletedAggregateQueryBuilder =
graphqlQueryParser.applyDeletedAtToBuilder(
- withFilterCountQueryBuilder,
+ withFilterAggregateQueryBuilder,
args.filter ?? ({} as Filter),
);
- const totalCount = isDefined(selectedFields.totalCount)
- ? await withDeletedCountQueryBuilder.getCount()
- : 0;
-
const cursor = getCursor(args);
let appliedFilters = args.filter ?? ({} as Filter);
@@ -110,7 +115,7 @@ export class GraphqlQueryFindManyResolverService
const cursorArgFilter = computeCursorArgFilter(
cursor,
orderByWithIdCondition,
- objectMetadataMapItem.fields,
+ objectMetadataItemWithFieldMaps.fieldsByName,
isForwardPagination,
);
@@ -123,14 +128,14 @@ export class GraphqlQueryFindManyResolverService
const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
queryBuilder,
- objectMetadataMapItem.nameSingular,
+ objectMetadataItemWithFieldMaps.nameSingular,
appliedFilters,
);
const withOrderByQueryBuilder = graphqlQueryParser.applyOrderToBuilder(
withFilterQueryBuilder,
orderByWithIdCondition,
- objectMetadataMapItem.nameSingular,
+ objectMetadataItemWithFieldMaps.nameSingular,
isForwardPagination,
);
@@ -139,14 +144,36 @@ export class GraphqlQueryFindManyResolverService
args.filter ?? ({} as Filter),
);
+ const isAggregationsEnabled =
+ await this.featureFlagService.isFeatureEnabled(
+ FeatureFlagKey.IsAggregateQueryEnabled,
+ authContext.workspace.id,
+ );
+
+ if (!isAggregationsEnabled) {
+ graphqlQuerySelectedFieldsResult.aggregate = {
+ totalCount: graphqlQuerySelectedFieldsResult.aggregate.totalCount,
+ };
+ }
+
+ const processAggregateHelper = new ProcessAggregateHelper();
+
+ processAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({
+ fieldMetadataMapByName: objectMetadataItemWithFieldMaps.fieldsByName,
+ selectedAggregatedFields: graphqlQuerySelectedFieldsResult.aggregate,
+ queryBuilder: withDeletedAggregateQueryBuilder,
+ });
+
+ const limit = args.first ?? args.last ?? QUERY_MAX_RECORDS;
+
const nonFormattedObjectRecords = await withDeletedQueryBuilder
.take(limit + 1)
.getMany();
const objectRecords = formatResult(
nonFormattedObjectRecords,
- objectMetadataMapItem,
- objectMetadataMap,
+ objectMetadataItemWithFieldMaps,
+ objectMetadataMaps,
);
const { hasNextPage, hasPreviousPage } = getPaginationInfo(
@@ -159,37 +186,42 @@ export class GraphqlQueryFindManyResolverService
objectRecords.pop();
}
+ const parentObjectRecordsAggregatedValues =
+ await withDeletedAggregateQueryBuilder.getRawOne();
+
const processNestedRelationsHelper = new ProcessNestedRelationsHelper();
- if (relations) {
- await processNestedRelationsHelper.processNestedRelations(
- objectMetadataMap,
- objectMetadataMapItem,
- objectRecords,
- relations,
+ if (graphqlQuerySelectedFieldsResult.relations) {
+ await processNestedRelationsHelper.processNestedRelations({
+ objectMetadataMaps,
+ parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
+ parentObjectRecords: objectRecords,
+ parentObjectRecordsAggregatedValues,
+ relations: graphqlQuerySelectedFieldsResult.relations,
+ aggregate: graphqlQuerySelectedFieldsResult.aggregate,
limit,
authContext,
dataSource,
- );
+ });
}
const typeORMObjectRecordsParser =
- new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
+ new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
- const result = typeORMObjectRecordsParser.createConnection({
+ return typeORMObjectRecordsParser.createConnection({
objectRecords,
- objectName: objectMetadataMapItem.nameSingular,
+ objectRecordsAggregatedValues: parentObjectRecordsAggregatedValues,
+ selectedAggregatedFields: graphqlQuerySelectedFieldsResult.aggregate,
+ objectName: objectMetadataItemWithFieldMaps.nameSingular,
take: limit,
- totalCount,
+ totalCount: parentObjectRecordsAggregatedValues.totalCount,
order: orderByWithIdCondition,
hasNextPage,
hasPreviousPage,
});
-
- return result;
}
- async validate(
+ async validate(
args: FindManyResolverArgs,
_options: WorkspaceQueryRunnerOptions,
): Promise {
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts
index 42c8daae8079..bcd076a1729d 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts
@@ -4,9 +4,9 @@ import graphqlFields from 'graphql-fields';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
import {
- Record as IRecord,
- RecordFilter,
-} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
+ ObjectRecord,
+ ObjectRecordFilter,
+} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
@@ -27,21 +27,25 @@ import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable()
export class GraphqlQueryFindOneResolverService
- implements ResolverService
+ implements ResolverService
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
async resolve<
- ObjectRecord extends IRecord = IRecord,
- Filter extends RecordFilter = RecordFilter,
+ T extends ObjectRecord = ObjectRecord,
+ Filter extends ObjectRecordFilter = ObjectRecordFilter,
>(
args: FindOneResolverArgs,
options: WorkspaceQueryRunnerOptions,
- ): Promise {
- const { authContext, objectMetadataMapItem, info, objectMetadataMap } =
- options;
+ ): Promise {
+ const {
+ authContext,
+ objectMetadataItemWithFieldMaps,
+ info,
+ objectMetadataMaps,
+ } = options;
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
@@ -49,28 +53,28 @@ export class GraphqlQueryFindOneResolverService
);
const repository = dataSource.getRepository(
- objectMetadataMapItem.nameSingular,
+ objectMetadataItemWithFieldMaps.nameSingular,
);
const queryBuilder = repository.createQueryBuilder(
- objectMetadataMapItem.nameSingular,
+ objectMetadataItemWithFieldMaps.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
- objectMetadataMapItem.fields,
- objectMetadataMap,
+ objectMetadataItemWithFieldMaps.fieldsByName,
+ objectMetadataMaps,
);
const selectedFields = graphqlFields(info);
const { relations } = graphqlQueryParser.parseSelectedFields(
- objectMetadataMapItem,
+ objectMetadataItemWithFieldMaps,
selectedFields,
);
const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
queryBuilder,
- objectMetadataMapItem.nameSingular,
+ objectMetadataItemWithFieldMaps.nameSingular,
args.filter ?? ({} as Filter),
);
@@ -83,8 +87,8 @@ export class GraphqlQueryFindOneResolverService
const objectRecord = formatResult(
nonFormattedObjectRecord,
- objectMetadataMapItem,
- objectMetadataMap,
+ objectMetadataItemWithFieldMaps,
+ objectMetadataMaps,
);
if (!objectRecord) {
@@ -99,29 +103,29 @@ export class GraphqlQueryFindOneResolverService
const objectRecords = [objectRecord];
if (relations) {
- await processNestedRelationsHelper.processNestedRelations(
- objectMetadataMap,
- objectMetadataMapItem,
- objectRecords,
+ await processNestedRelationsHelper.processNestedRelations({
+ objectMetadataMaps,
+ parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
+ parentObjectRecords: objectRecords,
relations,
- QUERY_MAX_RECORDS,
+ limit: QUERY_MAX_RECORDS,
authContext,
dataSource,
- );
+ });
}
const typeORMObjectRecordsParser =
- new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
+ new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
return typeORMObjectRecordsParser.processRecord({
objectRecord: objectRecords[0],
- objectName: objectMetadataMapItem.nameSingular,
+ objectName: objectMetadataItemWithFieldMaps.nameSingular,
take: 1,
totalCount: 1,
- }) as ObjectRecord;
+ }) as T;
}
- async validate(
+ async validate(
args: FindOneResolverArgs,
_options: WorkspaceQueryRunnerOptions,
): Promise {
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts
index 4cde38cfe6b4..c9e7455a37b1 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts
@@ -5,10 +5,10 @@ import { Brackets } from 'typeorm';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
import {
- Record as IRecord,
+ ObjectRecord,
+ ObjectRecordFilter,
OrderByDirection,
- RecordFilter,
-} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
+} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { SearchResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
@@ -22,40 +22,39 @@ import { isDefined } from 'src/utils/is-defined';
@Injectable()
export class GraphqlQuerySearchResolverService
- implements ResolverService>
+ implements ResolverService>
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
async resolve<
- ObjectRecord extends IRecord = IRecord,
- Filter extends RecordFilter = RecordFilter,
+ T extends ObjectRecord = ObjectRecord,
+ Filter extends ObjectRecordFilter = ObjectRecordFilter,
>(
args: SearchResolverArgs,
options: WorkspaceQueryRunnerOptions,
- ): Promise> {
+ ): Promise> {
const {
authContext,
- objectMetadataItem,
- objectMetadataMapItem,
- objectMetadataMap,
+ objectMetadataMaps,
+ objectMetadataItemWithFieldMaps,
info,
} = options;
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
authContext.workspace.id,
- objectMetadataItem.nameSingular,
+ objectMetadataItemWithFieldMaps.nameSingular,
);
const typeORMObjectRecordsParser =
- new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
+ new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
if (!isDefined(args.searchInput)) {
return typeORMObjectRecordsParser.createConnection({
objectRecords: [],
- objectName: objectMetadataItem.nameSingular,
+ objectName: objectMetadataItemWithFieldMaps.nameSingular,
take: 0,
totalCount: 0,
order: [{ id: OrderByDirection.AscNullsFirst }],
@@ -69,16 +68,16 @@ export class GraphqlQuerySearchResolverService
const limit = args?.limit ?? QUERY_MAX_RECORDS;
const queryBuilder = repository.createQueryBuilder(
- objectMetadataItem.nameSingular,
+ objectMetadataItemWithFieldMaps.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
- objectMetadataMapItem.fields,
- objectMetadataMap,
+ objectMetadataItemWithFieldMaps.fieldsByName,
+ objectMetadataMaps,
);
const queryBuilderWithFilter = graphqlQueryParser.applyFilterToBuilder(
queryBuilder,
- objectMetadataMapItem.nameSingular,
+ objectMetadataItemWithFieldMaps.nameSingular,
args.filter ?? ({} as Filter),
);
@@ -109,7 +108,7 @@ export class GraphqlQuerySearchResolverService
.setParameter('searchTerms', searchTerms)
.setParameter('searchTermsOr', searchTermsOr)
.take(limit)
- .getMany()) as ObjectRecord[];
+ .getMany()) as T[];
const objectRecords = await repository.formatResult(resultsWithTsVector);
@@ -122,7 +121,7 @@ export class GraphqlQuerySearchResolverService
return typeORMObjectRecordsParser.createConnection({
objectRecords: objectRecords ?? [],
- objectName: objectMetadataItem.nameSingular,
+ objectName: objectMetadataItemWithFieldMaps.nameSingular,
take: limit,
totalCount,
order,
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts
index 020bf08fa722..461940be3f3f 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts
@@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
import graphqlFields from 'graphql-fields';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
-import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
+import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { UpdateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
@@ -20,18 +20,22 @@ import { computeTableName } from 'src/engine/utils/compute-table-name.util';
@Injectable()
export class GraphqlQueryUpdateManyResolverService
- implements ResolverService
+ implements ResolverService