From 867619247f3e503529cd799208844fb814811516 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Wed, 9 Jul 2025 15:43:11 +0200 Subject: [PATCH] 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` 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>. Drawback is that when you do Object.values(toto), you'll get `Array`. Hence why we have to filter these behind image --- .../query-result-getters.factory.ts | 6 ++- .../workspace-resolver.factory.ts | 5 ++- .../api/graphql/workspace-schema.factory.ts | 9 +++-- .../core/interfaces/rest-api-base.handler.ts | 6 +++ ...p-field-metadata-to-graphql-query.utils.ts | 5 +++ .../mockObjectMetadataItemsWithFieldMaps.ts | 8 ++-- .../core-modules/search/search.resolver.ts | 18 +++++++-- .../search/services/search.service.ts | 12 ------ .../engine/dataloaders/dataloader.service.ts | 38 +++++++++++++------ .../field-metadata-relation.service.ts | 7 ++++ .../services/field-metadata.service.ts | 27 ++++++++++--- .../object-metadata-field-relation.service.ts | 12 +++--- .../field-permission.service.ts | 4 +- .../types/object-metadata-maps.ts | 4 +- ...t-metadata-map-item-by-name-plural.util.ts | 6 ++- ...metadata-map-item-by-name-singular.util.ts | 12 ++++-- ...ect-with-same-name-exists-or-throw.util.ts | 19 ++++++---- .../factories/workspace-datasource.factory.ts | 7 ++-- .../repository/permissions.utils.ts | 21 +++++++++- .../utils/generate-fake-form-response.ts | 3 +- .../utils/generate-object-record-fields.ts | 4 ++ .../database-event-trigger.listener.ts | 6 ++- ...relation-creation.integration-spec.ts.snap | 2 +- 23 files changed, 170 insertions(+), 71 deletions(-) diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts index 4d267cc96..4bcf9f91c 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts @@ -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 { 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) diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts index fb6b502ee..0e412c9bf 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts @@ -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); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts index 95c124322..f15d48dab 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts @@ -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( diff --git a/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts b/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts index 2e65fdc49..02cfdd6f7 100644 --- a/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts @@ -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, diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts index 59aeaefd5..4e90c2755 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts @@ -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 diff --git a/packages/twenty-server/src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts b/packages/twenty-server/src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts index cdf42c5fa..5e0d95efc 100644 --- a/packages/twenty-server/src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts +++ b/packages/twenty-server/src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts @@ -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', diff --git a/packages/twenty-server/src/engine/core-modules/search/search.resolver.ts b/packages/twenty-server/src/engine/core-modules/search/search.resolver.ts index 035ef05dc..d11a00e8c 100644 --- a/packages/twenty-server/src/engine/core-modules/search/search.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/search/search.resolver.ts @@ -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 ?? [], }); diff --git a/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts b/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts index 43bf1f771..bed79c4d5 100644 --- a/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts +++ b/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts @@ -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, diff --git a/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts b/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts index 7508aa7d8..675819154 100644 --- a/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts +++ b/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts @@ -114,8 +114,14 @@ export class DataloaderService { { workspaceId }, ); - const indexMetadataCollection = objectMetadataIds.map((id) => - Object.values(objectMetadataMaps.byId[id].indexMetadatas).map( + const indexMetadataCollection = objectMetadataIds.map((id) => { + const objectMetadata = objectMetadataMaps.byId[id]; + + if (!isDefined(objectMetadata)) { + return []; + } + + return Object.values(objectMetadata.indexMetadatas).map( (indexMetadata) => { return { ...indexMetadata, @@ -127,8 +133,8 @@ export class DataloaderService { workspaceId: workspaceId, }; }, - ), - ); + ); + }); return indexMetadataCollection; }, @@ -148,8 +154,14 @@ export class DataloaderService { { workspaceId }, ); - const fieldMetadataCollection = objectMetadataIds.map((id) => - Object.values(objectMetadataMaps.byId[id].fieldsById).map( + const fieldMetadataCollection = objectMetadataIds.map((id) => { + 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 (fieldMetadata) => { const overridesFieldToCompute = [ @@ -182,8 +194,8 @@ export class DataloaderService { ...overrides, }; }, - ), - ); + ); + }); return fieldMetadataCollection; }, @@ -207,9 +219,13 @@ export class DataloaderService { objectMetadata: { id: objectMetadataId }, indexMetadata: { id: indexMetadataId }, }) => { - const indexMetadataEntity = objectMetadataMaps.byId[ - objectMetadataId - ].indexMetadatas.find( + const objectMetadata = objectMetadataMaps.byId[objectMetadataId]; + + if (!isDefined(objectMetadata)) { + return []; + } + + const indexMetadataEntity = objectMetadata.indexMetadatas.find( (indexMetadata) => indexMetadata.id === indexMetadataId, ); diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service.ts index df930fcb6..3e2b18acb 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service.ts @@ -166,6 +166,13 @@ export class FieldMetadataRelationService { relationCreationPayload.targetObjectMetadataId ]; + if (!isDefined(objectMetadataTarget)) { + throw new FieldMetadataException( + `Object metadata relation target not found for relation creation payload`, + FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED, + ); + } + validateFieldNameAvailabilityOrThrow( computedMetadataNameFromLabel, objectMetadataTarget, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts index 6cc2057ae..ff9462902 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts @@ -107,7 +107,9 @@ export class FieldMetadataService extends TypeOrmQueryService; + objectMetadataMap: ObjectMetadataMaps['byId']; isRemoteCreation: boolean; }): Promise { if (isRemoteCreation) { @@ -726,10 +735,18 @@ export class FieldMetadataService extends TypeOrmQueryService - objectMetadata.standardId === relationObjectMetadataStandardId, - ); + const targetObjectMetadata = Object.values(objectMetadataMaps.byId) + .filter(isDefined) + .find( + (objectMetadata) => + objectMetadata.standardId === relationObjectMetadataStandardId, + ); if (!targetObjectMetadata) { throw new Error( diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts index 1f51fab49..8e0691a35 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts @@ -13,7 +13,7 @@ import { PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; 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 { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; @@ -126,7 +126,7 @@ export class FieldPermissionService { role, }: { fieldPermission: UpsertFieldPermissionsInput['fieldPermissions'][0]; - objectMetadataMapsById: Record; + objectMetadataMapsById: ObjectMetadataMaps['byId']; rolesPermissions: ObjectRecordsPermissionsByRoleId; role: RoleEntity; }) { diff --git a/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-maps.ts b/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-maps.ts index d3ff90d22..5283c1e73 100644 --- a/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-maps.ts +++ b/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-maps.ts @@ -1,6 +1,6 @@ import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; export type ObjectMetadataMaps = { - byId: Record; - idByNameSingular: Record; + byId: Partial>; + idByNameSingular: Partial>; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-plural.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-plural.util.ts index f08747fe4..4720b3a36 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-plural.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-plural.util.ts @@ -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 { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @@ -5,7 +7,9 @@ export const getObjectMetadataMapItemByNamePlural = ( objectMetadataMaps: ObjectMetadataMaps, namePlural: string, ): ObjectMetadataItemWithFieldMaps | undefined => { - const objectMetadataItems = Object.values(objectMetadataMaps.byId); + const objectMetadataItems = Object.values(objectMetadataMaps.byId).filter( + isDefined, + ); return objectMetadataItems.find( (objectMetadata) => objectMetadata.namePlural === namePlural, diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util.ts index aba7bf14e..7bd4ab52f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util.ts @@ -1,3 +1,5 @@ +import { isNonEmptyString } from '@sniptt/guards'; + 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'; @@ -5,7 +7,11 @@ export const getObjectMetadataMapItemByNameSingular = ( objectMetadataMaps: ObjectMetadataMaps, nameSingular: string, ): ObjectMetadataItemWithFieldMaps | undefined => { - return objectMetadataMaps.byId[ - objectMetadataMaps.idByNameSingular[nameSingular] - ]; + const objectMetadataId = objectMetadataMaps.idByNameSingular[nameSingular]; + + if (!isNonEmptyString(objectMetadataId)) { + return undefined; + } + + return objectMetadataMaps.byId[objectMetadataId]; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util.ts index 9c8e0bee9..9bb27e043 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util.ts @@ -1,4 +1,5 @@ import { t } from '@lingui/core/macro'; +import { isDefined } from 'twenty-shared/utils'; import { ObjectMetadataException, @@ -19,14 +20,16 @@ export const validatesNoOtherObjectWithSameNameExistsOrThrows = ({ existingObjectMetadataId, objectMetadataMaps, }: ValidateNoOtherObjectWithSameNameExistsOrThrowsParams) => { - const objectAlreadyExists = Object.values(objectMetadataMaps.byId).find( - (objectMetadata) => - (objectMetadata.nameSingular === objectMetadataNameSingular || - objectMetadata.namePlural === objectMetadataNamePlural || - objectMetadata.nameSingular === objectMetadataNamePlural || - objectMetadata.namePlural === objectMetadataNameSingular) && - objectMetadata.id !== existingObjectMetadataId, - ); + const objectAlreadyExists = Object.values(objectMetadataMaps.byId) + .filter(isDefined) + .find( + (objectMetadata) => + (objectMetadata.nameSingular === objectMetadataNameSingular || + objectMetadata.namePlural === objectMetadataNamePlural || + objectMetadata.nameSingular === objectMetadataNamePlural || + objectMetadata.namePlural === objectMetadataNameSingular) && + objectMetadata.id !== existingObjectMetadataId, + ); if (objectAlreadyExists) { throw new ObjectMetadataException( diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts index 29bb6d35c..3b2e7a045 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts @@ -166,15 +166,16 @@ export class WorkspaceDatasourceFactory { ); } else { const entitySchemas = await Promise.all( - Object.values(cachedObjectMetadataMaps.byId).map( - (objectMetadata) => + Object.values(cachedObjectMetadataMaps.byId) + .filter(isDefined) + .map((objectMetadata) => this.entitySchemaFactory.create( workspaceId, dataSourceMetadataVersion, objectMetadata, cachedObjectMetadataMaps, ), - ), + ), ); await this.workspaceCacheStorageService.setORMEntitySchema( diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/permissions.utils.ts b/packages/twenty-server/src/engine/twenty-orm/repository/permissions.utils.ts index 05c72648d..e34b90b7d 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/permissions.utils.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/permissions.utils.ts @@ -1,4 +1,6 @@ +import { isNonEmptyString } from '@sniptt/guards'; import { ObjectRecordsPermissions } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; import { QueryExpressionMap } from 'typeorm/query-builder/QueryExpressionMap'; import { @@ -40,8 +42,23 @@ export const validateOperationIsPermittedOrThrow = ({ const objectMetadataIdForEntity = objectMetadataMaps.idByNameSingular[entityName]; - const objectMetadataIsSystem = - objectMetadataMaps.byId[objectMetadataIdForEntity]?.isSystem === true; + if (!isNonEmptyString(objectMetadataIdForEntity)) { + 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) { return; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response.ts index 6e21c361f..eba9365bf 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response.ts @@ -30,10 +30,11 @@ export const generateFakeFormResponse = async ({ formFieldMetadata?.settings?.objectName, ); - if (!objectMetadataItemWithFieldsMaps) + if (!isDefined(objectMetadataItemWithFieldsMaps)) { throw new Error( `Object metadata not found for object name ${formFieldMetadata?.settings?.objectName}`, ); + } return { [formFieldMetadata.name]: { diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts index a638f22c2..78d2dafe4 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts @@ -43,6 +43,10 @@ export const generateObjectRecordFields = ({ field.relationTargetObjectMetadataId ]; + if (!isDefined(relationTargetObjectMetadata)) { + return acc; + } + acc[field.name] = { isLeaf: false, icon: field.icon, diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/database-event-trigger.listener.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/database-event-trigger.listener.ts index 321ea505e..29573f4a7 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/database-event-trigger.listener.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/database-event-trigger.listener.ts @@ -196,7 +196,11 @@ export class DatabaseEventTriggerListener { } const relatedObjectMetadataNameSingular = - objectMetadataMaps.byId[relatedObjectMetadataId].nameSingular; + objectMetadataMaps.byId[relatedObjectMetadataId]?.nameSingular; + + if (!isDefined(relatedObjectMetadataNameSingular)) { + continue; + } const relatedObjectRepository = await this.twentyORMGlobalManager.getRepositoryForWorkspace( diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/failing-field-metadata-relation-creation.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/failing-field-metadata-relation-creation.integration-spec.ts.snap index 2f20e7fbd..ca527c906 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/failing-field-metadata-relation-creation.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/failing-field-metadata-relation-creation.integration-spec.ts.snap @@ -60,7 +60,7 @@ exports[`Field metadata relation creation should fail relation when targetObject "exceptionEventId": "mocked-exception-id", "userFriendlyMessage": "An error occurred.", }, - "message": "Cannot read properties of undefined (reading 'fieldsById')", + "message": "Object metadata relation target not found for relation creation payload", }, ] `;