Fix relation field unknown target object (#13129)

Fixes https://github.com/twentyhq/twenty/issues/12867

Issue:
when you have a variable `toto` which is: `Record<string, MyType>` and
you do toto['xxx'], this will be typed as `MyType` instead of `MyType |
undefined`

Solutions:
- activate `noUncheckedIndexedAccess` check in tsconfig, this is the
preferred solution but will take time to get there (this raises 600+
errors)
- use a Map: cf https://github.com/twentyhq/twenty/pull/13125/files
- set the type to Partial<Record<string, MyType>>. Drawback is that when
you do Object.values(toto), you'll get `Array<MyType | undefined>`.
Hence why we have to filter these behind


<img width="1512" alt="image"
src="https://github.com/user-attachments/assets/d0a0bfed-c441-4e53-84c2-2da98ccbcf50"
/>
This commit is contained in:
Charles Bochet
2025-07-09 15:43:11 +02:00
committed by GitHub
parent 156cb1b52f
commit 867619247f
23 changed files with 170 additions and 71 deletions

View File

@ -19,9 +19,9 @@ import { PersonQueryResultGetterHandler } from 'src/engine/api/graphql/workspace
import { WorkspaceMemberQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/workspace-member-query-result-getter.handler';
import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
// TODO: find a way to prevent conflict between handlers executing logic on object relations
// And this factory that is also executing logic on object relations
@ -121,6 +121,10 @@ export class QueryResultGettersFactory {
): Promise<ObjectRecord> {
const objectMetadataMapItem = objectMetadataMaps.byId[objectMetadataItemId];
if (!isDefined(objectMetadataMapItem)) {
throw new Error('Object metadata map item is not defined');
}
const handler = this.getHandler(objectMetadataMapItem.nameSingular);
const relationFields = Object.keys(record)

View File

@ -1,6 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { IResolvers } from '@graphql-tools/utils';
import { isDefined } from 'twenty-shared/utils';
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';
@ -75,7 +76,9 @@ export class WorkspaceResolverFactory {
Mutation: {},
};
for (const objectMetadata of Object.values(objectMetadataMaps.byId)) {
for (const objectMetadata of Object.values(objectMetadataMaps.byId).filter(
isDefined,
)) {
// Generate query resolvers
for (const methodName of workspaceResolverBuilderMethods.queries) {
const resolverName = getResolverName(objectMetadata, methodName);

View File

@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { GraphQLSchema, printSchema } from 'graphql';
import { gql } from 'graphql-tag';
import { isDefined } from 'twenty-shared/utils';
import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-explorer.service';
import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/factories/factories';
@ -56,13 +57,13 @@ export class WorkspaceSchemaFactory {
);
}
const objectMetadataCollection = Object.values(objectMetadataMaps.byId).map(
(objectMetadataItem) => ({
const objectMetadataCollection = Object.values(objectMetadataMaps.byId)
.filter(isDefined)
.map((objectMetadataItem) => ({
...objectMetadataItem,
fields: Object.values(objectMetadataItem.fieldsById),
indexes: objectMetadataItem.indexMetadatas,
}),
);
}));
// Get typeDefs from cache
let typeDefs = await this.workspaceCacheStorageService.getGraphQLTypeDefs(

View File

@ -171,6 +171,12 @@ export abstract class RestApiBaseHandler {
objectMetadata.objectMetadataMaps.byId[
field.relationTargetObjectMetadataId
];
if (!isDefined(relationTargetObjectMetadata)) {
throw new BadRequestException(
`Object metadata relation target not found for relation creation payload`,
);
}
const depth2Relations = this.getRelations({
objectMetadata: {
objectMetadataMaps: objectMetadata.objectMetadataMaps,

View File

@ -1,4 +1,5 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
@ -58,6 +59,10 @@ export const mapFieldMetadataToGraphqlQuery = (
const relationMetadataItem =
objectMetadataMaps.byId[targetObjectMetadataId];
if (!isDefined(relationMetadataItem)) {
return '';
}
return `${field.name}
{
id