[WHEN_RELEASED_REQUIRES_CACHE_FLUSH] Object related record logic in v2 (#14937)

# Introduction
Initial motivation here was to migrate the object related records logic
from v1 to v2, please note that now in v2 views aren't records anymore
but core engine entities

## What's done
- Added specific label identifier targeting view field logic
- Handled side effects on viewField creation with lowest position on
object label identifier mutation
- Added viewField relations in field metadate entity + handled
optimistic in builder v2
- Added view relations in object metadata entity + handled optimistic in
builder v2
- Added integration tests covering the side effects and new validation
exceptions
- Sandardized cache computation
- Coverage on object metadata creation side effect on views and view
fields

## Coverage
```ts
 PASS  test/integration/graphql/suites/view/view-field/object-identifier-update-side-effect-on-view-field.integration-spec.ts
  View Field Resolver - Successful object metadata identifier update side effect on view field
    ✓ should create a view field on label identifier object metadata update if it does not exist on view (7 ms)
    ✓ Should not allow deleting a label identifier view field (17 ms)
    ✓ Should not allow destroying a label identifier view field (6 ms)
    ✓ Should not allow updating a label identifier view field visibility to false (8 ms)
    ✓ Should not allow creating a view field with a position lower than the label idenfitier view field (180 ms)
    ✓ Should not allow updated labelIdentifier view field with a position higher than existing other view field (346 ms)
    ✓ Should allow updated labelIdentifier view field with a position higher than existing other view field (434 ms)

Test Suites: 1 passed, 1 total
Tests:       7 passed, 7 total
Snapshots:   5 passed, 5 total
Time:        4.571 s, estimated 5 s
```

close https://github.com/twentyhq/core-team-issues/issues/1664
This commit is contained in:
Paul Rastoin
2025-10-08 16:23:37 +02:00
committed by GitHub
parent 1c1739ec80
commit 4ecc9c622d
80 changed files with 6858 additions and 627 deletions
@@ -0,0 +1,25 @@
import { isDefined } from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import { type FlatEntity } from 'src/engine/core-modules/common/types/flat-entity.type';
import { getSubFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/get-sub-flat-entity-maps-or-throw.util';
export type FindManyFlatEntityByIdInFlatEntityMapsOrThrowArgs<
T extends FlatEntity,
> = {
flatEntityMaps: FlatEntityMaps<T>;
flatEntityIds: string[];
};
export const findManyFlatEntityByIdInFlatEntityMapsOrThrow = <
T extends FlatEntity,
>({
flatEntityMaps,
flatEntityIds,
}: FindManyFlatEntityByIdInFlatEntityMapsOrThrowArgs<T>): T[] => {
const subFlatEntityMaps = getSubFlatEntityMapsOrThrow<T>({
flatEntityIds,
flatEntityMaps,
});
return Object.values(subFlatEntityMaps.byId).filter(isDefined);
};
@@ -6,6 +6,8 @@ import { Repository } from 'typeorm';
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
import { EMPTY_FLAT_ENTITY_MAPS } from 'src/engine/core-modules/common/constant/empty-flat-entity-maps.constant';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity';
import { FlatViewFieldMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-field-maps.type';
import { fromViewFieldEntityToFlatViewField } from 'src/engine/core-modules/view/flat-view/utils/from-view-field-entity-to-flat-view-field.util';
@@ -36,20 +38,13 @@ export class WorkspaceFlatViewFieldMapCacheService extends WorkspaceFlatMapCache
withDeleted: true,
});
const flatViewFieldMaps: FlatViewFieldMaps = {
byId: {},
idByUniversalIdentifier: {},
};
for (const viewFieldEntity of existingViewFields) {
return existingViewFields.reduce((flatViewFieldMaps, viewFieldEntity) => {
const flatViewField = fromViewFieldEntityToFlatViewField(viewFieldEntity);
flatViewFieldMaps.byId[flatViewField.id] = flatViewField;
flatViewFieldMaps.idByUniversalIdentifier[
flatViewField.universalIdentifier
] = flatViewField.id;
}
return flatViewFieldMaps;
return addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: flatViewField,
flatEntityMaps: flatViewFieldMaps,
});
}, EMPTY_FLAT_ENTITY_MAPS);
}
}
@@ -6,9 +6,11 @@ import { Repository } from 'typeorm';
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
import { EMPTY_FLAT_ENTITY_MAPS } from 'src/engine/core-modules/common/constant/empty-flat-entity-maps.constant';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
import { type FlatViewMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-maps.type';
import { generateFlatViewMaps } from 'src/engine/core-modules/view/flat-view/utils/generate-flat-view-maps.util';
import { fromViewEntityToFlatView } from 'src/engine/core-modules/view/flat-view/utils/from-view-entity-to-flat-view.util';
import { WorkspaceFlatMapCache } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator';
import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service';
@@ -33,9 +35,22 @@ export class WorkspaceFlatViewMapCacheService extends WorkspaceFlatMapCacheServi
where: {
workspaceId,
},
select: {
viewFields: {
id: true,
},
},
relations: ['viewFields'],
withDeleted: true,
});
return generateFlatViewMaps(views);
return views.reduce((flatViewMaps, viewEntity) => {
const flatView = fromViewEntityToFlatView(viewEntity);
return addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: flatView,
flatEntityMaps: flatViewMaps,
});
}, EMPTY_FLAT_ENTITY_MAPS);
}
}
@@ -1,8 +1,9 @@
import { v4 } from 'uuid';
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type CreateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-field.input';
import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type';
import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/constants/DEFAULT_VIEW_FIELD_SIZE';
export const fromCreateViewFieldInputToFlatViewFieldToCreate = ({
createViewFieldInput: rawCreateViewFieldInput,
@@ -30,7 +31,7 @@ export const fromCreateViewFieldInputToFlatViewFieldToCreate = ({
deletedAt: null,
universalIdentifier: viewFieldId,
isVisible: createViewFieldInput.isVisible ?? true,
size: createViewFieldInput.size ?? 0,
size: createViewFieldInput.size ?? DEFAULT_VIEW_FIELD_SIZE,
position: createViewFieldInput.position ?? 0,
aggregateOperation: createViewFieldInput.aggregateOperation ?? null,
};
@@ -15,6 +15,6 @@ export const fromViewEntityToFlatView = (viewEntity: ViewEntity): FlatView => {
universalIdentifier:
viewEntityWithoutRelations.universalIdentifier ??
viewEntityWithoutRelations.id,
viewFieldIds: viewEntity.viewFields?.map((viewField) => viewField.id) ?? [],
viewFieldIds: viewEntity.viewFields.map((viewField) => viewField.id) ?? [],
};
};
@@ -1,27 +0,0 @@
import { type ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
import { type FlatViewMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-maps.type';
import { type FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type';
export const generateFlatViewMaps = (
viewCollection: ViewEntity[],
): FlatViewMaps => {
const flatViewMaps: FlatViewMaps = {
byId: {},
idByUniversalIdentifier: {},
};
for (const view of viewCollection) {
const processedFlatView: FlatView = {
...view,
viewFieldIds: view.viewFields?.map((viewField) => viewField.id) ?? [],
universalIdentifier: view.universalIdentifier ?? view.id, // TODO: should not fallback to id, to remove once we have universalIdentifier required
};
flatViewMaps.byId[view.id] = processedFlatView;
flatViewMaps.idByUniversalIdentifier[
processedFlatView.universalIdentifier
] = view.id;
}
return flatViewMaps;
};
@@ -0,0 +1,17 @@
import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type';
export const isViewFieldInLowestPosition = ({
flatViewField,
otherFlatViewFields,
}: {
otherFlatViewFields: FlatViewField[];
flatViewField: FlatViewField;
}) => {
if (otherFlatViewFields.length === 0) {
return true;
}
const positions = otherFlatViewFields.map(({ position }) => position);
const lowestPosition = Math.min(...positions);
return flatViewField.position < lowestPosition;
};
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { EMPTY_FLAT_ENTITY_MAPS } from 'src/engine/core-modules/common/constant/empty-flat-entity-maps.constant';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/core-modules/common/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util';
@@ -38,6 +39,7 @@ export class ViewFieldV2Service {
flatViewFieldMaps: existingFlatViewFieldMaps,
flatViewMaps,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
@@ -45,6 +47,7 @@ export class ViewFieldV2Service {
'flatViewFieldMaps',
'flatViewMaps',
'flatFieldMetadataMaps',
'flatObjectMetadataMaps',
],
},
);
@@ -72,6 +75,7 @@ export class ViewFieldV2Service {
dependencyAllFlatEntityMaps: {
flatFieldMetadataMaps,
flatViewMaps,
flatObjectMetadataMaps,
},
buildOptions: {
isSystemBuild: false,
@@ -109,13 +113,20 @@ export class ViewFieldV2Service {
workspaceId: string;
updateViewFieldInput: UpdateViewFieldInput;
}): Promise<ViewFieldDTO> {
const { flatViewFieldMaps: existingFlatViewFieldMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewFieldMaps'],
},
);
const {
flatViewFieldMaps: existingFlatViewFieldMaps,
flatObjectMetadataMaps,
flatViewMaps,
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatViewFieldMaps',
'flatObjectMetadataMaps',
'flatViewMaps',
],
},
);
const optimisticallyUpdatedFlatView =
fromUpdateViewFieldInputToFlatViewFieldToUpdateOrThrow({
@@ -123,13 +134,9 @@ export class ViewFieldV2Service {
updateViewFieldInput,
});
const fromFlatViewFieldMaps = getSubFlatEntityMapsOrThrow({
flatEntityIds: [optimisticallyUpdatedFlatView.id],
flatEntityMaps: existingFlatViewFieldMaps,
});
const toFlatViewFieldMaps = replaceFlatEntityInFlatEntityMapsOrThrow({
const toFlatViewFieldMaps = addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: optimisticallyUpdatedFlatView,
flatEntityMaps: fromFlatViewFieldMaps,
flatEntityMaps: EMPTY_FLAT_ENTITY_MAPS,
});
const validateAndBuildResult =
@@ -137,10 +144,14 @@ export class ViewFieldV2Service {
{
fromToAllFlatEntityMaps: {
flatViewFieldMaps: {
from: fromFlatViewFieldMaps,
from: existingFlatViewFieldMaps,
to: toFlatViewFieldMaps,
},
},
dependencyAllFlatEntityMaps: {
flatObjectMetadataMaps,
flatViewMaps,
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
@@ -177,13 +188,20 @@ export class ViewFieldV2Service {
deleteViewFieldInput: DeleteViewFieldInput;
workspaceId: string;
}): Promise<ViewFieldDTO> {
const { flatViewFieldMaps: existingFlatViewFieldMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewFieldMaps'],
},
);
const {
flatViewFieldMaps: existingFlatViewFieldMaps,
flatObjectMetadataMaps,
flatViewMaps,
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatViewFieldMaps',
'flatObjectMetadataMaps',
'flatViewMaps',
],
},
);
const optimisticallyUpdatedFlatViewWithDeletedAt =
fromDeleteViewFieldInputToFlatViewFieldOrThrow({
@@ -205,6 +223,10 @@ export class ViewFieldV2Service {
to: toFlatViewFieldMaps,
},
},
dependencyAllFlatEntityMaps: {
flatObjectMetadataMaps,
flatViewMaps,
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
@@ -9,6 +9,7 @@ import { type IndexMetadataInterface } from 'src/engine/metadata-modules/index-m
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/core-modules/common/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { type IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
import { filterMorphRelationDuplicateFields } from 'src/engine/dataloaders/utils/filter-morph-relation-duplicate-fields.util';
@@ -18,7 +19,6 @@ import { RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/rel
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { resolveFieldMetadataStandardOverride } from 'src/engine/metadata-modules/field-metadata/utils/resolve-field-metadata-standard-override.util';
import { findAllOthersMorphRelationFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-all-others-morph-relation-flat-field-metadatas-or-throw.util';
import { findObjectFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util';
import { fromFlatFieldMetadataToFieldMetadataDto } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util';
import { fromMorphOrRelationFlatFieldMetadataToRelationDto } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-morph-or-relation-flat-field-metadata-to-relation-dto.util';
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
@@ -361,10 +361,10 @@ export class DataloaderService {
flatEntityId: objectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
const { objectFlatFieldMetadatas } =
findObjectFlatFieldMetadatasOrThrow({
flatFieldMetadataMaps: flatFieldMetadataMaps,
flatObjectMetadata,
const objectFlatFieldMetadatas =
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatFieldMetadataMaps,
flatEntityIds: flatObjectMetadata.fieldMetadataIds,
});
const overriddenFieldMetadataEntities =
@@ -19,6 +19,7 @@ import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-met
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity';
import { type FieldStandardOverridesDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-standard-overrides.dto';
import { AssignIfIsGivenFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/assign-if-is-given-field-metadata-type.type';
import { AssignTypeIfIsMorphOrRelationFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/assign-type-if-is-morph-or-relation-field-metadata-type.type';
@@ -133,8 +134,7 @@ export class FieldMetadataEntity<
@OneToOne(
() => FieldMetadataEntity,
(fieldMetadata: FieldMetadataEntity) =>
fieldMetadata.relationTargetFieldMetadataId,
(fieldMetadata) => fieldMetadata.relationTargetFieldMetadataId,
{ nullable: true },
)
@JoinColumn({ name: 'relationTargetFieldMetadataId' })
@@ -151,8 +151,7 @@ export class FieldMetadataEntity<
@ManyToOne(
() => ObjectMetadataEntity,
(objectMetadata: ObjectMetadataEntity) =>
objectMetadata.targetRelationFields,
(objectMetadata) => objectMetadata.targetRelationFields,
{ onDelete: 'CASCADE', nullable: true },
)
@JoinColumn({ name: 'relationTargetObjectMetadataId' })
@@ -170,8 +169,7 @@ export class FieldMetadataEntity<
@OneToMany(
() => IndexFieldMetadataEntity,
(indexFieldMetadata: IndexFieldMetadataEntity) =>
indexFieldMetadata.indexMetadata,
(indexFieldMetadata) => indexFieldMetadata.indexMetadata,
{
cascade: true,
},
@@ -186,7 +184,10 @@ export class FieldMetadataEntity<
@OneToMany(
() => FieldPermissionEntity,
(fieldPermission: FieldPermissionEntity) => fieldPermission.fieldMetadata,
(fieldPermission) => fieldPermission.fieldMetadata,
)
fieldPermissions: Relation<FieldPermissionEntity[]>;
@OneToMany(() => ViewFieldEntity, (viewField) => viewField.fieldMetadata)
viewFields: Relation<ViewFieldEntity[]>;
}
@@ -19,6 +19,7 @@ export const getFlatFieldMetadataMock = <T extends FieldMetadataType>(
const createdAt = '2024-01-01T00:00:00.000Z' as unknown as Date;
return {
viewFieldIds: [],
createdAt,
updatedAt: createdAt,
defaultValue: null,
@@ -32,6 +32,7 @@ export const getRelationTargetFlatFieldMetadataMock = ({
const createdAt = '2024-01-01T00:00:00.000Z' as unknown as Date;
return {
viewFieldIds: [],
createdAt,
updatedAt: createdAt,
description: 'default flat field metadata description',
@@ -113,6 +113,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"type": "MORPH_RELATION",
"universalIdentifier": Any<String>,
"updatedAt": Any<ClockDate>,
"viewFieldIds": [],
"workspaceId": Any<String>,
},
{
@@ -145,6 +146,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"type": "RELATION",
"universalIdentifier": Any<String>,
"updatedAt": Any<ClockDate>,
"viewFieldIds": [],
"workspaceId": Any<String>,
},
{
@@ -175,6 +177,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"type": "MORPH_RELATION",
"universalIdentifier": Any<String>,
"updatedAt": Any<ClockDate>,
"viewFieldIds": [],
"workspaceId": Any<String>,
},
{
@@ -207,6 +210,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"type": "RELATION",
"universalIdentifier": Any<String>,
"updatedAt": Any<ClockDate>,
"viewFieldIds": [],
"workspaceId": Any<String>,
},
],
@@ -8,6 +8,7 @@ import { CacheStorageService } from 'src/engine/core-modules/cache-storage/servi
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
import { EMPTY_FLAT_ENTITY_MAPS } from 'src/engine/core-modules/common/constant/empty-flat-entity-maps.constant';
import { FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { fromFieldMetadataEntityToFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-field-metadata-entity-to-flat-field-metadata.util';
@@ -37,28 +38,26 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceFlatMapC
where: {
workspaceId,
},
select: {
viewFields: {
id: true,
},
},
withDeleted: true,
relations: [],
relations: ['viewFields'],
});
const flatFieldMetadataMaps = fieldMetadatas.reduce<
FlatEntityMaps<FlatFieldMetadata>
>((flatEntityMaps, field) => {
const flatFieldMetadata =
fromFieldMetadataEntityToFlatFieldMetadata(field);
return fieldMetadatas.reduce(
(flatFieldMetadataMaps, fieldMetadataEntity) => {
const flatFieldMetadata =
fromFieldMetadataEntityToFlatFieldMetadata(fieldMetadataEntity);
return {
byId: {
...flatEntityMaps.byId,
[flatFieldMetadata.id]: flatFieldMetadata,
},
idByUniversalIdentifier: {
...flatEntityMaps.idByUniversalIdentifier,
[flatFieldMetadata.universalIdentifier]: flatFieldMetadata.id,
},
};
}, EMPTY_FLAT_ENTITY_MAPS);
return flatFieldMetadataMaps;
return addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: flatFieldMetadata,
flatEntityMaps: flatFieldMetadataMaps,
});
},
EMPTY_FLAT_ENTITY_MAPS,
);
}
}
@@ -8,6 +8,7 @@ export const fieldMetadataRelationProperties = [
'fieldPermissions',
'indexFieldMetadatas',
'object',
'viewFields',
] as const satisfies (keyof FieldMetadataEntity)[];
export type FieldMetadataEntityRelationProperties =
@@ -16,5 +17,5 @@ export type FieldMetadataEntityRelationProperties =
export type FlatFieldMetadata<T extends FieldMetadataType = FieldMetadataType> =
Omit<FieldMetadataEntity<T>, FieldMetadataEntityRelationProperties> & {
universalIdentifier: string;
// Could add indexField metadatas and so on ?
viewFieldIds: string[];
};
@@ -6,8 +6,8 @@ import {
FlatEntityMapsExceptionCode,
} from 'src/engine/core-modules/common/exceptions/flat-entity-maps.exception';
import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { findObjectFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util';
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
@@ -30,10 +30,11 @@ export const findAllOthersMorphRelationFlatFieldMetadatasOrThrow = ({
);
}
const { objectFlatFieldMetadatas } = findObjectFlatFieldMetadatasOrThrow({
flatFieldMetadataMaps,
flatObjectMetadata,
});
const objectFlatFieldMetadatas =
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatFieldMetadataMaps,
flatEntityIds: flatObjectMetadata.fieldMetadataIds,
});
return objectFlatFieldMetadatas.filter(
(
@@ -1,30 +0,0 @@
import { isDefined } from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import { getSubFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/get-sub-flat-entity-maps-or-throw.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
type FindObjectFieldsInFlatFieldMetadataMapsArgs = {
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
flatObjectMetadata: FlatObjectMetadata;
};
export const findObjectFlatFieldMetadatasOrThrow = ({
flatFieldMetadataMaps,
flatObjectMetadata,
}: FindObjectFieldsInFlatFieldMetadataMapsArgs): {
objectFlatFieldMetadatas: FlatFieldMetadata[];
} => {
const objectFlatFieldMetadataMaps = getSubFlatEntityMapsOrThrow({
flatEntityIds: flatObjectMetadata.fieldMetadataIds,
flatEntityMaps: flatFieldMetadataMaps,
});
const objectFlatFieldMetadatas = Object.values(
objectFlatFieldMetadataMaps.byId,
).filter(isDefined);
return {
objectFlatFieldMetadatas,
};
};
@@ -6,6 +6,7 @@ import {
import { type AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { type DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input';
import {
FieldMetadataException,
@@ -13,7 +14,6 @@ import {
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { computeFlatFieldMetadataRelatedFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-metadata-related-flat-field-metadata.util';
import { findObjectFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { generateFlatIndexMetadataWithNameOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util';
@@ -140,10 +140,11 @@ export const fromDeleteFieldInputToFlatFieldMetadatasToDelete = ({
flatEntityMaps: existingFlatObjectMetadataMaps,
flatEntityId: flatIndex.objectMetadataId,
});
const { objectFlatFieldMetadatas } = findObjectFlatFieldMetadatasOrThrow({
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
flatObjectMetadata,
});
const objectFlatFieldMetadatas =
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: existingFlatFieldMetadataMaps,
flatEntityIds: flatObjectMetadata.fieldMetadataIds,
});
const newIndex = generateFlatIndexMetadataWithNameOrThrow({
flatObjectMetadata,
@@ -20,6 +20,9 @@ export const fromFieldMetadataEntityToFlatFieldMetadata = <
return {
...fieldMetadataWithoutRelations,
viewFieldIds: fieldMetadataEntity.viewFields.map(
(viewField) => viewField.id,
),
universalIdentifier:
fieldMetadataWithoutRelations.standardId ??
fieldMetadataWithoutRelations.id,
@@ -23,6 +23,7 @@ export const getDefaultFlatFieldMetadata = ({
const createdAt = new Date();
return {
viewFieldIds: [],
description: createFieldInput.description ?? null,
id: fieldMetadataId,
icon: createFieldInput.icon ?? null,
@@ -1,8 +1,8 @@
import { isDefined } from 'twenty-shared/utils';
import { type AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { findObjectFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { generateFlatIndexMetadataWithNameOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util';
@@ -35,10 +35,11 @@ export const recomputeIndexOnFlatFieldMetadataNameUpdate = ({
return [];
}
const { objectFlatFieldMetadatas } = findObjectFlatFieldMetadatasOrThrow({
flatFieldMetadataMaps,
flatObjectMetadata,
});
const objectFlatFieldMetadatas =
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatFieldMetadataMaps,
flatEntityIds: flatObjectMetadata.fieldMetadataIds,
});
const optimisticObjectFlatFieldMetadatas = objectFlatFieldMetadatas.map(
(flatFieldMetadata) => {
if (flatFieldMetadata.id === fromFlatFieldMetadata.id) {
@@ -4,13 +4,13 @@ import { isDefined } from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { findObjectFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util';
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
@@ -50,10 +50,11 @@ export const validateFlatFieldMetadataNameAvailability = ({
remainingFlatEntityMapsToValidate?: FlatEntityMaps<FlatFieldMetadata>;
}): FlatFieldMetadataValidationError[] => {
const errors: FlatFieldMetadataValidationError[] = [];
const { objectFlatFieldMetadatas } = findObjectFlatFieldMetadatasOrThrow({
flatFieldMetadataMaps,
flatObjectMetadata,
});
const objectFlatFieldMetadatas =
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatFieldMetadataMaps,
flatEntityIds: flatObjectMetadata.fieldMetadataIds,
});
const reservedCompositeFieldsNames = getReservedCompositeFieldNames(
objectFlatFieldMetadatas,
);
@@ -8,6 +8,7 @@ import { CacheStorageService } from 'src/engine/core-modules/cache-storage/servi
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
import { EMPTY_FLAT_ENTITY_MAPS } from 'src/engine/core-modules/common/constant/empty-flat-entity-maps.constant';
import { FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { fromIndexMetadataEntityToFlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/utils/from-index-metadata-entity-to-flat-index-metadata.util';
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
@@ -38,27 +39,20 @@ export class WorkspaceFlatIndexMapCacheService extends WorkspaceFlatMapCacheServ
workspaceId,
},
withDeleted: true,
select: {
// Note: We need all IndexFieldMetadataEntity in order to build a FlatIndex
indexFieldMetadatas: true,
},
relations: ['indexFieldMetadatas'],
});
const flatIndexMaps = indexes.reduce<FlatEntityMaps<FlatIndexMetadata>>(
(flatEntityMaps, index) => {
const flatIndex = fromIndexMetadataEntityToFlatIndexMetadata(index);
return indexes.reduce((flatIndexMaps, indexEntity) => {
const flatIndex = fromIndexMetadataEntityToFlatIndexMetadata(indexEntity);
return {
byId: {
...flatEntityMaps.byId,
[flatIndex.id]: flatIndex,
},
idByUniversalIdentifier: {
...flatEntityMaps.idByUniversalIdentifier,
[flatIndex.universalIdentifier]: flatIndex.id,
},
};
},
EMPTY_FLAT_ENTITY_MAPS,
);
return flatIndexMaps;
return addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: flatIndex,
flatEntityMaps: flatIndexMaps,
});
}, EMPTY_FLAT_ENTITY_MAPS);
}
}
@@ -12,6 +12,7 @@ export const getFlatObjectMetadataMock = (
const createdAt = '2024-01-01T00:00:00.000Z' as unknown as Date;
return {
viewIds: [],
indexMetadataIds: [],
fieldMetadataIds: [],
description: 'default flat object metadata description',
@@ -39,7 +39,18 @@ export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceFlatMap
workspaceId,
},
withDeleted: true,
relations: ['fields', 'indexMetadatas'],
select: {
fields: {
id: true,
},
indexMetadatas: {
id: true,
},
views: {
id: true,
},
},
relations: ['fields', 'indexMetadatas', 'views'],
});
const flatObjectMetadataMaps = objectMetadatas.reduce<
@@ -10,6 +10,7 @@ export const objectMetadataEntityRelationProperties = [
'application',
'objectPermissions',
'fieldPermissions',
'views',
] as const satisfies ObjectMetadataRelationProperties[];
type ObjectMetadataRelationProperties = ExtractRecordTypeOrmRelationProperties<
@@ -24,4 +25,5 @@ export type FlatObjectMetadata = Omit<
universalIdentifier: string;
fieldMetadataIds: string[];
indexMetadataIds: string[];
viewIds: string[];
};
@@ -54,6 +54,7 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
const createdAt = new Date();
const flatObjectMetadataToCreate: FlatObjectMetadata = {
fieldMetadataIds: [],
viewIds: [],
indexMetadataIds: [],
createdAt,
updatedAt: createdAt,
@@ -5,8 +5,8 @@ import {
import { type AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { findObjectFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util';
import { findRelationFlatFieldMetadataTargetFlatFieldMetadataOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-relation-flat-field-metadatas-target-flat-field-metadata-or-throw.util';
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
@@ -52,10 +52,11 @@ export const fromDeleteObjectInputToFlatFieldMetadatasToDelete = ({
);
}
const { objectFlatFieldMetadatas } = findObjectFlatFieldMetadatasOrThrow({
flatFieldMetadataMaps,
flatObjectMetadata: flatObjectMetadataToDelete,
});
const objectFlatFieldMetadatas =
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatFieldMetadataMaps,
flatEntityIds: flatObjectMetadataToDelete.fieldMetadataIds,
});
const flatFieldMetadatasToDelete = objectFlatFieldMetadatas.flatMap(
(flatFieldMetadata) => {
if (isMorphOrRelationFlatFieldMetadata(flatFieldMetadata)) {
@@ -16,6 +16,7 @@ export const fromObjectMetadataEntityToFlatObjectMetadata = (
return {
...objectMetadataEntityWithoutRelations,
viewIds: objectMetadataEntity.views.map((viewEntity) => viewEntity.id),
indexMetadataIds: objectMetadataEntity.indexMetadatas.map(
(indexEntity) => indexEntity.id,
),
@@ -0,0 +1,231 @@
import {
extractAndSanitizeObjectStringFields,
isDefined,
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties,
} from 'twenty-shared/utils';
import { type AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { FLAT_OBJECT_METADATA_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-properties-to-compare.constant';
import { type FlatObjectMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-properties-to-compare.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { recomputeIndexAfterFlatObjectMetadataSingularNameUpdate } from 'src/engine/metadata-modules/flat-object-metadata/utils/recompute-index-after-flat-object-metadata-singular-name-update.util';
import { recomputeViewFieldIdentifierAfterFlatObjectIdentifierUpdate } from 'src/engine/metadata-modules/flat-object-metadata/utils/recompute-view-field-identifier-after-flat-object-identifier-update.util';
import { renameRelatedMorphFieldOnObjectNamesUpdate } from 'src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util';
import { OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES } from 'src/engine/metadata-modules/object-metadata/constants/object-metadata-standard-overrides-properties.constant';
import { type UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
import {
ObjectMetadataException,
ObjectMetadataExceptionCode,
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import { type ObjectMetadataStandardOverridesProperties } from 'src/engine/metadata-modules/object-metadata/types/object-metadata-standard-overrides-properties.types';
import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util';
type FromUpdateObjectInputToFlatObjectMetadataArgs = {
updateObjectInput: UpdateOneObjectInput;
} & Pick<
AllFlatEntityMaps,
| 'flatIndexMaps'
| 'flatObjectMetadataMaps'
| 'flatFieldMetadataMaps'
| 'flatViewFieldMaps'
| 'flatViewMaps'
>;
const objectMetadataEditableProperties =
FLAT_OBJECT_METADATA_PROPERTIES_TO_COMPARE.filter(
(
property,
): property is Exclude<
FlatObjectMetadataPropertiesToCompare,
'standardOverrides'
> => property !== 'standardOverrides',
);
type UpdatedFlatObjectAndRelatedFlatEntities = {
flatObjectMetadata: FlatObjectMetadata;
otherObjectFlatFieldMetadataToUpdate: FlatFieldMetadata[];
flatIndexMetadataToUpdate: FlatIndexMetadata[];
flatViewFieldToUpdate: FlatViewField[];
flatViewFieldToCreate: FlatViewField[];
};
export const fromUpdateObjectInputToFlatObjectMetadataAndRelatedFlatEntities =
({
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
updateObjectInput: rawUpdateObjectInput,
flatIndexMaps,
flatFieldMetadataMaps,
flatViewFieldMaps,
flatViewMaps,
}: FromUpdateObjectInputToFlatObjectMetadataArgs): UpdatedFlatObjectAndRelatedFlatEntities => {
const { id: objectMetadataIdToUpdate } =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
rawUpdateObjectInput,
['id'],
);
const updatedEditableObjectProperties =
extractAndSanitizeObjectStringFields(
rawUpdateObjectInput.update,
objectMetadataEditableProperties,
);
const flatObjectMetadataToUpdate = findFlatEntityByIdInFlatEntityMaps({
flatEntityMaps: existingFlatObjectMetadataMaps,
flatEntityId: objectMetadataIdToUpdate,
});
if (!isDefined(flatObjectMetadataToUpdate)) {
throw new ObjectMetadataException(
'Object to update not found',
ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
);
}
if (isStandardMetadata(flatObjectMetadataToUpdate)) {
const invalidUpdatedProperties = Object.keys(
updatedEditableObjectProperties,
).filter(
(property) =>
!OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES.includes(
property as ObjectMetadataStandardOverridesProperties,
),
);
if (invalidUpdatedProperties.length > 0) {
throw new ObjectMetadataException(
`Cannot edit standard object metadata properties: ${invalidUpdatedProperties.join(', ')}`,
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
);
}
const updatedStandardFlatObjectMetadata =
OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES.reduce(
(acc, property) => {
const isPropertyUpdated =
updatedEditableObjectProperties[property] !== undefined;
return {
...acc,
standardOverrides: {
...acc.standardOverrides,
...(isPropertyUpdated
? { [property]: updatedEditableObjectProperties[property] }
: {}),
},
};
},
flatObjectMetadataToUpdate,
);
return {
flatObjectMetadata: updatedStandardFlatObjectMetadata,
otherObjectFlatFieldMetadataToUpdate: [],
flatIndexMetadataToUpdate: [],
flatViewFieldToUpdate: [],
flatViewFieldToCreate: [],
};
}
const initialAccumulator: UpdatedFlatObjectAndRelatedFlatEntities = {
flatObjectMetadata: flatObjectMetadataToUpdate,
otherObjectFlatFieldMetadataToUpdate: [],
flatIndexMetadataToUpdate: [],
flatViewFieldToUpdate: [],
flatViewFieldToCreate: [],
};
return objectMetadataEditableProperties.reduce<UpdatedFlatObjectAndRelatedFlatEntities>(
(
{
flatObjectMetadata,
otherObjectFlatFieldMetadataToUpdate,
flatIndexMetadataToUpdate,
flatViewFieldToUpdate,
flatViewFieldToCreate,
},
property,
) => {
const updatedPropertyValue = updatedEditableObjectProperties[property];
const isPropertyUpdated =
updatedPropertyValue !== undefined &&
flatObjectMetadata[property] !== updatedPropertyValue;
if (!isPropertyUpdated) {
return {
flatObjectMetadata,
otherObjectFlatFieldMetadataToUpdate,
flatIndexMetadataToUpdate,
flatViewFieldToUpdate,
flatViewFieldToCreate,
};
}
const updatedFlatObjectMetadata = {
...flatObjectMetadata,
[property]: updatedPropertyValue,
};
const newUpdatedOtherObjectFlatFieldMetadatas =
property === 'nameSingular' || property === 'namePlural'
? renameRelatedMorphFieldOnObjectNamesUpdate({
flatFieldMetadataMaps,
fromFlatObjectMetadata: updatedFlatObjectMetadata,
toFlatObjectMetadata: updatedFlatObjectMetadata,
})
: [];
const newUpdatedFlatIndexMetadatas =
property === 'nameSingular'
? recomputeIndexAfterFlatObjectMetadataSingularNameUpdate({
flatFieldMetadataMaps,
existingFlatObjectMetadata: flatObjectMetadataToUpdate,
flatIndexMaps,
updatedSingularName: updatedFlatObjectMetadata.nameSingular,
})
: [];
const {
flatViewFieldToCreate: newFlatViewFieldToCreate,
flatViewFieldToUpdate: newFlatViewFieldToUpdate,
} =
property === 'labelIdentifierFieldMetadataId' &&
isDefined(updatedFlatObjectMetadata.labelIdentifierFieldMetadataId)
? recomputeViewFieldIdentifierAfterFlatObjectIdentifierUpdate({
existingFlatObjectMetadata: flatObjectMetadataToUpdate,
flatViewFieldMaps,
flatViewMaps,
updatedLabelIdentifierFieldMetadataId:
updatedFlatObjectMetadata.labelIdentifierFieldMetadataId,
})
: {
flatViewFieldToCreate: [],
flatViewFieldToUpdate: [],
};
return {
flatObjectMetadata: updatedFlatObjectMetadata,
otherObjectFlatFieldMetadataToUpdate: [
...otherObjectFlatFieldMetadataToUpdate,
...newUpdatedOtherObjectFlatFieldMetadatas,
],
flatViewFieldToUpdate: [
...flatViewFieldToUpdate,
...newFlatViewFieldToUpdate,
],
flatViewFieldToCreate: [
...flatViewFieldToCreate,
...newFlatViewFieldToCreate,
],
flatIndexMetadataToUpdate: [
...flatIndexMetadataToUpdate,
...newUpdatedFlatIndexMetadatas,
],
};
},
initialAccumulator,
);
};
@@ -1,182 +0,0 @@
import {
extractAndSanitizeObjectStringFields,
isDefined,
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties,
} from 'twenty-shared/utils';
import { type AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { FLAT_OBJECT_METADATA_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-properties-to-compare.constant';
import { type FlatObjectMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-properties-to-compare.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { recomputeIndexAfterFlatObjectMetadataSingularNameUpdate } from 'src/engine/metadata-modules/flat-object-metadata/utils/recompute-index-after-flat-object-metadata-singular-name-update.util';
import { renameRelatedMorphFieldOnObjectNamesUpdate } from 'src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util';
import { OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES } from 'src/engine/metadata-modules/object-metadata/constants/object-metadata-standard-overrides-properties.constant';
import { type UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
import {
ObjectMetadataException,
ObjectMetadataExceptionCode,
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import { type ObjectMetadataStandardOverridesProperties } from 'src/engine/metadata-modules/object-metadata/types/object-metadata-standard-overrides-properties.types';
import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util';
type FromUpdateObjectInputToFlatObjectMetadataArgs = {
updateObjectInput: UpdateOneObjectInput;
} & Pick<
AllFlatEntityMaps,
'flatIndexMaps' | 'flatObjectMetadataMaps' | 'flatFieldMetadataMaps'
>;
const objectMetadataEditableProperties =
FLAT_OBJECT_METADATA_PROPERTIES_TO_COMPARE.filter(
(
property,
): property is Exclude<
FlatObjectMetadataPropertiesToCompare,
'standardOverrides'
> => property !== 'standardOverrides',
);
type UpdatedFlatObjectAndOtherObjectFieldMetadatas = {
flatObjectMetadata: FlatObjectMetadata;
otherObjectFlatFieldMetadataToUpdate: FlatFieldMetadata[];
flatIndexMetadataToUpdate: FlatIndexMetadata[];
};
export const fromUpdateObjectInputToFlatObjectMetadata = ({
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
updateObjectInput: rawUpdateObjectInput,
flatIndexMaps,
flatFieldMetadataMaps,
}: FromUpdateObjectInputToFlatObjectMetadataArgs): UpdatedFlatObjectAndOtherObjectFieldMetadatas => {
const { id: objectMetadataIdToUpdate } =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
rawUpdateObjectInput,
['id'],
);
const updatedEditableObjectProperties = extractAndSanitizeObjectStringFields(
rawUpdateObjectInput.update,
objectMetadataEditableProperties,
);
const flatObjectMetadataToUpdate = findFlatEntityByIdInFlatEntityMaps({
flatEntityMaps: existingFlatObjectMetadataMaps,
flatEntityId: objectMetadataIdToUpdate,
});
if (!isDefined(flatObjectMetadataToUpdate)) {
throw new ObjectMetadataException(
'Object to update not found',
ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
);
}
if (isStandardMetadata(flatObjectMetadataToUpdate)) {
const invalidUpdatedProperties = Object.keys(
updatedEditableObjectProperties,
).filter(
(property) =>
!OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES.includes(
property as ObjectMetadataStandardOverridesProperties,
),
);
if (invalidUpdatedProperties.length > 0) {
throw new ObjectMetadataException(
`Cannot edit standard object metadata properties: ${invalidUpdatedProperties.join(', ')}`,
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
);
}
const updatedStandardFlatObjectdMetadata =
OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES.reduce((acc, property) => {
const isPropertyUpdated =
updatedEditableObjectProperties[property] !== undefined;
return {
...acc,
standardOverrides: {
...acc.standardOverrides,
...(isPropertyUpdated
? { [property]: updatedEditableObjectProperties[property] }
: {}),
},
};
}, flatObjectMetadataToUpdate);
return {
flatObjectMetadata: updatedStandardFlatObjectdMetadata,
otherObjectFlatFieldMetadataToUpdate: [],
flatIndexMetadataToUpdate: [],
};
}
const initialAccumulator: UpdatedFlatObjectAndOtherObjectFieldMetadatas = {
flatObjectMetadata: flatObjectMetadataToUpdate,
otherObjectFlatFieldMetadataToUpdate: [],
flatIndexMetadataToUpdate: [],
};
return objectMetadataEditableProperties.reduce<UpdatedFlatObjectAndOtherObjectFieldMetadatas>(
(
{
flatObjectMetadata,
otherObjectFlatFieldMetadataToUpdate: otherObjectFlatFieldMetadatas,
flatIndexMetadataToUpdate,
},
property,
) => {
const updatedPropertyValue = updatedEditableObjectProperties[property];
const isPropertyUpdated =
updatedPropertyValue !== undefined &&
flatObjectMetadata[property] !== updatedPropertyValue;
if (!isPropertyUpdated) {
return {
flatObjectMetadata,
otherObjectFlatFieldMetadataToUpdate: otherObjectFlatFieldMetadatas,
flatIndexMetadataToUpdate,
};
}
const updatedFlatObjectMetadata = {
...flatObjectMetadata,
[property]: updatedPropertyValue,
};
const newUpdatedOtherObjectFlatFieldMetadatas =
property === 'nameSingular' || property === 'namePlural'
? renameRelatedMorphFieldOnObjectNamesUpdate({
flatFieldMetadataMaps,
fromFlatObjectMetadata: updatedFlatObjectMetadata,
toFlatObjectMetadata: updatedFlatObjectMetadata,
})
: [];
const newUpdatedFlatIndexMetadatas =
property === 'nameSingular'
? recomputeIndexAfterFlatObjectMetadataSingularNameUpdate({
flatFieldMetadataMaps,
existingFlatObjectMetadata: flatObjectMetadataToUpdate,
flatIndexMaps,
updatedSingularName: updatedFlatObjectMetadata.nameSingular,
})
: [];
return {
flatObjectMetadata: updatedFlatObjectMetadata,
otherObjectFlatFieldMetadataToUpdate: [
...otherObjectFlatFieldMetadatas,
...newUpdatedOtherObjectFlatFieldMetadatas,
],
flatIndexMetadataToUpdate: [
...flatIndexMetadataToUpdate,
...newUpdatedFlatIndexMetadatas,
],
};
},
initialAccumulator,
);
};
@@ -1,7 +1,7 @@
import { isDefined } from 'twenty-shared/utils';
import { type AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type';
import { findObjectFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { generateFlatIndexMetadataWithNameOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util';
@@ -31,10 +31,11 @@ export const recomputeIndexAfterFlatObjectMetadataSingularNameUpdate = ({
nameSingular: updatedSingularName,
};
const { objectFlatFieldMetadatas } = findObjectFlatFieldMetadatasOrThrow({
flatFieldMetadataMaps,
flatObjectMetadata: optimisticFlatObjectMetadata,
});
const objectFlatFieldMetadatas =
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatFieldMetadataMaps,
flatEntityIds: optimisticFlatObjectMetadata.fieldMetadataIds,
});
return allRelatedFlatIndexMetadata.map<FlatIndexMetadata>((flatIndex) => {
const newIndex = generateFlatIndexMetadataWithNameOrThrow({
@@ -0,0 +1,85 @@
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/constants/DEFAULT_VIEW_FIELD_SIZE';
type RecomputeViewFieldIdentifierAfterFlatObjectIdentifierUpdateArgs = {
existingFlatObjectMetadata: FlatObjectMetadata;
updatedLabelIdentifierFieldMetadataId: string;
} & Pick<AllFlatEntityMaps, 'flatViewFieldMaps' | 'flatViewMaps'>;
type FlatViewFieldToCreateAndUpdate = {
flatViewFieldToCreate: FlatViewField[];
flatViewFieldToUpdate: FlatViewField[];
};
export const recomputeViewFieldIdentifierAfterFlatObjectIdentifierUpdate = ({
existingFlatObjectMetadata,
flatViewFieldMaps,
flatViewMaps,
updatedLabelIdentifierFieldMetadataId,
}: RecomputeViewFieldIdentifierAfterFlatObjectIdentifierUpdateArgs): FlatViewFieldToCreateAndUpdate => {
const flatViews = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatViewMaps,
flatEntityIds: existingFlatObjectMetadata.viewIds,
});
const accumulator: FlatViewFieldToCreateAndUpdate = {
flatViewFieldToCreate: [],
flatViewFieldToUpdate: [],
};
for (const flatView of flatViews) {
const flatViewFields = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityIds: flatView.viewFieldIds,
flatEntityMaps: flatViewFieldMaps,
});
const labelMetadataIdentifierViewField = flatViewFields.find(
(viewField) =>
viewField.fieldMetadataId === updatedLabelIdentifierFieldMetadataId,
);
const ascSortedViewFieldPositions = flatViewFields
.map(({ position }) => position)
.sort((a, b) => a - b);
const lowestViewFieldPosition =
ascSortedViewFieldPositions.length > 0
? ascSortedViewFieldPositions[0]
: 0;
if (!isDefined(labelMetadataIdentifierViewField)) {
const viewFieldId = v4();
const createdAt = new Date();
const flatViewFieldToCreate: FlatViewField = {
fieldMetadataId: updatedLabelIdentifierFieldMetadataId,
position: lowestViewFieldPosition - 1,
isVisible: true,
size: DEFAULT_VIEW_FIELD_SIZE,
viewId: flatView.id,
workspaceId: flatView.workspaceId,
id: viewFieldId,
createdAt: createdAt,
updatedAt: createdAt,
deletedAt: null,
universalIdentifier: viewFieldId,
aggregateOperation: null,
};
accumulator.flatViewFieldToCreate.push(flatViewFieldToCreate);
} else if (
labelMetadataIdentifierViewField.position > lowestViewFieldPosition
) {
const updatedFlatViewField = {
...labelMetadataIdentifierViewField,
position: lowestViewFieldPosition - 1,
};
accumulator.flatViewFieldToUpdate.push(updatedFlatViewField);
}
}
return accumulator;
};
@@ -6,9 +6,9 @@ import {
import { computeMorphRelationFieldName } from 'twenty-shared/utils';
import { type AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { findObjectFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { getFlatObjectMetadataTargetMorphRelationFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-flat-object-metadata-many-to-one-target-morph-relation-flat-field-metadatas-or-throw.util';
import { getMorphNameFromMorphFieldMetadataName } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-morph-name-from-morph-field-metadata-name.util';
@@ -24,10 +24,11 @@ export const renameRelatedMorphFieldOnObjectNamesUpdate = ({
toFlatObjectMetadata,
flatFieldMetadataMaps,
}: RenameRelatedMorphFieldOnObjectNamesUpdateArgs): FlatFieldMetadata<FieldMetadataType.MORPH_RELATION>[] => {
const { objectFlatFieldMetadatas } = findObjectFlatFieldMetadatasOrThrow({
flatFieldMetadataMaps,
flatObjectMetadata: fromFlatObjectMetadata,
});
const objectFlatFieldMetadatas =
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatFieldMetadataMaps,
flatEntityIds: fromFlatObjectMetadata.fieldMetadataIds,
});
const manyToOneMorphRelationFlatFieldMetadatas =
getFlatObjectMetadataTargetMorphRelationFlatFieldMetadatasOrThrow({
flatFieldMetadataMaps,
@@ -4,8 +4,10 @@ import { isDefined } from 'twenty-shared/utils';
import { EMPTY_FLAT_ENTITY_MAPS } from 'src/engine/core-modules/common/constant/empty-flat-entity-maps.constant';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/core-modules/common/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { getSubFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/get-sub-flat-entity-maps-or-throw.util';
import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util';
import { ViewKey } from 'src/engine/core-modules/view/enums/view-key.enum';
@@ -14,12 +16,11 @@ import { FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view
import { fromCreateViewFieldInputToFlatViewFieldToCreate } from 'src/engine/core-modules/view/flat-view/utils/from-create-view-field-input-to-flat-view-field-to-create.util';
import { fromCreateViewInputToFlatViewToCreate } from 'src/engine/core-modules/view/flat-view/utils/from-create-view-input-to-flat-view-to-create.util';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { findObjectFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreate } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util';
import { fromDeleteObjectInputToFlatFieldMetadatasToDelete } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-delete-object-input-to-flat-field-metadatas-to-delete.util';
import { fromFlatObjectMetadataToObjectMetadataDto } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util';
import { fromUpdateObjectInputToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata.util';
import { fromUpdateObjectInputToFlatObjectMetadataAndRelatedFlatEntities } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata-and-related-flat-entities.util';
import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input';
import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
@@ -55,6 +56,8 @@ export class ObjectMetadataServiceV2 {
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
flatIndexMaps: existingFlatIndexMaps,
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
flatViewFieldMaps: existingFlatViewFieldMaps,
flatViewMaps: existingFlatViewMaps,
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
@@ -62,6 +65,8 @@ export class ObjectMetadataServiceV2 {
'flatObjectMetadataMaps',
'flatIndexMaps',
'flatFieldMetadataMaps',
'flatViewFieldMaps',
'flatViewMaps',
],
},
);
@@ -70,11 +75,15 @@ export class ObjectMetadataServiceV2 {
flatObjectMetadata: optimisticallyUpdatedFlatObjectMetadata,
otherObjectFlatFieldMetadataToUpdate: otherObjectFlatFieldMetadatas,
flatIndexMetadataToUpdate,
} = fromUpdateObjectInputToFlatObjectMetadata({
flatViewFieldToUpdate,
flatViewFieldToCreate,
} = fromUpdateObjectInputToFlatObjectMetadataAndRelatedFlatEntities({
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
updateObjectInput,
flatIndexMaps: existingFlatIndexMaps,
flatViewFieldMaps: existingFlatViewFieldMaps,
flatViewMaps: existingFlatViewMaps,
});
const toFlatObjectMetadataMaps = addFlatEntityToFlatEntityMapsOrThrow({
@@ -106,6 +115,27 @@ export class ObjectMetadataServiceV2 {
}),
);
const flatViewFieldMapsWithUpdated = flatViewFieldToUpdate.reduce(
(flatViewFieldMaps, flatViewField) =>
replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: flatViewField,
flatEntityMaps: flatViewFieldMaps,
}),
getSubFlatEntityMapsOrThrow({
flatEntityIds: flatViewFieldToUpdate.map(({ id }) => id),
flatEntityMaps: existingFlatViewFieldMaps,
}),
);
const toFlatViewFieldMaps = flatViewFieldToCreate.reduce(
(flatViewFieldMaps, flatViewField) =>
addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: flatViewField,
flatEntityMaps: flatViewFieldMaps,
}),
flatViewFieldMapsWithUpdated,
);
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
@@ -122,6 +152,13 @@ export class ObjectMetadataServiceV2 {
from: existingFlatFieldMetadataMaps,
to: toFlatFieldMetadataMaps,
},
flatViewFieldMaps: {
from: existingFlatViewFieldMaps,
to: toFlatViewFieldMaps,
},
},
dependencyAllFlatEntityMaps: {
flatViewMaps: existingFlatViewMaps,
},
buildOptions: {
isSystemBuild: false,
@@ -329,7 +366,7 @@ export class ObjectMetadataServiceV2 {
const toFlatFieldMetadataMaps = [
...flatFieldMetadataToCreateOnObject,
...relationTargetFlatFieldMetadataToCreate,
].reduce(
].reduce<FlatEntityMaps<FlatFieldMetadata>>(
(flatFieldMaps, flatField) =>
addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: flatField,
@@ -353,13 +390,16 @@ export class ObjectMetadataServiceV2 {
flatEntityMaps: existingFlatViewMaps,
});
const { objectFlatFieldMetadatas } = findObjectFlatFieldMetadatasOrThrow({
flatFieldMetadataMaps: toFlatFieldMetadataMaps,
flatObjectMetadata: flatObjectMetadataToCreate,
});
const flatDefaultViewFieldsToCreate =
await this.createDefaultFlatViewFields({
objectFlatFieldMetadatas,
objectFlatFieldMetadatas: findManyFlatEntityByIdInFlatEntityMapsOrThrow(
{
flatEntityMaps: toFlatFieldMetadataMaps,
flatEntityIds: flatFieldMetadataToCreateOnObject.map(
({ id }) => id,
),
},
),
viewId: flatDefaultViewToCreate.id,
workspaceId,
});
@@ -13,6 +13,7 @@ import {
import { type WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
@@ -145,8 +146,7 @@ export class ObjectMetadataEntity implements Required<ObjectMetadataEntity> {
@OneToMany(
() => ObjectPermissionEntity,
(objectPermission: ObjectPermissionEntity) =>
objectPermission.objectMetadata,
(objectPermission) => objectPermission.objectMetadata,
{
cascade: true,
},
@@ -155,10 +155,15 @@ export class ObjectMetadataEntity implements Required<ObjectMetadataEntity> {
@OneToMany(
() => FieldPermissionEntity,
(fieldPermission: FieldPermissionEntity) => fieldPermission.objectMetadata,
(fieldPermission) => fieldPermission.objectMetadata,
{
cascade: true,
},
)
fieldPermissions: Relation<FieldPermissionEntity[]>;
@OneToMany(() => ViewEntity, (view) => view.objectMetadata, {
cascade: true,
})
views: Relation<ViewEntity[]>;
}
@@ -27,6 +27,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
const idField: FlatFieldMetadata<FieldMetadataType.UUID> = {
type: FieldMetadataType.UUID,
id: v4(),
viewFieldIds: [],
isLabelSyncedWithName: false,
isUnique: true,
objectMetadataId,
@@ -60,6 +61,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
const nameField: FlatFieldMetadata<FieldMetadataType.TEXT> = {
type: FieldMetadataType.TEXT,
id: v4(),
viewFieldIds: [],
isLabelSyncedWithName: false,
isUnique: false,
objectMetadataId,
@@ -93,6 +95,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
const createdAtField: FlatFieldMetadata<FieldMetadataType.DATE_TIME> = {
type: FieldMetadataType.DATE_TIME,
id: v4(),
viewFieldIds: [],
isLabelSyncedWithName: false,
isUnique: false,
objectMetadataId,
@@ -126,6 +129,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
const updatedAtField: FlatFieldMetadata<FieldMetadataType.DATE_TIME> = {
type: FieldMetadataType.DATE_TIME,
id: v4(),
viewFieldIds: [],
isLabelSyncedWithName: false,
isUnique: false,
objectMetadataId,
@@ -159,6 +163,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
const deletedAtField: FlatFieldMetadata<FieldMetadataType.DATE_TIME> = {
type: FieldMetadataType.DATE_TIME,
id: v4(),
viewFieldIds: [],
isLabelSyncedWithName: false,
isUnique: false,
objectMetadataId,
@@ -192,6 +197,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
const createdByField: FlatFieldMetadata<FieldMetadataType.ACTOR> = {
type: FieldMetadataType.ACTOR,
id: v4(),
viewFieldIds: [],
isLabelSyncedWithName: false,
isUnique: false,
objectMetadataId,
@@ -225,6 +231,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
const positionField: FlatFieldMetadata<FieldMetadataType.POSITION> = {
type: FieldMetadataType.POSITION,
id: v4(),
viewFieldIds: [],
isLabelSyncedWithName: false,
isUnique: false,
objectMetadataId,
@@ -257,6 +264,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
const searchVectorField: FlatFieldMetadata<FieldMetadataType.TS_VECTOR> = {
type: FieldMetadataType.TS_VECTOR,
viewFieldIds: [],
id: v4(),
isLabelSyncedWithName: false,
isUnique: false,
@@ -63,6 +63,7 @@ const generateSourceFlatFieldMetadata = ({
}
return {
viewFieldIds: [],
createdAt,
updatedAt: createdAt,
defaultValue: null,
@@ -127,6 +128,7 @@ const generateTargetFlatFieldMetadata = ({
return {
morphId: null,
viewFieldIds: [],
id: sourceFlatFieldMetadata.relationTargetFieldMetadataId,
name: sourceFlatObjectMetadata.nameSingular,
label: sourceFlatObjectMetadata.labelSingular,
@@ -1,42 +0,0 @@
import { isDefined } from 'twenty-shared/utils';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { type IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { type CachedFieldMetadataEntity } from 'src/engine/metadata-modules/types/cached-field-metadata-entity';
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
export const getFieldMetadataEntityFromCachedObjectMetadataMaps = ({
fieldMetadataId,
objectMetadataId,
objectMetadataMaps,
}: {
objectMetadataMaps: ObjectMetadataMaps;
objectMetadataId: string;
fieldMetadataId: string;
}): FieldMetadataEntity | undefined => {
const objectMetadataFromCache = objectMetadataMaps.byId[objectMetadataId];
if (!isDefined(objectMetadataFromCache)) {
return undefined;
}
const fieldMetadataFromCache: CachedFieldMetadataEntity =
objectMetadataFromCache.fieldsById[fieldMetadataId];
if (!isDefined(fieldMetadataFromCache)) {
return undefined;
}
// We need to determine if graphql will be able to handle relations crafting byitself
return {
...fieldMetadataFromCache,
object: {} as ObjectMetadataEntity,
relationTargetFieldMetadata: null,
relationTargetObjectMetadata: null,
indexFieldMetadatas: {} as IndexFieldMetadataEntity,
fieldPermissions: [],
createdAt: new Date(fieldMetadataFromCache.createdAt),
updatedAt: new Date(fieldMetadataFromCache.updatedAt),
};
};
@@ -14,9 +14,9 @@ import {
import { aggregateOrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration-v2/utils/aggregate-orchestrator-actions-report.util';
import { WorkspaceMigrationV2CronTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/cron-trigger/workspace-migration-v2-cron-trigger-action-builder.service';
import { WorkspaceMigrationV2DatabaseEventTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/database-event-trigger/workspace-migration-v2-database-event-trigger-actions-builder.service';
import { WorkspaceMigrationV2FieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/field/services/workspace-migration-v2-field-actions-builder.service';
import { WorkspaceMigrationV2FieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/field/workspace-migration-v2-field-actions-builder.service';
import { WorkspaceMigrationV2IndexActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/index/workspace-migration-v2-index-actions-builder.service';
import { WorkspaceMigrationV2ObjectActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/services/workspace-migration-v2-object-actions-builder.service';
import { WorkspaceMigrationV2ObjectActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/workspace-migration-v2-object-actions-builder.service';
import { WorkspaceMigrationV2RouteTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/route-trigger/workspace-migration-v2-route-trigger-actions-builder.service';
import { WorkspaceMigrationV2ServerlessFunctionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/serverless-function/workspace-migration-v2-serverless-function-actions-builder.service';
import { WorkspaceMigrationV2ViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-field/workspace-migration-v2-view-field-actions-builder.service';
@@ -224,6 +224,8 @@ export class WorkspaceMigrationBuildOrchestratorService {
optimisticAllFlatEntityMaps.flatViewMaps =
viewResult.optimisticFlatEntityMaps;
optimisticAllFlatEntityMaps.flatObjectMetadataMaps =
viewResult.dependencyOptimisticFlatEntityMaps.flatObjectMetadataMaps;
if (viewResult.status === 'fail') {
orchestratorFailureReport.view.push(...viewResult.errors);
@@ -242,6 +244,8 @@ export class WorkspaceMigrationBuildOrchestratorService {
to: toFlatViewFieldMaps,
buildOptions,
dependencyOptimisticFlatEntityMaps: {
flatObjectMetadataMaps:
optimisticAllFlatEntityMaps.flatObjectMetadataMaps,
flatFieldMetadataMaps:
optimisticAllFlatEntityMaps.flatFieldMetadataMaps,
flatViewMaps: optimisticAllFlatEntityMaps.flatViewMaps,
@@ -254,6 +258,8 @@ export class WorkspaceMigrationBuildOrchestratorService {
viewFieldResult.optimisticFlatEntityMaps;
optimisticAllFlatEntityMaps.flatViewMaps =
viewFieldResult.dependencyOptimisticFlatEntityMaps.flatViewMaps;
optimisticAllFlatEntityMaps.flatFieldMetadataMaps =
viewFieldResult.dependencyOptimisticFlatEntityMaps.flatFieldMetadataMaps;
if (viewFieldResult.status === 'fail') {
orchestratorFailureReport.viewField.push(...viewFieldResult.errors);
@@ -1,3 +1,5 @@
import { type ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { type ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
import { type DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { type IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity';
@@ -5,7 +7,6 @@ import { type IndexMetadataEntity } from 'src/engine/metadata-modules/index-meta
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { type FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
import { type ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
import { type ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
export type MetadataEntitiesRelationTarget =
| ObjectMetadataEntity
@@ -15,4 +16,5 @@ export type MetadataEntitiesRelationTarget =
| DataSourceEntity
| ApplicationEntity
| IndexMetadataEntity
| ObjectPermissionEntity;
| ObjectPermissionEntity
| ViewEntity;
@@ -19,9 +19,6 @@ export type ObjectMetadataRelatedFlatEntityMaps = Pick<
'flatFieldMetadataMaps'
>;
// TODO find a solution in order to handle label indentifier validation
// export type ObjectMetadataRelatedFlatEntityMaps = undefined;
@Injectable()
export class WorkspaceMigrationV2ObjectActionsBuilderService extends WorkspaceEntityMigrationBuilderV2Service<
'objectMetadata',
@@ -2,5 +2,5 @@ import { type AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all
export type ViewFieldRelatedFlatEntityMaps = Pick<
AllFlatEntityMaps,
'flatViewMaps' | 'flatFieldMetadataMaps'
'flatViewMaps' | 'flatFieldMetadataMaps' | 'flatObjectMetadataMaps'
>;
@@ -48,7 +48,7 @@ export class WorkspaceMigrationV2ViewFieldActionsBuilderService extends Workspac
>
> {
const validationResult =
await this.flatViewFieldValidatorService.validateFlatViewFieldCreation({
this.flatViewFieldValidatorService.validateFlatViewFieldCreation({
dependencyOptimisticFlatEntityMaps,
flatViewFieldToValidate,
optimisticFlatViewFieldMaps,
@@ -61,12 +61,30 @@ export class WorkspaceMigrationV2ViewFieldActionsBuilderService extends Workspac
};
}
const flatFieldMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatViewFieldToValidate.fieldMetadataId,
flatEntityMaps: dependencyOptimisticFlatEntityMaps.flatFieldMetadataMaps,
});
const updatedFlatFieldMetadataMaps =
replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: {
...flatFieldMetadata,
viewFieldIds: [
...flatFieldMetadata.viewFieldIds,
flatViewFieldToValidate.id,
],
},
flatEntityMaps:
dependencyOptimisticFlatEntityMaps.flatFieldMetadataMaps,
});
const flatView = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatViewFieldToValidate.viewId,
flatEntityMaps: dependencyOptimisticFlatEntityMaps.flatViewMaps,
});
const updatedFlatViewFields = replaceFlatEntityInFlatEntityMapsOrThrow({
const updatedFlatViewMaps = replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: {
...flatView,
viewFieldIds: [...flatView.viewFieldIds, flatViewFieldToValidate.id],
@@ -82,7 +100,8 @@ export class WorkspaceMigrationV2ViewFieldActionsBuilderService extends Workspac
},
dependencyOptimisticFlatEntityMaps: {
...dependencyOptimisticFlatEntityMaps,
flatViewMaps: updatedFlatViewFields,
flatViewMaps: updatedFlatViewMaps,
flatFieldMetadataMaps: updatedFlatFieldMetadataMaps,
},
};
}
@@ -115,12 +134,30 @@ export class WorkspaceMigrationV2ViewFieldActionsBuilderService extends Workspac
};
}
const flatFieldMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatViewFieldToValidate.fieldMetadataId,
flatEntityMaps: dependencyOptimisticFlatEntityMaps.flatFieldMetadataMaps,
});
const updatedFlatFieldMetadataMaps = isDefined(flatFieldMetadata)
? replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: {
...flatFieldMetadata,
viewFieldIds: flatFieldMetadata.viewFieldIds.filter(
(id) => id !== flatViewFieldToValidate.id,
),
},
flatEntityMaps:
dependencyOptimisticFlatEntityMaps.flatFieldMetadataMaps,
})
: dependencyOptimisticFlatEntityMaps.flatFieldMetadataMaps;
const flatView = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatViewFieldToValidate.viewId,
flatEntityMaps: dependencyOptimisticFlatEntityMaps.flatViewMaps,
});
const updatedFlatViewFields = isDefined(flatView)
const updatedFlatViewMaps = isDefined(flatView)
? replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: {
...flatView,
@@ -140,7 +177,8 @@ export class WorkspaceMigrationV2ViewFieldActionsBuilderService extends Workspac
},
dependencyOptimisticFlatEntityMaps: {
...dependencyOptimisticFlatEntityMaps,
flatViewMaps: updatedFlatViewFields,
flatViewMaps: updatedFlatViewMaps,
flatFieldMetadataMaps: updatedFlatFieldMetadataMaps,
},
};
}
@@ -1,6 +1,11 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util';
import { FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type';
import { compareTwoFlatView } from 'src/engine/core-modules/view/flat-view/utils/compare-two-flat-view.util';
import {
@@ -56,6 +61,19 @@ export class WorkspaceMigrationV2ViewActionsBuilderService extends WorkspaceEnti
...validationResult,
};
}
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatViewToValidate.objectMetadataId,
flatEntityMaps: dependencyOptimisticFlatEntityMaps.flatObjectMetadataMaps,
});
const updatedFlatObjectMetadataMaps =
replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: {
...flatObjectMetadata,
viewIds: [...flatObjectMetadata.viewIds, flatViewToValidate.id],
},
flatEntityMaps:
dependencyOptimisticFlatEntityMaps.flatObjectMetadataMaps,
});
return {
status: 'success',
@@ -63,7 +81,9 @@ export class WorkspaceMigrationV2ViewActionsBuilderService extends WorkspaceEnti
type: 'create_view',
view: flatViewToValidate,
},
dependencyOptimisticFlatEntityMaps,
dependencyOptimisticFlatEntityMaps: {
flatObjectMetadataMaps: updatedFlatObjectMetadataMaps,
},
};
}
@@ -92,13 +112,33 @@ export class WorkspaceMigrationV2ViewActionsBuilderService extends WorkspaceEnti
};
}
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatViewToValidate.objectMetadataId,
flatEntityMaps: dependencyOptimisticFlatEntityMaps.flatObjectMetadataMaps,
});
const updatedFlatObjectMetadataMaps = isDefined(flatObjectMetadata)
? replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: {
...flatObjectMetadata,
viewIds: flatObjectMetadata.viewIds.filter(
(id) => id !== flatViewToValidate.id,
),
},
flatEntityMaps:
dependencyOptimisticFlatEntityMaps.flatObjectMetadataMaps,
})
: dependencyOptimisticFlatEntityMaps.flatObjectMetadataMaps;
return {
status: 'success',
action: {
type: 'delete_view',
viewId: flatViewToValidate.id,
},
dependencyOptimisticFlatEntityMaps,
dependencyOptimisticFlatEntityMaps: {
flatObjectMetadataMaps: updatedFlatObjectMetadataMaps,
},
};
}
@@ -15,14 +15,16 @@ export type CreateViewFieldAction = {
viewField: FlatViewField;
};
export type FlatViewFieldPropertiesUpdates = Array<
{
[P in FlatViewFieldPropertiesToCompare]: FlatViewFieldPropertyUpdate<P>;
}[FlatViewFieldPropertiesToCompare]
>;
export type UpdateViewFieldAction = {
type: 'update_view_field';
viewFieldId: string;
updates: Array<
{
[P in FlatViewFieldPropertiesToCompare]: FlatViewFieldPropertyUpdate<P>;
}[FlatViewFieldPropertiesToCompare]
>;
updates: FlatViewFieldPropertiesUpdates;
};
export type DeleteViewFieldAction = {
@@ -9,7 +9,7 @@ import { validateFlatObjectMetadataIdentifiers } from 'src/engine/metadata-modul
import { validateFlatObjectMetadataNameAndLabels } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name-and-labels.util';
import { ObjectMetadataExceptionCode } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util';
import { ObjectMetadataRelatedFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/services/workspace-migration-v2-object-actions-builder.service';
import { ObjectMetadataRelatedFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/workspace-migration-v2-object-actions-builder.service';
import { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
import { WorkspaceMigrationBuilderOptions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-builder-options.type';
import { FlatObjectPropertiesUpdates } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2';
@@ -4,11 +4,14 @@ import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { ViewExceptionCode } from 'src/engine/core-modules/view/exceptions/view.exception';
import { FlatViewFieldMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-field-maps.type';
import { FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type';
import { isViewFieldInLowestPosition } from 'src/engine/core-modules/view/flat-view/utils/is-view-field-in-lowest-position.util';
import { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
import { ViewFieldRelatedFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-field/types/view-field-related-flat-entity-maps.type';
import { validateLabelIdentifierFieldMetadataIdFlatViewField } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-label-identifier-field-metadata-id-flat-view-field.util';
type ViewFieldValidationArgs = {
flatViewFieldToValidate: FlatViewField;
@@ -19,53 +22,119 @@ type ViewFieldValidationArgs = {
export class FlatViewFieldValidatorService {
constructor() {}
// Should implement strict checking on field compared field and their integrity passing the update array would be the best
public validateFlatViewFieldUpdate({
flatViewFieldToValidate: updatedFlatViewField,
optimisticFlatViewFieldMaps,
dependencyOptimisticFlatEntityMaps: {
flatViewMaps,
flatObjectMetadataMaps,
},
}: ViewFieldValidationArgs): FailedFlatEntityValidation<FlatViewField> {
const errors = [];
const validationResult: FailedFlatEntityValidation<FlatViewField> = {
type: 'update_view_field',
errors: [],
flatEntityMinimalInformation: {
id: updatedFlatViewField.id,
},
};
const existingFlatViewField =
optimisticFlatViewFieldMaps.byId[updatedFlatViewField.id];
if (!isDefined(existingFlatViewField)) {
errors.push({
validationResult.errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`View field to update not found`,
userFriendlyMessage: t`View field to update not found`,
});
return validationResult;
}
return {
type: 'update_view_field',
errors,
flatEntityMinimalInformation: {
id: updatedFlatViewField.id,
viewId: updatedFlatViewField.viewId,
fieldMetadataId: updatedFlatViewField.fieldMetadataId,
},
validationResult.flatEntityMinimalInformation = {
id: updatedFlatViewField.id,
viewId: updatedFlatViewField.viewId,
fieldMetadataId: updatedFlatViewField.fieldMetadataId,
};
const flatView = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: updatedFlatViewField.viewId,
flatEntityMaps: flatViewMaps,
});
if (!isDefined(flatView)) {
validationResult.errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`View field to update parent view not found`,
userFriendlyMessage: t`View field to update parent view not found`,
});
return validationResult;
}
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatView.objectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
if (!isDefined(flatObjectMetadata)) {
validationResult.errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`View field to update parent view object not found`,
userFriendlyMessage: t`View field to update parent view object not found`,
});
return validationResult;
}
if (
flatObjectMetadata.labelIdentifierFieldMetadataId ===
updatedFlatViewField.fieldMetadataId
) {
const otherFlatViewFields = findManyFlatEntityByIdInFlatEntityMapsOrThrow(
{
flatEntityIds: flatView.viewFieldIds.filter(
(flatViewId) => flatViewId !== updatedFlatViewField.id,
),
flatEntityMaps: optimisticFlatViewFieldMaps,
},
);
validationResult.errors.push(
...validateLabelIdentifierFieldMetadataIdFlatViewField({
otherFlatViewFields,
flatViewFieldToValidate: updatedFlatViewField,
}),
);
}
return validationResult;
}
public validateFlatViewFieldDeletion({
optimisticFlatViewFieldMaps,
flatViewFieldToValidate: { id: viewFieldIdToDelete },
}: ViewFieldValidationArgs): FailedFlatEntityValidation<FlatViewField> {
const errors = [];
const validationResult: FailedFlatEntityValidation<FlatViewField> = {
type: 'delete_view_field',
errors: [],
flatEntityMinimalInformation: {
id: viewFieldIdToDelete,
},
};
const existingFlatViewField =
optimisticFlatViewFieldMaps.byId[viewFieldIdToDelete];
if (!isDefined(existingFlatViewField)) {
errors.push({
validationResult.errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`View field to delete not found`,
userFriendlyMessage: t`View field to delete not found`,
});
} else {
if (!isDefined(existingFlatViewField.deletedAt)) {
errors.push({
validationResult.errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`View field to delete has not been soft deleted`,
userFriendlyMessage: t`View field to delete has not been soft deleted`,
@@ -73,28 +142,31 @@ export class FlatViewFieldValidatorService {
}
}
return {
type: 'delete_view_field',
errors,
flatEntityMinimalInformation: {
id: viewFieldIdToDelete,
},
};
return validationResult;
}
public async validateFlatViewFieldCreation({
public validateFlatViewFieldCreation({
flatViewFieldToValidate,
optimisticFlatViewFieldMaps,
dependencyOptimisticFlatEntityMaps: {
flatViewMaps: optimisticFlatViewMaps,
flatViewMaps,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
},
}: {
flatViewFieldToValidate: FlatViewField;
optimisticFlatViewFieldMaps: FlatViewFieldMaps;
dependencyOptimisticFlatEntityMaps: ViewFieldRelatedFlatEntityMaps;
}): Promise<FailedFlatEntityValidation<FlatViewField>> {
const errors = [];
}): FailedFlatEntityValidation<FlatViewField> {
const validationResult: FailedFlatEntityValidation<FlatViewField> = {
type: 'create_view_field',
errors: [],
flatEntityMinimalInformation: {
id: flatViewFieldToValidate.id,
viewId: flatViewFieldToValidate.viewId,
fieldMetadataId: flatViewFieldToValidate.fieldMetadataId,
},
};
const existingFlatViewField =
optimisticFlatViewFieldMaps.byId[flatViewFieldToValidate.id];
@@ -102,63 +174,100 @@ export class FlatViewFieldValidatorService {
if (isDefined(existingFlatViewField)) {
const flatViewFieldId = flatViewFieldToValidate.id;
errors.push({
validationResult.errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`View field metadata with id ${flatViewFieldId} already exists`,
userFriendlyMessage: t`View field metadata already exists`,
});
}
const relatedFlatView =
optimisticFlatViewMaps.byId[flatViewFieldToValidate.viewId];
if (!isDefined(relatedFlatView)) {
errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`View not found`,
userFriendlyMessage: t`View not found`,
});
}
const relatedFlatFieldMetadata = findFlatEntityByIdInFlatEntityMaps({
const flatFieldMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatViewFieldToValidate.fieldMetadataId,
flatEntityMaps: flatFieldMetadataMaps,
});
if (!isDefined(relatedFlatFieldMetadata)) {
errors.push({
if (!isDefined(flatFieldMetadata)) {
validationResult.errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`Field metadata not found`,
userFriendlyMessage: t`Field metadata not found`,
});
}
const allFlatViewFields = Object.values(
optimisticFlatViewFieldMaps.byId,
).filter(isDefined);
const matchingFlatView = allFlatViewFields.find(
const flatView = flatViewMaps.byId[flatViewFieldToValidate.viewId];
if (!isDefined(flatView)) {
validationResult.errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`View not found`,
userFriendlyMessage: t`View not found`,
});
return validationResult;
}
const otherFlatViewFields = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityIds: flatView.viewFieldIds,
flatEntityMaps: optimisticFlatViewFieldMaps,
});
const equivalentExistingFlatViewFieldExists = otherFlatViewFields.some(
(flatViewField) =>
flatViewField.viewId === flatViewFieldToValidate.viewId &&
flatViewField.fieldMetadataId ===
flatViewFieldToValidate.fieldMetadataId,
);
if (isDefined(matchingFlatView)) {
errors.push({
if (equivalentExistingFlatViewFieldExists) {
validationResult.errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`View field with same fieldmetadataId and viewId already exists`,
userFriendlyMessage: t`View field already exists`,
});
}
return {
type: 'create_view_field',
errors,
flatEntityMinimalInformation: {
id: flatViewFieldToValidate.id,
viewId: flatViewFieldToValidate.viewId,
fieldMetadataId: flatViewFieldToValidate.fieldMetadataId,
},
};
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: flatView.objectMetadataId,
});
if (!isDefined(flatObjectMetadata)) {
validationResult.errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`View field related view object metadata not found`,
userFriendlyMessage: t`View field related view object metadata not found`,
});
return validationResult;
}
if (
flatObjectMetadata.labelIdentifierFieldMetadataId ===
flatViewFieldToValidate.fieldMetadataId
) {
validationResult.errors.push(
...validateLabelIdentifierFieldMetadataIdFlatViewField({
flatViewFieldToValidate,
otherFlatViewFields,
}),
);
} else if (
otherFlatViewFields.some(
(flatViewField) =>
flatViewField.fieldMetadataId ===
flatObjectMetadata.labelIdentifierFieldMetadataId,
) &&
isViewFieldInLowestPosition({
flatViewField: flatViewFieldToValidate,
otherFlatViewFields,
})
) {
validationResult.errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`View field position cannot be lower than label identifier view field position`,
userFriendlyMessage: t`View field position cannot be lower than label identifier view field position`,
});
}
return validationResult;
}
}
@@ -0,0 +1,50 @@
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { ViewExceptionCode } from 'src/engine/core-modules/view/exceptions/view.exception';
import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type';
import { isViewFieldInLowestPosition } from 'src/engine/core-modules/view/flat-view/utils/is-view-field-in-lowest-position.util';
import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
type ValidateLabelIdentifierFieldMetadataIdFlatViewFieldArgs = {
otherFlatViewFields: FlatViewField[];
flatViewFieldToValidate: FlatViewField;
};
export const validateLabelIdentifierFieldMetadataIdFlatViewField = ({
otherFlatViewFields,
flatViewFieldToValidate,
}: ValidateLabelIdentifierFieldMetadataIdFlatViewFieldArgs): FlatEntityValidationError[] => {
const errors: FlatEntityValidationError[] = [];
if (
otherFlatViewFields.length > 0 &&
!isViewFieldInLowestPosition({
flatViewField: flatViewFieldToValidate,
otherFlatViewFields,
})
) {
errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`Label identifier view field has to be in the lowest position`,
userFriendlyMessage: t`Label identifier view field has to be in the lowest position`,
});
}
if (flatViewFieldToValidate.isVisible === false) {
errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`Label identifier view field has to be visible`,
userFriendlyMessage: t`Label identifier view field has to be visible`,
});
}
if (isDefined(flatViewFieldToValidate.deletedAt)) {
errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`Label identifier view field cannot be deleted`,
userFriendlyMessage: t`Label identifier view field cannot be deleted`,
});
}
return errors;
};
@@ -4,9 +4,9 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
import { FlatFieldMetadataTypeValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service';
import { WorkspaceMigrationV2CronTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/cron-trigger/workspace-migration-v2-cron-trigger-action-builder.service';
import { WorkspaceMigrationV2DatabaseEventTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/database-event-trigger/workspace-migration-v2-database-event-trigger-actions-builder.service';
import { WorkspaceMigrationV2FieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/field/services/workspace-migration-v2-field-actions-builder.service';
import { WorkspaceMigrationV2FieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/field/workspace-migration-v2-field-actions-builder.service';
import { WorkspaceMigrationV2IndexActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/index/workspace-migration-v2-index-actions-builder.service';
import { WorkspaceMigrationV2ObjectActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/services/workspace-migration-v2-object-actions-builder.service';
import { WorkspaceMigrationV2ObjectActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/workspace-migration-v2-object-actions-builder.service';
import { WorkspaceMigrationV2RouteTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/route-trigger/workspace-migration-v2-route-trigger-actions-builder.service';
import { WorkspaceMigrationV2ServerlessFunctionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/serverless-function/workspace-migration-v2-serverless-function-actions-builder.service';
import { WorkspaceMigrationV2ViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-field/workspace-migration-v2-view-field-actions-builder.service';
@@ -8,7 +8,7 @@ import {
import { AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type';
import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findObjectFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { isCompositeFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-composite-flat-field-metadata.util';
import { isEnumFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-flat-field-metadata.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@@ -90,10 +90,11 @@ export class DeleteObjectActionHandlerService extends WorkspaceMigrationRunnerAc
schemaName,
tableName,
});
const { objectFlatFieldMetadatas } = findObjectFlatFieldMetadatasOrThrow({
flatFieldMetadataMaps,
flatObjectMetadata,
});
const objectFlatFieldMetadatas =
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatFieldMetadataMaps,
flatEntityIds: flatObjectMetadata.fieldMetadataIds,
});
const enumOrCompositeFlatFieldMetadatas = objectFlatFieldMetadatas.filter(
(field) =>
isEnumFlatFieldMetadata(field) || isCompositeFlatFieldMetadata(field),
@@ -7,8 +7,8 @@ import {
import { AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util';
import { findObjectFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util';
import { isCompositeFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-composite-flat-field-metadata.util';
import { isEnumFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-flat-field-metadata.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@@ -121,10 +121,10 @@ export class UpdateObjectActionHandlerService extends WorkspaceMigrationRunnerAc
newTableName,
});
const { objectFlatFieldMetadatas } =
findObjectFlatFieldMetadatasOrThrow({
flatFieldMetadataMaps,
flatObjectMetadata: updatedObjectMetadata,
const objectFlatFieldMetadatas =
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatFieldMetadataMaps,
flatEntityIds: updatedObjectMetadata.fieldMetadataIds,
});
const enumOrCompositeFlatFieldMetadatas =
objectFlatFieldMetadatas.filter(
@@ -85,7 +85,6 @@ export class WorkspaceMigrationRunnerV2Service {
await queryRunner.commitTransaction();
this.logger.timeEnd('Runner', 'Transaction execution');
this.logger.time('Runner', 'Cache invalidation');
const flatEntitiesCacheToInvalidate = [
...new Set([
@@ -94,6 +93,10 @@ export class WorkspaceMigrationRunnerV2Service {
]),
];
this.logger.time(
'Runner',
`Cache invalidation ${flatEntitiesCacheToInvalidate.join()}`,
);
if (
flatEntitiesCacheToInvalidate.includes('flatObjectMetadataMaps') ||
flatEntitiesCacheToInvalidate.includes('flatFieldMetadataMaps')
@@ -119,7 +122,10 @@ export class WorkspaceMigrationRunnerV2Service {
],
});
this.logger.timeEnd('Runner', 'Cache invalidation');
this.logger.timeEnd(
'Runner',
`Cache invalidation ${flatEntitiesCacheToInvalidate.join()}`,
);
this.logger.timeEnd('Runner', 'Total execution');
return allFlatEntityMaps;
@@ -137,6 +137,7 @@ export class StandardFieldFactory {
return [
{
viewFields: [],
type: workspaceFieldMetadataArgs.type,
standardId: workspaceFieldMetadataArgs.standardId,
name: workspaceFieldMetadataArgs.name,
@@ -186,6 +187,7 @@ export class StandardFieldFactory {
}
fieldMetadataCollection.push({
viewFields: [],
type: FieldMetadataType.RELATION,
standardId: workspaceRelationMetadataArgs.standardId,
name: workspaceRelationMetadataArgs.name,
@@ -44,6 +44,7 @@ export const computeStandardFields = (
fields.push({
...data,
...rest,
viewFields: [],
standardId: relationStandardId,
defaultValue: null,
isNullable: true,
@@ -19,6 +19,7 @@ export const getMockFieldMetadataEntity = <
overrides: GetMockFieldMetadataEntityOverride<T>,
): FieldMetadataEntity => {
return {
viewFields: [],
morphId: null,
fieldPermissions: [],
icon: null,
@@ -46,6 +46,7 @@ export const getMockObjectMetadataEntity = (
targetRelationFields: [],
standardOverrides: null,
targetTableName: faker.string.uuid(),
views: [],
...overrides,
};
};
@@ -1,72 +0,0 @@
import { createOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/create-one-core-view-field.util';
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
import {
eachTestingContextFilter,
type EachTestingContext,
} from 'twenty-shared/testing';
import { faker } from '@faker-js/faker';
import { type CreateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-field.input';
import {
cleanupViewFieldTestV2,
setupViewFieldTestV2,
type ViewFieldTestSetup,
} from './utils/setup-view-field-test-v2.util';
describe('View Field Resolver - Failing Create Operation - v2', () => {
let testSetup: ViewFieldTestSetup;
beforeAll(async () => {
testSetup = await setupViewFieldTestV2();
});
afterAll(async () => {
await cleanupViewFieldTestV2(testSetup.testObjectMetadataId);
});
type CreateViewFieldTestCase = (testSetup: ViewFieldTestSetup) => {
input: CreateViewFieldInput;
};
const createViewFieldTestCases: EachTestingContext<CreateViewFieldTestCase>[] =
[
{
title: 'non-existent field metadata',
context: (testSetup) => ({
input: {
viewId: testSetup.testViewId,
fieldMetadataId: faker.string.uuid(),
},
}),
},
{
title: 'non-existent view metadata',
context: (testSetup) => ({
input: {
viewId: faker.string.uuid(),
fieldMetadataId: testSetup.testFieldMetadataId,
},
}),
},
];
it.each(eachTestingContextFilter(createViewFieldTestCases))(
'should fail to create view field when $title',
async ({ context }) => {
const { input } = context(testSetup);
const response = await createOneCoreViewField({
input,
expectToFail: true,
});
expect(response.errors).toBeDefined();
expect(response.errors.length).toBe(1);
const [firstError] = response.errors;
expect(firstError).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny(firstError),
);
},
);
});
@@ -1,5 +1,57 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`View Field Resolver - Failing Create Operation - v2 Should fail to create a conflicting view field on view and field metadata 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"errors": {
"cronTrigger": [],
"databaseEventTrigger": [],
"fieldMetadata": [],
"index": [],
"objectMetadata": [],
"routeTrigger": [],
"serverlessFunction": [],
"view": [],
"viewField": [
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "View field with same fieldmetadataId and viewId already exists",
"userFriendlyMessage": "View field already exists",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "create_view_field",
},
],
},
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
"invalidCronTrigger": 0,
"invalidDatabaseEventTrigger": 0,
"invalidFieldMetadata": 0,
"invalidIndex": 0,
"invalidObjectMetadata": 0,
"invalidRouteTrigger": 0,
"invalidServerlessFunction": 0,
"invalidView": 0,
"invalidViewField": 0,
"totalErrors": 0,
},
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
},
"message": "Multiple validation errors occurred while creating view field",
"name": "GraphQLError",
}
`;
exports[`View Field Resolver - Failing Create Operation - v2 should fail to create view field when non-existent field metadata 1`] = `
{
"extensions": {
@@ -0,0 +1,259 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`View Field Resolver - Successful object metadata identifier update side effect on view field Should not allow creating a view field with a position lower than the label identifier view field 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"errors": {
"cronTrigger": [],
"databaseEventTrigger": [],
"fieldMetadata": [],
"index": [],
"objectMetadata": [],
"routeTrigger": [],
"serverlessFunction": [],
"view": [],
"viewField": [
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "View field position cannot be lower than label identifier view field position",
"userFriendlyMessage": "View field position cannot be lower than label identifier view field position",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "create_view_field",
},
],
},
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
"invalidCronTrigger": 0,
"invalidDatabaseEventTrigger": 0,
"invalidFieldMetadata": 0,
"invalidIndex": 0,
"invalidObjectMetadata": 0,
"invalidRouteTrigger": 0,
"invalidServerlessFunction": 0,
"invalidView": 0,
"invalidViewField": 0,
"totalErrors": 0,
},
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
},
"message": "Multiple validation errors occurred while creating view field",
"name": "GraphQLError",
}
`;
exports[`View Field Resolver - Successful object metadata identifier update side effect on view field Should not allow deleting a label identifier view field 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"errors": {
"cronTrigger": [],
"databaseEventTrigger": [],
"fieldMetadata": [],
"index": [],
"objectMetadata": [],
"routeTrigger": [],
"serverlessFunction": [],
"view": [],
"viewField": [
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Label identifier view field cannot be deleted",
"userFriendlyMessage": "Label identifier view field cannot be deleted",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "update_view_field",
},
],
},
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
"invalidCronTrigger": 0,
"invalidDatabaseEventTrigger": 0,
"invalidFieldMetadata": 0,
"invalidIndex": 0,
"invalidObjectMetadata": 0,
"invalidRouteTrigger": 0,
"invalidServerlessFunction": 0,
"invalidView": 0,
"invalidViewField": 0,
"totalErrors": 0,
},
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
},
"message": "Multiple validation errors occurred while deleting view field",
"name": "GraphQLError",
}
`;
exports[`View Field Resolver - Successful object metadata identifier update side effect on view field Should not allow destroying a label identifier view field 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"errors": {
"cronTrigger": [],
"databaseEventTrigger": [],
"fieldMetadata": [],
"index": [],
"objectMetadata": [],
"routeTrigger": [],
"serverlessFunction": [],
"view": [],
"viewField": [
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "View field to delete has not been soft deleted",
"userFriendlyMessage": "View field to delete has not been soft deleted",
},
],
"flatEntityMinimalInformation": {
"id": Any<String>,
},
"status": "fail",
"type": "delete_view_field",
},
],
},
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
"invalidCronTrigger": 0,
"invalidDatabaseEventTrigger": 0,
"invalidFieldMetadata": 0,
"invalidIndex": 0,
"invalidObjectMetadata": 0,
"invalidRouteTrigger": 0,
"invalidServerlessFunction": 0,
"invalidView": 0,
"invalidViewField": 0,
"totalErrors": 0,
},
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
},
"message": "Multiple validation errors occurred while deleting view field",
"name": "GraphQLError",
}
`;
exports[`View Field Resolver - Successful object metadata identifier update side effect on view field Should not allow updating a label identifier view field visibility to false 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"errors": {
"cronTrigger": [],
"databaseEventTrigger": [],
"fieldMetadata": [],
"index": [],
"objectMetadata": [],
"routeTrigger": [],
"serverlessFunction": [],
"view": [],
"viewField": [
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Label identifier view field has to be visible",
"userFriendlyMessage": "Label identifier view field has to be visible",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "update_view_field",
},
],
},
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
"invalidCronTrigger": 0,
"invalidDatabaseEventTrigger": 0,
"invalidFieldMetadata": 0,
"invalidIndex": 0,
"invalidObjectMetadata": 0,
"invalidRouteTrigger": 0,
"invalidServerlessFunction": 0,
"invalidView": 0,
"invalidViewField": 0,
"totalErrors": 0,
},
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
},
"message": "Multiple validation errors occurred while updating view field",
"name": "GraphQLError",
}
`;
exports[`View Field Resolver - Successful object metadata identifier update side effect on view field Should not allow updating labelIdentifier view field with a position higher than existing other view field 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"errors": {
"cronTrigger": [],
"databaseEventTrigger": [],
"fieldMetadata": [],
"index": [],
"objectMetadata": [],
"routeTrigger": [],
"serverlessFunction": [],
"view": [],
"viewField": [
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Label identifier view field has to be in the lowest position",
"userFriendlyMessage": "Label identifier view field has to be in the lowest position",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "update_view_field",
},
],
},
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
"invalidCronTrigger": 0,
"invalidDatabaseEventTrigger": 0,
"invalidFieldMetadata": 0,
"invalidIndex": 0,
"invalidObjectMetadata": 0,
"invalidRouteTrigger": 0,
"invalidServerlessFunction": 0,
"invalidView": 0,
"invalidViewField": 0,
"totalErrors": 0,
},
"userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)",
},
"message": "Multiple validation errors occurred while updating view field",
"name": "GraphQLError",
}
`;
@@ -0,0 +1,149 @@
import { faker } from '@faker-js/faker';
import { createOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/create-one-core-view-field.util';
import { deleteOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/delete-one-core-view-field.util';
import { destroyOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/destroy-one-core-view-field.util';
import { findCoreViewFields } from 'test/integration/metadata/suites/view-field/utils/find-core-view-fields.util';
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
import {
eachTestingContextFilter,
type EachTestingContext,
} from 'twenty-shared/testing';
import {
type ViewFieldTestSetup,
cleanupViewFieldTestV2,
setupViewFieldTestV2,
} from 'test/integration/graphql/suites/view/utils/setup-view-field-test-v2.util';
import { type CreateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-field.input';
describe('View Field Resolver - Failing Create Operation - v2', () => {
let testSetup: ViewFieldTestSetup;
let createdFlatViewFieldIds: string[] = [];
beforeAll(async () => {
testSetup = await setupViewFieldTestV2();
});
afterEach(async () => {
for (const viewFieldId of createdFlatViewFieldIds) {
await deleteOneCoreViewField({
input: {
id: viewFieldId,
},
expectToFail: false,
});
await destroyOneCoreViewField({
input: {
id: viewFieldId,
},
expectToFail: false,
});
}
});
afterAll(async () => {
await cleanupViewFieldTestV2(testSetup.testObjectMetadataId);
});
type CreateViewFieldTestCase = (testSetup: ViewFieldTestSetup) => {
input: CreateViewFieldInput;
};
const createViewFieldTestCases: EachTestingContext<CreateViewFieldTestCase>[] =
[
{
title: 'non-existent field metadata',
context: (testSetup) => ({
input: {
viewId: testSetup.testViewId,
fieldMetadataId: faker.string.uuid(),
},
}),
},
{
title: 'non-existent view metadata',
context: (testSetup) => ({
input: {
viewId: faker.string.uuid(),
fieldMetadataId: testSetup.testFieldMetadataId,
},
}),
},
];
it.each(eachTestingContextFilter(createViewFieldTestCases))(
'should fail to create view field when $title',
async ({ context }) => {
const { input } = context(testSetup);
const response = await createOneCoreViewField({
input,
expectToFail: true,
});
expect(response.errors).toBeDefined();
expect(response.errors.length).toBe(1);
const [firstError] = response.errors;
expect(firstError.extensions.code).not.toBe('INTERNAL_SERVER_ERROR');
expect(firstError).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny(firstError),
);
},
);
it('Should fail to create a conflicting view field on view and field metadata', async () => {
const viewFieldId = '20202020-7ace-42ee-aecf-2b1c1bd34bce';
const {
data: {
createCoreViewField: { id: createdFlatViewFieldId },
},
} = await createOneCoreViewField({
input: {
id: viewFieldId,
fieldMetadataId: testSetup.testFieldMetadataId,
viewId: testSetup.testViewId,
},
expectToFail: false,
});
createdFlatViewFieldIds.push(createdFlatViewFieldId);
const {
data: { getCoreViewFields },
} = await findCoreViewFields({
viewId: testSetup.testViewId,
expectToFail: false,
gqlFields: `
id
fieldMetadataId
viewId
`,
});
expect(getCoreViewFields).toStrictEqual([
{
id: viewFieldId,
fieldMetadataId: testSetup.testFieldMetadataId,
viewId: testSetup.testViewId,
},
]);
const response = await createOneCoreViewField({
input: {
fieldMetadataId: testSetup.testFieldMetadataId,
viewId: testSetup.testViewId,
},
expectToFail: true,
});
expect(response.errors).toBeDefined();
expect(response.errors.length).toBe(1);
const [firstError] = response.errors;
expect(firstError.extensions.code).not.toBe('INTERNAL_SERVER_ERROR');
expect(firstError).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny(firstError),
);
});
});
@@ -1,18 +1,17 @@
import { TEST_NOT_EXISTING_VIEW_FIELD_ID } from 'test/integration/constants/test-view-ids.constants';
import { deleteOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/delete-one-core-view-field.util';
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
import {
eachTestingContextFilter,
type EachTestingContext,
} from 'twenty-shared/testing';
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
import { type DeleteViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/delete-view-field.input';
import {
type ViewFieldTestSetup,
cleanupViewFieldTestV2,
setupViewFieldTestV2,
type ViewFieldTestSetup,
} from './utils/setup-view-field-test-v2.util';
} from 'test/integration/graphql/suites/view/utils/setup-view-field-test-v2.util';
import { type DeleteViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/delete-view-field.input';
describe('View Field Resolver - Failing Delete Operation - v2', () => {
let testSetup: ViewFieldTestSetup;
@@ -1,18 +1,17 @@
import { TEST_NOT_EXISTING_VIEW_FIELD_ID } from 'test/integration/constants/test-view-ids.constants';
import { destroyOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/destroy-one-core-view-field.util';
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
import {
eachTestingContextFilter,
type EachTestingContext,
} from 'twenty-shared/testing';
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
import { type DestroyViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/destroy-view-field.input';
import {
type ViewFieldTestSetup,
cleanupViewFieldTestV2,
setupViewFieldTestV2,
type ViewFieldTestSetup,
} from './utils/setup-view-field-test-v2.util';
} from 'test/integration/graphql/suites/view/utils/setup-view-field-test-v2.util';
import { type DestroyViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/destroy-view-field.input';
describe('View Field Resolver - Failing Destroy Operation - v2', () => {
let testSetup: ViewFieldTestSetup;
@@ -1,18 +1,17 @@
import { TEST_NOT_EXISTING_VIEW_FIELD_ID } from 'test/integration/constants/test-view-ids.constants';
import { updateOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/update-one-core-view-field.util';
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
import {
eachTestingContextFilter,
type EachTestingContext,
} from 'twenty-shared/testing';
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
import { type UpdateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-field.input';
import {
type ViewFieldTestSetup,
cleanupViewFieldTestV2,
setupViewFieldTestV2,
type ViewFieldTestSetup,
} from './utils/setup-view-field-test-v2.util';
} from 'test/integration/graphql/suites/view/utils/setup-view-field-test-v2.util';
import { type UpdateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-field.input';
describe('View Field Resolver - Failing Update Operation - v2', () => {
let testSetup: ViewFieldTestSetup;
@@ -0,0 +1,246 @@
import {
type ViewFieldTestSetup,
cleanupViewFieldTestV2,
setupViewFieldTestV2,
} from 'test/integration/graphql/suites/view/utils/setup-view-field-test-v2.util';
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import { createOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/create-one-core-view-field.util';
import { deleteOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/delete-one-core-view-field.util';
import { destroyOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/destroy-one-core-view-field.util';
import { findCoreViewFields } from 'test/integration/metadata/suites/view-field/utils/find-core-view-fields.util';
import { updateOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/update-one-core-view-field.util';
import { FieldMetadataType } from 'twenty-shared/types';
import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type';
describe('View Field Resolver - Successful object metadata identifier update side effect on view field', () => {
let testSetup: ViewFieldTestSetup & {
testLabelIdentifierViewFieldId: string;
};
beforeAll(async () => {
const { testFieldMetadataId, testObjectMetadataId, testViewId } =
await setupViewFieldTestV2();
await updateOneObjectMetadata({
input: {
idToUpdate: testObjectMetadataId,
updatePayload: {
labelIdentifierFieldMetadataId: testFieldMetadataId,
},
},
expectToFail: false,
});
const {
data: { getCoreViewFields },
} = await findCoreViewFields({
viewId: testViewId,
expectToFail: false,
gqlFields: `
id
fieldMetadataId
viewId
`,
});
const testLabelIdentifierViewFieldId = getCoreViewFields[0].id;
testSetup = {
testFieldMetadataId,
testObjectMetadataId,
testViewId,
testLabelIdentifierViewFieldId,
};
});
afterAll(async () => {
await cleanupViewFieldTestV2(testSetup.testObjectMetadataId);
});
it('should create a view field on label identifier object metadata update if it does not exist on view', async () => {
const { testFieldMetadataId, testViewId } = testSetup;
const {
data: { getCoreViewFields },
} = await findCoreViewFields({
viewId: testViewId,
expectToFail: false,
gqlFields: `
id
fieldMetadataId
viewId
`,
});
expect(getCoreViewFields).toMatchObject<
Pick<FlatViewField, 'id' | 'fieldMetadataId' | 'viewId'>[]
>([
{
id: testSetup.testLabelIdentifierViewFieldId,
fieldMetadataId: testFieldMetadataId,
viewId: testViewId,
},
]);
});
it('Should not allow deleting a label identifier view field', async () => {
const { errors } = await deleteOneCoreViewField({
input: { id: testSetup.testLabelIdentifierViewFieldId },
expectToFail: true,
});
expectOneNotInternalServerErrorSnapshot({
errors,
});
});
it('Should not allow destroying a label identifier view field', async () => {
const { errors } = await destroyOneCoreViewField({
input: { id: testSetup.testLabelIdentifierViewFieldId },
expectToFail: true,
});
expectOneNotInternalServerErrorSnapshot({
errors,
});
});
it('Should not allow updating a label identifier view field visibility to false', async () => {
const { errors } = await updateOneCoreViewField({
input: {
id: testSetup.testLabelIdentifierViewFieldId,
update: { isVisible: false },
},
expectToFail: true,
});
expectOneNotInternalServerErrorSnapshot({
errors,
});
});
it('Should not allow creating a view field with a position lower than the label identifier view field', async () => {
const {
data: {
createOneField: { id: fieldMetadataId },
},
} = await createOneFieldMetadata({
expectToFail: false,
input: {
name: 'nonLabelIdentifierFieldFailing',
label: 'Test Field',
type: FieldMetadataType.TEXT,
objectMetadataId: testSetup.testObjectMetadataId,
isLabelSyncedWithName: false,
},
gqlFields: `
id
name
label
isLabelSyncedWithName
`,
});
const { errors } = await createOneCoreViewField({
input: {
fieldMetadataId: fieldMetadataId,
viewId: testSetup.testViewId,
position: -42,
},
expectToFail: true,
});
expectOneNotInternalServerErrorSnapshot({
errors,
});
});
it('Should not allow updating labelIdentifier view field with a position higher than existing other view field', async () => {
const {
data: {
createOneField: { id: fieldMetadataId },
},
} = await createOneFieldMetadata({
expectToFail: false,
input: {
name: 'nonLabelIdentifierFieldSuccessful',
label: 'Test Field',
type: FieldMetadataType.TEXT,
objectMetadataId: testSetup.testObjectMetadataId,
isLabelSyncedWithName: false,
},
gqlFields: `
id
name
label
isLabelSyncedWithName
`,
});
await createOneCoreViewField({
input: {
fieldMetadataId: fieldMetadataId,
viewId: testSetup.testViewId,
position: 42,
},
expectToFail: false,
});
const { errors } = await updateOneCoreViewField({
input: {
id: testSetup.testLabelIdentifierViewFieldId,
update: {
position: 43,
},
},
expectToFail: true,
});
expectOneNotInternalServerErrorSnapshot({
errors,
});
});
it('Should update labelIdentifier view field with a position higher than existing other view field', async () => {
const {
data: {
createOneField: { id: fieldMetadataId },
},
} = await createOneFieldMetadata({
expectToFail: false,
input: {
name: 'justafield',
label: 'Test Field',
type: FieldMetadataType.TEXT,
objectMetadataId: testSetup.testObjectMetadataId,
isLabelSyncedWithName: false,
},
gqlFields: `
id
name
label
isLabelSyncedWithName
`,
});
await createOneCoreViewField({
input: {
fieldMetadataId: fieldMetadataId,
viewId: testSetup.testViewId,
position: 42,
},
expectToFail: false,
});
await updateOneCoreViewField({
input: {
id: testSetup.testLabelIdentifierViewFieldId,
update: {
position: 41,
},
},
expectToFail: false,
});
});
});
@@ -7,16 +7,15 @@ import {
eachTestingContextFilter,
} from 'twenty-shared/testing';
import { isDefined } from 'twenty-shared/utils';
import {
type ViewFieldTestSetup,
cleanupViewFieldTestV2,
setupViewFieldTestV2,
} from 'test/integration/graphql/suites/view/utils/setup-view-field-test-v2.util';
import { type CreateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-field.input';
import { type ViewFieldDTO } from 'src/engine/core-modules/view/dtos/view-field.dto';
import {
cleanupViewFieldTestV2,
setupViewFieldTestV2,
type ViewFieldTestSetup,
} from './utils/setup-view-field-test-v2.util';
type TestContext = {
viewFieldInput: (testSetup: ViewFieldTestSetup) => CreateViewFieldInput;
expected: Partial<ViewFieldDTO>;
@@ -0,0 +1,17 @@
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
import { type BaseGraphQLError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
export const expectOneNotInternalServerErrorSnapshot = ({
errors,
}: {
errors: BaseGraphQLError[];
}) => {
expect(errors.length).toBe(1);
const [firstError] = errors;
expect(firstError.extensions.code).not.toBe('INTERNAL_SERVER_ERROR');
expect(firstError).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny(firstError),
);
};
@@ -31,7 +31,24 @@ exports[`Object metadata update should fail when labelIdentifier is not a TEXT o
"routeTrigger": [],
"serverlessFunction": [],
"view": [],
"viewField": [],
"viewField": [
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "View field position cannot be lower than label identifier view field position",
"userFriendlyMessage": "View field position cannot be lower than label identifier view field position",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "create_view_field",
},
],
},
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
@@ -85,7 +102,29 @@ exports[`Object metadata update should fail when labelIdentifier is not a known
"routeTrigger": [],
"serverlessFunction": [],
"view": [],
"viewField": [],
"viewField": [
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Field metadata not found",
"userFriendlyMessage": "Field metadata not found",
},
{
"code": "INVALID_VIEW_DATA",
"message": "View field position cannot be lower than label identifier view field position",
"userFriendlyMessage": "View field position cannot be lower than label identifier view field position",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "create_view_field",
},
],
},
"message": "Validation failed for 0 object(s) and 0 field(s)",
"summary": {
@@ -90,6 +90,7 @@ describe('Custom object renaming', () => {
// Act
const { data } = await createOneObjectMetadata({
expectToFail: false,
input: CUSTOM_OBJECT,
gqlFields: `
id
@@ -141,6 +142,7 @@ describe('Custom object renaming', () => {
// Act
const { data } = await updateOneObjectMetadata({
expectToFail: false,
gqlFields: `
nameSingular
labelSingular
@@ -0,0 +1,98 @@
import { CUSTOM_OBJECT_DISHES } from 'test/integration/metadata/suites/object-metadata/constants/custom-object-dishes.constants';
import { type CreateOneObjectFactoryInput } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata-query-factory.util';
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import { findCoreViewFields } from 'test/integration/metadata/suites/view-field/utils/find-core-view-fields.util';
import { findCoreViews } from 'test/integration/metadata/suites/view/utils/find-core-views.util';
import { isDefined } from 'twenty-shared/utils';
import { type FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type';
describe('View side effect on object creation', () => {
let createdObjectMetadataId: string | undefined = undefined;
afterEach(async () => {
if (!isDefined(createdObjectMetadataId)) {
return;
}
await updateOneObjectMetadata({
expectToFail: false,
input: {
idToUpdate: createdObjectMetadataId,
updatePayload: {
isActive: false,
},
},
});
await deleteOneObjectMetadata({
input: { idToDelete: createdObjectMetadataId },
expectToFail: false,
});
createdObjectMetadataId = undefined;
});
it('should create a view and view field for default custom objects standard fields on object metadata creation', async () => {
const {
labelPlural,
description,
labelSingular,
namePlural,
nameSingular,
} = CUSTOM_OBJECT_DISHES;
const createObjectInput: CreateOneObjectFactoryInput = {
labelPlural,
description,
labelSingular,
namePlural,
nameSingular,
icon: 'IconBuildingSkyscraper',
isLabelSyncedWithName: false,
};
const objectCreationGqlFields = `
id
labelPlural
description
labelSingular
namePlural
nameSingular
icon
isLabelSyncedWithName
`;
const {
data: { createOneObject },
} = await createOneObjectMetadata({
expectToFail: false,
input: createObjectInput,
gqlFields: objectCreationGqlFields,
});
createdObjectMetadataId = createOneObject.id;
const {
data: { getCoreViews: createdViews },
} = await findCoreViews({
objectMetadataId: createdObjectMetadataId,
expectToFail: false,
});
expect(createdViews).toBeDefined();
expect(createdViews.length).toBe(1);
const [firstView] = createdViews;
expect(firstView).toMatchObject<Partial<FlatView>>({
objectMetadataId: createdObjectMetadataId,
});
const {
data: { getCoreViewFields: createdViewFields },
} = await findCoreViewFields({
viewId: firstView.id,
expectToFail: false,
});
expect(createdViewFields.length).toBe(11);
});
});
@@ -0,0 +1,21 @@
import gql from 'graphql-tag';
import { VIEW_FIELD_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants';
export const findCoreViewFieldsQueryFactory = ({
gqlFields = VIEW_FIELD_GQL_FIELDS,
viewId,
}: {
gqlFields?: string;
viewId: string;
}) => ({
query: gql`
query GetCoreViewFields($viewId: String!) {
getCoreViewFields(viewId: $viewId) {
${gqlFields}
}
}
`,
variables: {
viewId,
},
});
@@ -0,0 +1,42 @@
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { findCoreViewFieldsQueryFactory } from 'test/integration/metadata/suites/view-field/utils/find-core-view-fields-query-factory.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity';
export const findCoreViewFields = async ({
viewId,
gqlFields,
expectToFail,
}: {
viewId: string;
gqlFields?: string;
expectToFail?: boolean;
}): CommonResponseBody<{
getCoreViewFields: ViewFieldEntity[];
}> => {
const graphqlOperation = findCoreViewFieldsQueryFactory({
viewId,
gqlFields,
});
const response = await makeMetadataAPIRequest(graphqlOperation);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'View Field search should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'View Field search has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};
@@ -0,0 +1,19 @@
import gql from 'graphql-tag';
import { VIEW_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants';
export const findCoreViewsQueryFactory = ({
gqlFields = VIEW_GQL_FIELDS,
objectMetadataId,
}: {
gqlFields?: string;
objectMetadataId?: string;
}) => ({
query: gql`
query GetCoreViews($objectMetadataId: String) {
getCoreViews(objectMetadataId: $objectMetadataId) {
${gqlFields}
}
}
`,
variables: objectMetadataId ? { objectMetadataId } : {},
});
@@ -0,0 +1,42 @@
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { findCoreViewsQueryFactory } from 'test/integration/metadata/suites/view/utils/find-core-views-query-factory.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
export const findCoreViews = async ({
objectMetadataId,
gqlFields,
expectToFail,
}: {
objectMetadataId?: string;
gqlFields?: string;
expectToFail?: boolean;
}): CommonResponseBody<{
getCoreViews: ViewEntity[];
}> => {
const graphqlOperation = findCoreViewsQueryFactory({
objectMetadataId,
gqlFields,
});
const response = await makeMetadataAPIRequest(graphqlOperation);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'View search should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'View search has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};