Deprecate object metadata maps in favor of flat entities (#16080)

## Context
Deprecating the old objectMetadataMap type in favour of split flat
entities to match with our new caching.
In the long run, trying to achieve:
- Better performance through caching
- Consistent data access patterns across the codebase
- Reduced database queries

Now that everything is based on flat entities, which are cached, we can
finish the refactoring of workspace context cache which should already
improve performances.
Then the last step will be to consume that new cache in the new global
datasource to get rid of the many workspace datasources stored in the
server
This commit is contained in:
Weiko
2025-11-27 13:43:34 +01:00
committed by GitHub
parent 978c0acb90
commit 1607aebcc6
281 changed files with 6510 additions and 6627 deletions
@@ -1,42 +0,0 @@
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
describe('getObjectMetadataMapItemByNameSingular', () => {
it('should return the correct metadata item when given a valid singular name', () => {
const mockMetadataItem = {
id: 'test-id',
nameSingular: 'company',
} as ObjectMetadataItemWithFieldMaps;
const mockObjectMetadataMaps: ObjectMetadataMaps = {
byId: {
'test-id': mockMetadataItem,
},
idByNameSingular: {
company: 'test-id',
},
};
const result = getObjectMetadataMapItemByNameSingular(
mockObjectMetadataMaps,
'company',
);
expect(result).toBe(mockMetadataItem);
});
it('should return undefined when the singular name does not exist', () => {
const mockObjectMetadataMaps: ObjectMetadataMaps = {
byId: {},
idByNameSingular: {},
};
const result = getObjectMetadataMapItemByNameSingular(
mockObjectMetadataMaps,
'nonexistent',
);
expect(result).toBeUndefined();
});
});
@@ -1,15 +1,113 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { type EachTestingContext } from 'twenty-shared/testing';
import {
FIELD_ACTOR_MOCK_NAME,
FIELD_ADDRESS_MOCK_NAME,
FIELD_CURRENCY_MOCK_NAME,
FIELD_FULL_NAME_MOCK_NAME,
FIELD_LINKS_MOCK_NAME,
objectMetadataMapItemMock,
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils';
const FIELD_LINKS_MOCK_NAME = 'fieldLinks';
const FIELD_CURRENCY_MOCK_NAME = 'fieldCurrency';
const FIELD_ADDRESS_MOCK_NAME = 'fieldAddress';
const FIELD_ACTOR_MOCK_NAME = 'fieldActor';
const FIELD_FULL_NAME_MOCK_NAME = 'fieldFullName';
const objectMetadataId = '20202020-0000-0000-0000-000000000001';
const workspaceId = '20202020-0000-0000-0000-000000000000';
const createFlatFieldMetadata = (
id: string,
name: string,
type: FieldMetadataType,
): FlatFieldMetadata => {
return getFlatFieldMetadataMock({
id,
name,
type,
objectMetadataId,
universalIdentifier: id,
workspaceId,
});
};
const fieldLinksMock = createFlatFieldMetadata(
'fieldLinksId',
FIELD_LINKS_MOCK_NAME,
FieldMetadataType.LINKS,
);
const fieldCurrencyMock = createFlatFieldMetadata(
'fieldCurrencyId',
FIELD_CURRENCY_MOCK_NAME,
FieldMetadataType.CURRENCY,
);
const fieldFullNameMock = createFlatFieldMetadata(
'fieldFullNameId',
FIELD_FULL_NAME_MOCK_NAME,
FieldMetadataType.FULL_NAME,
);
const fieldActorMock = createFlatFieldMetadata(
'fieldActorId',
FIELD_ACTOR_MOCK_NAME,
FieldMetadataType.ACTOR,
);
const fieldAddressMock = createFlatFieldMetadata(
'fieldAddressId',
FIELD_ADDRESS_MOCK_NAME,
FieldMetadataType.ADDRESS,
);
const FIELDS_MOCK = [
fieldLinksMock,
fieldCurrencyMock,
fieldFullNameMock,
fieldActorMock,
fieldAddressMock,
];
const flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata> =
FIELDS_MOCK.reduce(
(acc, field) => ({
...acc,
byId: {
...acc.byId,
[field.id]: field,
},
}),
createEmptyFlatEntityMaps() as FlatEntityMaps<FlatFieldMetadata>,
);
const flatObjectMetadata = getFlatObjectMetadataMock({
id: objectMetadataId,
workspaceId,
nameSingular: 'objectName',
namePlural: 'objectNames',
labelSingular: 'Object Name',
labelPlural: 'Object Names',
description: 'Object description',
icon: 'Icon123',
isCustom: false,
isRemote: false,
isActive: true,
isSystem: false,
isAuditLogged: true,
isSearchable: true,
fieldMetadataIds: FIELDS_MOCK.map((f) => f.id),
indexMetadataIds: [],
viewIds: [],
universalIdentifier: objectMetadataId,
applicationId: null,
labelIdentifierFieldMetadataId: null,
imageIdentifierFieldMetadataId: null,
shortcut: null,
isLabelSyncedWithName: true,
standardId: null,
standardOverrides: null,
targetTableName: 'DEPRECATED',
duplicateCriteria: null,
});
type ValidateFieldNameAvailabilityTestContext = EachTestingContext<{
input: string;
shouldNotThrow?: true;
@@ -64,14 +162,16 @@ describe('validateFieldNameAvailabilityOrThrow', () => {
expect(() =>
validateFieldNameAvailabilityOrThrow({
name: input,
fieldMetadataMapById: objectMetadataMapItemMock.fieldsById,
flatObjectMetadata,
flatFieldMetadataMaps,
}),
).not.toThrow();
} else {
expect(() =>
validateFieldNameAvailabilityOrThrow({
name: input,
fieldMetadataMapById: objectMetadataMapItemMock.fieldsById,
flatObjectMetadata,
flatFieldMetadataMaps,
}),
).toThrowErrorMatchingSnapshot();
}
@@ -1,60 +0,0 @@
import omit from 'lodash.omit';
import { FieldMetadataType } from 'twenty-shared/types';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
export const generateObjectMetadataMaps = (
objectMetadataCollection: ObjectMetadataEntity[],
): ObjectMetadataMaps => {
const objectMetadataMaps: ObjectMetadataMaps = {
byId: {},
idByNameSingular: {},
};
for (const objectMetadata of objectMetadataCollection) {
const fieldIdByJoinColumnNameMap: Record<string, string> = {};
for (const fieldMetadata of objectMetadata.fields) {
if (
isFieldMetadataEntityOfType(
fieldMetadata,
FieldMetadataType.RELATION,
) ||
isFieldMetadataEntityOfType(
fieldMetadata,
FieldMetadataType.MORPH_RELATION,
)
) {
if (fieldMetadata.settings?.joinColumnName) {
fieldIdByJoinColumnNameMap[fieldMetadata.settings.joinColumnName] =
fieldMetadata.id;
}
}
}
const fieldsByIdMap = objectMetadata.fields.reduce((acc, field) => {
acc[field.id] = field;
return acc;
}, {} as FieldMetadataMap);
const processedObjectMetadata: ObjectMetadataItemWithFieldMaps = {
...omit(objectMetadata, 'fields'),
fieldsById: fieldsByIdMap,
fieldIdByName: Object.fromEntries(
Object.entries(fieldsByIdMap).map(([id, field]) => [field.name, id]),
),
fieldIdByJoinColumnName: fieldIdByJoinColumnNameMap,
};
objectMetadataMaps.byId[objectMetadata.id] = processedObjectMetadata;
objectMetadataMaps.idByNameSingular[objectMetadata.nameSingular] =
objectMetadata.id;
}
return objectMetadataMaps;
};
@@ -1,17 +0,0 @@
import omit from 'lodash.omit';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
export const getObjectMetadataEntityFromObjectMetadataItemWithFieldsMap = (
objectMetadataItem: ObjectMetadataItemWithFieldMaps,
): ObjectMetadataEntity => {
return {
...omit(objectMetadataItem, [
'fieldsById',
'fieldIdByName',
'fieldIdByJoinColumnName',
]),
fields: Object.values(objectMetadataItem.fieldsById),
};
};
@@ -1,17 +0,0 @@
import omit from 'lodash.omit';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
export const getObjectMetadataFromObjectMetadataItemWithFieldMaps = (
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps,
): ObjectMetadataEntity => {
return {
...omit(objectMetadataMapItem, [
'fieldsById',
'fieldIdByName',
'fieldIdByJoinColumnName',
]),
fields: Object.values(objectMetadataMapItem.fieldsById),
};
};
@@ -1,17 +0,0 @@
import { isDefined } from 'twenty-shared/utils';
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
export const getObjectMetadataMapItemByNamePlural = (
objectMetadataMaps: ObjectMetadataMaps,
namePlural: string,
): ObjectMetadataItemWithFieldMaps | undefined => {
const objectMetadataItems = Object.values(objectMetadataMaps.byId).filter(
isDefined,
);
return objectMetadataItems.find(
(objectMetadata) => objectMetadata.namePlural === namePlural,
);
};
@@ -1,17 +0,0 @@
import { isNonEmptyString } from '@sniptt/guards';
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
export const getObjectMetadataMapItemByNameSingular = (
objectMetadataMaps: ObjectMetadataMaps,
nameSingular: string,
): ObjectMetadataItemWithFieldMaps | undefined => {
const objectMetadataId = objectMetadataMaps.idByNameSingular[nameSingular];
if (!isNonEmptyString(objectMetadataId)) {
return undefined;
}
return objectMetadataMaps.byId[objectMetadataId];
};
@@ -6,18 +6,27 @@ import {
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 FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-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 { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import {
InvalidMetadataException,
InvalidMetadataExceptionCode,
} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
const getReservedCompositeFieldNames = (
fieldMetadataMapById: FieldMetadataMap,
flatObjectMetadata: FlatObjectMetadata,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
) => {
const reservedCompositeFieldsNames: string[] = [];
for (const field of Object.values(fieldMetadataMapById)) {
for (const fieldId of flatObjectMetadata.fieldMetadataIds) {
const field = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatFieldMetadataMaps,
flatEntityId: fieldId,
});
if (isCompositeFieldMetadataType(field.type)) {
const base = field.name;
const compositeType = compositeTypeDefinitions.get(field.type);
@@ -35,30 +44,37 @@ const getReservedCompositeFieldNames = (
type ValidateFieldNameAvailabilityOrThrowArgs = {
name: string;
fieldMetadataMapById: FieldMetadataMap;
flatObjectMetadata: FlatObjectMetadata;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
};
export const validateFieldNameAvailabilityOrThrow = ({
name,
fieldMetadataMapById,
flatObjectMetadata,
flatFieldMetadataMaps,
}: ValidateFieldNameAvailabilityOrThrowArgs) => {
const reservedCompositeFieldsNames =
getReservedCompositeFieldNames(fieldMetadataMapById);
const reservedCompositeFieldsNames = getReservedCompositeFieldNames(
flatObjectMetadata,
flatFieldMetadataMaps,
);
if (
Object.values(fieldMetadataMapById).some(
(field) =>
field.name === name ||
(field.type === FieldMetadataType.RELATION &&
`${field.name}Id` === name),
)
) {
throw new InvalidMetadataException(
`Name "${name}" is not available as it is already used by another field`,
InvalidMetadataExceptionCode.NOT_AVAILABLE,
{
userFriendlyMessage: msg`This name is not available as it is already used by another field.`,
},
);
for (const fieldId of flatObjectMetadata.fieldMetadataIds) {
const field = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatFieldMetadataMaps,
flatEntityId: fieldId,
});
if (
field.name === name ||
(field.type === FieldMetadataType.RELATION && `${field.name}Id` === name)
) {
throw new InvalidMetadataException(
`Name "${name}" is not available as it is already used by another field`,
InvalidMetadataExceptionCode.NOT_AVAILABLE,
{
userFriendlyMessage: msg`This name is not available as it is already used by another field.`,
},
);
}
}
if (reservedCompositeFieldsNames.includes(name)) {
@@ -7,13 +7,12 @@ import {
ObjectMetadataException,
ObjectMetadataExceptionCode,
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
type ValidateNoOtherObjectWithSameNameExistsOrThrowsParams = {
objectMetadataNameSingular: string;
objectMetadataNamePlural: string;
existingObjectMetadataId?: string;
objectMetadataMaps: ObjectMetadataMaps | FlatEntityMaps<FlatObjectMetadata>;
objectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
};
export const doesOtherObjectWithSameNameExists = ({