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:
Paul Rastoin
2026-06-04 18:53:25 +02:00
committed by GitHub
parent 4e1cc2d831
commit 0877ba2ffd
4 changed files with 537 additions and 0 deletions
@@ -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']);
});
});
@@ -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,
});
}
}
};