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:
@ -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,
|
||||
|
||||
@ -107,7 +107,9 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
|
||||
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];
|
||||
|
||||
if (fieldMetadata) {
|
||||
@ -126,6 +128,13 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
const objectMetadataItemWithFieldMaps =
|
||||
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();
|
||||
|
||||
await queryRunner.connect();
|
||||
@ -703,7 +712,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
isRemoteCreation,
|
||||
}: {
|
||||
createdFieldMetadataItems: FieldMetadataEntity[];
|
||||
objectMetadataMap: Record<string, ObjectMetadataItemWithFieldMaps>;
|
||||
objectMetadataMap: ObjectMetadataMaps['byId'];
|
||||
isRemoteCreation: boolean;
|
||||
}): Promise<WorkspaceMigrationTableAction[]> {
|
||||
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({
|
||||
name: computeObjectTargetTable(
|
||||
objectMetadataMap[createdFieldMetadata.objectMetadataId],
|
||||
),
|
||||
name: computeObjectTargetTable(objectMetadata),
|
||||
action: WorkspaceMigrationTableActionType.ALTER,
|
||||
columns: this.workspaceMigrationFactory.createColumnActions(
|
||||
WorkspaceMigrationColumnActionType.CREATE,
|
||||
|
||||
@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
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 { v4 as uuidV4 } from 'uuid';
|
||||
|
||||
@ -82,10 +82,12 @@ export class ObjectMetadataFieldRelationService {
|
||||
relationObjectMetadataStandardId: string;
|
||||
queryRunner?: QueryRunner;
|
||||
}) {
|
||||
const targetObjectMetadata = Object.values(objectMetadataMaps.byId).find(
|
||||
(objectMetadata) =>
|
||||
objectMetadata.standardId === relationObjectMetadataStandardId,
|
||||
);
|
||||
const targetObjectMetadata = Object.values(objectMetadataMaps.byId)
|
||||
.filter(isDefined)
|
||||
.find(
|
||||
(objectMetadata) =>
|
||||
objectMetadata.standardId === relationObjectMetadataStandardId,
|
||||
);
|
||||
|
||||
if (!targetObjectMetadata) {
|
||||
throw new Error(
|
||||
|
||||
@ -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<string, ObjectMetadataItemWithFieldMaps>;
|
||||
objectMetadataMapsById: ObjectMetadataMaps['byId'];
|
||||
rolesPermissions: ObjectRecordsPermissionsByRoleId;
|
||||
role: RoleEntity;
|
||||
}) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
|
||||
export type ObjectMetadataMaps = {
|
||||
byId: Record<string, ObjectMetadataItemWithFieldMaps>;
|
||||
idByNameSingular: Record<string, string>;
|
||||
byId: Partial<Record<string, ObjectMetadataItemWithFieldMaps>>;
|
||||
idByNameSingular: Partial<Record<string, string>>;
|
||||
};
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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];
|
||||
};
|
||||
|
||||
@ -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(
|
||||
|
||||
Reference in New Issue
Block a user