[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
@@ -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,