Skip to content

Commit a799370

Browse files
ijreillycharlesBochetWeiko
authored
Aggregated queries #1 (#8345)
First step of #6868 Adds min.., max.. queries for DATETIME fields adds min.., max.., avg.., sum.. queries for NUMBER fields (count distinct operation and composite fields such as CURRENCY handling will be dealt with in a future PR) <img width="1422" alt="Capture d’écran 2024-11-06 à 15 48 46" src="https://github.com/user-attachments/assets/4bcdece0-ad3e-4536-9720-fe4044a36719"> --------- Co-authored-by: Charles Bochet <[email protected]> Co-authored-by: Weiko <[email protected]>
1 parent c966533 commit a799370

File tree

93 files changed

+1584
-1172
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

93 files changed

+1584
-1172
lines changed

.github/workflows/ci-tinybird.yaml

+11-20
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,24 @@ on:
33
push:
44
branches:
55
- main
6+
paths:
7+
- 'package.json'
8+
- 'packages/twenty-tinybird/**'
69

710
pull_request:
11+
paths:
12+
- 'package.json'
13+
- 'packages/twenty-tinybird/**'
814

915
concurrency:
1016
group: ${{ github.workflow }}-${{ github.ref }}
1117
cancel-in-progress: true
1218

1319
jobs:
1420
ci:
15-
timeout-minutes: 10
16-
runs-on: ubuntu-latest
1721
uses: tinybirdco/ci/.github/workflows/ci.yml@main
18-
steps:
19-
- name: Check for changed files
20-
id: changed-files
21-
uses: tj-actions/changed-files@v11
22-
with:
23-
files: |
24-
package.json
25-
packages/twenty-tinybird/**
26-
27-
- name: Skip if no relevant changes
28-
if: steps.changed-files.outputs.any_changed == 'false'
29-
run: echo "No relevant changes. Skipping CI."
30-
31-
- name: Check twenty-tinybird package
32-
with:
33-
data_project_dir: packages/twenty-tinybird
34-
tb_admin_token: ${{ secrets.TB_ADMIN_TOKEN }}
35-
tb_host: https://api.eu-central-1.aws.tinybird.co
22+
with:
23+
data_project_dir: packages/twenty-tinybird
24+
secrets:
25+
tb_admin_token: ${{ secrets.TB_ADMIN_TOKEN }}
26+
tb_host: https://api.eu-central-1.aws.tinybird.co

packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx

+3-15
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNaviga
1313
import { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState';
1414
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
1515
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
16-
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
1716
import styled from '@emotion/styled';
1817

1918
const StyledMainSection = styled(NavigationDrawerSection)`
@@ -27,9 +26,7 @@ export const MainNavigationDrawerItems = () => {
2726
const setNavigationMemorizedUrl = useSetRecoilState(
2827
navigationMemorizedUrlState,
2928
);
30-
const isWorkspaceFavoriteEnabled = useIsFeatureEnabled(
31-
'IS_WORKSPACE_FAVORITE_ENABLED',
32-
);
29+
3330
const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] =
3431
useRecoilState(isNavigationDrawerExpandedState);
3532
const setNavigationDrawerExpandedMemorized = useSetRecoilState(
@@ -58,18 +55,9 @@ export const MainNavigationDrawerItems = () => {
5855
/>
5956
</StyledMainSection>
6057
)}
61-
62-
{isWorkspaceFavoriteEnabled && <NavigationDrawerOpenedSection />}
63-
58+
<NavigationDrawerOpenedSection />
6459
<CurrentWorkspaceMemberFavorites />
65-
66-
{isWorkspaceFavoriteEnabled ? (
67-
<WorkspaceFavorites />
68-
) : (
69-
<NavigationDrawerSectionForObjectMetadataItemsWrapper
70-
isRemote={false}
71-
/>
72-
)}
60+
<WorkspaceFavorites />
7361
<NavigationDrawerSectionForObjectMetadataItemsWrapper isRemote={true} />
7462
</>
7563
);

packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,11 @@ export type FeatureFlagKey =
99
| 'IS_FREE_ACCESS_ENABLED'
1010
| 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED'
1111
| 'IS_WORKFLOW_ENABLED'
12-
| 'IS_WORKSPACE_FAVORITE_ENABLED'
13-
| 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED'
1412
| 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED'
1513
| 'IS_ANALYTICS_V2_ENABLED'
1614
| 'IS_SSO_ENABLED'
1715
| 'IS_UNIQUE_INDEXES_ENABLED'
1816
| 'IS_ARRAY_AND_JSON_FILTER_ENABLED'
1917
| 'IS_MICROSOFT_SYNC_ENABLED'
20-
| 'IS_ADVANCED_FILTERS_ENABLED';
18+
| 'IS_ADVANCED_FILTERS_ENABLED'
19+
| 'IS_AGGREGATE_QUERY_ENABLED';

packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,6 @@ export const seedFeatureFlags = async (
5050
workspaceId: workspaceId,
5151
value: false,
5252
},
53-
{
54-
key: FeatureFlagKey.IsWorkspaceFavoriteEnabled,
55-
workspaceId: workspaceId,
56-
value: true,
57-
},
5853
{
5954
key: FeatureFlagKey.IsAnalyticsV2Enabled,
6055
workspaceId: workspaceId,
@@ -85,6 +80,11 @@ export const seedFeatureFlags = async (
8580
workspaceId: workspaceId,
8681
value: false,
8782
},
83+
{
84+
key: FeatureFlagKey.IsAggregateQueryEnabled,
85+
workspaceId: workspaceId,
86+
value: false,
87+
},
8888
])
8989
.execute();
9090
};

packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts

+9-9
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,27 @@ import {
55
WhereExpressionBuilder,
66
} from 'typeorm';
77

8-
import { RecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
8+
import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
99

10-
import { FieldMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
10+
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
1111

1212
import { GraphqlQueryFilterFieldParser } from './graphql-query-filter-field.parser';
1313

1414
export class GraphqlQueryFilterConditionParser {
15-
private fieldMetadataMap: FieldMetadataMap;
15+
private fieldMetadataMapByName: FieldMetadataMap;
1616
private queryFilterFieldParser: GraphqlQueryFilterFieldParser;
1717

18-
constructor(fieldMetadataMap: FieldMetadataMap) {
19-
this.fieldMetadataMap = fieldMetadataMap;
18+
constructor(fieldMetadataMapByName: FieldMetadataMap) {
19+
this.fieldMetadataMapByName = fieldMetadataMapByName;
2020
this.queryFilterFieldParser = new GraphqlQueryFilterFieldParser(
21-
this.fieldMetadataMap,
21+
this.fieldMetadataMapByName,
2222
);
2323
}
2424

2525
public parse(
2626
queryBuilder: SelectQueryBuilder<any>,
2727
objectNameSingular: string,
28-
filter: Partial<RecordFilter>,
28+
filter: Partial<ObjectRecordFilter>,
2929
): SelectQueryBuilder<any> {
3030
if (!filter || Object.keys(filter).length === 0) {
3131
return queryBuilder;
@@ -50,7 +50,7 @@ export class GraphqlQueryFilterConditionParser {
5050
switch (key) {
5151
case 'and': {
5252
const andWhereCondition = new Brackets((qb) => {
53-
value.forEach((filter: RecordFilter, index: number) => {
53+
value.forEach((filter: ObjectRecordFilter, index: number) => {
5454
const whereCondition = new Brackets((qb2) => {
5555
Object.entries(filter).forEach(
5656
([subFilterkey, subFilterValue], index) => {
@@ -82,7 +82,7 @@ export class GraphqlQueryFilterConditionParser {
8282
}
8383
case 'or': {
8484
const orWhereCondition = new Brackets((qb) => {
85-
value.forEach((filter: RecordFilter, index: number) => {
85+
value.forEach((filter: ObjectRecordFilter, index: number) => {
8686
const whereCondition = new Brackets((qb2) => {
8787
Object.entries(filter).forEach(
8888
([subFilterkey, subFilterValue], index) => {

packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@ import {
99
import { computeWhereConditionParts } from 'src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts';
1010
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
1111
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
12-
import { FieldMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
12+
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
1313
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
1414
import { capitalize } from 'src/utils/capitalize';
1515

1616
const ARRAY_OPERATORS = ['in', 'contains', 'not_contains'];
1717

1818
export class GraphqlQueryFilterFieldParser {
19-
private fieldMetadataMap: FieldMetadataMap;
19+
private fieldMetadataMapByName: FieldMetadataMap;
2020

21-
constructor(fieldMetadataMap: FieldMetadataMap) {
22-
this.fieldMetadataMap = fieldMetadataMap;
21+
constructor(fieldMetadataMapByName: FieldMetadataMap) {
22+
this.fieldMetadataMapByName = fieldMetadataMapByName;
2323
}
2424

2525
public parse(
@@ -29,7 +29,7 @@ export class GraphqlQueryFilterFieldParser {
2929
filterValue: any,
3030
isFirst = false,
3131
): void {
32-
const fieldMetadata = this.fieldMetadataMap[`${key}`];
32+
const fieldMetadata = this.fieldMetadataMapByName[`${key}`];
3333

3434
if (!fieldMetadata) {
3535
throw new Error(`Field metadata not found for field: ${key}`);

packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
2+
ObjectRecordOrderBy,
23
OrderByDirection,
3-
RecordOrderBy,
4-
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
4+
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
55
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
66

77
import {
@@ -10,25 +10,25 @@ import {
1010
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
1111
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
1212
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
13-
import { FieldMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
13+
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
1414
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
1515
import { capitalize } from 'src/utils/capitalize';
1616
export class GraphqlQueryOrderFieldParser {
17-
private fieldMetadataMap: FieldMetadataMap;
17+
private fieldMetadataMapByName: FieldMetadataMap;
1818

19-
constructor(fieldMetadataMap: FieldMetadataMap) {
20-
this.fieldMetadataMap = fieldMetadataMap;
19+
constructor(fieldMetadataMapByName: FieldMetadataMap) {
20+
this.fieldMetadataMapByName = fieldMetadataMapByName;
2121
}
2222

2323
parse(
24-
orderBy: RecordOrderBy,
24+
orderBy: ObjectRecordOrderBy,
2525
objectNameSingular: string,
2626
isForwardPagination = true,
2727
): Record<string, string> {
2828
return orderBy.reduce(
2929
(acc, item) => {
3030
Object.entries(item).forEach(([key, value]) => {
31-
const fieldMetadata = this.fieldMetadataMap[key];
31+
const fieldMetadata = this.fieldMetadataMapByName[key];
3232

3333
if (!fieldMetadata || value === undefined) {
3434
throw new GraphqlQueryRunnerException(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
2+
3+
import { GraphqlQuerySelectedFieldsResult } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
4+
import {
5+
AggregationField,
6+
getAvailableAggregationsFromObjectFields,
7+
} from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util';
8+
9+
export class GraphqlQuerySelectedFieldsAggregateParser {
10+
parse(
11+
graphqlSelectedFields: Partial<Record<string, any>>,
12+
fieldMetadataMapByName: Record<string, FieldMetadataInterface>,
13+
accumulator: GraphqlQuerySelectedFieldsResult,
14+
): void {
15+
const availableAggregations: Record<string, AggregationField> =
16+
getAvailableAggregationsFromObjectFields(
17+
Object.values(fieldMetadataMapByName),
18+
);
19+
20+
for (const selectedField of Object.keys(graphqlSelectedFields)) {
21+
const selectedAggregation = availableAggregations[selectedField];
22+
23+
if (!selectedAggregation) {
24+
continue;
25+
}
26+
27+
accumulator.aggregate[selectedField] = selectedAggregation;
28+
}
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,47 @@
11
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
22

3-
import { GraphqlQuerySelectedFieldsParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
3+
import {
4+
GraphqlQuerySelectedFieldsParser,
5+
GraphqlQuerySelectedFieldsResult,
6+
} from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
47
import { getRelationObjectMetadata } from 'src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util';
5-
import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
8+
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
69

710
export class GraphqlQuerySelectedFieldsRelationParser {
8-
private objectMetadataMap: ObjectMetadataMap;
11+
private objectMetadataMaps: ObjectMetadataMaps;
912

10-
constructor(objectMetadataMap: ObjectMetadataMap) {
11-
this.objectMetadataMap = objectMetadataMap;
13+
constructor(objectMetadataMaps: ObjectMetadataMaps) {
14+
this.objectMetadataMaps = objectMetadataMaps;
1215
}
1316

1417
parseRelationField(
1518
fieldMetadata: FieldMetadataInterface,
1619
fieldKey: string,
1720
fieldValue: any,
18-
result: { select: Record<string, any>; relations: Record<string, any> },
21+
accumulator: GraphqlQuerySelectedFieldsResult,
1922
): void {
2023
if (!fieldValue || typeof fieldValue !== 'object') {
2124
return;
2225
}
2326

24-
result.relations[fieldKey] = true;
27+
accumulator.relations[fieldKey] = true;
2528

2629
const referencedObjectMetadata = getRelationObjectMetadata(
2730
fieldMetadata,
28-
this.objectMetadataMap,
31+
this.objectMetadataMaps,
2932
);
3033

31-
const relationFields = referencedObjectMetadata.fields;
34+
const relationFields = referencedObjectMetadata.fieldsByName;
3235
const fieldParser = new GraphqlQuerySelectedFieldsParser(
33-
this.objectMetadataMap,
36+
this.objectMetadataMaps,
3437
);
35-
const subResult = fieldParser.parse(fieldValue, relationFields);
38+
const relationAccumulator = fieldParser.parse(fieldValue, relationFields);
3639

37-
result.select[fieldKey] = {
40+
accumulator.select[fieldKey] = {
3841
id: true,
39-
...subResult.select,
42+
...relationAccumulator.select,
4043
};
41-
result.relations[fieldKey] = subResult.relations;
44+
accumulator.relations[fieldKey] = relationAccumulator.relations;
45+
accumulator.aggregate[fieldKey] = relationAccumulator.aggregate;
4246
}
4347
}

0 commit comments

Comments
 (0)