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 { 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 { 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 { 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 { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; 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 // 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 // And this factory that is also executing logic on object relations
@ -121,6 +121,10 @@ export class QueryResultGettersFactory {
): Promise<ObjectRecord> { ): Promise<ObjectRecord> {
const objectMetadataMapItem = objectMetadataMaps.byId[objectMetadataItemId]; 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 handler = this.getHandler(objectMetadataMapItem.nameSingular);
const relationFields = Object.keys(record) const relationFields = Object.keys(record)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,13 +29,11 @@ import {
} from 'src/engine/core-modules/search/exceptions/search.exception'; } from 'src/engine/core-modules/search/exceptions/search.exception';
import { RecordsWithObjectMetadataItem } from 'src/engine/core-modules/search/types/records-with-object-metadata-item'; 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 { 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 { 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 { 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 { generateObjectMetadataMaps } from 'src/engine/metadata-modules/utils/generate-object-metadata-maps.util';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; 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 }; type LastRanks = { tsRankCD: number; tsRank: number };
@ -51,18 +49,8 @@ export class SearchService {
constructor( constructor(
private readonly twentyORMManager: TwentyORMManager, private readonly twentyORMManager: TwentyORMManager,
private readonly fileService: FileService, 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({ async getAllRecordsWithObjectMetadataItems({
objectMetadataItemWithFieldMaps, objectMetadataItemWithFieldMaps,
includedObjectNameSingulars, includedObjectNameSingulars,

View File

@ -114,8 +114,14 @@ export class DataloaderService {
{ workspaceId }, { workspaceId },
); );
const indexMetadataCollection = objectMetadataIds.map((id) => const indexMetadataCollection = objectMetadataIds.map((id) => {
Object.values(objectMetadataMaps.byId[id].indexMetadatas).map( const objectMetadata = objectMetadataMaps.byId[id];
if (!isDefined(objectMetadata)) {
return [];
}
return Object.values(objectMetadata.indexMetadatas).map(
(indexMetadata) => { (indexMetadata) => {
return { return {
...indexMetadata, ...indexMetadata,
@ -127,8 +133,8 @@ export class DataloaderService {
workspaceId: workspaceId, workspaceId: workspaceId,
}; };
}, },
), );
); });
return indexMetadataCollection; return indexMetadataCollection;
}, },
@ -148,8 +154,14 @@ export class DataloaderService {
{ workspaceId }, { workspaceId },
); );
const fieldMetadataCollection = objectMetadataIds.map((id) => const fieldMetadataCollection = objectMetadataIds.map((id) => {
Object.values(objectMetadataMaps.byId[id].fieldsById).map( const objectMetadata = objectMetadataMaps.byId[id];
if (!isDefined(objectMetadata)) {
return [];
}
return Object.values(objectMetadata.fieldsById).map(
// TODO: fix this as we should merge FieldMetadataEntity and FieldMetadataInterface // TODO: fix this as we should merge FieldMetadataEntity and FieldMetadataInterface
(fieldMetadata) => { (fieldMetadata) => {
const overridesFieldToCompute = [ const overridesFieldToCompute = [
@ -182,8 +194,8 @@ export class DataloaderService {
...overrides, ...overrides,
}; };
}, },
), );
); });
return fieldMetadataCollection; return fieldMetadataCollection;
}, },
@ -207,9 +219,13 @@ export class DataloaderService {
objectMetadata: { id: objectMetadataId }, objectMetadata: { id: objectMetadataId },
indexMetadata: { id: indexMetadataId }, indexMetadata: { id: indexMetadataId },
}) => { }) => {
const indexMetadataEntity = objectMetadataMaps.byId[ const objectMetadata = objectMetadataMaps.byId[objectMetadataId];
objectMetadataId
].indexMetadatas.find( if (!isDefined(objectMetadata)) {
return [];
}
const indexMetadataEntity = objectMetadata.indexMetadatas.find(
(indexMetadata) => indexMetadata.id === indexMetadataId, (indexMetadata) => indexMetadata.id === indexMetadataId,
); );

View File

@ -166,6 +166,13 @@ export class FieldMetadataRelationService {
relationCreationPayload.targetObjectMetadataId relationCreationPayload.targetObjectMetadataId
]; ];
if (!isDefined(objectMetadataTarget)) {
throw new FieldMetadataException(
`Object metadata relation target not found for relation creation payload`,
FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
);
}
validateFieldNameAvailabilityOrThrow( validateFieldNameAvailabilityOrThrow(
computedMetadataNameFromLabel, computedMetadataNameFromLabel,
objectMetadataTarget, objectMetadataTarget,

View File

@ -107,7 +107,9 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
let existingFieldMetadata: FieldMetadataInterface | undefined; let existingFieldMetadata: FieldMetadataInterface | undefined;
for (const objectMetadataItem of Object.values(objectMetadataMaps.byId)) { for (const objectMetadataItem of Object.values(
objectMetadataMaps.byId,
).filter(isDefined)) {
const fieldMetadata = objectMetadataItem.fieldsById[id]; const fieldMetadata = objectMetadataItem.fieldsById[id];
if (fieldMetadata) { if (fieldMetadata) {
@ -126,6 +128,13 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
const objectMetadataItemWithFieldMaps = const objectMetadataItemWithFieldMaps =
objectMetadataMaps.byId[existingFieldMetadata.objectMetadataId]; objectMetadataMaps.byId[existingFieldMetadata.objectMetadataId];
if (!isDefined(objectMetadataItemWithFieldMaps)) {
throw new FieldMetadataException(
'Object metadata does not exist',
FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
);
}
const queryRunner = this.coreDataSource.createQueryRunner(); const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect(); await queryRunner.connect();
@ -703,7 +712,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
isRemoteCreation, isRemoteCreation,
}: { }: {
createdFieldMetadataItems: FieldMetadataEntity[]; createdFieldMetadataItems: FieldMetadataEntity[];
objectMetadataMap: Record<string, ObjectMetadataItemWithFieldMaps>; objectMetadataMap: ObjectMetadataMaps['byId'];
isRemoteCreation: boolean; isRemoteCreation: boolean;
}): Promise<WorkspaceMigrationTableAction[]> { }): Promise<WorkspaceMigrationTableAction[]> {
if (isRemoteCreation) { if (isRemoteCreation) {
@ -726,10 +735,18 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
} }
} }
const objectMetadata =
objectMetadataMap[createdFieldMetadata.objectMetadataId];
if (!isDefined(objectMetadata)) {
throw new FieldMetadataException(
'Object metadata does not exist',
FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
);
}
migrationActions.push({ migrationActions.push({
name: computeObjectTargetTable( name: computeObjectTargetTable(objectMetadata),
objectMetadataMap[createdFieldMetadata.objectMetadataId],
),
action: WorkspaceMigrationTableActionType.ALTER, action: WorkspaceMigrationTableActionType.ALTER,
columns: this.workspaceMigrationFactory.createColumnActions( columns: this.workspaceMigrationFactory.createColumnActions(
WorkspaceMigrationColumnActionType.CREATE, WorkspaceMigrationColumnActionType.CREATE,

View File

@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { capitalize } from 'twenty-shared/utils'; import { capitalize, isDefined } from 'twenty-shared/utils';
import { QueryRunner, Repository } from 'typeorm'; import { QueryRunner, Repository } from 'typeorm';
import { v4 as uuidV4 } from 'uuid'; import { v4 as uuidV4 } from 'uuid';
@ -82,10 +82,12 @@ export class ObjectMetadataFieldRelationService {
relationObjectMetadataStandardId: string; relationObjectMetadataStandardId: string;
queryRunner?: QueryRunner; queryRunner?: QueryRunner;
}) { }) {
const targetObjectMetadata = Object.values(objectMetadataMaps.byId).find( const targetObjectMetadata = Object.values(objectMetadataMaps.byId)
(objectMetadata) => .filter(isDefined)
objectMetadata.standardId === relationObjectMetadataStandardId, .find(
); (objectMetadata) =>
objectMetadata.standardId === relationObjectMetadataStandardId,
);
if (!targetObjectMetadata) { if (!targetObjectMetadata) {
throw new Error( throw new Error(

View File

@ -13,7 +13,7 @@ import {
PermissionsExceptionMessage, PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception'; } from 'src/engine/metadata-modules/permissions/permissions.exception';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
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 { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
@ -126,7 +126,7 @@ export class FieldPermissionService {
role, role,
}: { }: {
fieldPermission: UpsertFieldPermissionsInput['fieldPermissions'][0]; fieldPermission: UpsertFieldPermissionsInput['fieldPermissions'][0];
objectMetadataMapsById: Record<string, ObjectMetadataItemWithFieldMaps>; objectMetadataMapsById: ObjectMetadataMaps['byId'];
rolesPermissions: ObjectRecordsPermissionsByRoleId; rolesPermissions: ObjectRecordsPermissionsByRoleId;
role: RoleEntity; role: RoleEntity;
}) { }) {

View File

@ -1,6 +1,6 @@
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
export type ObjectMetadataMaps = { export type ObjectMetadataMaps = {
byId: Record<string, ObjectMetadataItemWithFieldMaps>; byId: Partial<Record<string, ObjectMetadataItemWithFieldMaps>>;
idByNameSingular: Record<string, string>; idByNameSingular: Partial<Record<string, string>>;
}; };

View File

@ -1,3 +1,5 @@
import { isDefined } from 'twenty-shared/utils';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; 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 { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
@ -5,7 +7,9 @@ export const getObjectMetadataMapItemByNamePlural = (
objectMetadataMaps: ObjectMetadataMaps, objectMetadataMaps: ObjectMetadataMaps,
namePlural: string, namePlural: string,
): ObjectMetadataItemWithFieldMaps | undefined => { ): ObjectMetadataItemWithFieldMaps | undefined => {
const objectMetadataItems = Object.values(objectMetadataMaps.byId); const objectMetadataItems = Object.values(objectMetadataMaps.byId).filter(
isDefined,
);
return objectMetadataItems.find( return objectMetadataItems.find(
(objectMetadata) => objectMetadata.namePlural === namePlural, (objectMetadata) => objectMetadata.namePlural === namePlural,

View File

@ -1,3 +1,5 @@
import { isNonEmptyString } from '@sniptt/guards';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; 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 { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
@ -5,7 +7,11 @@ export const getObjectMetadataMapItemByNameSingular = (
objectMetadataMaps: ObjectMetadataMaps, objectMetadataMaps: ObjectMetadataMaps,
nameSingular: string, nameSingular: string,
): ObjectMetadataItemWithFieldMaps | undefined => { ): ObjectMetadataItemWithFieldMaps | undefined => {
return objectMetadataMaps.byId[ const objectMetadataId = objectMetadataMaps.idByNameSingular[nameSingular];
objectMetadataMaps.idByNameSingular[nameSingular]
]; if (!isNonEmptyString(objectMetadataId)) {
return undefined;
}
return objectMetadataMaps.byId[objectMetadataId];
}; };

View File

@ -1,4 +1,5 @@
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { import {
ObjectMetadataException, ObjectMetadataException,
@ -19,14 +20,16 @@ export const validatesNoOtherObjectWithSameNameExistsOrThrows = ({
existingObjectMetadataId, existingObjectMetadataId,
objectMetadataMaps, objectMetadataMaps,
}: ValidateNoOtherObjectWithSameNameExistsOrThrowsParams) => { }: ValidateNoOtherObjectWithSameNameExistsOrThrowsParams) => {
const objectAlreadyExists = Object.values(objectMetadataMaps.byId).find( const objectAlreadyExists = Object.values(objectMetadataMaps.byId)
(objectMetadata) => .filter(isDefined)
(objectMetadata.nameSingular === objectMetadataNameSingular || .find(
objectMetadata.namePlural === objectMetadataNamePlural || (objectMetadata) =>
objectMetadata.nameSingular === objectMetadataNamePlural || (objectMetadata.nameSingular === objectMetadataNameSingular ||
objectMetadata.namePlural === objectMetadataNameSingular) && objectMetadata.namePlural === objectMetadataNamePlural ||
objectMetadata.id !== existingObjectMetadataId, objectMetadata.nameSingular === objectMetadataNamePlural ||
); objectMetadata.namePlural === objectMetadataNameSingular) &&
objectMetadata.id !== existingObjectMetadataId,
);
if (objectAlreadyExists) { if (objectAlreadyExists) {
throw new ObjectMetadataException( throw new ObjectMetadataException(

View File

@ -166,15 +166,16 @@ export class WorkspaceDatasourceFactory {
); );
} else { } else {
const entitySchemas = await Promise.all( const entitySchemas = await Promise.all(
Object.values(cachedObjectMetadataMaps.byId).map( Object.values(cachedObjectMetadataMaps.byId)
(objectMetadata) => .filter(isDefined)
.map((objectMetadata) =>
this.entitySchemaFactory.create( this.entitySchemaFactory.create(
workspaceId, workspaceId,
dataSourceMetadataVersion, dataSourceMetadataVersion,
objectMetadata, objectMetadata,
cachedObjectMetadataMaps, cachedObjectMetadataMaps,
), ),
), ),
); );
await this.workspaceCacheStorageService.setORMEntitySchema( await this.workspaceCacheStorageService.setORMEntitySchema(

View File

@ -1,4 +1,6 @@
import { isNonEmptyString } from '@sniptt/guards';
import { ObjectRecordsPermissions } from 'twenty-shared/types'; import { ObjectRecordsPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { QueryExpressionMap } from 'typeorm/query-builder/QueryExpressionMap'; import { QueryExpressionMap } from 'typeorm/query-builder/QueryExpressionMap';
import { import {
@ -40,8 +42,23 @@ export const validateOperationIsPermittedOrThrow = ({
const objectMetadataIdForEntity = const objectMetadataIdForEntity =
objectMetadataMaps.idByNameSingular[entityName]; objectMetadataMaps.idByNameSingular[entityName];
const objectMetadataIsSystem = if (!isNonEmptyString(objectMetadataIdForEntity)) {
objectMetadataMaps.byId[objectMetadataIdForEntity]?.isSystem === true; throw new PermissionsException(
PermissionsExceptionMessage.PERMISSION_DENIED,
PermissionsExceptionCode.PERMISSION_DENIED,
);
}
const objectMetadata = objectMetadataMaps.byId[objectMetadataIdForEntity];
if (!isDefined(objectMetadata)) {
throw new PermissionsException(
PermissionsExceptionMessage.PERMISSION_DENIED,
PermissionsExceptionCode.PERMISSION_DENIED,
);
}
const objectMetadataIsSystem = objectMetadata.isSystem === true;
if (objectMetadataIsSystem) { if (objectMetadataIsSystem) {
return; return;

View File

@ -30,10 +30,11 @@ export const generateFakeFormResponse = async ({
formFieldMetadata?.settings?.objectName, formFieldMetadata?.settings?.objectName,
); );
if (!objectMetadataItemWithFieldsMaps) if (!isDefined(objectMetadataItemWithFieldsMaps)) {
throw new Error( throw new Error(
`Object metadata not found for object name ${formFieldMetadata?.settings?.objectName}`, `Object metadata not found for object name ${formFieldMetadata?.settings?.objectName}`,
); );
}
return { return {
[formFieldMetadata.name]: { [formFieldMetadata.name]: {

View File

@ -43,6 +43,10 @@ export const generateObjectRecordFields = ({
field.relationTargetObjectMetadataId field.relationTargetObjectMetadataId
]; ];
if (!isDefined(relationTargetObjectMetadata)) {
return acc;
}
acc[field.name] = { acc[field.name] = {
isLeaf: false, isLeaf: false,
icon: field.icon, icon: field.icon,

View File

@ -196,7 +196,11 @@ export class DatabaseEventTriggerListener {
} }
const relatedObjectMetadataNameSingular = const relatedObjectMetadataNameSingular =
objectMetadataMaps.byId[relatedObjectMetadataId].nameSingular; objectMetadataMaps.byId[relatedObjectMetadataId]?.nameSingular;
if (!isDefined(relatedObjectMetadataNameSingular)) {
continue;
}
const relatedObjectRepository = const relatedObjectRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace( await this.twentyORMGlobalManager.getRepositoryForWorkspace(

View File

@ -60,7 +60,7 @@ exports[`Field metadata relation creation should fail relation when targetObject
"exceptionEventId": "mocked-exception-id", "exceptionEventId": "mocked-exception-id",
"userFriendlyMessage": "An error occurred.", "userFriendlyMessage": "An error occurred.",
}, },
"message": "Cannot read properties of undefined (reading 'fieldsById')", "message": "Object metadata relation target not found for relation creation payload",
}, },
] ]
`; `;