Fix getApplicationSubAllFlatEntityMaps to prune unrelated appIds universal identifier aggregators (#21234)
# Introduction Atm when computing the `fromAllFlatEntityMaps` we're retrieving all the applicationIds related metadata entities to build a flat entity maps scoped to them ( atm always the applicationId + twenty-standard application id ) only inter app dependency we manage for the moment A flat entity contains universal identifier aggregator to its related entities The issue was that the `getApplicationSubAllFlatEntityMaps` wasn't pruning the aggregator by app Now added a new process phase after the initial one that will check that all the aggregators contains universal identifiers that has been retrieve from the appId + appId standard intersection ## TDD test Created a very human readable ( that's a joke ) test
This commit is contained in:
+182
@@ -0,0 +1,182 @@
|
||||
import { createEmptyAllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-all-flat-entity-maps.constant';
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { pruneDanglingForeignKeyAggregatorsInAllFlatEntityMapsThroughMutation } from 'src/engine/metadata-modules/flat-entity/utils/prune-dangling-foreign-key-aggregators-in-all-flat-entity-maps-through-mutation.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';
|
||||
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
|
||||
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
|
||||
|
||||
const buildFlatViewField = (universalIdentifier: string): FlatViewField =>
|
||||
({
|
||||
id: universalIdentifier,
|
||||
universalIdentifier,
|
||||
}) as unknown as FlatViewField;
|
||||
|
||||
const buildFlatView = ({
|
||||
universalIdentifier,
|
||||
viewFieldUniversalIdentifiers,
|
||||
}: {
|
||||
universalIdentifier: string;
|
||||
viewFieldUniversalIdentifiers: string[];
|
||||
}): FlatView =>
|
||||
({
|
||||
id: universalIdentifier,
|
||||
universalIdentifier,
|
||||
viewFieldUniversalIdentifiers,
|
||||
viewFilterUniversalIdentifiers: [],
|
||||
viewGroupUniversalIdentifiers: [],
|
||||
viewFilterGroupUniversalIdentifiers: [],
|
||||
viewFieldGroupUniversalIdentifiers: [],
|
||||
viewSortUniversalIdentifiers: [],
|
||||
}) as unknown as FlatView;
|
||||
|
||||
const buildAllFlatEntityMapsWithView = ({
|
||||
view,
|
||||
viewFields,
|
||||
}: {
|
||||
view: FlatView;
|
||||
viewFields: FlatViewField[];
|
||||
}): AllFlatEntityMaps => {
|
||||
const allFlatEntityMaps = createEmptyAllFlatEntityMaps();
|
||||
|
||||
allFlatEntityMaps.flatViewMaps.byUniversalIdentifier[
|
||||
view.universalIdentifier
|
||||
] = view;
|
||||
|
||||
for (const viewField of viewFields) {
|
||||
allFlatEntityMaps.flatViewFieldMaps.byUniversalIdentifier[
|
||||
viewField.universalIdentifier
|
||||
] = viewField;
|
||||
}
|
||||
|
||||
return allFlatEntityMaps;
|
||||
};
|
||||
|
||||
describe('pruneDanglingForeignKeyAggregatorsInAllFlatEntityMapsThroughMutation', () => {
|
||||
it('should drop references to children missing from the slice while keeping present ones', () => {
|
||||
const view = buildFlatView({
|
||||
universalIdentifier: 'view-body',
|
||||
viewFieldUniversalIdentifiers: ['vf-arm', 'vf-head', 'vf-leg'],
|
||||
});
|
||||
|
||||
// vf-leg is owned by another application and therefore not part of the slice
|
||||
const allFlatEntityMaps = buildAllFlatEntityMapsWithView({
|
||||
view,
|
||||
viewFields: [buildFlatViewField('vf-arm'), buildFlatViewField('vf-head')],
|
||||
});
|
||||
|
||||
pruneDanglingForeignKeyAggregatorsInAllFlatEntityMapsThroughMutation({
|
||||
allFlatEntityMapsToMutate: allFlatEntityMaps,
|
||||
});
|
||||
|
||||
expect(
|
||||
allFlatEntityMaps.flatViewMaps.byUniversalIdentifier['view-body']
|
||||
?.viewFieldUniversalIdentifiers,
|
||||
).toEqual(['vf-arm', 'vf-head']);
|
||||
});
|
||||
|
||||
it('should leave aggregators untouched when every reference is present in the slice', () => {
|
||||
const view = buildFlatView({
|
||||
universalIdentifier: 'view-body',
|
||||
viewFieldUniversalIdentifiers: ['vf-arm', 'vf-head'],
|
||||
});
|
||||
|
||||
const allFlatEntityMaps = buildAllFlatEntityMapsWithView({
|
||||
view,
|
||||
viewFields: [buildFlatViewField('vf-arm'), buildFlatViewField('vf-head')],
|
||||
});
|
||||
|
||||
pruneDanglingForeignKeyAggregatorsInAllFlatEntityMapsThroughMutation({
|
||||
allFlatEntityMapsToMutate: allFlatEntityMaps,
|
||||
});
|
||||
|
||||
const prunedView =
|
||||
allFlatEntityMaps.flatViewMaps.byUniversalIdentifier['view-body'];
|
||||
|
||||
expect(prunedView?.viewFieldUniversalIdentifiers).toEqual([
|
||||
'vf-arm',
|
||||
'vf-head',
|
||||
]);
|
||||
// No dangling reference means the entity reference is preserved as-is
|
||||
expect(prunedView).toBe(view);
|
||||
});
|
||||
|
||||
it('should drop all references when no children remain in the slice', () => {
|
||||
const view = buildFlatView({
|
||||
universalIdentifier: 'view-body',
|
||||
viewFieldUniversalIdentifiers: ['vf-arm', 'vf-head'],
|
||||
});
|
||||
|
||||
const allFlatEntityMaps = buildAllFlatEntityMapsWithView({
|
||||
view,
|
||||
viewFields: [],
|
||||
});
|
||||
|
||||
pruneDanglingForeignKeyAggregatorsInAllFlatEntityMapsThroughMutation({
|
||||
allFlatEntityMapsToMutate: allFlatEntityMaps,
|
||||
});
|
||||
|
||||
expect(
|
||||
allFlatEntityMaps.flatViewMaps.byUniversalIdentifier['view-body']
|
||||
?.viewFieldUniversalIdentifiers,
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('should prune dangling references generically across entity types (objectMetadata fields)', () => {
|
||||
const allFlatEntityMaps = createEmptyAllFlatEntityMaps();
|
||||
|
||||
allFlatEntityMaps.flatObjectMetadataMaps.byUniversalIdentifier['object-1'] =
|
||||
{
|
||||
id: 'object-1',
|
||||
universalIdentifier: 'object-1',
|
||||
fieldUniversalIdentifiers: ['field-present', 'field-missing'],
|
||||
viewUniversalIdentifiers: [],
|
||||
indexMetadataUniversalIdentifiers: [],
|
||||
objectPermissionUniversalIdentifiers: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
} as unknown as FlatObjectMetadata;
|
||||
|
||||
allFlatEntityMaps.flatFieldMetadataMaps.byUniversalIdentifier[
|
||||
'field-present'
|
||||
] = {
|
||||
id: 'field-present',
|
||||
universalIdentifier: 'field-present',
|
||||
} as unknown as FlatFieldMetadata;
|
||||
|
||||
pruneDanglingForeignKeyAggregatorsInAllFlatEntityMapsThroughMutation({
|
||||
allFlatEntityMapsToMutate: allFlatEntityMaps,
|
||||
});
|
||||
|
||||
expect(
|
||||
allFlatEntityMaps.flatObjectMetadataMaps.byUniversalIdentifier['object-1']
|
||||
?.fieldUniversalIdentifiers,
|
||||
).toEqual(['field-present']);
|
||||
});
|
||||
|
||||
it('should not mutate the shared parent entity object when pruning', () => {
|
||||
const view = buildFlatView({
|
||||
universalIdentifier: 'view-body',
|
||||
viewFieldUniversalIdentifiers: ['vf-arm', 'vf-leg'],
|
||||
});
|
||||
|
||||
const allFlatEntityMaps = buildAllFlatEntityMapsWithView({
|
||||
view,
|
||||
viewFields: [buildFlatViewField('vf-arm')],
|
||||
});
|
||||
|
||||
pruneDanglingForeignKeyAggregatorsInAllFlatEntityMapsThroughMutation({
|
||||
allFlatEntityMapsToMutate: allFlatEntityMaps,
|
||||
});
|
||||
|
||||
// The map entry is swapped for a new pruned object, leaving the original
|
||||
// (shared with other slices/cache) entity untouched
|
||||
expect(view.viewFieldUniversalIdentifiers).toEqual(['vf-arm', 'vf-leg']);
|
||||
expect(
|
||||
allFlatEntityMaps.flatViewMaps.byUniversalIdentifier['view-body'],
|
||||
).not.toBe(view);
|
||||
expect(
|
||||
allFlatEntityMaps.flatViewMaps.byUniversalIdentifier['view-body']
|
||||
?.viewFieldUniversalIdentifiers,
|
||||
).toEqual(['vf-arm']);
|
||||
});
|
||||
});
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import {
|
||||
ALL_METADATA_NAME,
|
||||
type AllMetadataName,
|
||||
} from 'twenty-shared/metadata';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ALL_ONE_TO_MANY_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-one-to-many-metadata-relations.constant';
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
|
||||
type LooseAllFlatEntityMaps = Record<
|
||||
string,
|
||||
FlatEntityMaps<SyncableFlatEntity>
|
||||
>;
|
||||
|
||||
type OneToManyRelation = {
|
||||
metadataName: AllMetadataName;
|
||||
universalFlatEntityForeignKeyAggregator: string;
|
||||
} | null;
|
||||
|
||||
type OneToManyAggregatorWithChildIdentifiers = {
|
||||
aggregatorProperty: string;
|
||||
childUniversalIdentifiers: Set<string>;
|
||||
};
|
||||
|
||||
const pruneFlatEntityForeignKeyAggregators = ({
|
||||
flatEntity,
|
||||
aggregatorsWithChildIdentifiers,
|
||||
}: {
|
||||
flatEntity: SyncableFlatEntity;
|
||||
aggregatorsWithChildIdentifiers: OneToManyAggregatorWithChildIdentifiers[];
|
||||
}): SyncableFlatEntity =>
|
||||
aggregatorsWithChildIdentifiers.reduce(
|
||||
(prunedFlatEntity, { aggregatorProperty, childUniversalIdentifiers }) => {
|
||||
const aggregatedUniversalIdentifiers = (
|
||||
prunedFlatEntity as Record<string, unknown>
|
||||
)[aggregatorProperty];
|
||||
|
||||
if (
|
||||
!isDefined(aggregatedUniversalIdentifiers) ||
|
||||
!Array.isArray(aggregatedUniversalIdentifiers)
|
||||
) {
|
||||
return prunedFlatEntity;
|
||||
}
|
||||
|
||||
const prunedUniversalIdentifiers = aggregatedUniversalIdentifiers.filter(
|
||||
(childUniversalIdentifier: string) =>
|
||||
childUniversalIdentifiers.has(childUniversalIdentifier),
|
||||
);
|
||||
|
||||
if (
|
||||
prunedUniversalIdentifiers.length ===
|
||||
aggregatedUniversalIdentifiers.length
|
||||
) {
|
||||
return prunedFlatEntity;
|
||||
}
|
||||
|
||||
return {
|
||||
...prunedFlatEntity,
|
||||
[aggregatorProperty]: prunedUniversalIdentifiers,
|
||||
};
|
||||
},
|
||||
flatEntity,
|
||||
);
|
||||
|
||||
// Drops one-to-many aggregator references to children absent from the slice
|
||||
// (e.g. an app-owned view referencing a view field owned by another
|
||||
// application), keeping the slice a closed, internally consistent graph.
|
||||
// Mutates the passed maps in place: the parent entry is replaced by a new
|
||||
// pruned entity object, never mutating the shared entity referenced elsewhere.
|
||||
export const pruneDanglingForeignKeyAggregatorsInAllFlatEntityMapsThroughMutation =
|
||||
({
|
||||
allFlatEntityMapsToMutate,
|
||||
}: {
|
||||
allFlatEntityMapsToMutate: AllFlatEntityMaps;
|
||||
}): void => {
|
||||
const looseAllFlatEntityMaps =
|
||||
allFlatEntityMapsToMutate as unknown as LooseAllFlatEntityMaps;
|
||||
|
||||
for (const metadataName of Object.values(ALL_METADATA_NAME)) {
|
||||
const oneToManyRelations = Object.values(
|
||||
ALL_ONE_TO_MANY_METADATA_RELATIONS[metadataName],
|
||||
) as OneToManyRelation[];
|
||||
|
||||
const aggregatorsWithChildIdentifiers: OneToManyAggregatorWithChildIdentifiers[] =
|
||||
oneToManyRelations.filter(isDefined).map((relation) => ({
|
||||
aggregatorProperty: relation.universalFlatEntityForeignKeyAggregator,
|
||||
childUniversalIdentifiers: new Set(
|
||||
Object.keys(
|
||||
looseAllFlatEntityMaps[
|
||||
getMetadataFlatEntityMapsKey(relation.metadataName)
|
||||
].byUniversalIdentifier,
|
||||
),
|
||||
),
|
||||
}));
|
||||
|
||||
if (aggregatorsWithChildIdentifiers.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const flatEntityByUniversalIdentifier =
|
||||
looseAllFlatEntityMaps[getMetadataFlatEntityMapsKey(metadataName)]
|
||||
.byUniversalIdentifier;
|
||||
|
||||
for (const [universalIdentifier, parentFlatEntity] of Object.entries(
|
||||
flatEntityByUniversalIdentifier,
|
||||
)) {
|
||||
if (!isDefined(parentFlatEntity)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
flatEntityByUniversalIdentifier[universalIdentifier] =
|
||||
pruneFlatEntityForeignKeyAggregators({
|
||||
flatEntity: parentFlatEntity,
|
||||
aggregatorsWithChildIdentifiers,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user