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

@ -5,7 +5,7 @@ import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/typ
export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMaps[] =
[
{
id: '',
id: '20202020-8dec-43d5-b2ff-6eef05095bec',
standardId: '',
nameSingular: 'person',
namePlural: 'people',
@ -52,7 +52,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
fieldIdByJoinColumnName: {},
},
{
id: '',
id: '20202020-c03c-45d6-a4b0-04afe1357c5c',
standardId: '',
nameSingular: 'company',
namePlural: 'companies',
@ -112,7 +112,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
fieldIdByJoinColumnName: {},
},
{
id: '',
id: '20202020-3d75-4aab-bacd-ee176c5f63ca',
standardId: '',
nameSingular: 'regular-custom-object',
namePlural: 'regular-custom-objects',
@ -172,7 +172,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
fieldIdByJoinColumnName: {},
},
{
id: '',
id: '20202020-540c-4397-b872-2a90ea2fb809',
standardId: '',
nameSingular: 'non-searchable-object',
namePlural: 'non-searchable-objects',

View File

@ -1,6 +1,8 @@
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { Args, Query, Resolver } from '@nestjs/graphql';
import { isDefined } from 'twenty-shared/utils';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { SearchArgs } from 'src/engine/core-modules/search/dtos/search-args';
@ -10,12 +12,16 @@ import { SearchService } from 'src/engine/core-modules/search/services/search.se
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
@Resolver()
@UseFilters(SearchApiExceptionFilter, PreventNestToAutoLogGraphqlErrorsFilter)
@UsePipes(ResolverValidationPipe)
export class SearchResolver {
constructor(private readonly searchService: SearchService) {}
constructor(
private readonly searchService: SearchService,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
) {}
@Query(() => SearchResultConnectionDTO)
@UseGuards(WorkspaceAuthGuard)
@ -31,12 +37,16 @@ export class SearchResolver {
after,
}: SearchArgs,
) {
const objectMetadataItemWithFieldMaps =
await this.searchService.getObjectMetadataItemWithFieldMaps(workspace);
const objectMetadataMaps =
await this.workspaceCacheStorageService.getObjectMetadataMapsOrThrow(
workspace.id,
);
const filteredObjectMetadataItems =
this.searchService.filterObjectMetadataItems({
objectMetadataItemWithFieldMaps,
objectMetadataItemWithFieldMaps: Object.values(
objectMetadataMaps.byId,
).filter(isDefined),
includedObjectNameSingulars: includedObjectNameSingulars ?? [],
excludedObjectNameSingulars: excludedObjectNameSingulars ?? [],
});

View File

@ -29,13 +29,11 @@ import {
} from 'src/engine/core-modules/search/exceptions/search.exception';
import { RecordsWithObjectMetadataItem } from 'src/engine/core-modules/search/types/records-with-object-metadata-item';
import { formatSearchTerms } from 'src/engine/core-modules/search/utils/format-search-terms';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { generateObjectMetadataMaps } from 'src/engine/metadata-modules/utils/generate-object-metadata-maps.util';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
type LastRanks = { tsRankCD: number; tsRank: number };
@ -51,18 +49,8 @@ export class SearchService {
constructor(
private readonly twentyORMManager: TwentyORMManager,
private readonly fileService: FileService,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
) {}
async getObjectMetadataItemWithFieldMaps(workspace: Workspace) {
const objectMetadataMaps =
await this.workspaceCacheStorageService.getObjectMetadataMapsOrThrow(
workspace.id,
);
return Object.values(objectMetadataMaps.byId);
}
async getAllRecordsWithObjectMetadataItems({
objectMetadataItemWithFieldMaps,
includedObjectNameSingulars,