[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:
+25
@@ -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);
|
||||
};
|
||||
+8
-13
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+17
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -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,
|
||||
};
|
||||
|
||||
+1
-1
@@ -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) ?? [],
|
||||
};
|
||||
};
|
||||
|
||||
-27
@@ -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;
|
||||
};
|
||||
+17
@@ -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;
|
||||
};
|
||||
+43
-21
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user