mirror of
				https://github.com/lingble/twenty.git
				synced 2025-10-31 04:37:56 +00:00 
			
		
		
		
	First step of https://github.com/twentyhq/twenty/issues/6868 Adds min.., max.. queries for DATETIME fields adds min.., max.., avg.., sum.. queries for NUMBER fields (count distinct operation and composite fields such as CURRENCY handling will be dealt with in a future PR) <img width="1422" alt="Capture d’écran 2024-11-06 à 15 48 46" src="https://github.com/user-attachments/assets/4bcdece0-ad3e-4536-9720-fe4044a36719"> --------- Co-authored-by: Charles Bochet <charles@twenty.com> Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
		
							
								
								
									
										23
									
								
								.github/workflows/ci-tinybird.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										23
									
								
								.github/workflows/ci-tinybird.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -3,8 +3,14 @@ on: | |||||||
|   push: |   push: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  |     paths: | ||||||
|  |       - 'package.json' | ||||||
|  |       - 'packages/twenty-tinybird/**' | ||||||
|  |  | ||||||
|   pull_request: |   pull_request: | ||||||
|  |     paths: | ||||||
|  |       - 'package.json' | ||||||
|  |       - 'packages/twenty-tinybird/**' | ||||||
|  |  | ||||||
| concurrency: | concurrency: | ||||||
|   group: ${{ github.workflow }}-${{ github.ref }} |   group: ${{ github.workflow }}-${{ github.ref }} | ||||||
| @@ -12,24 +18,9 @@ concurrency: | |||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   ci: |   ci: | ||||||
|     timeout-minutes: 10 |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     uses: tinybirdco/ci/.github/workflows/ci.yml@main |     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: |     with: | ||||||
|       data_project_dir: packages/twenty-tinybird |       data_project_dir: packages/twenty-tinybird | ||||||
|  |     secrets: | ||||||
|       tb_admin_token: ${{ secrets.TB_ADMIN_TOKEN }} |       tb_admin_token: ${{ secrets.TB_ADMIN_TOKEN }} | ||||||
|       tb_host: https://api.eu-central-1.aws.tinybird.co |       tb_host: https://api.eu-central-1.aws.tinybird.co | ||||||
|   | |||||||
| @@ -13,7 +13,6 @@ import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNaviga | |||||||
| import { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState'; | import { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState'; | ||||||
| import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; | import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; | ||||||
| import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; | import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; | ||||||
| import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; |  | ||||||
| import styled from '@emotion/styled'; | import styled from '@emotion/styled'; | ||||||
|  |  | ||||||
| const StyledMainSection = styled(NavigationDrawerSection)` | const StyledMainSection = styled(NavigationDrawerSection)` | ||||||
| @@ -27,9 +26,7 @@ export const MainNavigationDrawerItems = () => { | |||||||
|   const setNavigationMemorizedUrl = useSetRecoilState( |   const setNavigationMemorizedUrl = useSetRecoilState( | ||||||
|     navigationMemorizedUrlState, |     navigationMemorizedUrlState, | ||||||
|   ); |   ); | ||||||
|   const isWorkspaceFavoriteEnabled = useIsFeatureEnabled( |  | ||||||
|     'IS_WORKSPACE_FAVORITE_ENABLED', |  | ||||||
|   ); |  | ||||||
|   const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] = |   const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] = | ||||||
|     useRecoilState(isNavigationDrawerExpandedState); |     useRecoilState(isNavigationDrawerExpandedState); | ||||||
|   const setNavigationDrawerExpandedMemorized = useSetRecoilState( |   const setNavigationDrawerExpandedMemorized = useSetRecoilState( | ||||||
| @@ -58,18 +55,9 @@ export const MainNavigationDrawerItems = () => { | |||||||
|           /> |           /> | ||||||
|         </StyledMainSection> |         </StyledMainSection> | ||||||
|       )} |       )} | ||||||
|  |       <NavigationDrawerOpenedSection /> | ||||||
|       {isWorkspaceFavoriteEnabled && <NavigationDrawerOpenedSection />} |  | ||||||
|  |  | ||||||
|       <CurrentWorkspaceMemberFavorites /> |       <CurrentWorkspaceMemberFavorites /> | ||||||
|  |  | ||||||
|       {isWorkspaceFavoriteEnabled ? ( |  | ||||||
|       <WorkspaceFavorites /> |       <WorkspaceFavorites /> | ||||||
|       ) : ( |  | ||||||
|         <NavigationDrawerSectionForObjectMetadataItemsWrapper |  | ||||||
|           isRemote={false} |  | ||||||
|         /> |  | ||||||
|       )} |  | ||||||
|       <NavigationDrawerSectionForObjectMetadataItemsWrapper isRemote={true} /> |       <NavigationDrawerSectionForObjectMetadataItemsWrapper isRemote={true} /> | ||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
|   | |||||||
| @@ -9,12 +9,11 @@ export type FeatureFlagKey = | |||||||
|   | 'IS_FREE_ACCESS_ENABLED' |   | 'IS_FREE_ACCESS_ENABLED' | ||||||
|   | 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED' |   | 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED' | ||||||
|   | 'IS_WORKFLOW_ENABLED' |   | 'IS_WORKFLOW_ENABLED' | ||||||
|   | 'IS_WORKSPACE_FAVORITE_ENABLED' |  | ||||||
|   | 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED' |  | ||||||
|   | 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED' |   | 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED' | ||||||
|   | 'IS_ANALYTICS_V2_ENABLED' |   | 'IS_ANALYTICS_V2_ENABLED' | ||||||
|   | 'IS_SSO_ENABLED' |   | 'IS_SSO_ENABLED' | ||||||
|   | 'IS_UNIQUE_INDEXES_ENABLED' |   | 'IS_UNIQUE_INDEXES_ENABLED' | ||||||
|   | 'IS_ARRAY_AND_JSON_FILTER_ENABLED' |   | 'IS_ARRAY_AND_JSON_FILTER_ENABLED' | ||||||
|   | 'IS_MICROSOFT_SYNC_ENABLED' |   | 'IS_MICROSOFT_SYNC_ENABLED' | ||||||
|   | 'IS_ADVANCED_FILTERS_ENABLED'; |   | 'IS_ADVANCED_FILTERS_ENABLED' | ||||||
|  |   | 'IS_AGGREGATE_QUERY_ENABLED'; | ||||||
|   | |||||||
| @@ -50,11 +50,6 @@ export const seedFeatureFlags = async ( | |||||||
|         workspaceId: workspaceId, |         workspaceId: workspaceId, | ||||||
|         value: false, |         value: false, | ||||||
|       }, |       }, | ||||||
|       { |  | ||||||
|         key: FeatureFlagKey.IsWorkspaceFavoriteEnabled, |  | ||||||
|         workspaceId: workspaceId, |  | ||||||
|         value: true, |  | ||||||
|       }, |  | ||||||
|       { |       { | ||||||
|         key: FeatureFlagKey.IsAnalyticsV2Enabled, |         key: FeatureFlagKey.IsAnalyticsV2Enabled, | ||||||
|         workspaceId: workspaceId, |         workspaceId: workspaceId, | ||||||
| @@ -85,6 +80,11 @@ export const seedFeatureFlags = async ( | |||||||
|         workspaceId: workspaceId, |         workspaceId: workspaceId, | ||||||
|         value: false, |         value: false, | ||||||
|       }, |       }, | ||||||
|  |       { | ||||||
|  |         key: FeatureFlagKey.IsAggregateQueryEnabled, | ||||||
|  |         workspaceId: workspaceId, | ||||||
|  |         value: false, | ||||||
|  |       }, | ||||||
|     ]) |     ]) | ||||||
|     .execute(); |     .execute(); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -5,27 +5,27 @@ import { | |||||||
|   WhereExpressionBuilder, |   WhereExpressionBuilder, | ||||||
| } from 'typeorm'; | } 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'; | import { GraphqlQueryFilterFieldParser } from './graphql-query-filter-field.parser'; | ||||||
|  |  | ||||||
| export class GraphqlQueryFilterConditionParser { | export class GraphqlQueryFilterConditionParser { | ||||||
|   private fieldMetadataMap: FieldMetadataMap; |   private fieldMetadataMapByName: FieldMetadataMap; | ||||||
|   private queryFilterFieldParser: GraphqlQueryFilterFieldParser; |   private queryFilterFieldParser: GraphqlQueryFilterFieldParser; | ||||||
|  |  | ||||||
|   constructor(fieldMetadataMap: FieldMetadataMap) { |   constructor(fieldMetadataMapByName: FieldMetadataMap) { | ||||||
|     this.fieldMetadataMap = fieldMetadataMap; |     this.fieldMetadataMapByName = fieldMetadataMapByName; | ||||||
|     this.queryFilterFieldParser = new GraphqlQueryFilterFieldParser( |     this.queryFilterFieldParser = new GraphqlQueryFilterFieldParser( | ||||||
|       this.fieldMetadataMap, |       this.fieldMetadataMapByName, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public parse( |   public parse( | ||||||
|     queryBuilder: SelectQueryBuilder<any>, |     queryBuilder: SelectQueryBuilder<any>, | ||||||
|     objectNameSingular: string, |     objectNameSingular: string, | ||||||
|     filter: Partial<RecordFilter>, |     filter: Partial<ObjectRecordFilter>, | ||||||
|   ): SelectQueryBuilder<any> { |   ): SelectQueryBuilder<any> { | ||||||
|     if (!filter || Object.keys(filter).length === 0) { |     if (!filter || Object.keys(filter).length === 0) { | ||||||
|       return queryBuilder; |       return queryBuilder; | ||||||
| @@ -50,7 +50,7 @@ export class GraphqlQueryFilterConditionParser { | |||||||
|     switch (key) { |     switch (key) { | ||||||
|       case 'and': { |       case 'and': { | ||||||
|         const andWhereCondition = new Brackets((qb) => { |         const andWhereCondition = new Brackets((qb) => { | ||||||
|           value.forEach((filter: RecordFilter, index: number) => { |           value.forEach((filter: ObjectRecordFilter, index: number) => { | ||||||
|             const whereCondition = new Brackets((qb2) => { |             const whereCondition = new Brackets((qb2) => { | ||||||
|               Object.entries(filter).forEach( |               Object.entries(filter).forEach( | ||||||
|                 ([subFilterkey, subFilterValue], index) => { |                 ([subFilterkey, subFilterValue], index) => { | ||||||
| @@ -82,7 +82,7 @@ export class GraphqlQueryFilterConditionParser { | |||||||
|       } |       } | ||||||
|       case 'or': { |       case 'or': { | ||||||
|         const orWhereCondition = new Brackets((qb) => { |         const orWhereCondition = new Brackets((qb) => { | ||||||
|           value.forEach((filter: RecordFilter, index: number) => { |           value.forEach((filter: ObjectRecordFilter, index: number) => { | ||||||
|             const whereCondition = new Brackets((qb2) => { |             const whereCondition = new Brackets((qb2) => { | ||||||
|               Object.entries(filter).forEach( |               Object.entries(filter).forEach( | ||||||
|                 ([subFilterkey, subFilterValue], index) => { |                 ([subFilterkey, subFilterValue], index) => { | ||||||
|   | |||||||
| @@ -9,17 +9,17 @@ import { | |||||||
| import { computeWhereConditionParts } from 'src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts'; | 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 { 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 { 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 { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; | ||||||
| import { capitalize } from 'src/utils/capitalize'; | import { capitalize } from 'src/utils/capitalize'; | ||||||
|  |  | ||||||
| const ARRAY_OPERATORS = ['in', 'contains', 'not_contains']; | const ARRAY_OPERATORS = ['in', 'contains', 'not_contains']; | ||||||
|  |  | ||||||
| export class GraphqlQueryFilterFieldParser { | export class GraphqlQueryFilterFieldParser { | ||||||
|   private fieldMetadataMap: FieldMetadataMap; |   private fieldMetadataMapByName: FieldMetadataMap; | ||||||
|  |  | ||||||
|   constructor(fieldMetadataMap: FieldMetadataMap) { |   constructor(fieldMetadataMapByName: FieldMetadataMap) { | ||||||
|     this.fieldMetadataMap = fieldMetadataMap; |     this.fieldMetadataMapByName = fieldMetadataMapByName; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public parse( |   public parse( | ||||||
| @@ -29,7 +29,7 @@ export class GraphqlQueryFilterFieldParser { | |||||||
|     filterValue: any, |     filterValue: any, | ||||||
|     isFirst = false, |     isFirst = false, | ||||||
|   ): void { |   ): void { | ||||||
|     const fieldMetadata = this.fieldMetadataMap[`${key}`]; |     const fieldMetadata = this.fieldMetadataMapByName[`${key}`]; | ||||||
|  |  | ||||||
|     if (!fieldMetadata) { |     if (!fieldMetadata) { | ||||||
|       throw new Error(`Field metadata not found for field: ${key}`); |       throw new Error(`Field metadata not found for field: ${key}`); | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import { | import { | ||||||
|  |   ObjectRecordOrderBy, | ||||||
|   OrderByDirection, |   OrderByDirection, | ||||||
|   RecordOrderBy, | } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; | ||||||
| } 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 { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; | ||||||
|  |  | ||||||
| import { | import { | ||||||
| @@ -10,25 +10,25 @@ import { | |||||||
| } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; | } 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 { 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 { 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 { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; | ||||||
| import { capitalize } from 'src/utils/capitalize'; | import { capitalize } from 'src/utils/capitalize'; | ||||||
| export class GraphqlQueryOrderFieldParser { | export class GraphqlQueryOrderFieldParser { | ||||||
|   private fieldMetadataMap: FieldMetadataMap; |   private fieldMetadataMapByName: FieldMetadataMap; | ||||||
|  |  | ||||||
|   constructor(fieldMetadataMap: FieldMetadataMap) { |   constructor(fieldMetadataMapByName: FieldMetadataMap) { | ||||||
|     this.fieldMetadataMap = fieldMetadataMap; |     this.fieldMetadataMapByName = fieldMetadataMapByName; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   parse( |   parse( | ||||||
|     orderBy: RecordOrderBy, |     orderBy: ObjectRecordOrderBy, | ||||||
|     objectNameSingular: string, |     objectNameSingular: string, | ||||||
|     isForwardPagination = true, |     isForwardPagination = true, | ||||||
|   ): Record<string, string> { |   ): Record<string, string> { | ||||||
|     return orderBy.reduce( |     return orderBy.reduce( | ||||||
|       (acc, item) => { |       (acc, item) => { | ||||||
|         Object.entries(item).forEach(([key, value]) => { |         Object.entries(item).forEach(([key, value]) => { | ||||||
|           const fieldMetadata = this.fieldMetadataMap[key]; |           const fieldMetadata = this.fieldMetadataMapByName[key]; | ||||||
|  |  | ||||||
|           if (!fieldMetadata || value === undefined) { |           if (!fieldMetadata || value === undefined) { | ||||||
|             throw new GraphqlQueryRunnerException( |             throw new GraphqlQueryRunnerException( | ||||||
|   | |||||||
| @@ -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<Record<string, any>>, | ||||||
|  |     fieldMetadataMapByName: Record<string, FieldMetadataInterface>, | ||||||
|  |     accumulator: GraphqlQuerySelectedFieldsResult, | ||||||
|  |   ): void { | ||||||
|  |     const availableAggregations: Record<string, AggregationField> = | ||||||
|  |       getAvailableAggregationsFromObjectFields( | ||||||
|  |         Object.values(fieldMetadataMapByName), | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |     for (const selectedField of Object.keys(graphqlSelectedFields)) { | ||||||
|  |       const selectedAggregation = availableAggregations[selectedField]; | ||||||
|  |  | ||||||
|  |       if (!selectedAggregation) { | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       accumulator.aggregate[selectedField] = selectedAggregation; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,43 +1,47 @@ | |||||||
| import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; | 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 { 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 { | export class GraphqlQuerySelectedFieldsRelationParser { | ||||||
|   private objectMetadataMap: ObjectMetadataMap; |   private objectMetadataMaps: ObjectMetadataMaps; | ||||||
|  |  | ||||||
|   constructor(objectMetadataMap: ObjectMetadataMap) { |   constructor(objectMetadataMaps: ObjectMetadataMaps) { | ||||||
|     this.objectMetadataMap = objectMetadataMap; |     this.objectMetadataMaps = objectMetadataMaps; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   parseRelationField( |   parseRelationField( | ||||||
|     fieldMetadata: FieldMetadataInterface, |     fieldMetadata: FieldMetadataInterface, | ||||||
|     fieldKey: string, |     fieldKey: string, | ||||||
|     fieldValue: any, |     fieldValue: any, | ||||||
|     result: { select: Record<string, any>; relations: Record<string, any> }, |     accumulator: GraphqlQuerySelectedFieldsResult, | ||||||
|   ): void { |   ): void { | ||||||
|     if (!fieldValue || typeof fieldValue !== 'object') { |     if (!fieldValue || typeof fieldValue !== 'object') { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     result.relations[fieldKey] = true; |     accumulator.relations[fieldKey] = true; | ||||||
|  |  | ||||||
|     const referencedObjectMetadata = getRelationObjectMetadata( |     const referencedObjectMetadata = getRelationObjectMetadata( | ||||||
|       fieldMetadata, |       fieldMetadata, | ||||||
|       this.objectMetadataMap, |       this.objectMetadataMaps, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const relationFields = referencedObjectMetadata.fields; |     const relationFields = referencedObjectMetadata.fieldsByName; | ||||||
|     const fieldParser = new GraphqlQuerySelectedFieldsParser( |     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, |       id: true, | ||||||
|       ...subResult.select, |       ...relationAccumulator.select, | ||||||
|     }; |     }; | ||||||
|     result.relations[fieldKey] = subResult.relations; |     accumulator.relations[fieldKey] = relationAccumulator.relations; | ||||||
|  |     accumulator.aggregate[fieldKey] = relationAccumulator.aggregate; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,59 +1,71 @@ | |||||||
| import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; | import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; | ||||||
|  |  | ||||||
| import { | import { GraphqlQuerySelectedFieldsAggregateParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-aggregate.parser'; | ||||||
|   GraphqlQueryRunnerException, |  | ||||||
|   GraphqlQueryRunnerExceptionCode, |  | ||||||
| } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; |  | ||||||
| import { GraphqlQuerySelectedFieldsRelationParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.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 { 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 { 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 { 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 { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; | ||||||
| import { capitalize } from 'src/utils/capitalize'; | import { capitalize } from 'src/utils/capitalize'; | ||||||
| import { isPlainObject } from 'src/utils/is-plain-object'; |  | ||||||
|  | export type GraphqlQuerySelectedFieldsResult = { | ||||||
|  |   select: Record<string, any>; | ||||||
|  |   relations: Record<string, any>; | ||||||
|  |   aggregate: Record<string, any>; | ||||||
|  | }; | ||||||
|  |  | ||||||
| export class GraphqlQuerySelectedFieldsParser { | export class GraphqlQuerySelectedFieldsParser { | ||||||
|   private graphqlQuerySelectedFieldsRelationParser: GraphqlQuerySelectedFieldsRelationParser; |   private graphqlQuerySelectedFieldsRelationParser: GraphqlQuerySelectedFieldsRelationParser; | ||||||
|  |   private aggregateParser: GraphqlQuerySelectedFieldsAggregateParser; | ||||||
|  |  | ||||||
|   constructor(objectMetadataMap: ObjectMetadataMap) { |   constructor(objectMetadataMaps: ObjectMetadataMaps) { | ||||||
|     this.graphqlQuerySelectedFieldsRelationParser = |     this.graphqlQuerySelectedFieldsRelationParser = | ||||||
|       new GraphqlQuerySelectedFieldsRelationParser(objectMetadataMap); |       new GraphqlQuerySelectedFieldsRelationParser(objectMetadataMaps); | ||||||
|  |     this.aggregateParser = new GraphqlQuerySelectedFieldsAggregateParser(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   parse( |   parse( | ||||||
|     graphqlSelectedFields: Partial<Record<string, any>>, |     graphqlSelectedFields: Partial<Record<string, any>>, | ||||||
|     fieldMetadataMap: Record<string, FieldMetadataInterface>, |     fieldMetadataMapByName: Record<string, FieldMetadataInterface>, | ||||||
|   ): { select: Record<string, any>; relations: Record<string, any> } { |   ): GraphqlQuerySelectedFieldsResult { | ||||||
|     const result: { |     const accumulator: GraphqlQuerySelectedFieldsResult = { | ||||||
|       select: Record<string, any>; |  | ||||||
|       relations: Record<string, any>; |  | ||||||
|     } = { |  | ||||||
|       select: {}, |       select: {}, | ||||||
|       relations: {}, |       relations: {}, | ||||||
|  |       aggregate: {}, | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|  |     if (this.isRootConnection(graphqlSelectedFields)) { | ||||||
|  |       this.parseConnectionField( | ||||||
|  |         graphqlSelectedFields, | ||||||
|  |         fieldMetadataMapByName, | ||||||
|  |         accumulator, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       return accumulator; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this.parseRecordField( | ||||||
|  |       graphqlSelectedFields, | ||||||
|  |       fieldMetadataMapByName, | ||||||
|  |       accumulator, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     return accumulator; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private parseRecordField( | ||||||
|  |     graphqlSelectedFields: Partial<Record<string, any>>, | ||||||
|  |     fieldMetadataMapByName: Record<string, FieldMetadataInterface>, | ||||||
|  |     accumulator: GraphqlQuerySelectedFieldsResult, | ||||||
|  |   ): void { | ||||||
|     for (const [fieldKey, fieldValue] of Object.entries( |     for (const [fieldKey, fieldValue] of Object.entries( | ||||||
|       graphqlSelectedFields, |       graphqlSelectedFields, | ||||||
|     )) { |     )) { | ||||||
|       if (this.shouldNotParseField(fieldKey)) { |       const fieldMetadata = fieldMetadataMapByName[fieldKey]; | ||||||
|         continue; |  | ||||||
|       } |  | ||||||
|       if (this.isConnectionField(fieldKey, fieldValue)) { |  | ||||||
|         const subResult = this.parse(fieldValue, fieldMetadataMap); |  | ||||||
|  |  | ||||||
|         Object.assign(result.select, subResult.select); |  | ||||||
|         Object.assign(result.relations, subResult.relations); |  | ||||||
|         continue; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       const fieldMetadata = fieldMetadataMap[fieldKey]; |  | ||||||
|  |  | ||||||
|       if (!fieldMetadata) { |       if (!fieldMetadata) { | ||||||
|         throw new GraphqlQueryRunnerException( |         continue; | ||||||
|           `Field "${fieldKey}" does not exist or is not selectable`, |  | ||||||
|           GraphqlQueryRunnerExceptionCode.FIELD_NOT_FOUND, |  | ||||||
|         ); |  | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       if (isRelationFieldMetadataType(fieldMetadata.type)) { |       if (isRelationFieldMetadataType(fieldMetadata.type)) { | ||||||
| @@ -61,7 +73,7 @@ export class GraphqlQuerySelectedFieldsParser { | |||||||
|           fieldMetadata, |           fieldMetadata, | ||||||
|           fieldKey, |           fieldKey, | ||||||
|           fieldValue, |           fieldValue, | ||||||
|           result, |           accumulator, | ||||||
|         ); |         ); | ||||||
|       } else if (isCompositeFieldMetadataType(fieldMetadata.type)) { |       } else if (isCompositeFieldMetadataType(fieldMetadata.type)) { | ||||||
|         const compositeResult = this.parseCompositeField( |         const compositeResult = this.parseCompositeField( | ||||||
| @@ -69,23 +81,33 @@ export class GraphqlQuerySelectedFieldsParser { | |||||||
|           fieldValue, |           fieldValue, | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         Object.assign(result.select, compositeResult); |         Object.assign(accumulator.select, compositeResult); | ||||||
|       } else { |       } else { | ||||||
|         result.select[fieldKey] = true; |         accumulator.select[fieldKey] = true; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|     return result; |   private parseConnectionField( | ||||||
|   } |     graphqlSelectedFields: Partial<Record<string, any>>, | ||||||
|  |     fieldMetadataMapByName: Record<string, FieldMetadataInterface>, | ||||||
|   private isConnectionField(fieldKey: string, fieldValue: any): boolean { |     accumulator: GraphqlQuerySelectedFieldsResult, | ||||||
|     return ['edges', 'node'].includes(fieldKey) && isPlainObject(fieldValue); |   ): void { | ||||||
|   } |     this.aggregateParser.parse( | ||||||
|  |       graphqlSelectedFields, | ||||||
|   private shouldNotParseField(fieldKey: string): boolean { |       fieldMetadataMapByName, | ||||||
|     return ['__typename', 'totalCount', 'pageInfo', 'cursor'].includes( |       accumulator, | ||||||
|       fieldKey, |  | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|  |     const node = graphqlSelectedFields.edges.node; | ||||||
|  |  | ||||||
|  |     this.parseRecordField(node, fieldMetadataMapByName, accumulator); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private isRootConnection( | ||||||
|  |     graphqlSelectedFields: Partial<Record<string, any>>, | ||||||
|  |   ): boolean { | ||||||
|  |     return Object.keys(graphqlSelectedFields).includes('edges'); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private parseCompositeField( |   private parseCompositeField( | ||||||
|   | |||||||
| @@ -6,43 +6,44 @@ import { | |||||||
| } from 'typeorm'; | } from 'typeorm'; | ||||||
|  |  | ||||||
| import { | import { | ||||||
|   RecordFilter, |   ObjectRecordFilter, | ||||||
|   RecordOrderBy, |   ObjectRecordOrderBy, | ||||||
| } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; | } 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 { 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 { 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 { | import { | ||||||
|   FieldMetadataMap, |   GraphqlQuerySelectedFieldsParser, | ||||||
|   ObjectMetadataMap, |   GraphqlQuerySelectedFieldsResult, | ||||||
|   ObjectMetadataMapItem, | } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser'; | ||||||
| } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; | 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 { | export class GraphqlQueryParser { | ||||||
|   private fieldMetadataMap: FieldMetadataMap; |   private fieldMetadataMapByName: FieldMetadataMap; | ||||||
|   private objectMetadataMap: ObjectMetadataMap; |   private objectMetadataMaps: ObjectMetadataMaps; | ||||||
|   private filterConditionParser: GraphqlQueryFilterConditionParser; |   private filterConditionParser: GraphqlQueryFilterConditionParser; | ||||||
|   private orderFieldParser: GraphqlQueryOrderFieldParser; |   private orderFieldParser: GraphqlQueryOrderFieldParser; | ||||||
|  |  | ||||||
|   constructor( |   constructor( | ||||||
|     fieldMetadataMap: FieldMetadataMap, |     fieldMetadataMapByName: FieldMetadataMap, | ||||||
|     objectMetadataMap: ObjectMetadataMap, |     objectMetadataMaps: ObjectMetadataMaps, | ||||||
|   ) { |   ) { | ||||||
|     this.objectMetadataMap = objectMetadataMap; |     this.objectMetadataMaps = objectMetadataMaps; | ||||||
|     this.fieldMetadataMap = fieldMetadataMap; |     this.fieldMetadataMapByName = fieldMetadataMapByName; | ||||||
|     this.filterConditionParser = new GraphqlQueryFilterConditionParser( |     this.filterConditionParser = new GraphqlQueryFilterConditionParser( | ||||||
|       this.fieldMetadataMap, |       this.fieldMetadataMapByName, | ||||||
|     ); |     ); | ||||||
|     this.orderFieldParser = new GraphqlQueryOrderFieldParser( |     this.orderFieldParser = new GraphqlQueryOrderFieldParser( | ||||||
|       this.fieldMetadataMap, |       this.fieldMetadataMapByName, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public applyFilterToBuilder( |   public applyFilterToBuilder( | ||||||
|     queryBuilder: SelectQueryBuilder<any>, |     queryBuilder: SelectQueryBuilder<any>, | ||||||
|     objectNameSingular: string, |     objectNameSingular: string, | ||||||
|     recordFilter: Partial<RecordFilter>, |     recordFilter: Partial<ObjectRecordFilter>, | ||||||
|   ): SelectQueryBuilder<any> { |   ): SelectQueryBuilder<any> { | ||||||
|     return this.filterConditionParser.parse( |     return this.filterConditionParser.parse( | ||||||
|       queryBuilder, |       queryBuilder, | ||||||
| @@ -53,7 +54,7 @@ export class GraphqlQueryParser { | |||||||
|  |  | ||||||
|   public applyDeletedAtToBuilder( |   public applyDeletedAtToBuilder( | ||||||
|     queryBuilder: SelectQueryBuilder<any>, |     queryBuilder: SelectQueryBuilder<any>, | ||||||
|     recordFilter: RecordFilter, |     recordFilter: ObjectRecordFilter, | ||||||
|   ): SelectQueryBuilder<any> { |   ): SelectQueryBuilder<any> { | ||||||
|     if (this.checkForDeletedAtFilter(recordFilter)) { |     if (this.checkForDeletedAtFilter(recordFilter)) { | ||||||
|       queryBuilder.withDeleted(); |       queryBuilder.withDeleted(); | ||||||
| @@ -90,7 +91,7 @@ export class GraphqlQueryParser { | |||||||
|  |  | ||||||
|   public applyOrderToBuilder( |   public applyOrderToBuilder( | ||||||
|     queryBuilder: SelectQueryBuilder<any>, |     queryBuilder: SelectQueryBuilder<any>, | ||||||
|     orderBy: RecordOrderBy, |     orderBy: ObjectRecordOrderBy, | ||||||
|     objectNameSingular: string, |     objectNameSingular: string, | ||||||
|     isForwardPagination = true, |     isForwardPagination = true, | ||||||
|   ): SelectQueryBuilder<any> { |   ): SelectQueryBuilder<any> { | ||||||
| @@ -104,11 +105,12 @@ export class GraphqlQueryParser { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   public parseSelectedFields( |   public parseSelectedFields( | ||||||
|     parentObjectMetadata: ObjectMetadataMapItem, |     parentObjectMetadata: ObjectMetadataItemWithFieldMaps, | ||||||
|     graphqlSelectedFields: Partial<Record<string, any>>, |     graphqlSelectedFields: Partial<Record<string, any>>, | ||||||
|   ): { select: Record<string, any>; relations: Record<string, any> } { |   ): GraphqlQuerySelectedFieldsResult { | ||||||
|     const parentFields = |     const parentFields = | ||||||
|       this.objectMetadataMap[parentObjectMetadata.nameSingular]?.fields; |       this.objectMetadataMaps.byNameSingular[parentObjectMetadata.nameSingular] | ||||||
|  |         ?.fieldsByName; | ||||||
|  |  | ||||||
|     if (!parentFields) { |     if (!parentFields) { | ||||||
|       throw new Error( |       throw new Error( | ||||||
| @@ -117,7 +119,7 @@ export class GraphqlQueryParser { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     const selectedFieldsParser = new GraphqlQuerySelectedFieldsParser( |     const selectedFieldsParser = new GraphqlQuerySelectedFieldsParser( | ||||||
|       this.objectMetadataMap, |       this.objectMetadataMaps, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     return selectedFieldsParser.parse(graphqlSelectedFields, parentFields); |     return selectedFieldsParser.parse(graphqlSelectedFields, parentFields); | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
| import { Injectable } from '@nestjs/common'; | import { Injectable } from '@nestjs/common'; | ||||||
|  |  | ||||||
| import { | import { | ||||||
|   Record as IRecord, |   ObjectRecord, | ||||||
|   RecordFilter, |   ObjectRecordFilter, | ||||||
|   RecordOrderBy, |   ObjectRecordOrderBy, | ||||||
| } 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 { 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 { 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'; | import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; | ||||||
| @@ -48,11 +48,11 @@ export class GraphqlQueryRunnerService { | |||||||
|   /** QUERIES */ |   /** QUERIES */ | ||||||
|  |  | ||||||
|   @LogExecutionTime() |   @LogExecutionTime() | ||||||
|   async findOne<ObjectRecord extends IRecord, Filter extends RecordFilter>( |   async findOne<T extends ObjectRecord, Filter extends ObjectRecordFilter>( | ||||||
|     args: FindOneResolverArgs<Filter>, |     args: FindOneResolverArgs<Filter>, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<ObjectRecord> { |   ): Promise<T> { | ||||||
|     return this.executeQuery<FindOneResolverArgs<Filter>, ObjectRecord>( |     return this.executeQuery<FindOneResolverArgs<Filter>, T>( | ||||||
|       'findOne', |       'findOne', | ||||||
|       args, |       args, | ||||||
|       options, |       options, | ||||||
| @@ -61,36 +61,36 @@ export class GraphqlQueryRunnerService { | |||||||
|  |  | ||||||
|   @LogExecutionTime() |   @LogExecutionTime() | ||||||
|   async findMany< |   async findMany< | ||||||
|     ObjectRecord extends IRecord, |     T extends ObjectRecord, | ||||||
|     Filter extends RecordFilter, |     Filter extends ObjectRecordFilter, | ||||||
|     OrderBy extends RecordOrderBy, |     OrderBy extends ObjectRecordOrderBy, | ||||||
|   >( |   >( | ||||||
|     args: FindManyResolverArgs<Filter, OrderBy>, |     args: FindManyResolverArgs<Filter, OrderBy>, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<IConnection<ObjectRecord, IEdge<ObjectRecord>>> { |   ): Promise<IConnection<T, IEdge<T>>> { | ||||||
|     return this.executeQuery< |     return this.executeQuery< | ||||||
|       FindManyResolverArgs<Filter, OrderBy>, |       FindManyResolverArgs<Filter, OrderBy>, | ||||||
|       IConnection<ObjectRecord, IEdge<ObjectRecord>> |       IConnection<T, IEdge<T>> | ||||||
|     >('findMany', args, options); |     >('findMany', args, options); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @LogExecutionTime() |   @LogExecutionTime() | ||||||
|   async findDuplicates<ObjectRecord extends IRecord>( |   async findDuplicates<T extends ObjectRecord>( | ||||||
|     args: FindDuplicatesResolverArgs<Partial<ObjectRecord>>, |     args: FindDuplicatesResolverArgs<Partial<T>>, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<IConnection<ObjectRecord>[]> { |   ): Promise<IConnection<T>[]> { | ||||||
|     return this.executeQuery< |     return this.executeQuery< | ||||||
|       FindDuplicatesResolverArgs<Partial<ObjectRecord>>, |       FindDuplicatesResolverArgs<Partial<T>>, | ||||||
|       IConnection<ObjectRecord>[] |       IConnection<T>[] | ||||||
|     >('findDuplicates', args, options); |     >('findDuplicates', args, options); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @LogExecutionTime() |   @LogExecutionTime() | ||||||
|   async search<ObjectRecord extends IRecord = IRecord>( |   async search<T extends ObjectRecord = ObjectRecord>( | ||||||
|     args: SearchResolverArgs, |     args: SearchResolverArgs, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<IConnection<ObjectRecord>> { |   ): Promise<IConnection<T>> { | ||||||
|     return this.executeQuery<SearchResolverArgs, IConnection<ObjectRecord>>( |     return this.executeQuery<SearchResolverArgs, IConnection<T>>( | ||||||
|       'search', |       'search', | ||||||
|       args, |       args, | ||||||
|       options, |       options, | ||||||
| @@ -100,13 +100,13 @@ export class GraphqlQueryRunnerService { | |||||||
|   /** MUTATIONS */ |   /** MUTATIONS */ | ||||||
|  |  | ||||||
|   @LogExecutionTime() |   @LogExecutionTime() | ||||||
|   async createOne<ObjectRecord extends IRecord>( |   async createOne<T extends ObjectRecord>( | ||||||
|     args: CreateOneResolverArgs<Partial<ObjectRecord>>, |     args: CreateOneResolverArgs<Partial<T>>, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<ObjectRecord> { |   ): Promise<T> { | ||||||
|     const results = await this.executeQuery< |     const results = await this.executeQuery< | ||||||
|       CreateManyResolverArgs<Partial<ObjectRecord>>, |       CreateManyResolverArgs<Partial<T>>, | ||||||
|       ObjectRecord[] |       T[] | ||||||
|     >('createMany', { data: [args.data], upsert: args.upsert }, options); |     >('createMany', { data: [args.data], upsert: args.upsert }, options); | ||||||
|  |  | ||||||
|     // TODO: emitCreateEvents should be moved to the ORM layer |     // TODO: emitCreateEvents should be moved to the ORM layer | ||||||
| @@ -114,7 +114,7 @@ export class GraphqlQueryRunnerService { | |||||||
|       this.apiEventEmitterService.emitCreateEvents( |       this.apiEventEmitterService.emitCreateEvents( | ||||||
|         results, |         results, | ||||||
|         options.authContext, |         options.authContext, | ||||||
|         options.objectMetadataItem, |         options.objectMetadataItemWithFieldMaps, | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -122,20 +122,20 @@ export class GraphqlQueryRunnerService { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   @LogExecutionTime() |   @LogExecutionTime() | ||||||
|   async createMany<ObjectRecord extends IRecord>( |   async createMany<T extends ObjectRecord>( | ||||||
|     args: CreateManyResolverArgs<Partial<ObjectRecord>>, |     args: CreateManyResolverArgs<Partial<T>>, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<ObjectRecord[]> { |   ): Promise<T[]> { | ||||||
|     const results = await this.executeQuery< |     const results = await this.executeQuery< | ||||||
|       CreateManyResolverArgs<Partial<ObjectRecord>>, |       CreateManyResolverArgs<Partial<T>>, | ||||||
|       ObjectRecord[] |       T[] | ||||||
|     >('createMany', args, options); |     >('createMany', args, options); | ||||||
|  |  | ||||||
|     if (results) { |     if (results) { | ||||||
|       this.apiEventEmitterService.emitCreateEvents( |       this.apiEventEmitterService.emitCreateEvents( | ||||||
|         results, |         results, | ||||||
|         options.authContext, |         options.authContext, | ||||||
|         options.objectMetadataItem, |         options.objectMetadataItemWithFieldMaps, | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -143,14 +143,11 @@ export class GraphqlQueryRunnerService { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   @LogExecutionTime() |   @LogExecutionTime() | ||||||
|   public async updateOne<ObjectRecord extends IRecord>( |   public async updateOne<T extends ObjectRecord>( | ||||||
|     args: UpdateOneResolverArgs<Partial<ObjectRecord>>, |     args: UpdateOneResolverArgs<Partial<T>>, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<ObjectRecord> { |   ): Promise<T> { | ||||||
|     const existingRecord = await this.executeQuery< |     const existingRecord = await this.executeQuery<FindOneResolverArgs, T>( | ||||||
|       FindOneResolverArgs, |  | ||||||
|       ObjectRecord |  | ||||||
|     >( |  | ||||||
|       'findOne', |       'findOne', | ||||||
|       { |       { | ||||||
|         filter: { id: { eq: args.id } }, |         filter: { id: { eq: args.id } }, | ||||||
| @@ -159,8 +156,8 @@ export class GraphqlQueryRunnerService { | |||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const result = await this.executeQuery< |     const result = await this.executeQuery< | ||||||
|       UpdateOneResolverArgs<Partial<ObjectRecord>>, |       UpdateOneResolverArgs<Partial<T>>, | ||||||
|       ObjectRecord |       T | ||||||
|     >('updateOne', args, options); |     >('updateOne', args, options); | ||||||
|  |  | ||||||
|     this.apiEventEmitterService.emitUpdateEvents( |     this.apiEventEmitterService.emitUpdateEvents( | ||||||
| @@ -168,20 +165,20 @@ export class GraphqlQueryRunnerService { | |||||||
|       [result], |       [result], | ||||||
|       Object.keys(args.data), |       Object.keys(args.data), | ||||||
|       options.authContext, |       options.authContext, | ||||||
|       options.objectMetadataItem, |       options.objectMetadataItemWithFieldMaps, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     return result; |     return result; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @LogExecutionTime() |   @LogExecutionTime() | ||||||
|   public async updateMany<ObjectRecord extends IRecord>( |   public async updateMany<T extends ObjectRecord>( | ||||||
|     args: UpdateManyResolverArgs<Partial<ObjectRecord>>, |     args: UpdateManyResolverArgs<Partial<T>>, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<ObjectRecord[]> { |   ): Promise<T[]> { | ||||||
|     const existingRecords = await this.executeQuery< |     const existingRecords = await this.executeQuery< | ||||||
|       FindManyResolverArgs, |       FindManyResolverArgs, | ||||||
|       IConnection<ObjectRecord, IEdge<ObjectRecord>> |       IConnection<T, IEdge<T>> | ||||||
|     >( |     >( | ||||||
|       'findMany', |       'findMany', | ||||||
|       { |       { | ||||||
| @@ -191,8 +188,8 @@ export class GraphqlQueryRunnerService { | |||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const result = await this.executeQuery< |     const result = await this.executeQuery< | ||||||
|       UpdateManyResolverArgs<Partial<ObjectRecord>>, |       UpdateManyResolverArgs<Partial<T>>, | ||||||
|       ObjectRecord[] |       T[] | ||||||
|     >('updateMany', args, options); |     >('updateMany', args, options); | ||||||
|  |  | ||||||
|     this.apiEventEmitterService.emitUpdateEvents( |     this.apiEventEmitterService.emitUpdateEvents( | ||||||
| @@ -200,25 +197,25 @@ export class GraphqlQueryRunnerService { | |||||||
|       result, |       result, | ||||||
|       Object.keys(args.data), |       Object.keys(args.data), | ||||||
|       options.authContext, |       options.authContext, | ||||||
|       options.objectMetadataItem, |       options.objectMetadataItemWithFieldMaps, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     return result; |     return result; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @LogExecutionTime() |   @LogExecutionTime() | ||||||
|   public async deleteOne<ObjectRecord extends IRecord & { deletedAt?: Date }>( |   public async deleteOne<T extends ObjectRecord & { deletedAt?: Date }>( | ||||||
|     args: DeleteOneResolverArgs, |     args: DeleteOneResolverArgs, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<ObjectRecord> { |   ): Promise<T> { | ||||||
|     const result = await this.executeQuery< |     const result = await this.executeQuery< | ||||||
|       UpdateOneResolverArgs<Partial<ObjectRecord>>, |       UpdateOneResolverArgs<Partial<T>>, | ||||||
|       ObjectRecord |       T | ||||||
|     >( |     >( | ||||||
|       'deleteOne', |       'deleteOne', | ||||||
|       { |       { | ||||||
|         id: args.id, |         id: args.id, | ||||||
|         data: { deletedAt: new Date() } as Partial<ObjectRecord>, |         data: { deletedAt: new Date() } as Partial<T>, | ||||||
|       }, |       }, | ||||||
|       options, |       options, | ||||||
|     ); |     ); | ||||||
| @@ -226,26 +223,26 @@ export class GraphqlQueryRunnerService { | |||||||
|     this.apiEventEmitterService.emitDeletedEvents( |     this.apiEventEmitterService.emitDeletedEvents( | ||||||
|       [result], |       [result], | ||||||
|       options.authContext, |       options.authContext, | ||||||
|       options.objectMetadataItem, |       options.objectMetadataItemWithFieldMaps, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     return result; |     return result; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @LogExecutionTime() |   @LogExecutionTime() | ||||||
|   public async deleteMany<ObjectRecord extends IRecord & { deletedAt?: Date }>( |   public async deleteMany<T extends ObjectRecord & { deletedAt?: Date }>( | ||||||
|     args: DeleteManyResolverArgs, |     args: DeleteManyResolverArgs, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<ObjectRecord[]> { |   ): Promise<T[]> { | ||||||
|     const result = await this.executeQuery< |     const result = await this.executeQuery< | ||||||
|       UpdateManyResolverArgs<Partial<ObjectRecord>>, |       UpdateManyResolverArgs<Partial<T>>, | ||||||
|       ObjectRecord[] |       T[] | ||||||
|     >( |     >( | ||||||
|       'deleteMany', |       'deleteMany', | ||||||
|       { |       { | ||||||
|         filter: args.filter, |         filter: args.filter, | ||||||
|  |  | ||||||
|         data: { deletedAt: new Date() } as Partial<ObjectRecord>, |         data: { deletedAt: new Date() } as Partial<T>, | ||||||
|       }, |       }, | ||||||
|       options, |       options, | ||||||
|     ); |     ); | ||||||
| @@ -253,63 +250,62 @@ export class GraphqlQueryRunnerService { | |||||||
|     this.apiEventEmitterService.emitDeletedEvents( |     this.apiEventEmitterService.emitDeletedEvents( | ||||||
|       result, |       result, | ||||||
|       options.authContext, |       options.authContext, | ||||||
|       options.objectMetadataItem, |       options.objectMetadataItemWithFieldMaps, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     return result; |     return result; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @LogExecutionTime() |   @LogExecutionTime() | ||||||
|   async destroyOne<ObjectRecord extends IRecord>( |   async destroyOne<T extends ObjectRecord>( | ||||||
|     args: DestroyOneResolverArgs, |     args: DestroyOneResolverArgs, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<ObjectRecord> { |   ): Promise<T> { | ||||||
|     const result = await this.executeQuery< |     const result = await this.executeQuery<DestroyOneResolverArgs, T>( | ||||||
|       DestroyOneResolverArgs, |       'destroyOne', | ||||||
|       ObjectRecord |       args, | ||||||
|     >('destroyOne', args, options); |       options, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     this.apiEventEmitterService.emitDestroyEvents( |     this.apiEventEmitterService.emitDestroyEvents( | ||||||
|       [result], |       [result], | ||||||
|       options.authContext, |       options.authContext, | ||||||
|       options.objectMetadataItem, |       options.objectMetadataItemWithFieldMaps, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     return result; |     return result; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @LogExecutionTime() |   @LogExecutionTime() | ||||||
|   async destroyMany<ObjectRecord extends IRecord>( |   async destroyMany<T extends ObjectRecord>( | ||||||
|     args: DestroyManyResolverArgs, |     args: DestroyManyResolverArgs, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<ObjectRecord[]> { |   ): Promise<T[]> { | ||||||
|     const result = await this.executeQuery< |     const result = await this.executeQuery<DestroyManyResolverArgs, T[]>( | ||||||
|       DestroyManyResolverArgs, |       'destroyMany', | ||||||
|       ObjectRecord[] |       args, | ||||||
|     >('destroyMany', args, options); |       options, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     this.apiEventEmitterService.emitDestroyEvents( |     this.apiEventEmitterService.emitDestroyEvents( | ||||||
|       result, |       result, | ||||||
|       options.authContext, |       options.authContext, | ||||||
|       options.objectMetadataItem, |       options.objectMetadataItemWithFieldMaps, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     return result; |     return result; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @LogExecutionTime() |   @LogExecutionTime() | ||||||
|   public async restoreMany<ObjectRecord extends IRecord>( |   public async restoreMany<T extends ObjectRecord>( | ||||||
|     args: RestoreManyResolverArgs, |     args: RestoreManyResolverArgs, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<ObjectRecord> { |   ): Promise<T> { | ||||||
|     return await this.executeQuery< |     return await this.executeQuery<UpdateManyResolverArgs<Partial<T>>, T>( | ||||||
|       UpdateManyResolverArgs<Partial<ObjectRecord>>, |  | ||||||
|       ObjectRecord |  | ||||||
|     >( |  | ||||||
|       'restoreMany', |       'restoreMany', | ||||||
|       { |       { | ||||||
|         filter: args.filter, |         filter: args.filter, | ||||||
|         data: { deletedAt: null } as Partial<ObjectRecord>, |         data: { deletedAt: null } as Partial<T>, | ||||||
|       }, |       }, | ||||||
|       options, |       options, | ||||||
|     ); |     ); | ||||||
| @@ -320,7 +316,7 @@ export class GraphqlQueryRunnerService { | |||||||
|     args: Input, |     args: Input, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<Response> { |   ): Promise<Response> { | ||||||
|     const { authContext, objectMetadataItem } = options; |     const { authContext, objectMetadataItemWithFieldMaps } = options; | ||||||
|  |  | ||||||
|     const resolver = |     const resolver = | ||||||
|       this.graphqlQueryResolverFactory.getResolver(operationName); |       this.graphqlQueryResolverFactory.getResolver(operationName); | ||||||
| @@ -330,7 +326,7 @@ export class GraphqlQueryRunnerService { | |||||||
|     const hookedArgs = |     const hookedArgs = | ||||||
|       await this.workspaceQueryHookService.executePreQueryHooks( |       await this.workspaceQueryHookService.executePreQueryHooks( | ||||||
|         authContext, |         authContext, | ||||||
|         objectMetadataItem.nameSingular, |         objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|         operationName, |         operationName, | ||||||
|         args, |         args, | ||||||
|       ); |       ); | ||||||
| @@ -345,7 +341,7 @@ export class GraphqlQueryRunnerService { | |||||||
|  |  | ||||||
|     const resultWithGetters = await this.queryResultGettersFactory.create( |     const resultWithGetters = await this.queryResultGettersFactory.create( | ||||||
|       results, |       results, | ||||||
|       objectMetadataItem, |       objectMetadataItemWithFieldMaps, | ||||||
|       authContext.workspace.id, |       authContext.workspace.id, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
| @@ -355,7 +351,7 @@ export class GraphqlQueryRunnerService { | |||||||
|  |  | ||||||
|     await this.workspaceQueryHookService.executePostQueryHooks( |     await this.workspaceQueryHookService.executePostQueryHooks( | ||||||
|       authContext, |       authContext, | ||||||
|       objectMetadataItem.nameSingular, |       objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|       operationName, |       operationName, | ||||||
|       resultWithGettersArray, |       resultWithGettersArray, | ||||||
|     ); |     ); | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import { | import { | ||||||
|   Record as IRecord, |   ObjectRecord, | ||||||
|   RecordOrderBy, |   ObjectRecordOrderBy, | ||||||
| } 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 { 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'; | 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'; | } 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 { 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 { 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 { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; | ||||||
| import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; | 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 { 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 { 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 { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; | ||||||
| import { isPlainObject } from 'src/utils/is-plain-object'; | import { isPlainObject } from 'src/utils/is-plain-object'; | ||||||
|  |  | ||||||
| export class ObjectRecordsToGraphqlConnectionHelper { | export class ObjectRecordsToGraphqlConnectionHelper { | ||||||
|   private objectMetadataMap: ObjectMetadataMap; |   private objectMetadataMaps: ObjectMetadataMaps; | ||||||
|  |  | ||||||
|   constructor(objectMetadataMap: ObjectMetadataMap) { |   constructor(objectMetadataMaps: ObjectMetadataMaps) { | ||||||
|     this.objectMetadataMap = objectMetadataMap; |     this.objectMetadataMaps = objectMetadataMaps; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public createConnection<ObjectRecord extends IRecord = IRecord>({ |   public createConnection<T extends ObjectRecord = ObjectRecord>({ | ||||||
|     objectRecords, |     objectRecords, | ||||||
|  |     parentObjectRecord, | ||||||
|  |     objectRecordsAggregatedValues = {}, | ||||||
|  |     selectedAggregatedFields = [], | ||||||
|     objectName, |     objectName, | ||||||
|     take, |     take, | ||||||
|     totalCount, |     totalCount, | ||||||
| @@ -37,19 +41,24 @@ export class ObjectRecordsToGraphqlConnectionHelper { | |||||||
|     hasPreviousPage, |     hasPreviousPage, | ||||||
|     depth = 0, |     depth = 0, | ||||||
|   }: { |   }: { | ||||||
|     objectRecords: ObjectRecord[]; |     objectRecords: T[]; | ||||||
|  |     parentObjectRecord?: T; | ||||||
|  |     objectRecordsAggregatedValues?: Record<string, any>; | ||||||
|  |     selectedAggregatedFields?: Record<string, any>; | ||||||
|     objectName: string; |     objectName: string; | ||||||
|     take: number; |     take: number; | ||||||
|     totalCount: number; |     totalCount: number; | ||||||
|     order?: RecordOrderBy; |     order?: ObjectRecordOrderBy; | ||||||
|     hasNextPage: boolean; |     hasNextPage: boolean; | ||||||
|     hasPreviousPage: boolean; |     hasPreviousPage: boolean; | ||||||
|     depth?: number; |     depth?: number; | ||||||
|   }): IConnection<ObjectRecord> { |   }): IConnection<T> { | ||||||
|     const edges = (objectRecords ?? []).map((objectRecord) => ({ |     const edges = (objectRecords ?? []).map((objectRecord) => ({ | ||||||
|       node: this.processRecord({ |       node: this.processRecord({ | ||||||
|         objectRecord, |         objectRecord, | ||||||
|         objectName, |         objectName, | ||||||
|  |         objectRecordsAggregatedValues, | ||||||
|  |         selectedAggregatedFields, | ||||||
|         take, |         take, | ||||||
|         totalCount, |         totalCount, | ||||||
|         order, |         order, | ||||||
| @@ -58,7 +67,15 @@ export class ObjectRecordsToGraphqlConnectionHelper { | |||||||
|       cursor: encodeCursor(objectRecord, order), |       cursor: encodeCursor(objectRecord, order), | ||||||
|     })); |     })); | ||||||
|  |  | ||||||
|  |     const aggregatedFieldsValues = this.extractAggregatedFieldsValues({ | ||||||
|  |       selectedAggregatedFields, | ||||||
|  |       objectRecordsAggregatedValues: parentObjectRecord | ||||||
|  |         ? objectRecordsAggregatedValues[parentObjectRecord.id] | ||||||
|  |         : objectRecordsAggregatedValues, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|     return { |     return { | ||||||
|  |       ...aggregatedFieldsValues, | ||||||
|       edges, |       edges, | ||||||
|       pageInfo: { |       pageInfo: { | ||||||
|         hasNextPage, |         hasNextPage, | ||||||
| @@ -70,9 +87,41 @@ export class ObjectRecordsToGraphqlConnectionHelper { | |||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private extractAggregatedFieldsValues = ({ | ||||||
|  |     selectedAggregatedFields, | ||||||
|  |     objectRecordsAggregatedValues, | ||||||
|  |   }: { | ||||||
|  |     selectedAggregatedFields: Record<string, AggregationField[]>; | ||||||
|  |     objectRecordsAggregatedValues: Record<string, any>; | ||||||
|  |   }) => { | ||||||
|  |     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<T extends Record<string, any>>({ |   public processRecord<T extends Record<string, any>>({ | ||||||
|     objectRecord, |     objectRecord, | ||||||
|     objectName, |     objectName, | ||||||
|  |     objectRecordsAggregatedValues = {}, | ||||||
|  |     selectedAggregatedFields = [], | ||||||
|     take, |     take, | ||||||
|     totalCount, |     totalCount, | ||||||
|     order, |     order, | ||||||
| @@ -80,9 +129,11 @@ export class ObjectRecordsToGraphqlConnectionHelper { | |||||||
|   }: { |   }: { | ||||||
|     objectRecord: T; |     objectRecord: T; | ||||||
|     objectName: string; |     objectName: string; | ||||||
|  |     objectRecordsAggregatedValues?: Record<string, any>; | ||||||
|  |     selectedAggregatedFields?: Record<string, any>; | ||||||
|     take: number; |     take: number; | ||||||
|     totalCount: number; |     totalCount: number; | ||||||
|     order?: RecordOrderBy; |     order?: ObjectRecordOrderBy; | ||||||
|     depth?: number; |     depth?: number; | ||||||
|   }): T { |   }): T { | ||||||
|     if (depth >= CONNECTION_MAX_DEPTH) { |     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) { |     if (!objectMetadata) { | ||||||
|       throw new GraphqlQueryRunnerException( |       throw new GraphqlQueryRunnerException( | ||||||
| @@ -104,7 +155,7 @@ export class ObjectRecordsToGraphqlConnectionHelper { | |||||||
|     const processedObjectRecord: Record<string, any> = {}; |     const processedObjectRecord: Record<string, any> = {}; | ||||||
|  |  | ||||||
|     for (const [key, value] of Object.entries(objectRecord)) { |     for (const [key, value] of Object.entries(objectRecord)) { | ||||||
|       const fieldMetadata = objectMetadata.fields[key]; |       const fieldMetadata = objectMetadata.fieldsByName[key]; | ||||||
|  |  | ||||||
|       if (!fieldMetadata) { |       if (!fieldMetadata) { | ||||||
|         processedObjectRecord[key] = value; |         processedObjectRecord[key] = value; | ||||||
| @@ -115,12 +166,19 @@ export class ObjectRecordsToGraphqlConnectionHelper { | |||||||
|         if (Array.isArray(value)) { |         if (Array.isArray(value)) { | ||||||
|           processedObjectRecord[key] = this.createConnection({ |           processedObjectRecord[key] = this.createConnection({ | ||||||
|             objectRecords: value, |             objectRecords: value, | ||||||
|  |             parentObjectRecord: objectRecord, | ||||||
|  |             objectRecordsAggregatedValues: | ||||||
|  |               objectRecordsAggregatedValues[fieldMetadata.name], | ||||||
|  |             selectedAggregatedFields: | ||||||
|  |               selectedAggregatedFields[fieldMetadata.name], | ||||||
|             objectName: getRelationObjectMetadata( |             objectName: getRelationObjectMetadata( | ||||||
|               fieldMetadata, |               fieldMetadata, | ||||||
|               this.objectMetadataMap, |               this.objectMetadataMaps, | ||||||
|             ).nameSingular, |             ).nameSingular, | ||||||
|             take, |             take, | ||||||
|             totalCount: value.length, |             totalCount: | ||||||
|  |               objectRecordsAggregatedValues[fieldMetadata.name]?.totalCount ?? | ||||||
|  |               value.length, | ||||||
|             order, |             order, | ||||||
|             hasNextPage: false, |             hasNextPage: false, | ||||||
|             hasPreviousPage: false, |             hasPreviousPage: false, | ||||||
| @@ -129,9 +187,13 @@ export class ObjectRecordsToGraphqlConnectionHelper { | |||||||
|         } else if (isPlainObject(value)) { |         } else if (isPlainObject(value)) { | ||||||
|           processedObjectRecord[key] = this.processRecord({ |           processedObjectRecord[key] = this.processRecord({ | ||||||
|             objectRecord: value, |             objectRecord: value, | ||||||
|  |             objectRecordsAggregatedValues: | ||||||
|  |               objectRecordsAggregatedValues[fieldMetadata.name], | ||||||
|  |             selectedAggregatedFields: | ||||||
|  |               selectedAggregatedFields[fieldMetadata.name], | ||||||
|             objectName: getRelationObjectMetadata( |             objectName: getRelationObjectMetadata( | ||||||
|               fieldMetadata, |               fieldMetadata, | ||||||
|               this.objectMetadataMap, |               this.objectMetadataMaps, | ||||||
|             ).nameSingular, |             ).nameSingular, | ||||||
|             take, |             take, | ||||||
|             totalCount, |             totalCount, | ||||||
|   | |||||||
| @@ -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<string, FieldMetadataInterface>; | ||||||
|  |     selectedAggregatedFields: Record<string, AggregationField>; | ||||||
|  |     queryBuilder: SelectQueryBuilder<any>; | ||||||
|  |   }) => { | ||||||
|  |     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}`, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | } | ||||||
| @@ -1,64 +1,95 @@ | |||||||
| import { | import { | ||||||
|   DataSource, |   DataSource, | ||||||
|   FindManyOptions, |  | ||||||
|   FindOptionsRelations, |   FindOptionsRelations, | ||||||
|   In, |  | ||||||
|   ObjectLiteral, |   ObjectLiteral, | ||||||
|   Repository, |   SelectQueryBuilder, | ||||||
| } from 'typeorm'; | } 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 { | import { | ||||||
|   getRelationMetadata, |   getRelationMetadata, | ||||||
|   getRelationObjectMetadata, |   getRelationObjectMetadata, | ||||||
| } from 'src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util'; | } from 'src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util'; | ||||||
| import { | import { AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; | ||||||
|   ObjectMetadataMap, | import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; | ||||||
|   ObjectMetadataMapItem, | import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; | ||||||
| } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; | import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; | ||||||
| import { deduceRelationDirection } from 'src/engine/utils/deduce-relation-direction.util'; | import { deduceRelationDirection } from 'src/engine/utils/deduce-relation-direction.util'; | ||||||
|  |  | ||||||
| export class ProcessNestedRelationsHelper { | export class ProcessNestedRelationsHelper { | ||||||
|   constructor() {} |   private processAggregateHelper: ProcessAggregateHelper; | ||||||
|  |  | ||||||
|   public async processNestedRelations<ObjectRecord extends IRecord = IRecord>( |   constructor() { | ||||||
|     objectMetadataMap: ObjectMetadataMap, |     this.processAggregateHelper = new ProcessAggregateHelper(); | ||||||
|     parentObjectMetadataItem: ObjectMetadataMapItem, |   } | ||||||
|     parentObjectRecords: ObjectRecord[], |  | ||||||
|     relations: Record<string, FindOptionsRelations<ObjectLiteral>>, |   public async processNestedRelations<T extends ObjectRecord = ObjectRecord>({ | ||||||
|     limit: number, |     objectMetadataMaps, | ||||||
|     authContext: any, |  | ||||||
|     dataSource: DataSource, |  | ||||||
|   ): Promise<void> { |  | ||||||
|     const processRelationTasks = Object.entries(relations).map( |  | ||||||
|       ([relationName, nestedRelations]) => |  | ||||||
|         this.processRelation( |  | ||||||
|           objectMetadataMap, |  | ||||||
|     parentObjectMetadataItem, |     parentObjectMetadataItem, | ||||||
|     parentObjectRecords, |     parentObjectRecords, | ||||||
|           relationName, |     parentObjectRecordsAggregatedValues = {}, | ||||||
|           nestedRelations, |     relations, | ||||||
|  |     aggregate = {}, | ||||||
|     limit, |     limit, | ||||||
|     authContext, |     authContext, | ||||||
|     dataSource, |     dataSource, | ||||||
|         ), |   }: { | ||||||
|  |     objectMetadataMaps: ObjectMetadataMaps; | ||||||
|  |     parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps; | ||||||
|  |     parentObjectRecords: T[]; | ||||||
|  |     parentObjectRecordsAggregatedValues?: Record<string, any>; | ||||||
|  |     relations: Record<string, FindOptionsRelations<ObjectLiteral>>; | ||||||
|  |     aggregate?: Record<string, AggregationField>; | ||||||
|  |     limit: number; | ||||||
|  |     authContext: any; | ||||||
|  |     dataSource: DataSource; | ||||||
|  |   }): Promise<void> { | ||||||
|  |     const processRelationTasks = Object.entries(relations).map( | ||||||
|  |       ([relationName, nestedRelations]) => | ||||||
|  |         this.processRelation({ | ||||||
|  |           objectMetadataMaps, | ||||||
|  |           parentObjectMetadataItem, | ||||||
|  |           parentObjectRecords, | ||||||
|  |           parentObjectRecordsAggregatedValues, | ||||||
|  |           relationName, | ||||||
|  |           nestedRelations, | ||||||
|  |           aggregate, | ||||||
|  |           limit, | ||||||
|  |           authContext, | ||||||
|  |           dataSource, | ||||||
|  |         }), | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     await Promise.all(processRelationTasks); |     await Promise.all(processRelationTasks); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async processRelation<ObjectRecord extends IRecord = IRecord>( |   private async processRelation<T extends ObjectRecord = ObjectRecord>({ | ||||||
|     objectMetadataMap: ObjectMetadataMap, |     objectMetadataMaps, | ||||||
|     parentObjectMetadataItem: ObjectMetadataMapItem, |     parentObjectMetadataItem, | ||||||
|     parentObjectRecords: ObjectRecord[], |     parentObjectRecords, | ||||||
|     relationName: string, |     parentObjectRecordsAggregatedValues, | ||||||
|     nestedRelations: any, |     relationName, | ||||||
|     limit: number, |     nestedRelations, | ||||||
|     authContext: any, |     aggregate, | ||||||
|     dataSource: DataSource, |     limit, | ||||||
|   ): Promise<void> { |     authContext, | ||||||
|     const relationFieldMetadata = parentObjectMetadataItem.fields[relationName]; |     dataSource, | ||||||
|  |   }: { | ||||||
|  |     objectMetadataMaps: ObjectMetadataMaps; | ||||||
|  |     parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps; | ||||||
|  |     parentObjectRecords: T[]; | ||||||
|  |     parentObjectRecordsAggregatedValues: Record<string, any>; | ||||||
|  |     relationName: string; | ||||||
|  |     nestedRelations: any; | ||||||
|  |     aggregate: Record<string, AggregationField>; | ||||||
|  |     limit: number; | ||||||
|  |     authContext: any; | ||||||
|  |     dataSource: DataSource; | ||||||
|  |   }): Promise<void> { | ||||||
|  |     const relationFieldMetadata = | ||||||
|  |       parentObjectMetadataItem.fieldsByName[relationName]; | ||||||
|     const relationMetadata = getRelationMetadata(relationFieldMetadata); |     const relationMetadata = getRelationMetadata(relationFieldMetadata); | ||||||
|     const relationDirection = deduceRelationDirection( |     const relationDirection = deduceRelationDirection( | ||||||
|       relationFieldMetadata, |       relationFieldMetadata, | ||||||
| @@ -70,181 +101,341 @@ export class ProcessNestedRelationsHelper { | |||||||
|         ? this.processToRelation |         ? this.processToRelation | ||||||
|         : this.processFromRelation; |         : this.processFromRelation; | ||||||
|  |  | ||||||
|     await processor.call( |     await processor.call(this, { | ||||||
|       this, |       objectMetadataMaps, | ||||||
|       objectMetadataMap, |  | ||||||
|       parentObjectMetadataItem, |       parentObjectMetadataItem, | ||||||
|       parentObjectRecords, |       parentObjectRecords, | ||||||
|  |       parentObjectRecordsAggregatedValues, | ||||||
|       relationName, |       relationName, | ||||||
|       nestedRelations, |       nestedRelations, | ||||||
|  |       aggregate, | ||||||
|       limit, |       limit, | ||||||
|       authContext, |       authContext, | ||||||
|       dataSource, |       dataSource, | ||||||
|     ); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async processFromRelation<ObjectRecord extends IRecord = IRecord>( |   private async processFromRelation<T extends ObjectRecord = ObjectRecord>({ | ||||||
|     objectMetadataMap: ObjectMetadataMap, |     objectMetadataMaps, | ||||||
|     parentObjectMetadataItem: ObjectMetadataMapItem, |     parentObjectMetadataItem, | ||||||
|     parentObjectRecords: ObjectRecord[], |     parentObjectRecords, | ||||||
|     relationName: string, |     parentObjectRecordsAggregatedValues, | ||||||
|     nestedRelations: any, |     relationName, | ||||||
|     limit: number, |     nestedRelations, | ||||||
|     authContext: any, |     aggregate, | ||||||
|     dataSource: DataSource, |     limit, | ||||||
|   ): Promise<void> { |     authContext, | ||||||
|  |     dataSource, | ||||||
|  |   }: { | ||||||
|  |     objectMetadataMaps: ObjectMetadataMaps; | ||||||
|  |     parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps; | ||||||
|  |     parentObjectRecords: T[]; | ||||||
|  |     parentObjectRecordsAggregatedValues: Record<string, any>; | ||||||
|  |     relationName: string; | ||||||
|  |     nestedRelations: any; | ||||||
|  |     aggregate: Record<string, AggregationField>; | ||||||
|  |     limit: number; | ||||||
|  |     authContext: any; | ||||||
|  |     dataSource: DataSource; | ||||||
|  |   }): Promise<void> { | ||||||
|     const { inverseRelationName, referenceObjectMetadata } = |     const { inverseRelationName, referenceObjectMetadata } = | ||||||
|       this.getRelationMetadata( |       this.getRelationMetadata({ | ||||||
|         objectMetadataMap, |         objectMetadataMaps, | ||||||
|         parentObjectMetadataItem, |         parentObjectMetadataItem, | ||||||
|         relationName, |         relationName, | ||||||
|       ); |       }); | ||||||
|     const relationRepository = dataSource.getRepository( |     const relationRepository = dataSource.getRepository( | ||||||
|       referenceObjectMetadata.nameSingular, |       referenceObjectMetadata.nameSingular, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const relationIds = this.getUniqueIds(parentObjectRecords, 'id'); |     const referenceQueryBuilder = relationRepository.createQueryBuilder( | ||||||
|     const relationResults = await this.findRelations( |       referenceObjectMetadata.nameSingular, | ||||||
|       relationRepository, |  | ||||||
|       inverseRelationName, |  | ||||||
|       relationIds, |  | ||||||
|       limit * parentObjectRecords.length, |  | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     this.assignRelationResults( |     const relationIds = this.getUniqueIds({ | ||||||
|       parentObjectRecords, |       records: parentObjectRecords, | ||||||
|       relationResults, |       idField: 'id', | ||||||
|  |     }); | ||||||
|  |     const { relationResults, relationAggregatedFieldsResult } = | ||||||
|  |       await this.findRelations({ | ||||||
|  |         referenceQueryBuilder, | ||||||
|  |         column: `"${inverseRelationName}Id"`, | ||||||
|  |         ids: relationIds, | ||||||
|  |         limit: limit * parentObjectRecords.length, | ||||||
|  |         objectMetadataMaps, | ||||||
|  |         referenceObjectMetadata, | ||||||
|  |         aggregate, | ||||||
|         relationName, |         relationName, | ||||||
|       `${inverseRelationName}Id`, |       }); | ||||||
|     ); |  | ||||||
|  |     this.assignFromRelationResults({ | ||||||
|  |       parentRecords: parentObjectRecords, | ||||||
|  |       parentObjectRecordsAggregatedValues, | ||||||
|  |       relationResults, | ||||||
|  |       relationAggregatedFieldsResult, | ||||||
|  |       relationName, | ||||||
|  |       joinField: `${inverseRelationName}Id`, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|     if (Object.keys(nestedRelations).length > 0) { |     if (Object.keys(nestedRelations).length > 0) { | ||||||
|       await this.processNestedRelations( |       await this.processNestedRelations({ | ||||||
|         objectMetadataMap, |         objectMetadataMaps, | ||||||
|         objectMetadataMap[referenceObjectMetadata.nameSingular], |         parentObjectMetadataItem: | ||||||
|         relationResults as ObjectRecord[], |           objectMetadataMaps.byNameSingular[ | ||||||
|         nestedRelations as Record<string, FindOptionsRelations<ObjectLiteral>>, |             referenceObjectMetadata.nameSingular | ||||||
|  |           ], | ||||||
|  |         parentObjectRecords: relationResults as ObjectRecord[], | ||||||
|  |         parentObjectRecordsAggregatedValues: relationAggregatedFieldsResult, | ||||||
|  |         relations: nestedRelations as Record< | ||||||
|  |           string, | ||||||
|  |           FindOptionsRelations<ObjectLiteral> | ||||||
|  |         >, | ||||||
|  |         aggregate, | ||||||
|         limit, |         limit, | ||||||
|         authContext, |         authContext, | ||||||
|         dataSource, |         dataSource, | ||||||
|       ); |       }); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async processToRelation<ObjectRecord extends IRecord = IRecord>( |   private async processToRelation<T extends ObjectRecord = ObjectRecord>({ | ||||||
|     objectMetadataMap: ObjectMetadataMap, |     objectMetadataMaps, | ||||||
|     parentObjectMetadataItem: ObjectMetadataMapItem, |     parentObjectMetadataItem, | ||||||
|     parentObjectRecords: ObjectRecord[], |     parentObjectRecords, | ||||||
|     relationName: string, |     parentObjectRecordsAggregatedValues, | ||||||
|     nestedRelations: any, |     relationName, | ||||||
|     limit: number, |     nestedRelations, | ||||||
|     authContext: any, |     aggregate, | ||||||
|     dataSource: DataSource, |     limit, | ||||||
|   ): Promise<void> { |     authContext, | ||||||
|     const { referenceObjectMetadata } = this.getRelationMetadata( |     dataSource, | ||||||
|       objectMetadataMap, |   }: { | ||||||
|  |     objectMetadataMaps: ObjectMetadataMaps; | ||||||
|  |     parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps; | ||||||
|  |     parentObjectRecords: T[]; | ||||||
|  |     parentObjectRecordsAggregatedValues: Record<string, any>; | ||||||
|  |     relationName: string; | ||||||
|  |     nestedRelations: any; | ||||||
|  |     aggregate: Record<string, AggregationField>; | ||||||
|  |     limit: number; | ||||||
|  |     authContext: any; | ||||||
|  |     dataSource: DataSource; | ||||||
|  |   }): Promise<void> { | ||||||
|  |     const { referenceObjectMetadata } = this.getRelationMetadata({ | ||||||
|  |       objectMetadataMaps, | ||||||
|       parentObjectMetadataItem, |       parentObjectMetadataItem, | ||||||
|       relationName, |       relationName, | ||||||
|     ); |     }); | ||||||
|     const relationRepository = dataSource.getRepository( |     const relationRepository = dataSource.getRepository( | ||||||
|       referenceObjectMetadata.nameSingular, |       referenceObjectMetadata.nameSingular, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const relationIds = this.getUniqueIds( |     const referenceQueryBuilder = relationRepository.createQueryBuilder( | ||||||
|       parentObjectRecords, |       referenceObjectMetadata.nameSingular, | ||||||
|       `${relationName}Id`, |  | ||||||
|     ); |  | ||||||
|     const relationResults = await this.findRelations( |  | ||||||
|       relationRepository, |  | ||||||
|       'id', |  | ||||||
|       relationIds, |  | ||||||
|       limit, |  | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     this.assignToRelationResults( |     const relationIds = this.getUniqueIds({ | ||||||
|       parentObjectRecords, |       records: parentObjectRecords, | ||||||
|       relationResults, |       idField: `${relationName}Id`, | ||||||
|  |     }); | ||||||
|  |     const { relationResults, relationAggregatedFieldsResult } = | ||||||
|  |       await this.findRelations({ | ||||||
|  |         referenceQueryBuilder, | ||||||
|  |         column: 'id', | ||||||
|  |         ids: relationIds, | ||||||
|  |         limit, | ||||||
|  |         objectMetadataMaps, | ||||||
|  |         referenceObjectMetadata, | ||||||
|  |         aggregate, | ||||||
|         relationName, |         relationName, | ||||||
|     ); |       }); | ||||||
|  |  | ||||||
|  |     this.assignToRelationResults({ | ||||||
|  |       parentRecords: parentObjectRecords, | ||||||
|  |       parentObjectRecordsAggregatedValues: parentObjectRecordsAggregatedValues, | ||||||
|  |       relationResults, | ||||||
|  |       relationAggregatedFieldsResult, | ||||||
|  |       relationName, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|     if (Object.keys(nestedRelations).length > 0) { |     if (Object.keys(nestedRelations).length > 0) { | ||||||
|       await this.processNestedRelations( |       await this.processNestedRelations({ | ||||||
|         objectMetadataMap, |         objectMetadataMaps, | ||||||
|         objectMetadataMap[referenceObjectMetadata.nameSingular], |         parentObjectMetadataItem: | ||||||
|         relationResults as ObjectRecord[], |           objectMetadataMaps.byNameSingular[ | ||||||
|         nestedRelations as Record<string, FindOptionsRelations<ObjectLiteral>>, |             referenceObjectMetadata.nameSingular | ||||||
|  |           ], | ||||||
|  |         parentObjectRecords: relationResults as ObjectRecord[], | ||||||
|  |         parentObjectRecordsAggregatedValues: relationAggregatedFieldsResult, | ||||||
|  |         relations: nestedRelations as Record< | ||||||
|  |           string, | ||||||
|  |           FindOptionsRelations<ObjectLiteral> | ||||||
|  |         >, | ||||||
|  |         aggregate, | ||||||
|         limit, |         limit, | ||||||
|         authContext, |         authContext, | ||||||
|         dataSource, |         dataSource, | ||||||
|       ); |       }); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private getRelationMetadata( |   private getRelationMetadata({ | ||||||
|     objectMetadataMap: ObjectMetadataMap, |     objectMetadataMaps, | ||||||
|     parentObjectMetadataItem: ObjectMetadataMapItem, |     parentObjectMetadataItem, | ||||||
|     relationName: string, |     relationName, | ||||||
|   ) { |   }: { | ||||||
|     const relationFieldMetadata = parentObjectMetadataItem.fields[relationName]; |     objectMetadataMaps: ObjectMetadataMaps; | ||||||
|  |     parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps; | ||||||
|  |     relationName: string; | ||||||
|  |   }) { | ||||||
|  |     const relationFieldMetadata = | ||||||
|  |       parentObjectMetadataItem.fieldsByName[relationName]; | ||||||
|     const relationMetadata = getRelationMetadata(relationFieldMetadata); |     const relationMetadata = getRelationMetadata(relationFieldMetadata); | ||||||
|     const referenceObjectMetadata = getRelationObjectMetadata( |     const referenceObjectMetadata = getRelationObjectMetadata( | ||||||
|       relationFieldMetadata, |       relationFieldMetadata, | ||||||
|       objectMetadataMap, |       objectMetadataMaps, | ||||||
|     ); |     ); | ||||||
|     const inverseRelationName = |     const inverseRelationName = | ||||||
|       objectMetadataMap[relationMetadata.toObjectMetadataId]?.fields[ |       objectMetadataMaps.byId[relationMetadata.toObjectMetadataId]?.fieldsById[ | ||||||
|         relationMetadata.toFieldMetadataId |         relationMetadata.toFieldMetadataId | ||||||
|       ]?.name; |       ]?.name; | ||||||
|  |  | ||||||
|     return { inverseRelationName, referenceObjectMetadata }; |     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]))]; |     return [...new Set(records.map((item) => item[idField]))]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async findRelations( |   private async findRelations({ | ||||||
|     repository: Repository<any>, |     referenceQueryBuilder, | ||||||
|     field: string, |     column, | ||||||
|     ids: any[], |     ids, | ||||||
|     limit: number, |     limit, | ||||||
|   ): Promise<any[]> { |     objectMetadataMaps, | ||||||
|  |     referenceObjectMetadata, | ||||||
|  |     aggregate, | ||||||
|  |     relationName, | ||||||
|  |   }: { | ||||||
|  |     referenceQueryBuilder: SelectQueryBuilder<any>; | ||||||
|  |     column: string; | ||||||
|  |     ids: any[]; | ||||||
|  |     limit: number; | ||||||
|  |     objectMetadataMaps: ObjectMetadataMaps; | ||||||
|  |     referenceObjectMetadata: ObjectMetadataItemWithFieldMaps; | ||||||
|  |     aggregate: Record<string, any>; | ||||||
|  |     relationName: string; | ||||||
|  |   }): Promise<{ relationResults: any[]; relationAggregatedFieldsResult: any }> { | ||||||
|     if (ids.length === 0) { |     if (ids.length === 0) { | ||||||
|       return []; |       return { relationResults: [], relationAggregatedFieldsResult: {} }; | ||||||
|     } |  | ||||||
|     const findOptions: FindManyOptions = { |  | ||||||
|       where: { [field]: In(ids) }, |  | ||||||
|       take: limit, |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     return repository.find(findOptions); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   private assignRelationResults( |     const aggregateForRelation = aggregate[relationName]; | ||||||
|     parentRecords: IRecord[], |     let relationAggregatedFieldsResult: Record<string, any> = {}; | ||||||
|     relationResults: any[], |  | ||||||
|     relationName: string, |     if (aggregateForRelation) { | ||||||
|     joinField: string, |       const aggregateQueryBuilder = referenceQueryBuilder.clone(); | ||||||
|   ): void { |  | ||||||
|  |       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 result = await referenceQueryBuilder | ||||||
|  |       .where(`${column} IN (:...ids)`, { | ||||||
|  |         ids, | ||||||
|  |       }) | ||||||
|  |       .take(limit) | ||||||
|  |       .getMany(); | ||||||
|  |  | ||||||
|  |     const relationResults = formatResult( | ||||||
|  |       result, | ||||||
|  |       referenceObjectMetadata, | ||||||
|  |       objectMetadataMaps, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     return { relationResults, relationAggregatedFieldsResult }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private assignFromRelationResults({ | ||||||
|  |     parentRecords, | ||||||
|  |     parentObjectRecordsAggregatedValues, | ||||||
|  |     relationResults, | ||||||
|  |     relationAggregatedFieldsResult, | ||||||
|  |     relationName, | ||||||
|  |     joinField, | ||||||
|  |   }: { | ||||||
|  |     parentRecords: ObjectRecord[]; | ||||||
|  |     parentObjectRecordsAggregatedValues: Record<string, any>; | ||||||
|  |     relationResults: any[]; | ||||||
|  |     relationAggregatedFieldsResult: Record<string, any>; | ||||||
|  |     relationName: string; | ||||||
|  |     joinField: string; | ||||||
|  |   }): void { | ||||||
|     parentRecords.forEach((item) => { |     parentRecords.forEach((item) => { | ||||||
|       (item as any)[relationName] = relationResults.filter( |       item[relationName] = relationResults.filter( | ||||||
|         (rel) => rel[joinField] === item.id, |         (rel) => rel[joinField] === item.id, | ||||||
|       ); |       ); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |     parentObjectRecordsAggregatedValues[relationName] = | ||||||
|  |       relationAggregatedFieldsResult; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private assignToRelationResults( |   private assignToRelationResults({ | ||||||
|     parentRecords: IRecord[], |     parentRecords, | ||||||
|     relationResults: any[], |     parentObjectRecordsAggregatedValues, | ||||||
|     relationName: string, |     relationResults, | ||||||
|   ): void { |     relationAggregatedFieldsResult, | ||||||
|  |     relationName, | ||||||
|  |   }: { | ||||||
|  |     parentRecords: ObjectRecord[]; | ||||||
|  |     parentObjectRecordsAggregatedValues: Record<string, any>; | ||||||
|  |     relationResults: any[]; | ||||||
|  |     relationAggregatedFieldsResult: Record<string, any>; | ||||||
|  |     relationName: string; | ||||||
|  |   }): void { | ||||||
|     parentRecords.forEach((item) => { |     parentRecords.forEach((item) => { | ||||||
|       if (relationResults.length === 0) { |       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`]) ?? |         relationResults.find((rel) => rel.id === item[`${relationName}Id`]) ?? | ||||||
|         null; |         null; | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |     parentObjectRecordsAggregatedValues[relationName] = | ||||||
|  |       relationAggregatedFieldsResult; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ import graphqlFields from 'graphql-fields'; | |||||||
| import { In, InsertResult } from 'typeorm'; | import { In, InsertResult } from 'typeorm'; | ||||||
|  |  | ||||||
| import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; | 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 { 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'; | 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() | @Injectable() | ||||||
| export class GraphqlQueryCreateManyResolverService | export class GraphqlQueryCreateManyResolverService | ||||||
|   implements ResolverService<CreateManyResolverArgs, IRecord[]> |   implements ResolverService<CreateManyResolverArgs, ObjectRecord[]> | ||||||
| { | { | ||||||
|   constructor( |   constructor( | ||||||
|     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, |     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   async resolve<ObjectRecord extends IRecord = IRecord>( |   async resolve<T extends ObjectRecord = ObjectRecord>( | ||||||
|     args: CreateManyResolverArgs<Partial<ObjectRecord>>, |     args: CreateManyResolverArgs<Partial<ObjectRecord>>, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<ObjectRecord[]> { |   ): Promise<T[]> { | ||||||
|     const { authContext, info, objectMetadataMap, objectMetadataMapItem } = |     const { | ||||||
|       options; |       authContext, | ||||||
|  |       info, | ||||||
|  |       objectMetadataMaps, | ||||||
|  |       objectMetadataItemWithFieldMaps, | ||||||
|  |     } = options; | ||||||
|     const dataSource = |     const dataSource = | ||||||
|       await this.twentyORMGlobalManager.getDataSourceForWorkspace( |       await this.twentyORMGlobalManager.getDataSourceForWorkspace( | ||||||
|         authContext.workspace.id, |         authContext.workspace.id, | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|     const repository = dataSource.getRepository( |     const repository = dataSource.getRepository( | ||||||
|       objectMetadataMapItem.nameSingular, |       objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const graphqlQueryParser = new GraphqlQueryParser( |     const graphqlQueryParser = new GraphqlQueryParser( | ||||||
|       objectMetadataMapItem.fields, |       objectMetadataItemWithFieldMaps.fieldsByName, | ||||||
|       objectMetadataMap, |       objectMetadataMaps, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const selectedFields = graphqlFields(info); |     const selectedFields = graphqlFields(info); | ||||||
|  |  | ||||||
|     const { relations } = graphqlQueryParser.parseSelectedFields( |     const { relations } = graphqlQueryParser.parseSelectedFields( | ||||||
|       objectMetadataMapItem, |       objectMetadataItemWithFieldMaps, | ||||||
|       selectedFields, |       selectedFields, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
| @@ -59,7 +64,7 @@ export class GraphqlQueryCreateManyResolverService | |||||||
|         }); |         }); | ||||||
|  |  | ||||||
|     const queryBuilder = repository.createQueryBuilder( |     const queryBuilder = repository.createQueryBuilder( | ||||||
|       objectMetadataMapItem.nameSingular, |       objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const nonFormattedUpsertedRecords = (await queryBuilder |     const nonFormattedUpsertedRecords = (await queryBuilder | ||||||
| @@ -71,42 +76,42 @@ export class GraphqlQueryCreateManyResolverService | |||||||
|  |  | ||||||
|     const upsertedRecords = formatResult( |     const upsertedRecords = formatResult( | ||||||
|       nonFormattedUpsertedRecords, |       nonFormattedUpsertedRecords, | ||||||
|       objectMetadataMapItem, |       objectMetadataItemWithFieldMaps, | ||||||
|       objectMetadataMap, |       objectMetadataMaps, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); |     const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); | ||||||
|  |  | ||||||
|     if (relations) { |     if (relations) { | ||||||
|       await processNestedRelationsHelper.processNestedRelations( |       await processNestedRelationsHelper.processNestedRelations({ | ||||||
|         objectMetadataMap, |         objectMetadataMaps, | ||||||
|         objectMetadataMapItem, |         parentObjectMetadataItem: objectMetadataItemWithFieldMaps, | ||||||
|         upsertedRecords, |         parentObjectRecords: upsertedRecords, | ||||||
|         relations, |         relations, | ||||||
|         QUERY_MAX_RECORDS, |         limit: QUERY_MAX_RECORDS, | ||||||
|         authContext, |         authContext, | ||||||
|         dataSource, |         dataSource, | ||||||
|       ); |       }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const typeORMObjectRecordsParser = |     const typeORMObjectRecordsParser = | ||||||
|       new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); |       new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); | ||||||
|  |  | ||||||
|     return upsertedRecords.map((record: ObjectRecord) => |     return upsertedRecords.map((record: T) => | ||||||
|       typeORMObjectRecordsParser.processRecord({ |       typeORMObjectRecordsParser.processRecord({ | ||||||
|         objectRecord: record, |         objectRecord: record, | ||||||
|         objectName: objectMetadataMapItem.nameSingular, |         objectName: objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|         take: 1, |         take: 1, | ||||||
|         totalCount: 1, |         totalCount: 1, | ||||||
|       }), |       }), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async validate<ObjectRecord extends IRecord>( |   async validate<T extends ObjectRecord>( | ||||||
|     args: CreateManyResolverArgs<Partial<ObjectRecord>>, |     args: CreateManyResolverArgs<Partial<T>>, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
|     assertMutationNotOnRemoteObject(options.objectMetadataItem); |     assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps); | ||||||
|  |  | ||||||
|     args.data.forEach((record) => { |     args.data.forEach((record) => { | ||||||
|       if (record?.id) { |       if (record?.id) { | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common'; | |||||||
| import graphqlFields from 'graphql-fields'; | import graphqlFields from 'graphql-fields'; | ||||||
|  |  | ||||||
| import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; | 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 { 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'; | 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() | @Injectable() | ||||||
| export class GraphqlQueryDestroyManyResolverService | export class GraphqlQueryDestroyManyResolverService | ||||||
|   implements ResolverService<DestroyManyResolverArgs, IRecord[]> |   implements ResolverService<DestroyManyResolverArgs, ObjectRecord[]> | ||||||
| { | { | ||||||
|   constructor( |   constructor( | ||||||
|     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, |     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   async resolve<ObjectRecord extends IRecord = IRecord>( |   async resolve<T extends ObjectRecord = ObjectRecord>( | ||||||
|     args: DestroyManyResolverArgs, |     args: DestroyManyResolverArgs, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<ObjectRecord[]> { |   ): Promise<T[]> { | ||||||
|     const { authContext, objectMetadataMapItem, objectMetadataMap, info } = |     const { | ||||||
|       options; |       authContext, | ||||||
|  |       objectMetadataItemWithFieldMaps, | ||||||
|  |       objectMetadataMaps, | ||||||
|  |       info, | ||||||
|  |     } = options; | ||||||
|  |  | ||||||
|     const dataSource = |     const dataSource = | ||||||
|       await this.twentyORMGlobalManager.getDataSourceForWorkspace( |       await this.twentyORMGlobalManager.getDataSourceForWorkspace( | ||||||
|         authContext.workspace.id, |         authContext.workspace.id, | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|     const repository = dataSource.getRepository( |     const repository = dataSource.getRepository( | ||||||
|       objectMetadataMapItem.nameSingular, |       objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const graphqlQueryParser = new GraphqlQueryParser( |     const graphqlQueryParser = new GraphqlQueryParser( | ||||||
|       objectMetadataMapItem.fields, |       objectMetadataItemWithFieldMaps.fieldsByName, | ||||||
|       objectMetadataMap, |       objectMetadataMaps, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const selectedFields = graphqlFields(info); |     const selectedFields = graphqlFields(info); | ||||||
|  |  | ||||||
|     const { relations } = graphqlQueryParser.parseSelectedFields( |     const { relations } = graphqlQueryParser.parseSelectedFields( | ||||||
|       objectMetadataMapItem, |       objectMetadataItemWithFieldMaps, | ||||||
|       selectedFields, |       selectedFields, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const queryBuilder = repository.createQueryBuilder( |     const queryBuilder = repository.createQueryBuilder( | ||||||
|       objectMetadataMapItem.nameSingular, |       objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder( |     const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder( | ||||||
|       queryBuilder, |       queryBuilder, | ||||||
|       objectMetadataMapItem.nameSingular, |       objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|       args.filter, |       args.filter, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
| @@ -66,31 +71,31 @@ export class GraphqlQueryDestroyManyResolverService | |||||||
|  |  | ||||||
|     const deletedRecords = formatResult( |     const deletedRecords = formatResult( | ||||||
|       nonFormattedDeletedObjectRecords.raw, |       nonFormattedDeletedObjectRecords.raw, | ||||||
|       objectMetadataMapItem, |       objectMetadataItemWithFieldMaps, | ||||||
|       objectMetadataMap, |       objectMetadataMaps, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); |     const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); | ||||||
|  |  | ||||||
|     if (relations) { |     if (relations) { | ||||||
|       await processNestedRelationsHelper.processNestedRelations( |       await processNestedRelationsHelper.processNestedRelations({ | ||||||
|         objectMetadataMap, |         objectMetadataMaps, | ||||||
|         objectMetadataMapItem, |         parentObjectMetadataItem: objectMetadataItemWithFieldMaps, | ||||||
|         deletedRecords, |         parentObjectRecords: deletedRecords, | ||||||
|         relations, |         relations, | ||||||
|         QUERY_MAX_RECORDS, |         limit: QUERY_MAX_RECORDS, | ||||||
|         authContext, |         authContext, | ||||||
|         dataSource, |         dataSource, | ||||||
|       ); |       }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const typeORMObjectRecordsParser = |     const typeORMObjectRecordsParser = | ||||||
|       new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); |       new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); | ||||||
|  |  | ||||||
|     return deletedRecords.map((record: ObjectRecord) => |     return deletedRecords.map((record: T) => | ||||||
|       typeORMObjectRecordsParser.processRecord({ |       typeORMObjectRecordsParser.processRecord({ | ||||||
|         objectRecord: record, |         objectRecord: record, | ||||||
|         objectName: objectMetadataMapItem.nameSingular, |         objectName: objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|         take: 1, |         take: 1, | ||||||
|         totalCount: 1, |         totalCount: 1, | ||||||
|       }), |       }), | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common'; | |||||||
| import graphqlFields from 'graphql-fields'; | import graphqlFields from 'graphql-fields'; | ||||||
|  |  | ||||||
| import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; | 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 { 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'; | 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() | @Injectable() | ||||||
| export class GraphqlQueryDestroyOneResolverService | export class GraphqlQueryDestroyOneResolverService | ||||||
|   implements ResolverService<DestroyOneResolverArgs, IRecord> |   implements ResolverService<DestroyOneResolverArgs, ObjectRecord> | ||||||
| { | { | ||||||
|   constructor( |   constructor( | ||||||
|     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, |     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   async resolve<ObjectRecord extends IRecord = IRecord>( |   async resolve<T extends ObjectRecord = ObjectRecord>( | ||||||
|     args: DestroyOneResolverArgs, |     args: DestroyOneResolverArgs, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<ObjectRecord> { |   ): Promise<T> { | ||||||
|     const { authContext, objectMetadataMapItem, objectMetadataMap, info } = |     const { | ||||||
|       options; |       authContext, | ||||||
|  |       objectMetadataItemWithFieldMaps, | ||||||
|  |       objectMetadataMaps, | ||||||
|  |       info, | ||||||
|  |     } = options; | ||||||
|  |  | ||||||
|     const dataSource = |     const dataSource = | ||||||
|       await this.twentyORMGlobalManager.getDataSourceForWorkspace( |       await this.twentyORMGlobalManager.getDataSourceForWorkspace( | ||||||
|         authContext.workspace.id, |         authContext.workspace.id, | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|     const repository = dataSource.getRepository( |     const repository = dataSource.getRepository( | ||||||
|       objectMetadataMapItem.nameSingular, |       objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const graphqlQueryParser = new GraphqlQueryParser( |     const graphqlQueryParser = new GraphqlQueryParser( | ||||||
|       objectMetadataMapItem.fields, |       objectMetadataItemWithFieldMaps.fieldsByName, | ||||||
|       objectMetadataMap, |       objectMetadataMaps, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const selectedFields = graphqlFields(info); |     const selectedFields = graphqlFields(info); | ||||||
|  |  | ||||||
|     const { relations } = graphqlQueryParser.parseSelectedFields( |     const { relations } = graphqlQueryParser.parseSelectedFields( | ||||||
|       objectMetadataMapItem, |       objectMetadataItemWithFieldMaps, | ||||||
|       selectedFields, |       selectedFields, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const queryBuilder = repository.createQueryBuilder( |     const queryBuilder = repository.createQueryBuilder( | ||||||
|       objectMetadataMapItem.nameSingular, |       objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const nonFormattedDeletedObjectRecords = await queryBuilder |     const nonFormattedDeletedObjectRecords = await queryBuilder | ||||||
|       .where(`"${objectMetadataMapItem.nameSingular}".id = :id`, { |       .where(`"${objectMetadataItemWithFieldMaps.nameSingular}".id = :id`, { | ||||||
|         id: args.id, |         id: args.id, | ||||||
|       }) |       }) | ||||||
|       .take(1) |       .take(1) | ||||||
| @@ -75,30 +80,30 @@ export class GraphqlQueryDestroyOneResolverService | |||||||
|  |  | ||||||
|     const recordBeforeDeletion = formatResult( |     const recordBeforeDeletion = formatResult( | ||||||
|       nonFormattedDeletedObjectRecords.raw, |       nonFormattedDeletedObjectRecords.raw, | ||||||
|       objectMetadataMapItem, |       objectMetadataItemWithFieldMaps, | ||||||
|       objectMetadataMap, |       objectMetadataMaps, | ||||||
|     )[0]; |     )[0]; | ||||||
|  |  | ||||||
|     const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); |     const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); | ||||||
|  |  | ||||||
|     if (relations) { |     if (relations) { | ||||||
|       await processNestedRelationsHelper.processNestedRelations( |       await processNestedRelationsHelper.processNestedRelations({ | ||||||
|         objectMetadataMap, |         objectMetadataMaps, | ||||||
|         objectMetadataMapItem, |         parentObjectMetadataItem: objectMetadataItemWithFieldMaps, | ||||||
|         [recordBeforeDeletion], |         parentObjectRecords: [recordBeforeDeletion], | ||||||
|         relations, |         relations, | ||||||
|         QUERY_MAX_RECORDS, |         limit: QUERY_MAX_RECORDS, | ||||||
|         authContext, |         authContext, | ||||||
|         dataSource, |         dataSource, | ||||||
|       ); |       }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const typeORMObjectRecordsParser = |     const typeORMObjectRecordsParser = | ||||||
|       new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); |       new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); | ||||||
|  |  | ||||||
|     return typeORMObjectRecordsParser.processRecord({ |     return typeORMObjectRecordsParser.processRecord({ | ||||||
|       objectRecord: recordBeforeDeletion, |       objectRecord: recordBeforeDeletion, | ||||||
|       objectName: objectMetadataMapItem.nameSingular, |       objectName: objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|       take: 1, |       take: 1, | ||||||
|       totalCount: 1, |       totalCount: 1, | ||||||
|     }); |     }); | ||||||
|   | |||||||
| @@ -5,10 +5,10 @@ import { In } from 'typeorm'; | |||||||
|  |  | ||||||
| import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; | import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; | ||||||
| import { | import { | ||||||
|   Record as IRecord, |   ObjectRecord, | ||||||
|  |   ObjectRecordFilter, | ||||||
|   OrderByDirection, |   OrderByDirection, | ||||||
|   RecordFilter, | } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; | ||||||
| } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; |  | ||||||
| import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.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 { 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'; | 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 { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; | ||||||
| import { settings } from 'src/engine/constants/settings'; | import { settings } from 'src/engine/constants/settings'; | ||||||
| import { DUPLICATE_CRITERIA_COLLECTION } from 'src/engine/core-modules/duplicate/constants/duplicate-criteria.constants'; | 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 { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; | ||||||
| import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; | import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; | ||||||
| import { formatResult } from 'src/engine/twenty-orm/utils/format-result.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() | @Injectable() | ||||||
| export class GraphqlQueryFindDuplicatesResolverService | export class GraphqlQueryFindDuplicatesResolverService | ||||||
|   implements |   implements | ||||||
|     ResolverService<FindDuplicatesResolverArgs, IConnection<IRecord>[]> |     ResolverService<FindDuplicatesResolverArgs, IConnection<ObjectRecord>[]> | ||||||
| { | { | ||||||
|   constructor( |   constructor( | ||||||
|     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, |     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   async resolve<ObjectRecord extends IRecord = IRecord>( |   async resolve<T extends ObjectRecord = ObjectRecord>( | ||||||
|     args: FindDuplicatesResolverArgs<Partial<ObjectRecord>>, |     args: FindDuplicatesResolverArgs<Partial<T>>, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<IConnection<ObjectRecord>[]> { |   ): Promise<IConnection<T>[]> { | ||||||
|     const { authContext, objectMetadataMapItem, objectMetadataMap } = options; |     const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = | ||||||
|  |       options; | ||||||
|  |  | ||||||
|     const dataSource = |     const dataSource = | ||||||
|       await this.twentyORMGlobalManager.getDataSourceForWorkspace( |       await this.twentyORMGlobalManager.getDataSourceForWorkspace( | ||||||
|         authContext.workspace.id, |         authContext.workspace.id, | ||||||
|       ); |       ); | ||||||
|     const repository = dataSource.getRepository( |     const repository = dataSource.getRepository( | ||||||
|       objectMetadataMapItem.nameSingular, |       objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|     ); |     ); | ||||||
|     const existingRecordsQueryBuilder = repository.createQueryBuilder( |     const existingRecordsQueryBuilder = repository.createQueryBuilder( | ||||||
|       objectMetadataMapItem.nameSingular, |       objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|     ); |     ); | ||||||
|     const duplicateRecordsQueryBuilder = repository.createQueryBuilder( |     const duplicateRecordsQueryBuilder = repository.createQueryBuilder( | ||||||
|       objectMetadataMapItem.nameSingular, |       objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const graphqlQueryParser = new GraphqlQueryParser( |     const graphqlQueryParser = new GraphqlQueryParser( | ||||||
|       objectMetadataMap[objectMetadataMapItem.nameSingular].fields, |       objectMetadataMaps.byNameSingular[ | ||||||
|       objectMetadataMap, |         objectMetadataItemWithFieldMaps.nameSingular | ||||||
|  |       ].fieldsByName, | ||||||
|  |       objectMetadataMaps, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const typeORMObjectRecordsParser = |     const typeORMObjectRecordsParser = | ||||||
|       new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); |       new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); | ||||||
|  |  | ||||||
|     let objectRecords: Partial<ObjectRecord>[] = []; |     let objectRecords: Partial<T>[] = []; | ||||||
|  |  | ||||||
|     if (args.ids) { |     if (args.ids) { | ||||||
|       const nonFormattedObjectRecords = (await existingRecordsQueryBuilder |       const nonFormattedObjectRecords = (await existingRecordsQueryBuilder | ||||||
|         .where({ id: In(args.ids) }) |         .where({ id: In(args.ids) }) | ||||||
|         .getMany()) as ObjectRecord[]; |         .getMany()) as T[]; | ||||||
|  |  | ||||||
|       objectRecords = formatResult( |       objectRecords = formatResult( | ||||||
|         nonFormattedObjectRecords, |         nonFormattedObjectRecords, | ||||||
|         objectMetadataMapItem, |         objectMetadataItemWithFieldMaps, | ||||||
|         objectMetadataMap, |         objectMetadataMaps, | ||||||
|       ); |       ); | ||||||
|     } else if (args.data && !isEmpty(args.data)) { |     } else if (args.data && !isEmpty(args.data)) { | ||||||
|       objectRecords = formatData(args.data, objectMetadataMapItem); |       objectRecords = formatData(args.data, objectMetadataItemWithFieldMaps); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const duplicateConnections: IConnection<ObjectRecord>[] = await Promise.all( |     const duplicateConnections: IConnection<T>[] = await Promise.all( | ||||||
|       objectRecords.map(async (record) => { |       objectRecords.map(async (record) => { | ||||||
|         const duplicateConditions = this.buildDuplicateConditions( |         const duplicateConditions = this.buildDuplicateConditions( | ||||||
|           objectMetadataMapItem, |           objectMetadataItemWithFieldMaps, | ||||||
|           [record], |           [record], | ||||||
|           record.id, |           record.id, | ||||||
|         ); |         ); | ||||||
| @@ -90,7 +93,7 @@ export class GraphqlQueryFindDuplicatesResolverService | |||||||
|         if (isEmpty(duplicateConditions)) { |         if (isEmpty(duplicateConditions)) { | ||||||
|           return typeORMObjectRecordsParser.createConnection({ |           return typeORMObjectRecordsParser.createConnection({ | ||||||
|             objectRecords: [], |             objectRecords: [], | ||||||
|             objectName: objectMetadataMapItem.nameSingular, |             objectName: objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|             take: 0, |             take: 0, | ||||||
|             totalCount: 0, |             totalCount: 0, | ||||||
|             order: [{ id: OrderByDirection.AscNullsFirst }], |             order: [{ id: OrderByDirection.AscNullsFirst }], | ||||||
| @@ -101,22 +104,22 @@ export class GraphqlQueryFindDuplicatesResolverService | |||||||
|  |  | ||||||
|         const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder( |         const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder( | ||||||
|           duplicateRecordsQueryBuilder, |           duplicateRecordsQueryBuilder, | ||||||
|           objectMetadataMapItem.nameSingular, |           objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|           duplicateConditions, |           duplicateConditions, | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         const nonFormattedDuplicates = |         const nonFormattedDuplicates = | ||||||
|           (await withFilterQueryBuilder.getMany()) as ObjectRecord[]; |           (await withFilterQueryBuilder.getMany()) as T[]; | ||||||
|  |  | ||||||
|         const duplicates = formatResult( |         const duplicates = formatResult( | ||||||
|           nonFormattedDuplicates, |           nonFormattedDuplicates, | ||||||
|           objectMetadataMapItem, |           objectMetadataItemWithFieldMaps, | ||||||
|           objectMetadataMap, |           objectMetadataMaps, | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return typeORMObjectRecordsParser.createConnection({ |         return typeORMObjectRecordsParser.createConnection({ | ||||||
|           objectRecords: duplicates, |           objectRecords: duplicates, | ||||||
|           objectName: objectMetadataMapItem.nameSingular, |           objectName: objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|           take: duplicates.length, |           take: duplicates.length, | ||||||
|           totalCount: duplicates.length, |           totalCount: duplicates.length, | ||||||
|           order: [{ id: OrderByDirection.AscNullsFirst }], |           order: [{ id: OrderByDirection.AscNullsFirst }], | ||||||
| @@ -130,16 +133,16 @@ export class GraphqlQueryFindDuplicatesResolverService | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private buildDuplicateConditions( |   private buildDuplicateConditions( | ||||||
|     objectMetadataMapItem: ObjectMetadataMapItem, |     objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, | ||||||
|     records?: Partial<IRecord>[] | undefined, |     records?: Partial<ObjectRecord>[] | undefined, | ||||||
|     filteringByExistingRecordId?: string, |     filteringByExistingRecordId?: string, | ||||||
|   ): Partial<RecordFilter> { |   ): Partial<ObjectRecordFilter> { | ||||||
|     if (!records || records.length === 0) { |     if (!records || records.length === 0) { | ||||||
|       return {}; |       return {}; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const criteriaCollection = this.getApplicableDuplicateCriteriaCollection( |     const criteriaCollection = this.getApplicableDuplicateCriteriaCollection( | ||||||
|       objectMetadataMapItem, |       objectMetadataItemWithFieldMaps, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const conditions = records.flatMap((record) => { |     const conditions = records.flatMap((record) => { | ||||||
| @@ -164,7 +167,7 @@ export class GraphqlQueryFindDuplicatesResolverService | |||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     const filter: Partial<RecordFilter> = {}; |     const filter: Partial<ObjectRecordFilter> = {}; | ||||||
|  |  | ||||||
|     if (conditions && !isEmpty(conditions)) { |     if (conditions && !isEmpty(conditions)) { | ||||||
|       filter.or = conditions; |       filter.or = conditions; | ||||||
| @@ -178,11 +181,12 @@ export class GraphqlQueryFindDuplicatesResolverService | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private getApplicableDuplicateCriteriaCollection( |   private getApplicableDuplicateCriteriaCollection( | ||||||
|     objectMetadataMapItem: ObjectMetadataMapItem, |     objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, | ||||||
|   ) { |   ) { | ||||||
|     return DUPLICATE_CRITERIA_COLLECTION.filter( |     return DUPLICATE_CRITERIA_COLLECTION.filter( | ||||||
|       (duplicateCriteria) => |       (duplicateCriteria) => | ||||||
|         duplicateCriteria.objectName === objectMetadataMapItem.nameSingular, |         duplicateCriteria.objectName === | ||||||
|  |         objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,15 +1,14 @@ | |||||||
| import { Injectable } from '@nestjs/common'; | import { Injectable } from '@nestjs/common'; | ||||||
|  |  | ||||||
| import { isDefined } from 'class-validator'; |  | ||||||
| import graphqlFields from 'graphql-fields'; | import graphqlFields from 'graphql-fields'; | ||||||
|  |  | ||||||
| import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; | import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; | ||||||
| import { | import { | ||||||
|   Record as IRecord, |   ObjectRecord, | ||||||
|  |   ObjectRecordFilter, | ||||||
|  |   ObjectRecordOrderBy, | ||||||
|   OrderByDirection, |   OrderByDirection, | ||||||
|   RecordFilter, | } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; | ||||||
|   RecordOrderBy, |  | ||||||
| } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; |  | ||||||
| import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.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 { 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'; | import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; | ||||||
| @@ -19,35 +18,45 @@ import { | |||||||
|   GraphqlQueryRunnerException, |   GraphqlQueryRunnerException, | ||||||
|   GraphqlQueryRunnerExceptionCode, |   GraphqlQueryRunnerExceptionCode, | ||||||
| } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; | } 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 { 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 { 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 { 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 { computeCursorArgFilter } from 'src/engine/api/graphql/graphql-query-runner/utils/compute-cursor-arg-filter'; | ||||||
| import { | import { | ||||||
|   getCursor, |   getCursor, | ||||||
|   getPaginationInfo, |   getPaginationInfo, | ||||||
| } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util'; | } 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 { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; | ||||||
| import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; | import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; | ||||||
|  | import { isDefined } from 'src/utils/is-defined'; | ||||||
|  |  | ||||||
| @Injectable() | @Injectable() | ||||||
| export class GraphqlQueryFindManyResolverService | export class GraphqlQueryFindManyResolverService | ||||||
|   implements ResolverService<FindManyResolverArgs, IConnection<IRecord>> |   implements ResolverService<FindManyResolverArgs, IConnection<ObjectRecord>> | ||||||
| { | { | ||||||
|   constructor( |   constructor( | ||||||
|     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, |     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, | ||||||
|  |     private readonly featureFlagService: FeatureFlagService, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   async resolve< |   async resolve< | ||||||
|     ObjectRecord extends IRecord = IRecord, |     T extends ObjectRecord = ObjectRecord, | ||||||
|     Filter extends RecordFilter = RecordFilter, |     Filter extends ObjectRecordFilter = ObjectRecordFilter, | ||||||
|     OrderBy extends RecordOrderBy = RecordOrderBy, |     OrderBy extends ObjectRecordOrderBy = ObjectRecordOrderBy, | ||||||
|   >( |   >( | ||||||
|     args: FindManyResolverArgs<Filter, OrderBy>, |     args: FindManyResolverArgs<Filter, OrderBy>, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<IConnection<ObjectRecord>> { |   ): Promise<IConnection<T>> { | ||||||
|     const { authContext, objectMetadataMapItem, info, objectMetadataMap } = |     const { | ||||||
|       options; |       authContext, | ||||||
|  |       objectMetadataItemWithFieldMaps, | ||||||
|  |       info, | ||||||
|  |       objectMetadataMaps, | ||||||
|  |     } = options; | ||||||
|  |  | ||||||
|     const dataSource = |     const dataSource = | ||||||
|       await this.twentyORMGlobalManager.getDataSourceForWorkspace( |       await this.twentyORMGlobalManager.getDataSourceForWorkspace( | ||||||
| @@ -55,48 +64,44 @@ export class GraphqlQueryFindManyResolverService | |||||||
|       ); |       ); | ||||||
|  |  | ||||||
|     const repository = dataSource.getRepository( |     const repository = dataSource.getRepository( | ||||||
|       objectMetadataMapItem.nameSingular, |       objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const queryBuilder = repository.createQueryBuilder( |     const queryBuilder = repository.createQueryBuilder( | ||||||
|       objectMetadataMapItem.nameSingular, |       objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const countQueryBuilder = repository.createQueryBuilder( |     const aggregateQueryBuilder = repository.createQueryBuilder( | ||||||
|       objectMetadataMapItem.nameSingular, |       objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const graphqlQueryParser = new GraphqlQueryParser( |     const graphqlQueryParser = new GraphqlQueryParser( | ||||||
|       objectMetadataMapItem.fields, |       objectMetadataItemWithFieldMaps.fieldsByName, | ||||||
|       objectMetadataMap, |       objectMetadataMaps, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const withFilterCountQueryBuilder = graphqlQueryParser.applyFilterToBuilder( |     const withFilterAggregateQueryBuilder = | ||||||
|       countQueryBuilder, |       graphqlQueryParser.applyFilterToBuilder( | ||||||
|       objectMetadataMapItem.nameSingular, |         aggregateQueryBuilder, | ||||||
|  |         objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|         args.filter ?? ({} as Filter), |         args.filter ?? ({} as Filter), | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|     const selectedFields = graphqlFields(info); |     const selectedFields = graphqlFields(info); | ||||||
|  |  | ||||||
|     const { relations } = graphqlQueryParser.parseSelectedFields( |     const graphqlQuerySelectedFieldsResult: GraphqlQuerySelectedFieldsResult = | ||||||
|       objectMetadataMapItem, |       graphqlQueryParser.parseSelectedFields( | ||||||
|  |         objectMetadataItemWithFieldMaps, | ||||||
|         selectedFields, |         selectedFields, | ||||||
|       ); |       ); | ||||||
|     const isForwardPagination = !isDefined(args.before); |     const isForwardPagination = !isDefined(args.before); | ||||||
|  |  | ||||||
|     const limit = args.first ?? args.last ?? QUERY_MAX_RECORDS; |     const withDeletedAggregateQueryBuilder = | ||||||
|  |  | ||||||
|     const withDeletedCountQueryBuilder = |  | ||||||
|       graphqlQueryParser.applyDeletedAtToBuilder( |       graphqlQueryParser.applyDeletedAtToBuilder( | ||||||
|         withFilterCountQueryBuilder, |         withFilterAggregateQueryBuilder, | ||||||
|         args.filter ?? ({} as Filter), |         args.filter ?? ({} as Filter), | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|     const totalCount = isDefined(selectedFields.totalCount) |  | ||||||
|       ? await withDeletedCountQueryBuilder.getCount() |  | ||||||
|       : 0; |  | ||||||
|  |  | ||||||
|     const cursor = getCursor(args); |     const cursor = getCursor(args); | ||||||
|  |  | ||||||
|     let appliedFilters = args.filter ?? ({} as Filter); |     let appliedFilters = args.filter ?? ({} as Filter); | ||||||
| @@ -110,7 +115,7 @@ export class GraphqlQueryFindManyResolverService | |||||||
|       const cursorArgFilter = computeCursorArgFilter( |       const cursorArgFilter = computeCursorArgFilter( | ||||||
|         cursor, |         cursor, | ||||||
|         orderByWithIdCondition, |         orderByWithIdCondition, | ||||||
|         objectMetadataMapItem.fields, |         objectMetadataItemWithFieldMaps.fieldsByName, | ||||||
|         isForwardPagination, |         isForwardPagination, | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
| @@ -123,14 +128,14 @@ export class GraphqlQueryFindManyResolverService | |||||||
|  |  | ||||||
|     const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder( |     const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder( | ||||||
|       queryBuilder, |       queryBuilder, | ||||||
|       objectMetadataMapItem.nameSingular, |       objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|       appliedFilters, |       appliedFilters, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const withOrderByQueryBuilder = graphqlQueryParser.applyOrderToBuilder( |     const withOrderByQueryBuilder = graphqlQueryParser.applyOrderToBuilder( | ||||||
|       withFilterQueryBuilder, |       withFilterQueryBuilder, | ||||||
|       orderByWithIdCondition, |       orderByWithIdCondition, | ||||||
|       objectMetadataMapItem.nameSingular, |       objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|       isForwardPagination, |       isForwardPagination, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
| @@ -139,14 +144,36 @@ export class GraphqlQueryFindManyResolverService | |||||||
|       args.filter ?? ({} as Filter), |       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 |     const nonFormattedObjectRecords = await withDeletedQueryBuilder | ||||||
|       .take(limit + 1) |       .take(limit + 1) | ||||||
|       .getMany(); |       .getMany(); | ||||||
|  |  | ||||||
|     const objectRecords = formatResult( |     const objectRecords = formatResult( | ||||||
|       nonFormattedObjectRecords, |       nonFormattedObjectRecords, | ||||||
|       objectMetadataMapItem, |       objectMetadataItemWithFieldMaps, | ||||||
|       objectMetadataMap, |       objectMetadataMaps, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const { hasNextPage, hasPreviousPage } = getPaginationInfo( |     const { hasNextPage, hasPreviousPage } = getPaginationInfo( | ||||||
| @@ -159,37 +186,42 @@ export class GraphqlQueryFindManyResolverService | |||||||
|       objectRecords.pop(); |       objectRecords.pop(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     const parentObjectRecordsAggregatedValues = | ||||||
|  |       await withDeletedAggregateQueryBuilder.getRawOne(); | ||||||
|  |  | ||||||
|     const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); |     const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); | ||||||
|  |  | ||||||
|     if (relations) { |     if (graphqlQuerySelectedFieldsResult.relations) { | ||||||
|       await processNestedRelationsHelper.processNestedRelations( |       await processNestedRelationsHelper.processNestedRelations({ | ||||||
|         objectMetadataMap, |         objectMetadataMaps, | ||||||
|         objectMetadataMapItem, |         parentObjectMetadataItem: objectMetadataItemWithFieldMaps, | ||||||
|         objectRecords, |         parentObjectRecords: objectRecords, | ||||||
|         relations, |         parentObjectRecordsAggregatedValues, | ||||||
|  |         relations: graphqlQuerySelectedFieldsResult.relations, | ||||||
|  |         aggregate: graphqlQuerySelectedFieldsResult.aggregate, | ||||||
|         limit, |         limit, | ||||||
|         authContext, |         authContext, | ||||||
|         dataSource, |         dataSource, | ||||||
|       ); |       }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const typeORMObjectRecordsParser = |     const typeORMObjectRecordsParser = | ||||||
|       new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); |       new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); | ||||||
|  |  | ||||||
|     const result = typeORMObjectRecordsParser.createConnection({ |     return typeORMObjectRecordsParser.createConnection({ | ||||||
|       objectRecords, |       objectRecords, | ||||||
|       objectName: objectMetadataMapItem.nameSingular, |       objectRecordsAggregatedValues: parentObjectRecordsAggregatedValues, | ||||||
|  |       selectedAggregatedFields: graphqlQuerySelectedFieldsResult.aggregate, | ||||||
|  |       objectName: objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|       take: limit, |       take: limit, | ||||||
|       totalCount, |       totalCount: parentObjectRecordsAggregatedValues.totalCount, | ||||||
|       order: orderByWithIdCondition, |       order: orderByWithIdCondition, | ||||||
|       hasNextPage, |       hasNextPage, | ||||||
|       hasPreviousPage, |       hasPreviousPage, | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     return result; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async validate<Filter extends RecordFilter>( |   async validate<Filter extends ObjectRecordFilter>( | ||||||
|     args: FindManyResolverArgs<Filter>, |     args: FindManyResolverArgs<Filter>, | ||||||
|     _options: WorkspaceQueryRunnerOptions, |     _options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
|   | |||||||
| @@ -4,9 +4,9 @@ import graphqlFields from 'graphql-fields'; | |||||||
|  |  | ||||||
| import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; | import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; | ||||||
| import { | import { | ||||||
|   Record as IRecord, |   ObjectRecord, | ||||||
|   RecordFilter, |   ObjectRecordFilter, | ||||||
| } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; | } 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 { 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'; | 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() | @Injectable() | ||||||
| export class GraphqlQueryFindOneResolverService | export class GraphqlQueryFindOneResolverService | ||||||
|   implements ResolverService<FindOneResolverArgs, IRecord> |   implements ResolverService<FindOneResolverArgs, ObjectRecord> | ||||||
| { | { | ||||||
|   constructor( |   constructor( | ||||||
|     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, |     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   async resolve< |   async resolve< | ||||||
|     ObjectRecord extends IRecord = IRecord, |     T extends ObjectRecord = ObjectRecord, | ||||||
|     Filter extends RecordFilter = RecordFilter, |     Filter extends ObjectRecordFilter = ObjectRecordFilter, | ||||||
|   >( |   >( | ||||||
|     args: FindOneResolverArgs<Filter>, |     args: FindOneResolverArgs<Filter>, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<ObjectRecord> { |   ): Promise<T> { | ||||||
|     const { authContext, objectMetadataMapItem, info, objectMetadataMap } = |     const { | ||||||
|       options; |       authContext, | ||||||
|  |       objectMetadataItemWithFieldMaps, | ||||||
|  |       info, | ||||||
|  |       objectMetadataMaps, | ||||||
|  |     } = options; | ||||||
|  |  | ||||||
|     const dataSource = |     const dataSource = | ||||||
|       await this.twentyORMGlobalManager.getDataSourceForWorkspace( |       await this.twentyORMGlobalManager.getDataSourceForWorkspace( | ||||||
| @@ -49,28 +53,28 @@ export class GraphqlQueryFindOneResolverService | |||||||
|       ); |       ); | ||||||
|  |  | ||||||
|     const repository = dataSource.getRepository( |     const repository = dataSource.getRepository( | ||||||
|       objectMetadataMapItem.nameSingular, |       objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const queryBuilder = repository.createQueryBuilder( |     const queryBuilder = repository.createQueryBuilder( | ||||||
|       objectMetadataMapItem.nameSingular, |       objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const graphqlQueryParser = new GraphqlQueryParser( |     const graphqlQueryParser = new GraphqlQueryParser( | ||||||
|       objectMetadataMapItem.fields, |       objectMetadataItemWithFieldMaps.fieldsByName, | ||||||
|       objectMetadataMap, |       objectMetadataMaps, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const selectedFields = graphqlFields(info); |     const selectedFields = graphqlFields(info); | ||||||
|  |  | ||||||
|     const { relations } = graphqlQueryParser.parseSelectedFields( |     const { relations } = graphqlQueryParser.parseSelectedFields( | ||||||
|       objectMetadataMapItem, |       objectMetadataItemWithFieldMaps, | ||||||
|       selectedFields, |       selectedFields, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder( |     const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder( | ||||||
|       queryBuilder, |       queryBuilder, | ||||||
|       objectMetadataMapItem.nameSingular, |       objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|       args.filter ?? ({} as Filter), |       args.filter ?? ({} as Filter), | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
| @@ -83,8 +87,8 @@ export class GraphqlQueryFindOneResolverService | |||||||
|  |  | ||||||
|     const objectRecord = formatResult( |     const objectRecord = formatResult( | ||||||
|       nonFormattedObjectRecord, |       nonFormattedObjectRecord, | ||||||
|       objectMetadataMapItem, |       objectMetadataItemWithFieldMaps, | ||||||
|       objectMetadataMap, |       objectMetadataMaps, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     if (!objectRecord) { |     if (!objectRecord) { | ||||||
| @@ -99,29 +103,29 @@ export class GraphqlQueryFindOneResolverService | |||||||
|     const objectRecords = [objectRecord]; |     const objectRecords = [objectRecord]; | ||||||
|  |  | ||||||
|     if (relations) { |     if (relations) { | ||||||
|       await processNestedRelationsHelper.processNestedRelations( |       await processNestedRelationsHelper.processNestedRelations({ | ||||||
|         objectMetadataMap, |         objectMetadataMaps, | ||||||
|         objectMetadataMapItem, |         parentObjectMetadataItem: objectMetadataItemWithFieldMaps, | ||||||
|         objectRecords, |         parentObjectRecords: objectRecords, | ||||||
|         relations, |         relations, | ||||||
|         QUERY_MAX_RECORDS, |         limit: QUERY_MAX_RECORDS, | ||||||
|         authContext, |         authContext, | ||||||
|         dataSource, |         dataSource, | ||||||
|       ); |       }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const typeORMObjectRecordsParser = |     const typeORMObjectRecordsParser = | ||||||
|       new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); |       new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); | ||||||
|  |  | ||||||
|     return typeORMObjectRecordsParser.processRecord({ |     return typeORMObjectRecordsParser.processRecord({ | ||||||
|       objectRecord: objectRecords[0], |       objectRecord: objectRecords[0], | ||||||
|       objectName: objectMetadataMapItem.nameSingular, |       objectName: objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|       take: 1, |       take: 1, | ||||||
|       totalCount: 1, |       totalCount: 1, | ||||||
|     }) as ObjectRecord; |     }) as T; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async validate<Filter extends RecordFilter>( |   async validate<Filter extends ObjectRecordFilter>( | ||||||
|     args: FindOneResolverArgs<Filter>, |     args: FindOneResolverArgs<Filter>, | ||||||
|     _options: WorkspaceQueryRunnerOptions, |     _options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
|   | |||||||
| @@ -5,10 +5,10 @@ import { Brackets } from 'typeorm'; | |||||||
|  |  | ||||||
| import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; | import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; | ||||||
| import { | import { | ||||||
|   Record as IRecord, |   ObjectRecord, | ||||||
|  |   ObjectRecordFilter, | ||||||
|   OrderByDirection, |   OrderByDirection, | ||||||
|   RecordFilter, | } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; | ||||||
| } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; |  | ||||||
| import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.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 { 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'; | 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() | @Injectable() | ||||||
| export class GraphqlQuerySearchResolverService | export class GraphqlQuerySearchResolverService | ||||||
|   implements ResolverService<SearchResolverArgs, IConnection<IRecord>> |   implements ResolverService<SearchResolverArgs, IConnection<ObjectRecord>> | ||||||
| { | { | ||||||
|   constructor( |   constructor( | ||||||
|     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, |     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   async resolve< |   async resolve< | ||||||
|     ObjectRecord extends IRecord = IRecord, |     T extends ObjectRecord = ObjectRecord, | ||||||
|     Filter extends RecordFilter = RecordFilter, |     Filter extends ObjectRecordFilter = ObjectRecordFilter, | ||||||
|   >( |   >( | ||||||
|     args: SearchResolverArgs, |     args: SearchResolverArgs, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<IConnection<ObjectRecord>> { |   ): Promise<IConnection<T>> { | ||||||
|     const { |     const { | ||||||
|       authContext, |       authContext, | ||||||
|       objectMetadataItem, |       objectMetadataMaps, | ||||||
|       objectMetadataMapItem, |       objectMetadataItemWithFieldMaps, | ||||||
|       objectMetadataMap, |  | ||||||
|       info, |       info, | ||||||
|     } = options; |     } = options; | ||||||
|  |  | ||||||
|     const repository = |     const repository = | ||||||
|       await this.twentyORMGlobalManager.getRepositoryForWorkspace( |       await this.twentyORMGlobalManager.getRepositoryForWorkspace( | ||||||
|         authContext.workspace.id, |         authContext.workspace.id, | ||||||
|         objectMetadataItem.nameSingular, |         objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|     const typeORMObjectRecordsParser = |     const typeORMObjectRecordsParser = | ||||||
|       new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); |       new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); | ||||||
|  |  | ||||||
|     if (!isDefined(args.searchInput)) { |     if (!isDefined(args.searchInput)) { | ||||||
|       return typeORMObjectRecordsParser.createConnection({ |       return typeORMObjectRecordsParser.createConnection({ | ||||||
|         objectRecords: [], |         objectRecords: [], | ||||||
|         objectName: objectMetadataItem.nameSingular, |         objectName: objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|         take: 0, |         take: 0, | ||||||
|         totalCount: 0, |         totalCount: 0, | ||||||
|         order: [{ id: OrderByDirection.AscNullsFirst }], |         order: [{ id: OrderByDirection.AscNullsFirst }], | ||||||
| @@ -69,16 +68,16 @@ export class GraphqlQuerySearchResolverService | |||||||
|     const limit = args?.limit ?? QUERY_MAX_RECORDS; |     const limit = args?.limit ?? QUERY_MAX_RECORDS; | ||||||
|  |  | ||||||
|     const queryBuilder = repository.createQueryBuilder( |     const queryBuilder = repository.createQueryBuilder( | ||||||
|       objectMetadataItem.nameSingular, |       objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|     ); |     ); | ||||||
|     const graphqlQueryParser = new GraphqlQueryParser( |     const graphqlQueryParser = new GraphqlQueryParser( | ||||||
|       objectMetadataMapItem.fields, |       objectMetadataItemWithFieldMaps.fieldsByName, | ||||||
|       objectMetadataMap, |       objectMetadataMaps, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const queryBuilderWithFilter = graphqlQueryParser.applyFilterToBuilder( |     const queryBuilderWithFilter = graphqlQueryParser.applyFilterToBuilder( | ||||||
|       queryBuilder, |       queryBuilder, | ||||||
|       objectMetadataMapItem.nameSingular, |       objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|       args.filter ?? ({} as Filter), |       args.filter ?? ({} as Filter), | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
| @@ -109,7 +108,7 @@ export class GraphqlQuerySearchResolverService | |||||||
|       .setParameter('searchTerms', searchTerms) |       .setParameter('searchTerms', searchTerms) | ||||||
|       .setParameter('searchTermsOr', searchTermsOr) |       .setParameter('searchTermsOr', searchTermsOr) | ||||||
|       .take(limit) |       .take(limit) | ||||||
|       .getMany()) as ObjectRecord[]; |       .getMany()) as T[]; | ||||||
|  |  | ||||||
|     const objectRecords = await repository.formatResult(resultsWithTsVector); |     const objectRecords = await repository.formatResult(resultsWithTsVector); | ||||||
|  |  | ||||||
| @@ -122,7 +121,7 @@ export class GraphqlQuerySearchResolverService | |||||||
|  |  | ||||||
|     return typeORMObjectRecordsParser.createConnection({ |     return typeORMObjectRecordsParser.createConnection({ | ||||||
|       objectRecords: objectRecords ?? [], |       objectRecords: objectRecords ?? [], | ||||||
|       objectName: objectMetadataItem.nameSingular, |       objectName: objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|       take: limit, |       take: limit, | ||||||
|       totalCount, |       totalCount, | ||||||
|       order, |       order, | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common'; | |||||||
| import graphqlFields from 'graphql-fields'; | import graphqlFields from 'graphql-fields'; | ||||||
|  |  | ||||||
| import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; | 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 { 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'; | 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() | @Injectable() | ||||||
| export class GraphqlQueryUpdateManyResolverService | export class GraphqlQueryUpdateManyResolverService | ||||||
|   implements ResolverService<UpdateManyResolverArgs, IRecord[]> |   implements ResolverService<UpdateManyResolverArgs, ObjectRecord[]> | ||||||
| { | { | ||||||
|   constructor( |   constructor( | ||||||
|     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, |     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   async resolve<ObjectRecord extends IRecord = IRecord>( |   async resolve<T extends ObjectRecord = ObjectRecord>( | ||||||
|     args: UpdateManyResolverArgs<Partial<ObjectRecord>>, |     args: UpdateManyResolverArgs<Partial<T>>, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<ObjectRecord[]> { |   ): Promise<T[]> { | ||||||
|     const { authContext, objectMetadataMapItem, objectMetadataMap, info } = |     const { | ||||||
|       options; |       authContext, | ||||||
|  |       objectMetadataItemWithFieldMaps, | ||||||
|  |       objectMetadataMaps, | ||||||
|  |       info, | ||||||
|  |     } = options; | ||||||
|  |  | ||||||
|     const dataSource = |     const dataSource = | ||||||
|       await this.twentyORMGlobalManager.getDataSourceForWorkspace( |       await this.twentyORMGlobalManager.getDataSourceForWorkspace( | ||||||
| @@ -39,28 +43,28 @@ export class GraphqlQueryUpdateManyResolverService | |||||||
|       ); |       ); | ||||||
|  |  | ||||||
|     const repository = dataSource.getRepository( |     const repository = dataSource.getRepository( | ||||||
|       objectMetadataMapItem.nameSingular, |       objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const graphqlQueryParser = new GraphqlQueryParser( |     const graphqlQueryParser = new GraphqlQueryParser( | ||||||
|       objectMetadataMapItem.fields, |       objectMetadataItemWithFieldMaps.fieldsByName, | ||||||
|       objectMetadataMap, |       objectMetadataMaps, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const selectedFields = graphqlFields(info); |     const selectedFields = graphqlFields(info); | ||||||
|  |  | ||||||
|     const { relations } = graphqlQueryParser.parseSelectedFields( |     const { relations } = graphqlQueryParser.parseSelectedFields( | ||||||
|       objectMetadataMapItem, |       objectMetadataItemWithFieldMaps, | ||||||
|       selectedFields, |       selectedFields, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const queryBuilder = repository.createQueryBuilder( |     const queryBuilder = repository.createQueryBuilder( | ||||||
|       objectMetadataMapItem.nameSingular, |       objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const tableName = computeTableName( |     const tableName = computeTableName( | ||||||
|       objectMetadataMapItem.nameSingular, |       objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|       objectMetadataMapItem.isCustom, |       objectMetadataItemWithFieldMaps.isCustom, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder( |     const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder( | ||||||
| @@ -69,7 +73,7 @@ export class GraphqlQueryUpdateManyResolverService | |||||||
|       args.filter, |       args.filter, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const data = formatData(args.data, objectMetadataMapItem); |     const data = formatData(args.data, objectMetadataItemWithFieldMaps); | ||||||
|  |  | ||||||
|     const nonFormattedUpdatedObjectRecords = await withFilterQueryBuilder |     const nonFormattedUpdatedObjectRecords = await withFilterQueryBuilder | ||||||
|       .update(data) |       .update(data) | ||||||
| @@ -78,42 +82,42 @@ export class GraphqlQueryUpdateManyResolverService | |||||||
|  |  | ||||||
|     const updatedRecords = formatResult( |     const updatedRecords = formatResult( | ||||||
|       nonFormattedUpdatedObjectRecords.raw, |       nonFormattedUpdatedObjectRecords.raw, | ||||||
|       objectMetadataMapItem, |       objectMetadataItemWithFieldMaps, | ||||||
|       objectMetadataMap, |       objectMetadataMaps, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); |     const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); | ||||||
|  |  | ||||||
|     if (relations) { |     if (relations) { | ||||||
|       await processNestedRelationsHelper.processNestedRelations( |       await processNestedRelationsHelper.processNestedRelations({ | ||||||
|         objectMetadataMap, |         objectMetadataMaps, | ||||||
|         objectMetadataMapItem, |         parentObjectMetadataItem: objectMetadataItemWithFieldMaps, | ||||||
|         updatedRecords, |         parentObjectRecords: updatedRecords, | ||||||
|         relations, |         relations, | ||||||
|         QUERY_MAX_RECORDS, |         limit: QUERY_MAX_RECORDS, | ||||||
|         authContext, |         authContext, | ||||||
|         dataSource, |         dataSource, | ||||||
|       ); |       }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const typeORMObjectRecordsParser = |     const typeORMObjectRecordsParser = | ||||||
|       new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); |       new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); | ||||||
|  |  | ||||||
|     return updatedRecords.map((record: ObjectRecord) => |     return updatedRecords.map((record: T) => | ||||||
|       typeORMObjectRecordsParser.processRecord({ |       typeORMObjectRecordsParser.processRecord({ | ||||||
|         objectRecord: record, |         objectRecord: record, | ||||||
|         objectName: objectMetadataMapItem.nameSingular, |         objectName: objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|         take: 1, |         take: 1, | ||||||
|         totalCount: 1, |         totalCount: 1, | ||||||
|       }), |       }), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async validate<ObjectRecord extends IRecord = IRecord>( |   async validate<T extends ObjectRecord = ObjectRecord>( | ||||||
|     args: UpdateManyResolverArgs<Partial<ObjectRecord>>, |     args: UpdateManyResolverArgs<Partial<T>>, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
|     assertMutationNotOnRemoteObject(options.objectMetadataMapItem); |     assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps); | ||||||
|     if (!args.filter) { |     if (!args.filter) { | ||||||
|       throw new Error('Filter is required'); |       throw new Error('Filter is required'); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common'; | |||||||
| import graphqlFields from 'graphql-fields'; | import graphqlFields from 'graphql-fields'; | ||||||
|  |  | ||||||
| import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; | 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 { 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'; | 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() | @Injectable() | ||||||
| export class GraphqlQueryUpdateOneResolverService | export class GraphqlQueryUpdateOneResolverService | ||||||
|   implements ResolverService<UpdateOneResolverArgs, IRecord> |   implements ResolverService<UpdateOneResolverArgs, ObjectRecord> | ||||||
| { | { | ||||||
|   constructor( |   constructor( | ||||||
|     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, |     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   async resolve<ObjectRecord extends IRecord = IRecord>( |   async resolve<T extends ObjectRecord = ObjectRecord>( | ||||||
|     args: UpdateOneResolverArgs<Partial<ObjectRecord>>, |     args: UpdateOneResolverArgs<Partial<T>>, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<ObjectRecord> { |   ): Promise<T> { | ||||||
|     const { authContext, objectMetadataMapItem, objectMetadataMap, info } = |     const { | ||||||
|       options; |       authContext, | ||||||
|  |       objectMetadataItemWithFieldMaps, | ||||||
|  |       objectMetadataMaps, | ||||||
|  |       info, | ||||||
|  |     } = options; | ||||||
|  |  | ||||||
|     const dataSource = |     const dataSource = | ||||||
|       await this.twentyORMGlobalManager.getDataSourceForWorkspace( |       await this.twentyORMGlobalManager.getDataSourceForWorkspace( | ||||||
| @@ -42,26 +46,26 @@ export class GraphqlQueryUpdateOneResolverService | |||||||
|       ); |       ); | ||||||
|  |  | ||||||
|     const repository = dataSource.getRepository( |     const repository = dataSource.getRepository( | ||||||
|       objectMetadataMapItem.nameSingular, |       objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const graphqlQueryParser = new GraphqlQueryParser( |     const graphqlQueryParser = new GraphqlQueryParser( | ||||||
|       objectMetadataMapItem.fields, |       objectMetadataItemWithFieldMaps.fieldsByName, | ||||||
|       objectMetadataMap, |       objectMetadataMaps, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const selectedFields = graphqlFields(info); |     const selectedFields = graphqlFields(info); | ||||||
|  |  | ||||||
|     const { relations } = graphqlQueryParser.parseSelectedFields( |     const { relations } = graphqlQueryParser.parseSelectedFields( | ||||||
|       objectMetadataMapItem, |       objectMetadataItemWithFieldMaps, | ||||||
|       selectedFields, |       selectedFields, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const queryBuilder = repository.createQueryBuilder( |     const queryBuilder = repository.createQueryBuilder( | ||||||
|       objectMetadataMapItem.nameSingular, |       objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const data = formatData(args.data, objectMetadataMapItem); |     const data = formatData(args.data, objectMetadataItemWithFieldMaps); | ||||||
|  |  | ||||||
|     const result = await queryBuilder |     const result = await queryBuilder | ||||||
|       .update(data) |       .update(data) | ||||||
| @@ -73,8 +77,8 @@ export class GraphqlQueryUpdateOneResolverService | |||||||
|  |  | ||||||
|     const updatedRecords = formatResult( |     const updatedRecords = formatResult( | ||||||
|       nonFormattedUpdatedObjectRecords, |       nonFormattedUpdatedObjectRecords, | ||||||
|       objectMetadataMapItem, |       objectMetadataItemWithFieldMaps, | ||||||
|       objectMetadataMap, |       objectMetadataMaps, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     if (updatedRecords.length === 0) { |     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(); |     const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); | ||||||
|  |  | ||||||
|     if (relations) { |     if (relations) { | ||||||
|       await processNestedRelationsHelper.processNestedRelations( |       await processNestedRelationsHelper.processNestedRelations({ | ||||||
|         objectMetadataMap, |         objectMetadataMaps, | ||||||
|         objectMetadataMapItem, |         parentObjectMetadataItem: objectMetadataItemWithFieldMaps, | ||||||
|         [updatedRecord], |         parentObjectRecords: [updatedRecord], | ||||||
|         relations, |         relations, | ||||||
|         QUERY_MAX_RECORDS, |         limit: QUERY_MAX_RECORDS, | ||||||
|         authContext, |         authContext, | ||||||
|         dataSource, |         dataSource, | ||||||
|       ); |       }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const typeORMObjectRecordsParser = |     const typeORMObjectRecordsParser = | ||||||
|       new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); |       new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); | ||||||
|  |  | ||||||
|     return typeORMObjectRecordsParser.processRecord<ObjectRecord>({ |     return typeORMObjectRecordsParser.processRecord<T>({ | ||||||
|       objectRecord: updatedRecord, |       objectRecord: updatedRecord, | ||||||
|       objectName: objectMetadataMapItem.nameSingular, |       objectName: objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|       take: 1, |       take: 1, | ||||||
|       totalCount: 1, |       totalCount: 1, | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async validate<ObjectRecord extends IRecord = IRecord>( |   async validate<T extends ObjectRecord = ObjectRecord>( | ||||||
|     args: UpdateOneResolverArgs<Partial<ObjectRecord>>, |     args: UpdateOneResolverArgs<Partial<T>>, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
|     assertMutationNotOnRemoteObject(options.objectMetadataMapItem); |     assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps); | ||||||
|     assertIsValidUuid(args.id); |     assertIsValidUuid(args.id); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,18 +1,18 @@ | |||||||
| import { Injectable } from '@nestjs/common'; | 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 { 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 { 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 { 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 { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-values'; | ||||||
|  | import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; | ||||||
|  |  | ||||||
| @Injectable() | @Injectable() | ||||||
| export class ApiEventEmitterService { | export class ApiEventEmitterService { | ||||||
|   constructor(private readonly workspaceEventEmitter: WorkspaceEventEmitter) {} |   constructor(private readonly workspaceEventEmitter: WorkspaceEventEmitter) {} | ||||||
|  |  | ||||||
|   public emitCreateEvents<T extends IRecord>( |   public emitCreateEvents<T extends ObjectRecord>( | ||||||
|     records: T[], |     records: T[], | ||||||
|     authContext: AuthContext, |     authContext: AuthContext, | ||||||
|     objectMetadataItem: ObjectMetadataInterface, |     objectMetadataItem: ObjectMetadataInterface, | ||||||
| @@ -32,7 +32,7 @@ export class ApiEventEmitterService { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public emitUpdateEvents<T extends IRecord>( |   public emitUpdateEvents<T extends ObjectRecord>( | ||||||
|     existingRecords: T[], |     existingRecords: T[], | ||||||
|     records: T[], |     records: T[], | ||||||
|     updatedFields: string[], |     updatedFields: string[], | ||||||
| @@ -77,7 +77,7 @@ export class ApiEventEmitterService { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public emitDeletedEvents<T extends IRecord>( |   public emitDeletedEvents<T extends ObjectRecord>( | ||||||
|     records: T[], |     records: T[], | ||||||
|     authContext: AuthContext, |     authContext: AuthContext, | ||||||
|     objectMetadataItem: ObjectMetadataInterface, |     objectMetadataItem: ObjectMetadataInterface, | ||||||
| @@ -99,7 +99,7 @@ export class ApiEventEmitterService { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public emitDestroyEvents<T extends IRecord>( |   public emitDestroyEvents<T extends ObjectRecord>( | ||||||
|     records: T[], |     records: T[], | ||||||
|     authContext: AuthContext, |     authContext: AuthContext, | ||||||
|     objectMetadataItem: ObjectMetadataInterface, |     objectMetadataItem: ObjectMetadataInterface, | ||||||
| @@ -121,9 +121,7 @@ export class ApiEventEmitterService { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private removeGraphQLAndNestedProperties<ObjectRecord extends IRecord>( |   private removeGraphQLAndNestedProperties<T extends ObjectRecord>(record: T) { | ||||||
|     record: ObjectRecord, |  | ||||||
|   ) { |  | ||||||
|     if (!record) { |     if (!record) { | ||||||
|       return {}; |       return {}; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| import { | import { | ||||||
|  |   ObjectRecordFilter, | ||||||
|  |   ObjectRecordOrderBy, | ||||||
|   OrderByDirection, |   OrderByDirection, | ||||||
|   RecordFilter, | } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; | ||||||
|   RecordOrderBy, |  | ||||||
| } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; |  | ||||||
|  |  | ||||||
| import { | import { | ||||||
|   GraphqlQueryRunnerException, |   GraphqlQueryRunnerException, | ||||||
| @@ -11,14 +11,14 @@ import { | |||||||
| import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; | import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; | ||||||
| import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; | 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 { 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 = ( | export const computeCursorArgFilter = ( | ||||||
|   cursor: Record<string, any>, |   cursor: Record<string, any>, | ||||||
|   orderBy: RecordOrderBy, |   orderBy: ObjectRecordOrderBy, | ||||||
|   fieldMetadataMap: FieldMetadataMap, |   fieldMetadataMapByName: FieldMetadataMap, | ||||||
|   isForwardPagination = true, |   isForwardPagination = true, | ||||||
| ): RecordFilter[] => { | ): ObjectRecordFilter[] => { | ||||||
|   const cursorKeys = Object.keys(cursor ?? {}); |   const cursorKeys = Object.keys(cursor ?? {}); | ||||||
|   const cursorValues = Object.values(cursor ?? {}); |   const cursorValues = Object.values(cursor ?? {}); | ||||||
|  |  | ||||||
| @@ -39,7 +39,7 @@ export const computeCursorArgFilter = ( | |||||||
|         ...buildWhereCondition( |         ...buildWhereCondition( | ||||||
|           cursorKeys[subConditionIndex], |           cursorKeys[subConditionIndex], | ||||||
|           cursorValues[subConditionIndex], |           cursorValues[subConditionIndex], | ||||||
|           fieldMetadataMap, |           fieldMetadataMapByName, | ||||||
|           'eq', |           'eq', | ||||||
|         ), |         ), | ||||||
|       }; |       }; | ||||||
| @@ -68,18 +68,18 @@ export const computeCursorArgFilter = ( | |||||||
|  |  | ||||||
|     return { |     return { | ||||||
|       ...whereCondition, |       ...whereCondition, | ||||||
|       ...buildWhereCondition(key, value, fieldMetadataMap, operator), |       ...buildWhereCondition(key, value, fieldMetadataMapByName, operator), | ||||||
|     } as RecordFilter; |     } as ObjectRecordFilter; | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const buildWhereCondition = ( | const buildWhereCondition = ( | ||||||
|   key: string, |   key: string, | ||||||
|   value: any, |   value: any, | ||||||
|   fieldMetadataMap: FieldMetadataMap, |   fieldMetadataMapByName: FieldMetadataMap, | ||||||
|   operator: string, |   operator: string, | ||||||
| ): Record<string, any> => { | ): Record<string, any> => { | ||||||
|   const fieldMetadata = fieldMetadataMap[key]; |   const fieldMetadata = fieldMetadataMapByName[key]; | ||||||
|  |  | ||||||
|   if (!fieldMetadata) { |   if (!fieldMetadata) { | ||||||
|     throw new GraphqlQueryRunnerException( |     throw new GraphqlQueryRunnerException( | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import { | import { | ||||||
|   Record as IRecord, |   ObjectRecord, | ||||||
|   RecordOrderBy, |   ObjectRecordOrderBy, | ||||||
| } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; | } 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 { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; | ||||||
|  |  | ||||||
| import { | import { | ||||||
| @@ -24,9 +24,9 @@ export const decodeCursor = (cursor: string): CursorData => { | |||||||
|   } |   } | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const encodeCursor = <ObjectRecord extends IRecord = IRecord>( | export const encodeCursor = <T extends ObjectRecord = ObjectRecord>( | ||||||
|   objectRecord: ObjectRecord, |   objectRecord: T, | ||||||
|   order: RecordOrderBy | undefined, |   order: ObjectRecordOrderBy | undefined, | ||||||
| ): string => { | ): string => { | ||||||
|   const orderByValues: Record<string, any> = {}; |   const orderByValues: Record<string, any> = {}; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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<string, any>, |  | ||||||
|   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; |  | ||||||
| }; |  | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; | 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 { 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 { | import { | ||||||
|   deduceRelationDirection, |   deduceRelationDirection, | ||||||
|   RelationDirection, |   RelationDirection, | ||||||
| @@ -9,7 +9,7 @@ import { | |||||||
|  |  | ||||||
| export const getRelationObjectMetadata = ( | export const getRelationObjectMetadata = ( | ||||||
|   fieldMetadata: FieldMetadataInterface, |   fieldMetadata: FieldMetadataInterface, | ||||||
|   objectMetadataMap: ObjectMetadataMap, |   objectMetadataMaps: ObjectMetadataMaps, | ||||||
| ) => { | ) => { | ||||||
|   const relationMetadata = getRelationMetadata(fieldMetadata); |   const relationMetadata = getRelationMetadata(fieldMetadata); | ||||||
|  |  | ||||||
| @@ -20,8 +20,8 @@ export const getRelationObjectMetadata = ( | |||||||
|  |  | ||||||
|   const referencedObjectMetadata = |   const referencedObjectMetadata = | ||||||
|     relationDirection === RelationDirection.TO |     relationDirection === RelationDirection.TO | ||||||
|       ? objectMetadataMap[relationMetadata.fromObjectMetadataId] |       ? objectMetadataMaps.byId[relationMetadata.fromObjectMetadataId] | ||||||
|       : objectMetadataMap[relationMetadata.toObjectMetadataId]; |       : objectMetadataMaps.byId[relationMetadata.toObjectMetadataId]; | ||||||
|  |  | ||||||
|   if (!referencedObjectMetadata) { |   if (!referencedObjectMetadata) { | ||||||
|     throw new Error( |     throw new Error( | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| export interface Record { | export interface ObjectRecord { | ||||||
|   id: string; |   id: string; | ||||||
|   [key: string]: any; |   [key: string]: any; | ||||||
|   createdAt: string; |   createdAt: string; | ||||||
| @@ -6,8 +6,8 @@ export interface Record { | |||||||
|   deletedAt: string | null; |   deletedAt: string | null; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export type RecordFilter = { | export type ObjectRecordFilter = { | ||||||
|   [Property in keyof Record]: any; |   [Property in keyof ObjectRecord]: any; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export enum OrderByDirection { | export enum OrderByDirection { | ||||||
| @@ -17,11 +17,11 @@ export enum OrderByDirection { | |||||||
|   DescNullsLast = 'DescNullsLast', |   DescNullsLast = 'DescNullsLast', | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export type RecordOrderBy = Array<{ | export type ObjectRecordOrderBy = Array<{ | ||||||
|   [Property in keyof Record]?: OrderByDirection; |   [Property in keyof ObjectRecord]?: OrderByDirection; | ||||||
| }>; | }>; | ||||||
| 
 | 
 | ||||||
| export interface RecordDuplicateCriteria { | export interface ObjectRecordDuplicateCriteria { | ||||||
|   objectName: string; |   objectName: string; | ||||||
|   columnNames: string[]; |   columnNames: string[]; | ||||||
| } | } | ||||||
| @@ -1,12 +1,12 @@ | |||||||
| import { Test, TestingModule } from '@nestjs/testing'; | import { Test, TestingModule } from '@nestjs/testing'; | ||||||
|  |  | ||||||
| import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; | 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 { 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 { 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 { 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', () => { | describe('QueryRunnerArgsFactory', () => { | ||||||
|   const recordPositionFactory = { |   const recordPositionFactory = { | ||||||
| @@ -14,13 +14,29 @@ describe('QueryRunnerArgsFactory', () => { | |||||||
|   }; |   }; | ||||||
|   const workspaceId = 'workspaceId'; |   const workspaceId = 'workspaceId'; | ||||||
|   const options = { |   const options = { | ||||||
|     fieldMetadataCollection: [ |  | ||||||
|       { name: 'position', type: FieldMetadataType.POSITION }, |  | ||||||
|       { name: 'testNumber', type: FieldMetadataType.NUMBER }, |  | ||||||
|     ] as FieldMetadataInterface[], |  | ||||||
|     objectMetadataItem: { isCustom: true, nameSingular: 'test' }, |  | ||||||
|     authContext: { workspace: { id: workspaceId } }, |     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; |   let factory: QueryRunnerArgsFactory; | ||||||
|  |  | ||||||
| @@ -61,7 +77,7 @@ describe('QueryRunnerArgsFactory', () => { | |||||||
|     it('createMany type should override data position and number', async () => { |     it('createMany type should override data position and number', async () => { | ||||||
|       const args = { |       const args = { | ||||||
|         id: 'uuid', |         id: 'uuid', | ||||||
|         data: [{ position: 'last', testNumber: '1' }], |         data: [{ position: 'last', testNumber: 1 }], | ||||||
|       }; |       }; | ||||||
|  |  | ||||||
|       const result = await factory.create( |       const result = await factory.create( | ||||||
| @@ -72,7 +88,7 @@ describe('QueryRunnerArgsFactory', () => { | |||||||
|  |  | ||||||
|       expect(recordPositionFactory.create).toHaveBeenCalledWith( |       expect(recordPositionFactory.create).toHaveBeenCalledWith( | ||||||
|         'last', |         'last', | ||||||
|         { isCustom: true, nameSingular: 'test' }, |         { isCustom: true, nameSingular: 'testNumber' }, | ||||||
|         workspaceId, |         workspaceId, | ||||||
|         0, |         0, | ||||||
|       ); |       ); | ||||||
| @@ -85,7 +101,7 @@ describe('QueryRunnerArgsFactory', () => { | |||||||
|     it('createMany type should override position if not present', async () => { |     it('createMany type should override position if not present', async () => { | ||||||
|       const args = { |       const args = { | ||||||
|         id: 'uuid', |         id: 'uuid', | ||||||
|         data: [{ testNumber: '1' }], |         data: [{ testNumber: 1 }], | ||||||
|       }; |       }; | ||||||
|  |  | ||||||
|       const result = await factory.create( |       const result = await factory.create( | ||||||
| @@ -96,7 +112,7 @@ describe('QueryRunnerArgsFactory', () => { | |||||||
|  |  | ||||||
|       expect(recordPositionFactory.create).toHaveBeenCalledWith( |       expect(recordPositionFactory.create).toHaveBeenCalledWith( | ||||||
|         'first', |         'first', | ||||||
|         { isCustom: true, nameSingular: 'test' }, |         { isCustom: true, nameSingular: 'testNumber' }, | ||||||
|         workspaceId, |         workspaceId, | ||||||
|         0, |         0, | ||||||
|       ); |       ); | ||||||
| @@ -109,7 +125,7 @@ describe('QueryRunnerArgsFactory', () => { | |||||||
|     it('findMany type should override data position and number', async () => { |     it('findMany type should override data position and number', async () => { | ||||||
|       const args = { |       const args = { | ||||||
|         id: 'uuid', |         id: 'uuid', | ||||||
|         filter: { testNumber: { eq: '1' }, otherField: { eq: 'test' } }, |         filter: { testNumber: { eq: 1 }, otherField: { eq: 'test' } }, | ||||||
|       }; |       }; | ||||||
|  |  | ||||||
|       const result = await factory.create( |       const result = await factory.create( | ||||||
| @@ -127,7 +143,7 @@ describe('QueryRunnerArgsFactory', () => { | |||||||
|     it('findOne type should override number in filter', async () => { |     it('findOne type should override number in filter', async () => { | ||||||
|       const args = { |       const args = { | ||||||
|         id: 'uuid', |         id: 'uuid', | ||||||
|         filter: { testNumber: { eq: '1' }, otherField: { eq: 'test' } }, |         filter: { testNumber: { eq: 1 }, otherField: { eq: 'test' } }, | ||||||
|       }; |       }; | ||||||
|  |  | ||||||
|       const result = await factory.create( |       const result = await factory.create( | ||||||
| @@ -143,23 +159,14 @@ describe('QueryRunnerArgsFactory', () => { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it('findDuplicates type should override number in data and id', async () => { |     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 = { |       const args = { | ||||||
|         ids: ['123'], |         ids: [123], | ||||||
|         data: [{ testNumber: '1', otherField: 'test' }], |         data: [{ testNumber: 1, otherField: 'test' }], | ||||||
|       }; |       }; | ||||||
|  |  | ||||||
|       const result = await factory.create( |       const result = await factory.create( | ||||||
|         args, |         args, | ||||||
|         optionsDuplicate, |         options, | ||||||
|         ResolverArgsType.FindDuplicates, |         ResolverArgsType.FindDuplicates, | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,9 @@ | |||||||
| import { Injectable } from '@nestjs/common'; | 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 { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; | ||||||
| import { | import { | ||||||
|   CreateManyResolverArgs, |   CreateManyResolverArgs, | ||||||
| @@ -10,13 +13,11 @@ import { | |||||||
|   ResolverArgs, |   ResolverArgs, | ||||||
|   ResolverArgsType, |   ResolverArgsType, | ||||||
| } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; | } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; | ||||||
| import { | import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; | ||||||
|   Record, |  | ||||||
|   RecordFilter, |  | ||||||
| } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; |  | ||||||
|  |  | ||||||
| import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; | 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 { 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'; | import { RecordPositionFactory } from './record-position.factory'; | ||||||
|  |  | ||||||
| @@ -34,27 +35,28 @@ export class QueryRunnerArgsFactory { | |||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|     resolverArgsType: ResolverArgsType, |     resolverArgsType: ResolverArgsType, | ||||||
|   ) { |   ) { | ||||||
|     const fieldMetadataCollection = options.fieldMetadataCollection; |     const fieldMetadataMapByNameByName = | ||||||
|  |       options.objectMetadataItemWithFieldMaps.fieldsByName; | ||||||
|  |  | ||||||
|     const fieldMetadataMap = new Map( |     const shouldBackfillPosition = hasPositionField( | ||||||
|       fieldMetadataCollection.map((fieldMetadata) => [ |       options.objectMetadataItemWithFieldMaps, | ||||||
|         fieldMetadata.name, |  | ||||||
|         fieldMetadata, |  | ||||||
|       ]), |  | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const shouldBackfillPosition = hasPositionField(options.objectMetadataItem); |  | ||||||
|  |  | ||||||
|     switch (resolverArgsType) { |     switch (resolverArgsType) { | ||||||
|       case ResolverArgsType.CreateMany: |       case ResolverArgsType.CreateMany: | ||||||
|         return { |         return { | ||||||
|           ...args, |           ...args, | ||||||
|           data: await Promise.all( |           data: await Promise.all( | ||||||
|             (args as CreateManyResolverArgs).data?.map((arg, index) => |             (args as CreateManyResolverArgs).data?.map((arg, index) => | ||||||
|               this.overrideDataByFieldMetadata(arg, options, fieldMetadataMap, { |               this.overrideDataByFieldMetadata( | ||||||
|  |                 arg, | ||||||
|  |                 options, | ||||||
|  |                 fieldMetadataMapByNameByName, | ||||||
|  |                 { | ||||||
|                   argIndex: index, |                   argIndex: index, | ||||||
|                   shouldBackfillPosition, |                   shouldBackfillPosition, | ||||||
|               }), |                 }, | ||||||
|  |               ), | ||||||
|             ) ?? [], |             ) ?? [], | ||||||
|           ), |           ), | ||||||
|         } satisfies CreateManyResolverArgs; |         } satisfies CreateManyResolverArgs; | ||||||
| @@ -63,7 +65,7 @@ export class QueryRunnerArgsFactory { | |||||||
|           ...args, |           ...args, | ||||||
|           filter: await this.overrideFilterByFieldMetadata( |           filter: await this.overrideFilterByFieldMetadata( | ||||||
|             (args as FindOneResolverArgs).filter, |             (args as FindOneResolverArgs).filter, | ||||||
|             fieldMetadataMap, |             fieldMetadataMapByNameByName, | ||||||
|           ), |           ), | ||||||
|         }; |         }; | ||||||
|       case ResolverArgsType.FindMany: |       case ResolverArgsType.FindMany: | ||||||
| @@ -71,7 +73,7 @@ export class QueryRunnerArgsFactory { | |||||||
|           ...args, |           ...args, | ||||||
|           filter: await this.overrideFilterByFieldMetadata( |           filter: await this.overrideFilterByFieldMetadata( | ||||||
|             (args as FindManyResolverArgs).filter, |             (args as FindManyResolverArgs).filter, | ||||||
|             fieldMetadataMap, |             fieldMetadataMapByNameByName, | ||||||
|           ), |           ), | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
| @@ -80,15 +82,24 @@ export class QueryRunnerArgsFactory { | |||||||
|           ...args, |           ...args, | ||||||
|           ids: (await Promise.all( |           ids: (await Promise.all( | ||||||
|             (args as FindDuplicatesResolverArgs).ids?.map((id) => |             (args as FindDuplicatesResolverArgs).ids?.map((id) => | ||||||
|               this.overrideValueByFieldMetadata('id', id, fieldMetadataMap), |               this.overrideValueByFieldMetadata( | ||||||
|  |                 'id', | ||||||
|  |                 id, | ||||||
|  |                 fieldMetadataMapByNameByName, | ||||||
|  |               ), | ||||||
|             ) ?? [], |             ) ?? [], | ||||||
|           )) as string[], |           )) as string[], | ||||||
|           data: await Promise.all( |           data: await Promise.all( | ||||||
|             (args as FindDuplicatesResolverArgs).data?.map((arg, index) => |             (args as FindDuplicatesResolverArgs).data?.map((arg, index) => | ||||||
|               this.overrideDataByFieldMetadata(arg, options, fieldMetadataMap, { |               this.overrideDataByFieldMetadata( | ||||||
|  |                 arg, | ||||||
|  |                 options, | ||||||
|  |                 fieldMetadataMapByNameByName, | ||||||
|  |                 { | ||||||
|                   argIndex: index, |                   argIndex: index, | ||||||
|                   shouldBackfillPosition, |                   shouldBackfillPosition, | ||||||
|               }), |                 }, | ||||||
|  |               ), | ||||||
|             ) ?? [], |             ) ?? [], | ||||||
|           ), |           ), | ||||||
|         } satisfies FindDuplicatesResolverArgs; |         } satisfies FindDuplicatesResolverArgs; | ||||||
| @@ -98,9 +109,9 @@ export class QueryRunnerArgsFactory { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async overrideDataByFieldMetadata( |   private async overrideDataByFieldMetadata( | ||||||
|     data: Partial<Record> | undefined, |     data: Partial<ObjectRecord> | undefined, | ||||||
|     options: WorkspaceQueryRunnerOptions, |     options: WorkspaceQueryRunnerOptions, | ||||||
|     fieldMetadataMap: Map<string, FieldMetadataInterface>, |     fieldMetadataMapByNameByName: Record<string, FieldMetadataInterface>, | ||||||
|     argPositionBackfillInput: ArgPositionBackfillInput, |     argPositionBackfillInput: ArgPositionBackfillInput, | ||||||
|   ) { |   ) { | ||||||
|     if (!data) { |     if (!data) { | ||||||
| @@ -111,7 +122,7 @@ export class QueryRunnerArgsFactory { | |||||||
|  |  | ||||||
|     const createArgPromiseByArgKey = Object.entries(data).map( |     const createArgPromiseByArgKey = Object.entries(data).map( | ||||||
|       async ([key, value]) => { |       async ([key, value]) => { | ||||||
|         const fieldMetadata = fieldMetadataMap.get(key); |         const fieldMetadata = fieldMetadataMapByNameByName[key]; | ||||||
|  |  | ||||||
|         if (!fieldMetadata) { |         if (!fieldMetadata) { | ||||||
|           return [key, await Promise.resolve(value)]; |           return [key, await Promise.resolve(value)]; | ||||||
| @@ -126,8 +137,9 @@ export class QueryRunnerArgsFactory { | |||||||
|               await this.recordPositionFactory.create( |               await this.recordPositionFactory.create( | ||||||
|                 value, |                 value, | ||||||
|                 { |                 { | ||||||
|                   isCustom: options.objectMetadataItem.isCustom, |                   isCustom: options.objectMetadataItemWithFieldMaps.isCustom, | ||||||
|                   nameSingular: options.objectMetadataItem.nameSingular, |                   nameSingular: | ||||||
|  |                     options.objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|                 }, |                 }, | ||||||
|                 options.authContext.workspace.id, |                 options.authContext.workspace.id, | ||||||
|                 argPositionBackfillInput.argIndex, |                 argPositionBackfillInput.argIndex, | ||||||
| @@ -154,8 +166,9 @@ export class QueryRunnerArgsFactory { | |||||||
|           await this.recordPositionFactory.create( |           await this.recordPositionFactory.create( | ||||||
|             'first', |             'first', | ||||||
|             { |             { | ||||||
|               isCustom: options.objectMetadataItem.isCustom, |               isCustom: options.objectMetadataItemWithFieldMaps.isCustom, | ||||||
|               nameSingular: options.objectMetadataItem.nameSingular, |               nameSingular: | ||||||
|  |                 options.objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|             }, |             }, | ||||||
|             options.authContext.workspace.id, |             options.authContext.workspace.id, | ||||||
|             argPositionBackfillInput.argIndex, |             argPositionBackfillInput.argIndex, | ||||||
| @@ -168,23 +181,27 @@ export class QueryRunnerArgsFactory { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private overrideFilterByFieldMetadata( |   private overrideFilterByFieldMetadata( | ||||||
|     filter: RecordFilter | undefined, |     filter: ObjectRecordFilter | undefined, | ||||||
|     fieldMetadataMap: Map<string, FieldMetadataInterface>, |     fieldMetadataMapByName: Record<string, FieldMetadataInterface>, | ||||||
|   ) { |   ) { | ||||||
|     if (!filter) { |     if (!filter) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const overrideFilter = (filterObject: RecordFilter) => { |     const overrideFilter = (filterObject: ObjectRecordFilter) => { | ||||||
|       return Object.entries(filterObject).reduce((acc, [key, value]) => { |       return Object.entries(filterObject).reduce((acc, [key, value]) => { | ||||||
|         if (key === 'and' || key === 'or') { |         if (key === 'and' || key === 'or') { | ||||||
|           acc[key] = value.map((nestedFilter: RecordFilter) => |           acc[key] = value.map((nestedFilter: ObjectRecordFilter) => | ||||||
|             overrideFilter(nestedFilter), |             overrideFilter(nestedFilter), | ||||||
|           ); |           ); | ||||||
|         } else if (key === 'not') { |         } else if (key === 'not') { | ||||||
|           acc[key] = overrideFilter(value); |           acc[key] = overrideFilter(value); | ||||||
|         } else { |         } else { | ||||||
|           acc[key] = this.transformValueByType(key, value, fieldMetadataMap); |           acc[key] = this.transformValueByType( | ||||||
|  |             key, | ||||||
|  |             value, | ||||||
|  |             fieldMetadataMapByName, | ||||||
|  |           ); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return acc; |         return acc; | ||||||
| @@ -197,9 +214,9 @@ export class QueryRunnerArgsFactory { | |||||||
|   private transformValueByType( |   private transformValueByType( | ||||||
|     key: string, |     key: string, | ||||||
|     value: any, |     value: any, | ||||||
|     fieldMetadataMap: Map<string, FieldMetadataInterface>, |     fieldMetadataMapByName: FieldMetadataMap, | ||||||
|   ) { |   ) { | ||||||
|     const fieldMetadata = fieldMetadataMap.get(key); |     const fieldMetadata = fieldMetadataMapByName[key]; | ||||||
|  |  | ||||||
|     if (!fieldMetadata) { |     if (!fieldMetadata) { | ||||||
|       return value; |       return value; | ||||||
| @@ -226,9 +243,9 @@ export class QueryRunnerArgsFactory { | |||||||
|   private async overrideValueByFieldMetadata( |   private async overrideValueByFieldMetadata( | ||||||
|     key: string, |     key: string, | ||||||
|     value: any, |     value: any, | ||||||
|     fieldMetadataMap: Map<string, FieldMetadataInterface>, |     fieldMetadataMapByName: FieldMetadataMap, | ||||||
|   ) { |   ) { | ||||||
|     const fieldMetadata = fieldMetadataMap.get(key); |     const fieldMetadata = fieldMetadataMapByName[key]; | ||||||
|  |  | ||||||
|     if (!fieldMetadata) { |     if (!fieldMetadata) { | ||||||
|       return value; |       return value; | ||||||
|   | |||||||
| @@ -1,15 +0,0 @@ | |||||||
| import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; |  | ||||||
|  |  | ||||||
| export interface PGGraphQLResponse<Data = any> { |  | ||||||
|   resolve: { |  | ||||||
|     data: Data; |  | ||||||
|     errors: any[]; |  | ||||||
|   }; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export type PGGraphQLResult<Data = any> = [PGGraphQLResponse<Data>]; |  | ||||||
|  |  | ||||||
| export interface PGGraphQLMutation<Record = IRecord> { |  | ||||||
|   affectedRows: number; |  | ||||||
|   records: Record[]; |  | ||||||
| } |  | ||||||
| @@ -1,20 +1,12 @@ | |||||||
| import { GraphQLResolveInfo } from 'graphql'; | 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 { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; | ||||||
| import { | import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; | ||||||
|   ObjectMetadataMap, | import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; | ||||||
|   ObjectMetadataMapItem, |  | ||||||
| } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; |  | ||||||
|  |  | ||||||
| export interface WorkspaceQueryRunnerOptions { | export interface WorkspaceQueryRunnerOptions { | ||||||
|   authContext: AuthContext; |   authContext: AuthContext; | ||||||
|   info: GraphQLResolveInfo; |   info: GraphQLResolveInfo; | ||||||
|   objectMetadataItem: ObjectMetadataInterface; |   objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; | ||||||
|   fieldMetadataCollection: FieldMetadataInterface[]; |   objectMetadataMaps: ObjectMetadataMaps; | ||||||
|   objectMetadataCollection: ObjectMetadataInterface[]; |  | ||||||
|   objectMetadataMap: ObjectMetadataMap; |  | ||||||
|   objectMetadataMapItem: ObjectMetadataMapItem; |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,12 +1,12 @@ | |||||||
| import { Injectable } from '@nestjs/common'; | import { Injectable } from '@nestjs/common'; | ||||||
| import { OnEvent } from '@nestjs/event-emitter'; | 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 { AnalyticsService } from 'src/engine/core-modules/analytics/analytics.service'; | ||||||
| import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; | 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 { TelemetryService } from 'src/engine/core-modules/telemetry/telemetry.service'; | ||||||
| import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/workspace-event.type'; | 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() | @Injectable() | ||||||
| export class TelemetryListener { | export class TelemetryListener { | ||||||
|   | |||||||
| @@ -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'; | import { isDefined } from 'src/utils/is-defined'; | ||||||
|  |  | ||||||
| export const withSoftDeleted = <T extends RecordFilter>( | export const withSoftDeleted = <T extends ObjectRecordFilter>( | ||||||
|   filter: T | undefined | null, |   filter: T | undefined | null, | ||||||
| ): boolean => { | ): boolean => { | ||||||
|   if (!isDefined(filter)) { |   if (!isDefined(filter)) { | ||||||
|   | |||||||
| @@ -31,19 +31,20 @@ export const workspaceQueryRunnerGraphqlApiExceptionHandler = ( | |||||||
|       if (indexNameMatch) { |       if (indexNameMatch) { | ||||||
|         const indexName = indexNameMatch[1]; |         const indexName = indexNameMatch[1]; | ||||||
|  |  | ||||||
|         const deletedAtFieldMetadata = context.objectMetadataItem.fields.find( |         const deletedAtFieldMetadata = | ||||||
|           (field) => field.name === 'deletedAt', |           context.objectMetadataItemWithFieldMaps.fieldsByName['deletedAt']; | ||||||
|         ); |  | ||||||
|  |  | ||||||
|         const affectedColumns = context.objectMetadataItem.indexMetadatas |         const affectedColumns = | ||||||
|  |           context.objectMetadataItemWithFieldMaps.indexMetadatas | ||||||
|             .find((index) => index.name === indexName) |             .find((index) => index.name === indexName) | ||||||
|             ?.indexFieldMetadatas?.filter( |             ?.indexFieldMetadatas?.filter( | ||||||
|               (field) => field.fieldMetadataId !== deletedAtFieldMetadata?.id, |               (field) => field.fieldMetadataId !== deletedAtFieldMetadata?.id, | ||||||
|             ) |             ) | ||||||
|             .map((indexField) => { |             .map((indexField) => { | ||||||
|             const fieldMetadata = context.objectMetadataItem.fields.find( |               const fieldMetadata = | ||||||
|               (objectField) => indexField.fieldMetadataId === objectField.id, |                 context.objectMetadataItemWithFieldMaps.fieldsById[ | ||||||
|             ); |                   indexField.fieldMetadataId | ||||||
|  |                 ]; | ||||||
|  |  | ||||||
|               return fieldMetadata?.label; |               return fieldMetadata?.label; | ||||||
|             }); |             }); | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; | |||||||
|  |  | ||||||
| import merge from 'lodash.merge'; | 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 { 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'; | 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< |   public async executePostQueryHooks< | ||||||
|     T extends WorkspaceResolverBuilderMethodNames, |     T extends WorkspaceResolverBuilderMethodNames, | ||||||
|     Record extends IRecord = IRecord, |     U extends ObjectRecord = ObjectRecord, | ||||||
|   >( |   >( | ||||||
|     authContext: AuthContext, |     authContext: AuthContext, | ||||||
|     // TODO: We should allow wildcard for object name |     // TODO: We should allow wildcard for object name | ||||||
|     objectName: string, |     objectName: string, | ||||||
|     methodName: T, |     methodName: T, | ||||||
|     payload: Record[], |     payload: U[], | ||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
|     const key: WorkspaceQueryHookKey = `${objectName}.${methodName}`; |     const key: WorkspaceQueryHookKey = `${objectName}.${methodName}`; | ||||||
|     const postHookInstances = |     const postHookInstances = | ||||||
|   | |||||||
| @@ -30,12 +30,10 @@ export class CreateManyResolverFactory | |||||||
|       try { |       try { | ||||||
|         const options: WorkspaceQueryRunnerOptions = { |         const options: WorkspaceQueryRunnerOptions = { | ||||||
|           authContext: internalContext.authContext, |           authContext: internalContext.authContext, | ||||||
|           objectMetadataItem: internalContext.objectMetadataItem, |  | ||||||
|           info, |           info, | ||||||
|           fieldMetadataCollection: internalContext.fieldMetadataCollection, |           objectMetadataMaps: internalContext.objectMetadataMaps, | ||||||
|           objectMetadataCollection: internalContext.objectMetadataCollection, |           objectMetadataItemWithFieldMaps: | ||||||
|           objectMetadataMap: internalContext.objectMetadataMap, |             internalContext.objectMetadataItemWithFieldMaps, | ||||||
|           objectMetadataMapItem: internalContext.objectMetadataMapItem, |  | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         return await this.graphqlQueryRunnerService.createMany(args, options); |         return await this.graphqlQueryRunnerService.createMany(args, options); | ||||||
|   | |||||||
| @@ -30,12 +30,10 @@ export class CreateOneResolverFactory | |||||||
|       try { |       try { | ||||||
|         const options: WorkspaceQueryRunnerOptions = { |         const options: WorkspaceQueryRunnerOptions = { | ||||||
|           authContext: internalContext.authContext, |           authContext: internalContext.authContext, | ||||||
|           objectMetadataItem: internalContext.objectMetadataItem, |  | ||||||
|           info, |           info, | ||||||
|           fieldMetadataCollection: internalContext.fieldMetadataCollection, |           objectMetadataMaps: internalContext.objectMetadataMaps, | ||||||
|           objectMetadataCollection: internalContext.objectMetadataCollection, |           objectMetadataItemWithFieldMaps: | ||||||
|           objectMetadataMap: internalContext.objectMetadataMap, |             internalContext.objectMetadataItemWithFieldMaps, | ||||||
|           objectMetadataMapItem: internalContext.objectMetadataMapItem, |  | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         return await this.graphqlQueryRunnerService.createOne(args, options); |         return await this.graphqlQueryRunnerService.createOne(args, options); | ||||||
|   | |||||||
| @@ -30,12 +30,10 @@ export class DeleteManyResolverFactory | |||||||
|       try { |       try { | ||||||
|         const options: WorkspaceQueryRunnerOptions = { |         const options: WorkspaceQueryRunnerOptions = { | ||||||
|           authContext: internalContext.authContext, |           authContext: internalContext.authContext, | ||||||
|           objectMetadataItem: internalContext.objectMetadataItem, |  | ||||||
|           info, |           info, | ||||||
|           fieldMetadataCollection: internalContext.fieldMetadataCollection, |           objectMetadataMaps: internalContext.objectMetadataMaps, | ||||||
|           objectMetadataCollection: internalContext.objectMetadataCollection, |           objectMetadataItemWithFieldMaps: | ||||||
|           objectMetadataMap: internalContext.objectMetadataMap, |             internalContext.objectMetadataItemWithFieldMaps, | ||||||
|           objectMetadataMapItem: internalContext.objectMetadataMapItem, |  | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         return await this.graphqlQueryRunnerService.deleteMany(args, options); |         return await this.graphqlQueryRunnerService.deleteMany(args, options); | ||||||
|   | |||||||
| @@ -30,12 +30,10 @@ export class DeleteOneResolverFactory | |||||||
|       try { |       try { | ||||||
|         const options: WorkspaceQueryRunnerOptions = { |         const options: WorkspaceQueryRunnerOptions = { | ||||||
|           authContext: internalContext.authContext, |           authContext: internalContext.authContext, | ||||||
|           objectMetadataItem: internalContext.objectMetadataItem, |  | ||||||
|           info, |           info, | ||||||
|           fieldMetadataCollection: internalContext.fieldMetadataCollection, |           objectMetadataMaps: internalContext.objectMetadataMaps, | ||||||
|           objectMetadataCollection: internalContext.objectMetadataCollection, |           objectMetadataItemWithFieldMaps: | ||||||
|           objectMetadataMap: internalContext.objectMetadataMap, |             internalContext.objectMetadataItemWithFieldMaps, | ||||||
|           objectMetadataMapItem: internalContext.objectMetadataMapItem, |  | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         return await this.graphqlQueryRunnerService.deleteOne(args, options); |         return await this.graphqlQueryRunnerService.deleteOne(args, options); | ||||||
|   | |||||||
| @@ -30,12 +30,10 @@ export class DestroyManyResolverFactory | |||||||
|       try { |       try { | ||||||
|         const options: WorkspaceQueryRunnerOptions = { |         const options: WorkspaceQueryRunnerOptions = { | ||||||
|           authContext: internalContext.authContext, |           authContext: internalContext.authContext, | ||||||
|           objectMetadataItem: internalContext.objectMetadataItem, |  | ||||||
|           info, |           info, | ||||||
|           fieldMetadataCollection: internalContext.fieldMetadataCollection, |           objectMetadataMaps: internalContext.objectMetadataMaps, | ||||||
|           objectMetadataCollection: internalContext.objectMetadataCollection, |           objectMetadataItemWithFieldMaps: | ||||||
|           objectMetadataMap: internalContext.objectMetadataMap, |             internalContext.objectMetadataItemWithFieldMaps, | ||||||
|           objectMetadataMapItem: internalContext.objectMetadataMapItem, |  | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         return await this.graphqlQueryRunnerService.destroyMany(args, options); |         return await this.graphqlQueryRunnerService.destroyMany(args, options); | ||||||
|   | |||||||
| @@ -30,12 +30,10 @@ export class DestroyOneResolverFactory | |||||||
|       try { |       try { | ||||||
|         const options: WorkspaceQueryRunnerOptions = { |         const options: WorkspaceQueryRunnerOptions = { | ||||||
|           authContext: internalContext.authContext, |           authContext: internalContext.authContext, | ||||||
|           objectMetadataItem: internalContext.objectMetadataItem, |  | ||||||
|           info, |           info, | ||||||
|           fieldMetadataCollection: internalContext.fieldMetadataCollection, |           objectMetadataMaps: internalContext.objectMetadataMaps, | ||||||
|           objectMetadataCollection: internalContext.objectMetadataCollection, |           objectMetadataItemWithFieldMaps: | ||||||
|           objectMetadataMap: internalContext.objectMetadataMap, |             internalContext.objectMetadataItemWithFieldMaps, | ||||||
|           objectMetadataMapItem: internalContext.objectMetadataMapItem, |  | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         return await this.graphQLQueryRunnerService.destroyOne(args, options); |         return await this.graphQLQueryRunnerService.destroyOne(args, options); | ||||||
|   | |||||||
| @@ -30,12 +30,10 @@ export class FindDuplicatesResolverFactory | |||||||
|       try { |       try { | ||||||
|         const options: WorkspaceQueryRunnerOptions = { |         const options: WorkspaceQueryRunnerOptions = { | ||||||
|           authContext: internalContext.authContext, |           authContext: internalContext.authContext, | ||||||
|           objectMetadataItem: internalContext.objectMetadataItem, |  | ||||||
|           info, |           info, | ||||||
|           fieldMetadataCollection: internalContext.fieldMetadataCollection, |           objectMetadataMaps: internalContext.objectMetadataMaps, | ||||||
|           objectMetadataCollection: internalContext.objectMetadataCollection, |           objectMetadataItemWithFieldMaps: | ||||||
|           objectMetadataMap: internalContext.objectMetadataMap, |             internalContext.objectMetadataItemWithFieldMaps, | ||||||
|           objectMetadataMapItem: internalContext.objectMetadataMapItem, |  | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         return await this.graphqlQueryRunnerService.findDuplicates( |         return await this.graphqlQueryRunnerService.findDuplicates( | ||||||
|   | |||||||
| @@ -30,12 +30,10 @@ export class FindManyResolverFactory | |||||||
|       try { |       try { | ||||||
|         const options: WorkspaceQueryRunnerOptions = { |         const options: WorkspaceQueryRunnerOptions = { | ||||||
|           authContext: internalContext.authContext, |           authContext: internalContext.authContext, | ||||||
|           objectMetadataItem: internalContext.objectMetadataItem, |  | ||||||
|           info, |           info, | ||||||
|           fieldMetadataCollection: internalContext.fieldMetadataCollection, |           objectMetadataMaps: internalContext.objectMetadataMaps, | ||||||
|           objectMetadataCollection: internalContext.objectMetadataCollection, |           objectMetadataItemWithFieldMaps: | ||||||
|           objectMetadataMap: internalContext.objectMetadataMap, |             internalContext.objectMetadataItemWithFieldMaps, | ||||||
|           objectMetadataMapItem: internalContext.objectMetadataMapItem, |  | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         return await this.graphqlQueryRunnerService.findMany(args, options); |         return await this.graphqlQueryRunnerService.findMany(args, options); | ||||||
|   | |||||||
| @@ -30,12 +30,10 @@ export class FindOneResolverFactory | |||||||
|       try { |       try { | ||||||
|         const options: WorkspaceQueryRunnerOptions = { |         const options: WorkspaceQueryRunnerOptions = { | ||||||
|           authContext: internalContext.authContext, |           authContext: internalContext.authContext, | ||||||
|           objectMetadataItem: internalContext.objectMetadataItem, |  | ||||||
|           info, |           info, | ||||||
|           fieldMetadataCollection: internalContext.fieldMetadataCollection, |           objectMetadataMaps: internalContext.objectMetadataMaps, | ||||||
|           objectMetadataCollection: internalContext.objectMetadataCollection, |           objectMetadataItemWithFieldMaps: | ||||||
|           objectMetadataMap: internalContext.objectMetadataMap, |             internalContext.objectMetadataItemWithFieldMaps, | ||||||
|           objectMetadataMapItem: internalContext.objectMetadataMapItem, |  | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         return await this.graphqlQueryRunnerService.findOne(args, options); |         return await this.graphqlQueryRunnerService.findOne(args, options); | ||||||
|   | |||||||
| @@ -30,12 +30,10 @@ export class RestoreManyResolverFactory | |||||||
|       try { |       try { | ||||||
|         const options: WorkspaceQueryRunnerOptions = { |         const options: WorkspaceQueryRunnerOptions = { | ||||||
|           authContext: internalContext.authContext, |           authContext: internalContext.authContext, | ||||||
|           objectMetadataItem: internalContext.objectMetadataItem, |  | ||||||
|           info, |           info, | ||||||
|           fieldMetadataCollection: internalContext.fieldMetadataCollection, |           objectMetadataMaps: internalContext.objectMetadataMaps, | ||||||
|           objectMetadataCollection: internalContext.objectMetadataCollection, |           objectMetadataItemWithFieldMaps: | ||||||
|           objectMetadataMap: internalContext.objectMetadataMap, |             internalContext.objectMetadataItemWithFieldMaps, | ||||||
|           objectMetadataMapItem: internalContext.objectMetadataMapItem, |  | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         return await this.graphqlQueryRunnerService.restoreMany(args, options); |         return await this.graphqlQueryRunnerService.restoreMany(args, options); | ||||||
|   | |||||||
| @@ -28,12 +28,10 @@ export class SearchResolverFactory | |||||||
|       try { |       try { | ||||||
|         const options: WorkspaceQueryRunnerOptions = { |         const options: WorkspaceQueryRunnerOptions = { | ||||||
|           authContext: internalContext.authContext, |           authContext: internalContext.authContext, | ||||||
|           objectMetadataItem: internalContext.objectMetadataItem, |  | ||||||
|           info, |           info, | ||||||
|           fieldMetadataCollection: internalContext.fieldMetadataCollection, |           objectMetadataMaps: internalContext.objectMetadataMaps, | ||||||
|           objectMetadataCollection: internalContext.objectMetadataCollection, |           objectMetadataItemWithFieldMaps: | ||||||
|           objectMetadataMap: internalContext.objectMetadataMap, |             internalContext.objectMetadataItemWithFieldMaps, | ||||||
|           objectMetadataMapItem: internalContext.objectMetadataMapItem, |  | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         return await this.graphqlQueryRunnerService.search(args, options); |         return await this.graphqlQueryRunnerService.search(args, options); | ||||||
|   | |||||||
| @@ -30,12 +30,10 @@ export class UpdateManyResolverFactory | |||||||
|       try { |       try { | ||||||
|         const options: WorkspaceQueryRunnerOptions = { |         const options: WorkspaceQueryRunnerOptions = { | ||||||
|           authContext: internalContext.authContext, |           authContext: internalContext.authContext, | ||||||
|           objectMetadataItem: internalContext.objectMetadataItem, |  | ||||||
|           info, |           info, | ||||||
|           fieldMetadataCollection: internalContext.fieldMetadataCollection, |           objectMetadataMaps: internalContext.objectMetadataMaps, | ||||||
|           objectMetadataCollection: internalContext.objectMetadataCollection, |           objectMetadataItemWithFieldMaps: | ||||||
|           objectMetadataMap: internalContext.objectMetadataMap, |             internalContext.objectMetadataItemWithFieldMaps, | ||||||
|           objectMetadataMapItem: internalContext.objectMetadataMapItem, |  | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         return await this.graphqlQueryRunnerService.updateMany(args, options); |         return await this.graphqlQueryRunnerService.updateMany(args, options); | ||||||
|   | |||||||
| @@ -30,12 +30,10 @@ export class UpdateOneResolverFactory | |||||||
|       try { |       try { | ||||||
|         const options: WorkspaceQueryRunnerOptions = { |         const options: WorkspaceQueryRunnerOptions = { | ||||||
|           authContext: internalContext.authContext, |           authContext: internalContext.authContext, | ||||||
|           objectMetadataItem: internalContext.objectMetadataItem, |  | ||||||
|           info, |           info, | ||||||
|           fieldMetadataCollection: internalContext.fieldMetadataCollection, |           objectMetadataMaps: internalContext.objectMetadataMaps, | ||||||
|           objectMetadataCollection: internalContext.objectMetadataCollection, |           objectMetadataItemWithFieldMaps: | ||||||
|           objectMetadataMap: internalContext.objectMetadataMap, |             internalContext.objectMetadataItemWithFieldMaps, | ||||||
|           objectMetadataMapItem: internalContext.objectMetadataMapItem, |  | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         return await this.graphqlQueryRunnerService.updateOne(args, options); |         return await this.graphqlQueryRunnerService.updateOne(args, options); | ||||||
|   | |||||||
| @@ -1,14 +0,0 @@ | |||||||
| import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; |  | ||||||
|  |  | ||||||
| export interface PGGraphQLResponse<Data = any> { |  | ||||||
|   resolve: { |  | ||||||
|     data: Data; |  | ||||||
|   }; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export type PGGraphQLResult<Data = any> = [PGGraphQLResponse<Data>]; |  | ||||||
|  |  | ||||||
| export interface PGGraphQLMutation<Record = IRecord> { |  | ||||||
|   affectedRows: number; |  | ||||||
|   records: Record[]; |  | ||||||
| } |  | ||||||
| @@ -1,10 +1,10 @@ | |||||||
| import { GraphQLFieldResolver } from 'graphql'; | import { GraphQLFieldResolver } from 'graphql'; | ||||||
|  |  | ||||||
| import { | import { | ||||||
|   Record, |   ObjectRecord, | ||||||
|   RecordFilter, |   ObjectRecordFilter, | ||||||
|   RecordOrderBy, |   ObjectRecordOrderBy, | ||||||
| } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; | } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; | ||||||
|  |  | ||||||
| import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/factories/factories'; | import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/factories/factories'; | ||||||
|  |  | ||||||
| @@ -26,8 +26,8 @@ export enum ResolverArgsType { | |||||||
| } | } | ||||||
|  |  | ||||||
| export interface FindManyResolverArgs< | export interface FindManyResolverArgs< | ||||||
|   Filter extends RecordFilter = RecordFilter, |   Filter extends ObjectRecordFilter = ObjectRecordFilter, | ||||||
|   OrderBy extends RecordOrderBy = RecordOrderBy, |   OrderBy extends ObjectRecordOrderBy = ObjectRecordOrderBy, | ||||||
| > { | > { | ||||||
|   first?: number; |   first?: number; | ||||||
|   last?: number; |   last?: number; | ||||||
| @@ -42,14 +42,14 @@ export interface FindOneResolverArgs<Filter = any> { | |||||||
| } | } | ||||||
|  |  | ||||||
| export interface FindDuplicatesResolverArgs< | export interface FindDuplicatesResolverArgs< | ||||||
|   Data extends Partial<Record> = Partial<Record>, |   Data extends Partial<ObjectRecord> = Partial<ObjectRecord>, | ||||||
| > { | > { | ||||||
|   ids?: string[]; |   ids?: string[]; | ||||||
|   data?: Data[]; |   data?: Data[]; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface SearchResolverArgs< | export interface SearchResolverArgs< | ||||||
|   Filter extends RecordFilter = RecordFilter, |   Filter extends ObjectRecordFilter = ObjectRecordFilter, | ||||||
| > { | > { | ||||||
|   searchInput?: string; |   searchInput?: string; | ||||||
|   filter?: Filter; |   filter?: Filter; | ||||||
| @@ -57,28 +57,28 @@ export interface SearchResolverArgs< | |||||||
| } | } | ||||||
|  |  | ||||||
| export interface CreateOneResolverArgs< | export interface CreateOneResolverArgs< | ||||||
|   Data extends Partial<Record> = Partial<Record>, |   Data extends Partial<ObjectRecord> = Partial<ObjectRecord>, | ||||||
| > { | > { | ||||||
|   data: Data; |   data: Data; | ||||||
|   upsert?: boolean; |   upsert?: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface CreateManyResolverArgs< | export interface CreateManyResolverArgs< | ||||||
|   Data extends Partial<Record> = Partial<Record>, |   Data extends Partial<ObjectRecord> = Partial<ObjectRecord>, | ||||||
| > { | > { | ||||||
|   data: Data[]; |   data: Data[]; | ||||||
|   upsert?: boolean; |   upsert?: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface UpdateOneResolverArgs< | export interface UpdateOneResolverArgs< | ||||||
|   Data extends Partial<Record> = Partial<Record>, |   Data extends Partial<ObjectRecord> = Partial<ObjectRecord>, | ||||||
| > { | > { | ||||||
|   id: string; |   id: string; | ||||||
|   data: Data; |   data: Data; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface UpdateManyResolverArgs< | export interface UpdateManyResolverArgs< | ||||||
|   Data extends Partial<Record> = Partial<Record>, |   Data extends Partial<ObjectRecord> = Partial<ObjectRecord>, | ||||||
|   Filter = any, |   Filter = any, | ||||||
| > { | > { | ||||||
|   filter: Filter; |   filter: Filter; | ||||||
|   | |||||||
| @@ -2,8 +2,6 @@ import { Injectable, Logger } from '@nestjs/common'; | |||||||
|  |  | ||||||
| import { IResolvers } from '@graphql-tools/utils'; | 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 { 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 { 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'; | 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 { 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 { 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 { 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 { getResolverName } from 'src/engine/utils/get-resolver-name.util'; | ||||||
|  |  | ||||||
| import { CreateManyResolverFactory } from './factories/create-many-resolver.factory'; | import { CreateManyResolverFactory } from './factories/create-many-resolver.factory'; | ||||||
| @@ -49,8 +47,7 @@ export class WorkspaceResolverFactory { | |||||||
|  |  | ||||||
|   async create( |   async create( | ||||||
|     authContext: AuthContext, |     authContext: AuthContext, | ||||||
|     objectMetadataCollection: ObjectMetadataInterface[], |     objectMetadataMaps: ObjectMetadataMaps, | ||||||
|     objectMetadataMap: ObjectMetadataMap, |  | ||||||
|     workspaceResolverBuilderMethods: WorkspaceResolverBuilderMethods, |     workspaceResolverBuilderMethods: WorkspaceResolverBuilderMethods, | ||||||
|   ): Promise<IResolvers> { |   ): Promise<IResolvers> { | ||||||
|     const factories = new Map< |     const factories = new Map< | ||||||
| @@ -76,7 +73,7 @@ export class WorkspaceResolverFactory { | |||||||
|       Mutation: {}, |       Mutation: {}, | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     for (const objectMetadata of objectMetadataCollection) { |     for (const objectMetadata of Object.values(objectMetadataMaps.byId)) { | ||||||
|       // Generate query resolvers |       // Generate query resolvers | ||||||
|       for (const methodName of workspaceResolverBuilderMethods.queries) { |       for (const methodName of workspaceResolverBuilderMethods.queries) { | ||||||
|         const resolverName = getResolverName(objectMetadata, methodName); |         const resolverName = getResolverName(objectMetadata, methodName); | ||||||
| @@ -94,11 +91,8 @@ export class WorkspaceResolverFactory { | |||||||
|  |  | ||||||
|         resolvers.Query[resolverName] = resolverFactory.create({ |         resolvers.Query[resolverName] = resolverFactory.create({ | ||||||
|           authContext, |           authContext, | ||||||
|           objectMetadataItem: objectMetadata, |           objectMetadataMaps, | ||||||
|           fieldMetadataCollection: objectMetadata.fields, |           objectMetadataItemWithFieldMaps: objectMetadata, | ||||||
|           objectMetadataCollection, |  | ||||||
|           objectMetadataMap, |  | ||||||
|           objectMetadataMapItem: objectMetadataMap[objectMetadata.nameSingular], |  | ||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
|  |  | ||||||
| @@ -119,11 +113,8 @@ export class WorkspaceResolverFactory { | |||||||
|  |  | ||||||
|         resolvers.Mutation[resolverName] = resolverFactory.create({ |         resolvers.Mutation[resolverName] = resolverFactory.create({ | ||||||
|           authContext, |           authContext, | ||||||
|           objectMetadataItem: objectMetadata, |           objectMetadataMaps, | ||||||
|           fieldMetadataCollection: objectMetadata.fields, |           objectMetadataItemWithFieldMaps: objectMetadata, | ||||||
|           objectMetadataCollection, |  | ||||||
|           objectMetadataMap, |  | ||||||
|           objectMetadataMapItem: objectMetadataMap[objectMetadata.nameSingular], |  | ||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -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<AggregationField, 'type' | 'description'>; | ||||||
|  |  | ||||||
|  | @Injectable() | ||||||
|  | export class AggregationTypeFactory { | ||||||
|  |   public create( | ||||||
|  |     objectMetadata: ObjectMetadataInterface, | ||||||
|  |   ): Record<string, AggregationGraphQLType> { | ||||||
|  |     const availableAggregations = getAvailableAggregationsFromObjectFields( | ||||||
|  |       objectMetadata.fields, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     return Object.entries(availableAggregations).reduce< | ||||||
|  |       Record<string, AggregationGraphQLType> | ||||||
|  |     >((acc, [key, agg]) => { | ||||||
|  |       acc[key] = { | ||||||
|  |         type: agg.type, | ||||||
|  |         description: agg.description, | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       return acc; | ||||||
|  |     }, {}); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,17 +1,18 @@ | |||||||
| import { Injectable } from '@nestjs/common'; | 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 { 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 { 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 { pascalCase } from 'src/utils/pascal-case'; | ||||||
|  |  | ||||||
|  | import { ConnectionTypeFactory } from './connection-type.factory'; | ||||||
| import { | import { | ||||||
|   ObjectTypeDefinition, |   ObjectTypeDefinition, | ||||||
|   ObjectTypeDefinitionKind, |   ObjectTypeDefinitionKind, | ||||||
| } from './object-type-definition.factory'; | } from './object-type-definition.factory'; | ||||||
| import { ConnectionTypeFactory } from './connection-type.factory'; |  | ||||||
|  |  | ||||||
| export enum ConnectionTypeDefinitionKind { | export enum ConnectionTypeDefinitionKind { | ||||||
|   Edge = 'Edge', |   Edge = 'Edge', | ||||||
| @@ -20,7 +21,10 @@ export enum ConnectionTypeDefinitionKind { | |||||||
|  |  | ||||||
| @Injectable() | @Injectable() | ||||||
| export class ConnectionTypeDefinitionFactory { | export class ConnectionTypeDefinitionFactory { | ||||||
|   constructor(private readonly connectionTypeFactory: ConnectionTypeFactory) {} |   constructor( | ||||||
|  |     private readonly connectionTypeFactory: ConnectionTypeFactory, | ||||||
|  |     private readonly aggregationTypeFactory: AggregationTypeFactory, | ||||||
|  |   ) {} | ||||||
|  |  | ||||||
|   public create( |   public create( | ||||||
|     objectMetadata: ObjectMetadataInterface, |     objectMetadata: ObjectMetadataInterface, | ||||||
| @@ -45,6 +49,10 @@ export class ConnectionTypeDefinitionFactory { | |||||||
|   ): GraphQLFieldConfigMap<any, any> { |   ): GraphQLFieldConfigMap<any, any> { | ||||||
|     const fields: GraphQLFieldConfigMap<any, any> = {}; |     const fields: GraphQLFieldConfigMap<any, any> = {}; | ||||||
|  |  | ||||||
|  |     const aggregatedFields = this.aggregationTypeFactory.create(objectMetadata); | ||||||
|  |  | ||||||
|  |     Object.assign(fields, aggregatedFields); | ||||||
|  |  | ||||||
|     fields.edges = { |     fields.edges = { | ||||||
|       type: this.connectionTypeFactory.create( |       type: this.connectionTypeFactory.create( | ||||||
|         objectMetadata, |         objectMetadata, | ||||||
| @@ -69,11 +77,6 @@ export class ConnectionTypeDefinitionFactory { | |||||||
|       ), |       ), | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     fields.totalCount = { |  | ||||||
|       type: GraphQLInt, |  | ||||||
|       description: 'Total number of records in the connection', |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     return fields; |     return fields; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,23 +1,24 @@ | |||||||
| import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory'; | import { AggregationTypeFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/aggregation-type.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 { CompositeEnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-enum-type-definition.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 { 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 { 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 { ObjectTypeDefinitionFactory } from './object-type-definition.factory'; | ||||||
|  | import { OrphanedTypesFactory } from './orphaned-types.factory'; | ||||||
| import { OutputTypeFactory } from './output-type.factory'; | import { OutputTypeFactory } from './output-type.factory'; | ||||||
| import { QueryTypeFactory } from './query-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 { RelationTypeFactory } from './relation-type.factory'; | ||||||
| import { ExtendObjectTypeDefinitionFactory } from './extend-object-type-definition.factory'; | import { RootTypeFactory } from './root-type.factory'; | ||||||
| import { OrphanedTypesFactory } from './orphaned-types.factory'; |  | ||||||
|  |  | ||||||
| export const workspaceSchemaBuilderFactories = [ | export const workspaceSchemaBuilderFactories = [ | ||||||
|   ArgsFactory, |   ArgsFactory, | ||||||
| @@ -39,4 +40,5 @@ export const workspaceSchemaBuilderFactories = [ | |||||||
|   QueryTypeFactory, |   QueryTypeFactory, | ||||||
|   MutationTypeFactory, |   MutationTypeFactory, | ||||||
|   OrphanedTypesFactory, |   OrphanedTypesFactory, | ||||||
|  |   AggregationTypeFactory, | ||||||
| ]; | ]; | ||||||
|   | |||||||
| @@ -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; |  | ||||||
|   }, |  | ||||||
| }); |  | ||||||
| @@ -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 { BigFloatScalarType } from './big-float.scalar'; | ||||||
| import { BigIntScalarType } from './big-int.scalar'; | import { BigIntScalarType } from './big-int.scalar'; | ||||||
|  | import { CursorScalarType } from './cursor.scalar'; | ||||||
| import { DateScalarType } from './date.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 { TimeScalarType } from './time.scalar'; | ||||||
| import { UUIDScalarType } from './uuid.scalar'; | import { UUIDScalarType } from './uuid.scalar'; | ||||||
|  |  | ||||||
| @@ -12,7 +11,6 @@ export * from './big-float.scalar'; | |||||||
| export * from './big-int.scalar'; | export * from './big-int.scalar'; | ||||||
| export * from './cursor.scalar'; | export * from './cursor.scalar'; | ||||||
| export * from './date.scalar'; | export * from './date.scalar'; | ||||||
| export * from './date-time.scalar'; |  | ||||||
| export * from './time.scalar'; | export * from './time.scalar'; | ||||||
| export * from './uuid.scalar'; | export * from './uuid.scalar'; | ||||||
|  |  | ||||||
| @@ -20,7 +18,6 @@ export const scalars = [ | |||||||
|   BigFloatScalarType, |   BigFloatScalarType, | ||||||
|   BigIntScalarType, |   BigIntScalarType, | ||||||
|   DateScalarType, |   DateScalarType, | ||||||
|   DateTimeScalarType, |  | ||||||
|   TimeScalarType, |   TimeScalarType, | ||||||
|   UUIDScalarType, |   UUIDScalarType, | ||||||
|   CursorScalarType, |   CursorScalarType, | ||||||
|   | |||||||
| @@ -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 { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; | ||||||
| import { | import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; | ||||||
|   ObjectMetadataMap, | import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; | ||||||
|   ObjectMetadataMapItem, |  | ||||||
| } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; |  | ||||||
|  |  | ||||||
| export interface WorkspaceSchemaBuilderContext { | export interface WorkspaceSchemaBuilderContext { | ||||||
|   authContext: AuthContext; |   authContext: AuthContext; | ||||||
|   fieldMetadataCollection: FieldMetadataInterface[]; |   objectMetadataMaps: ObjectMetadataMaps; | ||||||
|   objectMetadataCollection: ObjectMetadataInterface[]; |   objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; | ||||||
|   objectMetadataItem: ObjectMetadataInterface; |  | ||||||
|   objectMetadataMap: ObjectMetadataMap; |  | ||||||
|   objectMetadataMapItem: ObjectMetadataMapItem; |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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<string, AggregationField> => { | ||||||
|  |   return fields.reduce<Record<string, AggregationField>>((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; | ||||||
|  |   }, {}); | ||||||
|  | }; | ||||||
| @@ -7,10 +7,10 @@ import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metad | |||||||
|  |  | ||||||
| import { TypeDefinitionsGenerator } from './type-definitions.generator'; | 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 { MutationTypeFactory } from './factories/mutation-type.factory'; | ||||||
| import { OrphanedTypesFactory } from './factories/orphaned-types.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() | @Injectable() | ||||||
| export class WorkspaceGraphQLSchemaFactory { | export class WorkspaceGraphQLSchemaFactory { | ||||||
|   | |||||||
| @@ -56,13 +56,13 @@ export class WorkspaceSchemaFactory { | |||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const objectMetadataMap = |     const objectMetadataMaps = | ||||||
|       await this.workspaceCacheStorageService.getObjectMetadataMap( |       await this.workspaceCacheStorageService.getObjectMetadataMaps( | ||||||
|         authContext.workspace.id, |         authContext.workspace.id, | ||||||
|         currentCacheVersion, |         currentCacheVersion, | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|     if (!objectMetadataMap) { |     if (!objectMetadataMaps) { | ||||||
|       await this.workspaceMetadataCacheService.recomputeMetadataCache({ |       await this.workspaceMetadataCacheService.recomputeMetadataCache({ | ||||||
|         workspaceId: authContext.workspace.id, |         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) => ({ | ||||||
|         ...objectMetadataItem, |         ...objectMetadataItem, | ||||||
|         fields: Object.values(objectMetadataItem.fields), |         fields: objectMetadataItem.fields, | ||||||
|         indexes: objectMetadataItem.indexMetadatas, |         indexes: objectMetadataItem.indexMetadatas, | ||||||
|       }), |       }), | ||||||
|     ); |     ); | ||||||
| @@ -117,8 +117,7 @@ export class WorkspaceSchemaFactory { | |||||||
|  |  | ||||||
|     const autoGeneratedResolvers = await this.workspaceResolverFactory.create( |     const autoGeneratedResolvers = await this.workspaceResolverFactory.create( | ||||||
|       authContext, |       authContext, | ||||||
|       objectMetadataCollection, |       objectMetadataMaps, | ||||||
|       objectMetadataMap, |  | ||||||
|       workspaceResolverBuilderMethodNames, |       workspaceResolverBuilderMethodNames, | ||||||
|     ); |     ); | ||||||
|     const scalarsResolvers = |     const scalarsResolvers = | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import { BadRequestException } from '@nestjs/common'; | 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 { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; | ||||||
|  |  | ||||||
| import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; | 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 = ( | export const checkArrayFields = ( | ||||||
|   objectMetadata: ObjectMetadataInterface, |   objectMetadata: ObjectMetadataInterface, | ||||||
|   fields: Array<Partial<Record>>, |   fields: Array<Partial<ObjectRecord>>, | ||||||
| ): void => { | ): void => { | ||||||
|   const fieldMetadataNames = objectMetadata.fields |   const fieldMetadataNames = objectMetadata.fields | ||||||
|     .map((field) => { |     .map((field) => { | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import { Test, TestingModule } from '@nestjs/testing'; | 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 { objectMetadataItemMock } from 'src/engine/api/__mocks__/object-metadata-item.mock'; | ||||||
| import { OrderByInputFactory } from 'src/engine/api/rest/input-factories/order-by-input.factory'; | import { OrderByInputFactory } from 'src/engine/api/rest/input-factories/order-by-input.factory'; | ||||||
|   | |||||||
| @@ -3,9 +3,9 @@ import { BadRequestException, Injectable } from '@nestjs/common'; | |||||||
| import { Request } from 'express'; | import { Request } from 'express'; | ||||||
|  |  | ||||||
| import { | import { | ||||||
|  |   ObjectRecordOrderBy, | ||||||
|   OrderByDirection, |   OrderByDirection, | ||||||
|   RecordOrderBy, | } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; | ||||||
| } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; |  | ||||||
|  |  | ||||||
| import { checkArrayFields } from 'src/engine/api/rest/core/query-builder/utils/check-order-by.utils'; | 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() | @Injectable() | ||||||
| export class OrderByInputFactory { | export class OrderByInputFactory { | ||||||
|   create(request: Request, objectMetadata): RecordOrderBy { |   create(request: Request, objectMetadata): ObjectRecordOrderBy { | ||||||
|     const orderByQuery = request.query.order_by; |     const orderByQuery = request.query.order_by; | ||||||
|  |  | ||||||
|     if (typeof orderByQuery !== 'string') { |     if (typeof orderByQuery !== 'string') { | ||||||
|   | |||||||
| @@ -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. |  * 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`. |  * 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` |  * 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', |     objectName: 'company', | ||||||
|     columnNames: ['domainName'], |     columnNames: ['domainName'], | ||||||
|   | |||||||
| @@ -1,11 +1,13 @@ | |||||||
| import deepEqual from 'deep-equal'; | 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'; | import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; | ||||||
|  |  | ||||||
| export const objectRecordChangedProperties = < | export const objectRecordChangedProperties = < | ||||||
|   PRecord extends Partial<Record | BaseWorkspaceEntity> = Partial<Record>, |   PRecord extends Partial< | ||||||
|  |     ObjectRecord | BaseWorkspaceEntity | ||||||
|  |   > = Partial<ObjectRecord>, | ||||||
| >( | >( | ||||||
|   oldRecord: PRecord, |   oldRecord: PRecord, | ||||||
|   newRecord: PRecord, |   newRecord: PRecord, | ||||||
|   | |||||||
| @@ -1,23 +1,19 @@ | |||||||
| import deepEqual from 'deep-equal'; | 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 { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; | ||||||
|  |  | ||||||
| import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; | import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; | ||||||
|  |  | ||||||
| export const objectRecordChangedValues = ( | export const objectRecordChangedValues = ( | ||||||
|   oldRecord: Partial<IRecord>, |   oldRecord: Partial<ObjectRecord>, | ||||||
|   newRecord: Partial<IRecord>, |   newRecord: Partial<ObjectRecord>, | ||||||
|   updatedKeys: string[] | undefined, |   updatedKeys: string[] | undefined, | ||||||
|   objectMetadata: ObjectMetadataInterface, |   objectMetadataItem: ObjectMetadataInterface, | ||||||
| ) => { | ) => { | ||||||
|   const fieldsByKey = new Map( |  | ||||||
|     objectMetadata.fields.map((field) => [field.name, field]), |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   return Object.keys(newRecord).reduce( |   return Object.keys(newRecord).reduce( | ||||||
|     (acc, key) => { |     (acc, key) => { | ||||||
|       const field = fieldsByKey.get(key); |       const field = objectMetadataItem.fields.find((f) => f.name === key); | ||||||
|       const oldRecordValue = oldRecord[key]; |       const oldRecordValue = oldRecord[key]; | ||||||
|       const newRecordValue = newRecord[key]; |       const newRecordValue = newRecord[key]; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -8,12 +8,11 @@ export enum FeatureFlagKey { | |||||||
|   IsFunctionSettingsEnabled = 'IS_FUNCTION_SETTINGS_ENABLED', |   IsFunctionSettingsEnabled = 'IS_FUNCTION_SETTINGS_ENABLED', | ||||||
|   IsWorkflowEnabled = 'IS_WORKFLOW_ENABLED', |   IsWorkflowEnabled = 'IS_WORKFLOW_ENABLED', | ||||||
|   IsMessageThreadSubscriberEnabled = 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED', |   IsMessageThreadSubscriberEnabled = 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED', | ||||||
|   IsQueryRunnerTwentyORMEnabled = 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED', |  | ||||||
|   IsWorkspaceFavoriteEnabled = 'IS_WORKSPACE_FAVORITE_ENABLED', |  | ||||||
|   IsSSOEnabled = 'IS_SSO_ENABLED', |   IsSSOEnabled = 'IS_SSO_ENABLED', | ||||||
|   IsGmailSendEmailScopeEnabled = 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED', |   IsGmailSendEmailScopeEnabled = 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED', | ||||||
|   IsAnalyticsV2Enabled = 'IS_ANALYTICS_V2_ENABLED', |   IsAnalyticsV2Enabled = 'IS_ANALYTICS_V2_ENABLED', | ||||||
|   IsUniqueIndexesEnabled = 'IS_UNIQUE_INDEXES_ENABLED', |   IsUniqueIndexesEnabled = 'IS_UNIQUE_INDEXES_ENABLED', | ||||||
|   IsMicrosoftSyncEnabled = 'IS_MICROSOFT_SYNC_ENABLED', |   IsMicrosoftSyncEnabled = 'IS_MICROSOFT_SYNC_ENABLED', | ||||||
|   IsAdvancedFiltersEnabled = 'IS_ADVANCED_FILTERS_ENABLED', |   IsAdvancedFiltersEnabled = 'IS_ADVANCED_FILTERS_ENABLED', | ||||||
|  |   IsAggregateQueryEnabled = 'IS_AGGREGATE_QUERY_ENABLED', | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 { 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'; | import { FilterComparators } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-base-filter.utils'; | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import { OpenAPIV3_1 } from 'openapi-types'; | 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 { 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'; | import { FilterComparators } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-base-filter.utils'; | ||||||
|   | |||||||
| @@ -467,13 +467,13 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat | |||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const objectMetadataMap = |     const objectMetadataMaps = | ||||||
|       await this.workspaceCacheStorageService.getObjectMetadataMap( |       await this.workspaceCacheStorageService.getObjectMetadataMaps( | ||||||
|         workspaceId, |         workspaceId, | ||||||
|         metadataVersion, |         metadataVersion, | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|     if (!objectMetadataMap) { |     if (!objectMetadataMaps) { | ||||||
|       throw new NotFoundException( |       throw new NotFoundException( | ||||||
|         `Object metadata map not found for workspace ${workspaceId} and metadata version ${metadataVersion}`, |         `Object metadata map not found for workspace ${workspaceId} and metadata version ${metadataVersion}`, | ||||||
|       ); |       ); | ||||||
| @@ -481,9 +481,9 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat | |||||||
|  |  | ||||||
|     const mappedResult = fieldMetadataItems.map((fieldMetadataItem) => { |     const mappedResult = fieldMetadataItems.map((fieldMetadataItem) => { | ||||||
|       const objectMetadata = |       const objectMetadata = | ||||||
|         objectMetadataMap[fieldMetadataItem.objectMetadataId]; |         objectMetadataMaps.byId[fieldMetadataItem.objectMetadataId]; | ||||||
|  |  | ||||||
|       const fieldMetadata = objectMetadata.fields[fieldMetadataItem.id]; |       const fieldMetadata = objectMetadata.fieldsById[fieldMetadataItem.id]; | ||||||
|  |  | ||||||
|       const relationMetadata = |       const relationMetadata = | ||||||
|         fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata; |         fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata; | ||||||
| @@ -495,18 +495,18 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       const fromObjectMetadata = |       const fromObjectMetadata = | ||||||
|         objectMetadataMap[relationMetadata.fromObjectMetadataId]; |         objectMetadataMaps.byId[relationMetadata.fromObjectMetadataId]; | ||||||
|  |  | ||||||
|       const toObjectMetadata = |       const toObjectMetadata = | ||||||
|         objectMetadataMap[relationMetadata.toObjectMetadataId]; |         objectMetadataMaps.byId[relationMetadata.toObjectMetadataId]; | ||||||
|  |  | ||||||
|       const fromFieldMetadata = |       const fromFieldMetadata = | ||||||
|         objectMetadataMap[fromObjectMetadata.id].fields[ |         objectMetadataMaps.byId[fromObjectMetadata.id].fieldsById[ | ||||||
|           relationMetadata.fromFieldMetadataId |           relationMetadata.fromFieldMetadataId | ||||||
|         ]; |         ]; | ||||||
|  |  | ||||||
|       const toFieldMetadata = |       const toFieldMetadata = | ||||||
|         objectMetadataMap[toObjectMetadata.id].fields[ |         objectMetadataMaps.byId[toObjectMetadata.id].fieldsById[ | ||||||
|           relationMetadata.toFieldMetadataId |           relationMetadata.toFieldMetadataId | ||||||
|         ]; |         ]; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -0,0 +1,3 @@ | |||||||
|  | import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; | ||||||
|  |  | ||||||
|  | export type FieldMetadataMap = Record<string, FieldMetadataInterface>; | ||||||
| @@ -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; | ||||||
|  | }; | ||||||
| @@ -0,0 +1,7 @@ | |||||||
|  | import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; | ||||||
|  |  | ||||||
|  | export type ObjectMetadataMaps = { | ||||||
|  |   byId: Record<string, ObjectMetadataItemWithFieldMaps>; | ||||||
|  |   byNameSingular: Record<string, ObjectMetadataItemWithFieldMaps>; | ||||||
|  |   byNamePlural: Record<string, ObjectMetadataItemWithFieldMaps>; | ||||||
|  | }; | ||||||
| @@ -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<string, FieldMetadataInterface>; |  | ||||||
|  |  | ||||||
| export type ObjectMetadataMapItem = Omit<ObjectMetadataInterface, 'fields'> & { |  | ||||||
|   fields: FieldMetadataMap; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export type ObjectMetadataMap = Record<string, ObjectMetadataMapItem>; |  | ||||||
|  |  | ||||||
| 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; |  | ||||||
| }; |  | ||||||
| @@ -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; | ||||||
|  | }; | ||||||
| @@ -1,12 +1,14 @@ | |||||||
| import { Injectable, Logger } from '@nestjs/common'; | import { Injectable, Logger } from '@nestjs/common'; | ||||||
| import { InjectRepository } from '@nestjs/typeorm'; | import { InjectRepository } from '@nestjs/typeorm'; | ||||||
|  |  | ||||||
|  | import console from 'console'; | ||||||
|  |  | ||||||
| import { Repository } from 'typeorm'; | import { Repository } from 'typeorm'; | ||||||
|  |  | ||||||
| import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; | import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; | ||||||
| import { LogExecutionTime } from 'src/engine/decorators/observability/log-execution-time.decorator'; | import { LogExecutionTime } from 'src/engine/decorators/observability/log-execution-time.decorator'; | ||||||
| import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; | 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 { | import { | ||||||
|   WorkspaceMetadataCacheException, |   WorkspaceMetadataCacheException, | ||||||
|   WorkspaceMetadataCacheExceptionCode, |   WorkspaceMetadataCacheExceptionCode, | ||||||
| @@ -85,15 +87,15 @@ export class WorkspaceMetadataCacheService { | |||||||
|     console.timeEnd('fetching object metadata'); |     console.timeEnd('fetching object metadata'); | ||||||
|  |  | ||||||
|     console.time('generating object metadata map'); |     console.time('generating object metadata map'); | ||||||
|     const freshObjectMetadataMap = |     const freshObjectMetadataMaps = | ||||||
|       generateObjectMetadataMap(objectMetadataItems); |       generateObjectMetadataMaps(objectMetadataItems); | ||||||
|  |  | ||||||
|     console.timeEnd('generating object metadata map'); |     console.timeEnd('generating object metadata map'); | ||||||
|  |  | ||||||
|     await this.workspaceCacheStorageService.setObjectMetadataMap( |     await this.workspaceCacheStorageService.setObjectMetadataMaps( | ||||||
|       workspaceId, |       workspaceId, | ||||||
|       currentDatabaseVersion, |       currentDatabaseVersion, | ||||||
|       freshObjectMetadataMap, |       freshObjectMetadataMaps, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     await this.workspaceCacheStorageService.removeObjectMetadataOngoingCachingLock( |     await this.workspaceCacheStorageService.removeObjectMetadataOngoingCachingLock( | ||||||
|   | |||||||
| @@ -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 { 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 { 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 { 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 { 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'; | import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; | ||||||
|  |  | ||||||
| @@ -20,10 +20,10 @@ type EntitySchemaColumnMap = { | |||||||
|  |  | ||||||
| @Injectable() | @Injectable() | ||||||
| export class EntitySchemaColumnFactory { | export class EntitySchemaColumnFactory { | ||||||
|   create(fieldMetadataMap: FieldMetadataMap): EntitySchemaColumnMap { |   create(fieldMetadataMapByName: FieldMetadataMap): EntitySchemaColumnMap { | ||||||
|     let entitySchemaColumnMap: EntitySchemaColumnMap = {}; |     let entitySchemaColumnMap: EntitySchemaColumnMap = {}; | ||||||
|  |  | ||||||
|     const fieldMetadataCollection = Object.values(fieldMetadataMap); |     const fieldMetadataCollection = Object.values(fieldMetadataMapByName); | ||||||
|  |  | ||||||
|     for (const fieldMetadata of fieldMetadataCollection) { |     for (const fieldMetadata of fieldMetadataCollection) { | ||||||
|       const key = fieldMetadata.name; |       const key = fieldMetadata.name; | ||||||
|   | |||||||
| @@ -2,10 +2,8 @@ import { Injectable } from '@nestjs/common'; | |||||||
|  |  | ||||||
| import { EntitySchemaRelationOptions } from 'typeorm'; | import { EntitySchemaRelationOptions } from 'typeorm'; | ||||||
|  |  | ||||||
| import { | import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; | ||||||
|   FieldMetadataMap, | import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; | ||||||
|   ObjectMetadataMap, |  | ||||||
| } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; |  | ||||||
| import { determineRelationDetails } from 'src/engine/twenty-orm/utils/determine-relation-details.util'; | import { determineRelationDetails } from 'src/engine/twenty-orm/utils/determine-relation-details.util'; | ||||||
| import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; | import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; | ||||||
|  |  | ||||||
| @@ -18,12 +16,12 @@ export class EntitySchemaRelationFactory { | |||||||
|   constructor() {} |   constructor() {} | ||||||
|  |  | ||||||
|   async create( |   async create( | ||||||
|     fieldMetadataMap: FieldMetadataMap, |     fieldMetadataMapByName: FieldMetadataMap, | ||||||
|     objectMetadataMap: ObjectMetadataMap, |     objectMetadataMaps: ObjectMetadataMaps, | ||||||
|   ): Promise<EntitySchemaRelationMap> { |   ): Promise<EntitySchemaRelationMap> { | ||||||
|     const entitySchemaRelationMap: EntitySchemaRelationMap = {}; |     const entitySchemaRelationMap: EntitySchemaRelationMap = {}; | ||||||
|  |  | ||||||
|     const fieldMetadataCollection = Object.values(fieldMetadataMap); |     const fieldMetadataCollection = Object.values(fieldMetadataMapByName); | ||||||
|  |  | ||||||
|     for (const fieldMetadata of fieldMetadataCollection) { |     for (const fieldMetadata of fieldMetadataCollection) { | ||||||
|       if (!isRelationFieldMetadataType(fieldMetadata.type)) { |       if (!isRelationFieldMetadataType(fieldMetadata.type)) { | ||||||
| @@ -42,7 +40,7 @@ export class EntitySchemaRelationFactory { | |||||||
|       const relationDetails = await determineRelationDetails( |       const relationDetails = await determineRelationDetails( | ||||||
|         fieldMetadata, |         fieldMetadata, | ||||||
|         relationMetadata, |         relationMetadata, | ||||||
|         objectMetadataMap, |         objectMetadataMaps, | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|       entitySchemaRelationMap[fieldMetadata.name] = { |       entitySchemaRelationMap[fieldMetadata.name] = { | ||||||
|   | |||||||
| @@ -2,10 +2,8 @@ import { Injectable } from '@nestjs/common'; | |||||||
|  |  | ||||||
| import { EntitySchema } from 'typeorm'; | import { EntitySchema } from 'typeorm'; | ||||||
|  |  | ||||||
| import { | import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; | ||||||
|   ObjectMetadataMap, | import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; | ||||||
|   ObjectMetadataMapItem, |  | ||||||
| } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; |  | ||||||
| import { EntitySchemaColumnFactory } from 'src/engine/twenty-orm/factories/entity-schema-column.factory'; | 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 { EntitySchemaRelationFactory } from 'src/engine/twenty-orm/factories/entity-schema-relation.factory'; | ||||||
| import { WorkspaceEntitiesStorage } from 'src/engine/twenty-orm/storage/workspace-entities.storage'; | import { WorkspaceEntitiesStorage } from 'src/engine/twenty-orm/storage/workspace-entities.storage'; | ||||||
| @@ -20,17 +18,17 @@ export class EntitySchemaFactory { | |||||||
|  |  | ||||||
|   async create( |   async create( | ||||||
|     workspaceId: string, |     workspaceId: string, | ||||||
|     metadataVersion: number, |     _metadataVersion: number, | ||||||
|     objectMetadata: ObjectMetadataMapItem, |     objectMetadata: ObjectMetadataItemWithFieldMaps, | ||||||
|     objectMetadataMap: ObjectMetadataMap, |     objectMetadataMaps: ObjectMetadataMaps, | ||||||
|   ): Promise<EntitySchema> { |   ): Promise<EntitySchema> { | ||||||
|     const columns = this.entitySchemaColumnFactory.create( |     const columns = this.entitySchemaColumnFactory.create( | ||||||
|       objectMetadata.fields, |       objectMetadata.fieldsByName, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const relations = await this.entitySchemaRelationFactory.create( |     const relations = await this.entitySchemaRelationFactory.create( | ||||||
|       objectMetadata.fields, |       objectMetadata.fieldsByName, | ||||||
|       objectMetadataMap, |       objectMetadataMaps, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const entitySchema = new EntitySchema({ |     const entitySchema = new EntitySchema({ | ||||||
|   | |||||||
| @@ -86,13 +86,13 @@ export class WorkspaceDatasourceFactory { | |||||||
|  |  | ||||||
|             let cachedEntitySchemas: EntitySchema[]; |             let cachedEntitySchemas: EntitySchema[]; | ||||||
|  |  | ||||||
|             const cachedObjectMetadataMap = |             const cachedObjectMetadataMaps = | ||||||
|               await this.workspaceCacheStorageService.getObjectMetadataMap( |               await this.workspaceCacheStorageService.getObjectMetadataMaps( | ||||||
|                 workspaceId, |                 workspaceId, | ||||||
|                 cachedWorkspaceMetadataVersion, |                 cachedWorkspaceMetadataVersion, | ||||||
|               ); |               ); | ||||||
|  |  | ||||||
|             if (!cachedObjectMetadataMap) { |             if (!cachedObjectMetadataMaps) { | ||||||
|               throw new TwentyORMException( |               throw new TwentyORMException( | ||||||
|                 `Workspace Schema not found for workspace ${workspaceId}`, |                 `Workspace Schema not found for workspace ${workspaceId}`, | ||||||
|                 TwentyORMExceptionCode.METADATA_COLLECTION_NOT_FOUND, |                 TwentyORMExceptionCode.METADATA_COLLECTION_NOT_FOUND, | ||||||
| @@ -105,12 +105,13 @@ export class WorkspaceDatasourceFactory { | |||||||
|               ); |               ); | ||||||
|             } else { |             } else { | ||||||
|               const entitySchemas = await Promise.all( |               const entitySchemas = await Promise.all( | ||||||
|                 Object.values(cachedObjectMetadataMap).map((objectMetadata) => |                 Object.values(cachedObjectMetadataMaps.byId).map( | ||||||
|  |                   (objectMetadata) => | ||||||
|                     this.entitySchemaFactory.create( |                     this.entitySchemaFactory.create( | ||||||
|                       workspaceId, |                       workspaceId, | ||||||
|                       cachedWorkspaceMetadataVersion, |                       cachedWorkspaceMetadataVersion, | ||||||
|                       objectMetadata, |                       objectMetadata, | ||||||
|                     cachedObjectMetadataMap, |                       cachedObjectMetadataMaps, | ||||||
|                     ), |                     ), | ||||||
|                 ), |                 ), | ||||||
|               ); |               ); | ||||||
| @@ -127,7 +128,7 @@ export class WorkspaceDatasourceFactory { | |||||||
|             const workspaceDataSource = new WorkspaceDataSource( |             const workspaceDataSource = new WorkspaceDataSource( | ||||||
|               { |               { | ||||||
|                 workspaceId, |                 workspaceId, | ||||||
|                 objectMetadataMap: cachedObjectMetadataMap, |                 objectMetadataMaps: cachedObjectMetadataMaps, | ||||||
|               }, |               }, | ||||||
|               { |               { | ||||||
|                 url: |                 url: | ||||||
|   | |||||||
| @@ -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 { | export interface WorkspaceInternalContext { | ||||||
|   workspaceId: string; |   workspaceId: string; | ||||||
|   objectMetadataMap: ObjectMetadataMap; |   objectMetadataMaps: ObjectMetadataMaps; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -22,7 +22,7 @@ import { UpsertOptions } from 'typeorm/repository/UpsertOptions'; | |||||||
|  |  | ||||||
| import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface'; | 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 { WorkspaceEntitiesStorage } from 'src/engine/twenty-orm/storage/workspace-entities.storage'; | ||||||
| import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; | import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; | ||||||
| import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; | import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; | ||||||
| @@ -631,13 +631,15 @@ export class WorkspaceRepository< | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     const objectMetadata = |     const objectMetadata = | ||||||
|       this.internalContext.objectMetadataMap[objectMetadataName]; |       this.internalContext.objectMetadataMaps.byNameSingular[ | ||||||
|  |         objectMetadataName | ||||||
|  |       ]; | ||||||
|  |  | ||||||
|     if (!objectMetadata) { |     if (!objectMetadata) { | ||||||
|       throw new Error( |       throw new Error( | ||||||
|         `Object metadata for object "${objectMetadataName}" is missing ` + |         `Object metadata for object "${objectMetadataName}" is missing ` + | ||||||
|           `in workspace "${this.internalContext.workspaceId}" ` + |           `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<T>( |   async formatResult<T>( | ||||||
|     data: T, |     data: T, | ||||||
|     objectMetadata?: ObjectMetadataMapItem, |     objectMetadata?: ObjectMetadataItemWithFieldMaps, | ||||||
|   ): Promise<T> { |   ): Promise<T> { | ||||||
|     objectMetadata ??= await this.getObjectMetadataFromTarget(); |     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; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 { 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 { 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'; | import { computeRelationType } from 'src/engine/twenty-orm/utils/compute-relation-type.util'; | ||||||
|  |  | ||||||
| interface RelationDetails { | interface RelationDetails { | ||||||
| @@ -16,22 +16,25 @@ interface RelationDetails { | |||||||
| export async function determineRelationDetails( | export async function determineRelationDetails( | ||||||
|   fieldMetadata: FieldMetadataInterface, |   fieldMetadata: FieldMetadataInterface, | ||||||
|   relationMetadata: RelationMetadataEntity, |   relationMetadata: RelationMetadataEntity, | ||||||
|   objectMetadataMap: ObjectMetadataMap, |   objectMetadataMaps: ObjectMetadataMaps, | ||||||
| ): Promise<RelationDetails> { | ): Promise<RelationDetails> { | ||||||
|   const relationType = computeRelationType(fieldMetadata, relationMetadata); |   const relationType = computeRelationType(fieldMetadata, relationMetadata); | ||||||
|   const fromObjectMetadata = objectMetadataMap[fieldMetadata.objectMetadataId]; |   const fromObjectMetadata = | ||||||
|   let toObjectMetadata = objectMetadataMap[relationMetadata.toObjectMetadataId]; |     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 |   // 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') { |   if (relationType === 'many-to-one') { | ||||||
|     toObjectMetadata = objectMetadataMap[relationMetadata.fromObjectMetadataId]; |     toObjectMetadata = | ||||||
|  |       objectMetadataMaps.byId[relationMetadata.fromObjectMetadataId]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (!fromObjectMetadata || !toObjectMetadata) { |   if (!fromObjectMetadata || !toObjectMetadata) { | ||||||
|     throw new Error('Object metadata not found'); |     throw new Error('Object metadata not found'); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const toFieldMetadata = Object.values(toObjectMetadata.fields).find( |   const toFieldMetadata = Object.values(toObjectMetadata.fieldsById).find( | ||||||
|     (field) => |     (field) => | ||||||
|       relationType === 'many-to-one' |       relationType === 'many-to-one' | ||||||
|         ? field.id === relationMetadata.fromFieldMetadataId |         ? field.id === relationMetadata.fromFieldMetadataId | ||||||
|   | |||||||
| @@ -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 { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; | ||||||
| import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; | 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 { 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 { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; | ||||||
| import { capitalize } from 'src/utils/capitalize'; | import { capitalize } from 'src/utils/capitalize'; | ||||||
|  |  | ||||||
| export function formatData<T>( | export function formatData<T>( | ||||||
|   data: T, |   data: T, | ||||||
|   objectMetadata: ObjectMetadataMapItem, |   objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, | ||||||
| ): T { | ): T { | ||||||
|   if (!data) { |   if (!data) { | ||||||
|     return data; |     return data; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (Array.isArray(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<string, any> = {}; |   const newData: Record<string, any> = {}; | ||||||
|  |  | ||||||
|   for (const [key, value] of Object.entries(data)) { |   for (const [key, value] of Object.entries(data)) { | ||||||
|     const fieldMetadata = objectMetadata.fields[key]; |     const fieldMetadata = objectMetadataItemWithFieldMaps.fieldsByName[key]; | ||||||
|  |  | ||||||
|     if (!fieldMetadata) { |     if (!fieldMetadata) { | ||||||
|       throw new Error( |       throw new Error( | ||||||
|   | |||||||
| @@ -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 { 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 { 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 { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; | ||||||
| import { | import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; | ||||||
|   ObjectMetadataMap, | import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; | ||||||
|   ObjectMetadataMapItem, |  | ||||||
| } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; |  | ||||||
| import { computeRelationType } from 'src/engine/twenty-orm/utils/compute-relation-type.util'; | 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 { getCompositeFieldMetadataCollection } from 'src/engine/twenty-orm/utils/get-composite-field-metadata-collection'; | ||||||
| import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; | import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; | ||||||
|  |  | ||||||
| export function formatResult<T>( | export function formatResult<T>( | ||||||
|   data: T, |   data: T, | ||||||
|   objectMetadata: ObjectMetadataMapItem, |   ObjectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, | ||||||
|   objectMetadataMap: ObjectMetadataMap, |   objectMetadataMaps: ObjectMetadataMaps, | ||||||
| ): T { | ): T { | ||||||
|   if (!data) { |   if (!data) { | ||||||
|     return data; |     return data; | ||||||
| @@ -25,7 +23,7 @@ export function formatResult<T>( | |||||||
|  |  | ||||||
|   if (Array.isArray(data)) { |   if (Array.isArray(data)) { | ||||||
|     return data.map((item) => |     return data.map((item) => | ||||||
|       formatResult(item, objectMetadata, objectMetadataMap), |       formatResult(item, ObjectMetadataItemWithFieldMaps, objectMetadataMaps), | ||||||
|     ) as T; |     ) as T; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -33,12 +31,13 @@ export function formatResult<T>( | |||||||
|     return data; |     return data; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (!objectMetadata) { |   if (!ObjectMetadataItemWithFieldMaps) { | ||||||
|     throw new Error('Object metadata is missing'); |     throw new Error('Object metadata is missing'); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const compositeFieldMetadataCollection = |   const compositeFieldMetadataCollection = getCompositeFieldMetadataCollection( | ||||||
|     getCompositeFieldMetadataCollection(objectMetadata); |     ObjectMetadataItemWithFieldMaps, | ||||||
|  |   ); | ||||||
|  |  | ||||||
|   const compositeFieldMetadataMap = new Map( |   const compositeFieldMetadataMap = new Map( | ||||||
|     compositeFieldMetadataCollection.flatMap((fieldMetadata) => { |     compositeFieldMetadataCollection.flatMap((fieldMetadata) => { | ||||||
| @@ -58,7 +57,7 @@ export function formatResult<T>( | |||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   const relationMetadataMap = new Map( |   const relationMetadataMap = new Map( | ||||||
|     Object.values(objectMetadata.fields) |     Object.values(ObjectMetadataItemWithFieldMaps.fieldsById) | ||||||
|       .filter(({ type }) => isRelationFieldMetadataType(type)) |       .filter(({ type }) => isRelationFieldMetadataType(type)) | ||||||
|       .map((fieldMetadata) => [ |       .map((fieldMetadata) => [ | ||||||
|         fieldMetadata.name, |         fieldMetadata.name, | ||||||
| @@ -75,6 +74,8 @@ export function formatResult<T>( | |||||||
|       ]), |       ]), | ||||||
|   ); |   ); | ||||||
|   const newData: object = {}; |   const newData: object = {}; | ||||||
|  |   const objectMetadaItemFieldsByName = | ||||||
|  |     objectMetadataMaps.byId[ObjectMetadataItemWithFieldMaps.id]?.fieldsByName; | ||||||
|  |  | ||||||
|   for (const [key, value] of Object.entries(data)) { |   for (const [key, value] of Object.entries(data)) { | ||||||
|     const compositePropertyArgs = compositeFieldMetadataMap.get(key); |     const compositePropertyArgs = compositeFieldMetadataMap.get(key); | ||||||
| @@ -83,11 +84,15 @@ export function formatResult<T>( | |||||||
|  |  | ||||||
|     if (!compositePropertyArgs && !relationMetadata) { |     if (!compositePropertyArgs && !relationMetadata) { | ||||||
|       if (isPlainObject(value)) { |       if (isPlainObject(value)) { | ||||||
|         newData[key] = formatResult(value, objectMetadata, objectMetadataMap); |         newData[key] = formatResult( | ||||||
|       } else if (objectMetadata.fields[key]) { |           value, | ||||||
|  |           ObjectMetadataItemWithFieldMaps, | ||||||
|  |           objectMetadataMaps, | ||||||
|  |         ); | ||||||
|  |       } else if (objectMetadaItemFieldsByName[key]) { | ||||||
|         newData[key] = formatFieldMetadataValue( |         newData[key] = formatFieldMetadataValue( | ||||||
|           value, |           value, | ||||||
|           objectMetadata.fields[key], |           objectMetadaItemFieldsByName[key], | ||||||
|         ); |         ); | ||||||
|       } else { |       } else { | ||||||
|         newData[key] = value; |         newData[key] = value; | ||||||
| @@ -98,10 +103,10 @@ export function formatResult<T>( | |||||||
|  |  | ||||||
|     if (relationMetadata) { |     if (relationMetadata) { | ||||||
|       const toObjectMetadata = |       const toObjectMetadata = | ||||||
|         objectMetadataMap[relationMetadata.toObjectMetadataId]; |         objectMetadataMaps.byId[relationMetadata.toObjectMetadataId]; | ||||||
|  |  | ||||||
|       const fromObjectMetadata = |       const fromObjectMetadata = | ||||||
|         objectMetadataMap[relationMetadata.fromObjectMetadataId]; |         objectMetadataMaps.byId[relationMetadata.fromObjectMetadataId]; | ||||||
|  |  | ||||||
|       if (!toObjectMetadata) { |       if (!toObjectMetadata) { | ||||||
|         throw new Error( |         throw new Error( | ||||||
| @@ -118,7 +123,7 @@ export function formatResult<T>( | |||||||
|       newData[key] = formatResult( |       newData[key] = formatResult( | ||||||
|         value, |         value, | ||||||
|         relationType === 'one-to-many' ? toObjectMetadata : fromObjectMetadata, |         relationType === 'one-to-many' ? toObjectMetadata : fromObjectMetadata, | ||||||
|         objectMetadataMap, |         objectMetadataMaps, | ||||||
|       ); |       ); | ||||||
|       continue; |       continue; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,12 +1,14 @@ | |||||||
| import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; | 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( | export function getCompositeFieldMetadataCollection( | ||||||
|   objectMetadata: ObjectMetadataMapItem, |   ObjectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, | ||||||
| ) { | ) { | ||||||
|   const compositeFieldMetadataCollection = Object.values( |   const compositeFieldMetadataCollection = Object.values( | ||||||
|     objectMetadata.fields, |     ObjectMetadataItemWithFieldMaps.fieldsById, | ||||||
|   ).filter((fieldMetadata) => isCompositeFieldMetadataType(fieldMetadata.type)); |   ).filter((fieldMetadataItem) => | ||||||
|  |     isCompositeFieldMetadataType(fieldMetadataItem.type), | ||||||
|  |   ); | ||||||
|  |  | ||||||
|   return compositeFieldMetadataCollection; |   return compositeFieldMetadataCollection; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,14 +5,14 @@ import { EntitySchemaOptions } from 'typeorm'; | |||||||
| import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; | 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 { 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 { 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 { | export enum WorkspaceCacheKeys { | ||||||
|   GraphQLTypeDefs = 'graphql:type-defs', |   GraphQLTypeDefs = 'graphql:type-defs', | ||||||
|   GraphQLUsedScalarNames = 'graphql:used-scalar-names', |   GraphQLUsedScalarNames = 'graphql:used-scalar-names', | ||||||
|   GraphQLOperations = 'graphql:operations', |   GraphQLOperations = 'graphql:operations', | ||||||
|   ORMEntitySchemas = 'orm:entity-schemas', |   ORMEntitySchemas = 'orm:entity-schemas', | ||||||
|   MetadataObjectMetadataMap = 'metadata:object-metadata-map', |   MetadataObjectMetadataMaps = 'metadata:object-metadata-maps', | ||||||
|   MetadataObjectMetadataOngoingCachingLock = 'metadata:object-metadata-ongoing-caching-lock', |   MetadataObjectMetadataOngoingCachingLock = 'metadata:object-metadata-ongoing-caching-lock', | ||||||
|   MetadataVersion = 'metadata:workspace-metadata-version', |   MetadataVersion = 'metadata:workspace-metadata-version', | ||||||
| } | } | ||||||
| @@ -88,23 +88,23 @@ export class WorkspaceCacheStorageService { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   setObjectMetadataMap( |   setObjectMetadataMaps( | ||||||
|     workspaceId: string, |     workspaceId: string, | ||||||
|     metadataVersion: number, |     metadataVersion: number, | ||||||
|     objectMetadataMap: ObjectMetadataMap, |     objectMetadataMaps: ObjectMetadataMaps, | ||||||
|   ) { |   ) { | ||||||
|     return this.cacheStorageService.set<ObjectMetadataMap>( |     return this.cacheStorageService.set<ObjectMetadataMaps>( | ||||||
|       `${WorkspaceCacheKeys.MetadataObjectMetadataMap}:${workspaceId}:${metadataVersion}`, |       `${WorkspaceCacheKeys.MetadataObjectMetadataMaps}:${workspaceId}:${metadataVersion}`, | ||||||
|       objectMetadataMap, |       objectMetadataMaps, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getObjectMetadataMap( |   getObjectMetadataMaps( | ||||||
|     workspaceId: string, |     workspaceId: string, | ||||||
|     metadataVersion: number, |     metadataVersion: number, | ||||||
|   ): Promise<ObjectMetadataMap | undefined> { |   ): Promise<ObjectMetadataMaps | undefined> { | ||||||
|     return this.cacheStorageService.get<ObjectMetadataMap>( |     return this.cacheStorageService.get<ObjectMetadataMaps>( | ||||||
|       `${WorkspaceCacheKeys.MetadataObjectMetadataMap}:${workspaceId}:${metadataVersion}`, |       `${WorkspaceCacheKeys.MetadataObjectMetadataMaps}:${workspaceId}:${metadataVersion}`, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -150,7 +150,7 @@ export class WorkspaceCacheStorageService { | |||||||
|  |  | ||||||
|   async flush(workspaceId: string, metadataVersion: number): Promise<void> { |   async flush(workspaceId: string, metadataVersion: number): Promise<void> { | ||||||
|     await this.cacheStorageService.del( |     await this.cacheStorageService.del( | ||||||
|       `${WorkspaceCacheKeys.MetadataObjectMetadataMap}:${workspaceId}:${metadataVersion}`, |       `${WorkspaceCacheKeys.MetadataObjectMetadataMaps}:${workspaceId}:${metadataVersion}`, | ||||||
|     ); |     ); | ||||||
|     await this.cacheStorageService.del( |     await this.cacheStorageService.del( | ||||||
|       `${WorkspaceCacheKeys.MetadataVersion}:${workspaceId}:${metadataVersion}`, |       `${WorkspaceCacheKeys.MetadataVersion}:${workspaceId}:${metadataVersion}`, | ||||||
|   | |||||||
| @@ -2,10 +2,10 @@ import { Injectable } from '@nestjs/common'; | |||||||
|  |  | ||||||
| import { EntityManager } from 'typeorm'; | 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 { objectRecordDiffMerge } from 'src/engine/core-modules/event-emitter/utils/object-record-diff-merge'; | ||||||
|  | import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; | ||||||
|  |  | ||||||
| @Injectable() | @Injectable() | ||||||
| export class TimelineActivityRepository { | export class TimelineActivityRepository { | ||||||
| @@ -15,7 +15,7 @@ export class TimelineActivityRepository { | |||||||
|  |  | ||||||
|   async upsertOne( |   async upsertOne( | ||||||
|     name: string, |     name: string, | ||||||
|     properties: Partial<Record>, |     properties: Partial<ObjectRecord>, | ||||||
|     objectName: string, |     objectName: string, | ||||||
|     recordId: string, |     recordId: string, | ||||||
|     workspaceId: string, |     workspaceId: string, | ||||||
| @@ -105,7 +105,7 @@ export class TimelineActivityRepository { | |||||||
|   private async updateTimelineActivity( |   private async updateTimelineActivity( | ||||||
|     dataSourceSchema: string, |     dataSourceSchema: string, | ||||||
|     id: string, |     id: string, | ||||||
|     properties: Partial<Record>, |     properties: Partial<ObjectRecord>, | ||||||
|     workspaceMemberId: string | undefined, |     workspaceMemberId: string | undefined, | ||||||
|     workspaceId: string, |     workspaceId: string, | ||||||
|   ) { |   ) { | ||||||
| @@ -121,7 +121,7 @@ export class TimelineActivityRepository { | |||||||
|   private async insertTimelineActivity( |   private async insertTimelineActivity( | ||||||
|     dataSourceSchema: string, |     dataSourceSchema: string, | ||||||
|     name: string, |     name: string, | ||||||
|     properties: Partial<Record>, |     properties: Partial<ObjectRecord>, | ||||||
|     objectName: string, |     objectName: string, | ||||||
|     recordId: string, |     recordId: string, | ||||||
|     workspaceMemberId: string | undefined, |     workspaceMemberId: string | undefined, | ||||||
| @@ -151,7 +151,7 @@ export class TimelineActivityRepository { | |||||||
|     objectName: string, |     objectName: string, | ||||||
|     activities: { |     activities: { | ||||||
|       name: string; |       name: string; | ||||||
|       properties: Partial<Record> | null; |       properties: Partial<ObjectRecord> | null; | ||||||
|       workspaceMemberId: string | undefined; |       workspaceMemberId: string | undefined; | ||||||
|       recordId: string | null; |       recordId: string | null; | ||||||
|       linkedRecordCachedName: string; |       linkedRecordCachedName: string; | ||||||
|   | |||||||
| @@ -1,12 +1,12 @@ | |||||||
| import { v4 } from 'uuid'; | 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 { 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 { 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 { 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 { 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 { 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 { 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 = <Entity>( | export const generateFakeObjectRecordEvent = <Entity>( | ||||||
|   objectMetadataEntity: ObjectMetadataEntity, |   objectMetadataEntity: ObjectMetadataEntity, | ||||||
|   | |||||||
| @@ -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. | 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  | ### Workspace Query Runner  | ||||||
|  |  | ||||||
| Runs the generated queries on the database and parses the result. | Runs the generated queries on the database and parses the result. | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Marie
					Marie