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:
-42
@@ -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();
|
||||
});
|
||||
});
|
||||
+110
-10
@@ -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();
|
||||
}
|
||||
|
||||
-60
@@ -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;
|
||||
};
|
||||
-17
@@ -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),
|
||||
};
|
||||
};
|
||||
-17
@@ -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),
|
||||
};
|
||||
};
|
||||
-17
@@ -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,
|
||||
);
|
||||
};
|
||||
-17
@@ -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];
|
||||
};
|
||||
+38
-22
@@ -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)) {
|
||||
|
||||
+1
-2
@@ -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 = ({
|
||||
|
||||
Reference in New Issue
Block a user