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