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,6 +1,4 @@
import { type FieldMetadataType } from 'twenty-shared/types';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
export type UniqueFieldCondition = [field: string, value: string];
@@ -11,7 +9,7 @@ export type RelationConnectQueryConfig = {
recordToConnectConditions: UniqueConstraintCondition[];
relationFieldName: string;
connectFieldName: string;
uniqueConstraintFields: FieldMetadataEntity<FieldMetadataType>[];
uniqueConstraintFields: FlatFieldMetadata[];
recordToConnectConditionByEntityIndex: {
[entityIndex: number]: UniqueConstraintCondition;
};
@@ -1,11 +1,16 @@
import { type ObjectsPermissions } from 'twenty-shared/types';
import {
type FieldMetadataType,
type ObjectsPermissions,
} from 'twenty-shared/types';
import { EntityManager } from 'typeorm';
import { EntityPersistExecutor } from 'typeorm/persistence/EntityPersistExecutor';
import { PlainObjectToDatabaseEntityTransformer } from 'typeorm/query-builder/transformer/PlainObjectToDatabaseEntityTransformer';
import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
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 { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
import { validateOperationIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.utils';
@@ -79,47 +84,105 @@ describe('WorkspaceEntityManager', () => {
};
beforeEach(() => {
const mockFlatObjectMetadata: FlatObjectMetadata = {
id: 'test-entity-id',
nameSingular: 'test-entity',
namePlural: 'test-entities',
labelSingular: 'Test Entity',
labelPlural: 'Test Entities',
workspaceId: 'test-workspace-id',
icon: 'test-icon',
isCustom: false,
isRemote: false,
isAuditLogged: false,
isSearchable: false,
isSystem: false,
isActive: true,
targetTableName: 'test_entity',
fieldMetadataIds: ['field-id'],
indexMetadataIds: [],
viewIds: [],
universalIdentifier: 'test-entity-id',
description: null,
imageIdentifierFieldMetadataId: null,
labelIdentifierFieldMetadataId: null,
shortcut: null,
standardId: null,
standardOverrides: null,
applicationId: null,
isLabelSyncedWithName: false,
isUIReadOnly: false,
duplicateCriteria: null,
createdAt: new Date(),
updatedAt: new Date(),
};
const mockFlatFieldMetadata: FlatFieldMetadata = {
id: 'field-id',
type: 'TEXT' as FieldMetadataType,
name: 'fieldName',
label: 'Field Name',
objectMetadataId: 'test-entity-id',
isNullable: true,
isLabelSyncedWithName: false,
createdAt: new Date(),
updatedAt: new Date(),
universalIdentifier: 'field-id',
defaultValue: null,
description: null,
icon: null,
isActive: true,
isCustom: false,
isSystem: false,
isUIReadOnly: false,
isUnique: false,
options: null,
settings: null,
standardId: null,
standardOverrides: null,
workspaceId: 'test-workspace-id',
viewFieldIds: [],
viewFilterIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
calendarViewIds: [],
relationTargetFieldMetadataId: null,
relationTargetObjectMetadataId: null,
morphId: null,
applicationId: null,
};
const flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata> = {
byId: {
'test-entity-id': mockFlatObjectMetadata,
},
idByUniversalIdentifier: {
'test-entity-id': 'test-entity-id',
},
universalIdentifiersByApplicationId: {},
};
const flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata> = {
byId: {
'field-id': mockFlatFieldMetadata,
},
idByUniversalIdentifier: {
'field-id': 'field-id',
},
universalIdentifiersByApplicationId: {},
};
mockInternalContext = {
workspaceId: 'test-workspace-id',
objectMetadataMaps: {
byId: {
'test-entity-id': {
id: 'test-entity-id',
nameSingular: 'test-entity',
namePlural: 'test-entities',
labelSingular: 'Test Entity',
labelPlural: 'Test Entities',
workspaceId: 'test-workspace-id',
icon: 'test-icon',
color: 'test-color',
isCustom: false,
isRemote: false,
isAuditLogged: false,
isSearchable: false,
isSystem: false,
isActive: true,
targetTableName: 'test_entity',
indexMetadatas: [],
fieldsById: {
'field-id': {
id: 'field-id',
type: 'TEXT',
name: 'fieldName',
label: 'Field Name',
objectMetadataId: 'test-entity-id',
isNullable: true,
isLabelSyncedWithName: false,
createdAt: new Date(),
updatedAt: new Date(),
},
},
fieldIdByName: { fieldName: 'field-id' },
fieldIdByJoinColumnName: {},
} as unknown as ObjectMetadataItemWithFieldMaps,
},
idByNameSingular: {
'test-entity': 'test-entity-id',
},
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatIndexMaps: {
byId: {},
idByUniversalIdentifier: {},
universalIdentifiersByApplicationId: {},
},
objectIdByNameSingular: {
'test-entity': 'test-entity-id',
},
featureFlagsMap: {
IS_AIRTABLE_INTEGRATION_ENABLED: false,
@@ -333,7 +396,9 @@ describe('WorkspaceEntityManager', () => {
expect(validateOperationIsPermittedOrThrow).toHaveBeenCalledWith({
entityName: 'test-entity',
operationType: 'update',
objectMetadataMaps: mockInternalContext.objectMetadataMaps,
flatObjectMetadataMaps: mockInternalContext.flatObjectMetadataMaps,
flatFieldMetadataMaps: mockInternalContext.flatFieldMetadataMaps,
objectIdByNameSingular: mockInternalContext.objectIdByNameSingular,
objectsPermissions: mockPermissionOptions.objectRecordsPermissions,
selectedColumns: [],
allFieldsSelected: false,
@@ -366,7 +431,9 @@ describe('WorkspaceEntityManager', () => {
expect(validateOperationIsPermittedOrThrow).toHaveBeenCalledWith({
entityName: 'test-entity',
operationType: 'delete',
objectMetadataMaps: mockInternalContext.objectMetadataMaps,
flatObjectMetadataMaps: mockInternalContext.flatObjectMetadataMaps,
flatFieldMetadataMaps: mockInternalContext.flatFieldMetadataMaps,
objectIdByNameSingular: mockInternalContext.objectIdByNameSingular,
objectsPermissions: mockPermissionOptions.objectRecordsPermissions,
selectedColumns: [],
allFieldsSelected: false,
@@ -39,11 +39,11 @@ import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import {
PermissionsException,
PermissionsExceptionCode,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { type WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
import { type DeepPartialWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/deep-partial-entity-with-nested-relation-fields.type';
import { type QueryDeepPartialEntityWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-nested-relation-fields.type';
@@ -442,7 +442,9 @@ export class WorkspaceEntityManager extends EntityManager {
entityName,
operationType,
objectsPermissions: permissionOptions?.objectRecordsPermissions ?? {},
objectMetadataMaps: this.internalContext.objectMetadataMaps,
flatObjectMetadataMaps: this.internalContext.flatObjectMetadataMaps,
flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps,
objectIdByNameSingular: this.internalContext.objectIdByNameSingular,
selectedColumns,
allFieldsSelected: false,
updatedColumns,
@@ -956,7 +958,11 @@ export class WorkspaceEntityManager extends EntityManager {
this.internalContext,
);
const formattedEntityLike = formatData(entityLike, objectMetadataItem);
const formattedEntityLike = formatData(
entityLike,
objectMetadataItem,
this.internalContext.flatFieldMetadataMaps,
);
const managerWithPermissionOptions = Object.assign(
Object.create(Object.getPrototypeOf(this)),
@@ -1190,6 +1196,7 @@ export class WorkspaceEntityManager extends EntityManager {
const formattedEntityOrEntities = formatData(
entityWithConnectedRelations,
objectMetadataItem,
this.internalContext.flatFieldMetadataMaps,
);
const updatedColumns = formattedEntityOrEntities
@@ -1221,7 +1228,8 @@ export class WorkspaceEntityManager extends EntityManager {
let formattedResult = formatResult<Entity[]>(
resultArray,
objectMetadataItem,
this.internalContext.objectMetadataMaps,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
);
const updatedEntities = formattedResult.filter(
@@ -1235,6 +1243,7 @@ export class WorkspaceEntityManager extends EntityManager {
formatTwentyOrmEventToDatabaseBatchEvent({
action: DatabaseEventAction.UPDATED,
objectMetadataItem,
flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps,
workspaceId: this.internalContext.workspaceId,
entities: updatedEntities,
beforeEntities: updatedEntities.map(
@@ -1247,6 +1256,7 @@ export class WorkspaceEntityManager extends EntityManager {
formatTwentyOrmEventToDatabaseBatchEvent({
action: DatabaseEventAction.CREATED,
objectMetadataItem,
flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps,
workspaceId: this.internalContext.workspaceId,
entities: createdEntities,
}),
@@ -1283,7 +1293,7 @@ export class WorkspaceEntityManager extends EntityManager {
permissionOptionsFromArgs,
}: {
formattedResult: Entity[];
objectMetadataItem: ObjectMetadataItemWithFieldMaps;
objectMetadataItem: FlatObjectMetadata;
permissionOptionsFromArgs: PermissionOptions | undefined;
}): Entity[] {
if (permissionOptionsFromArgs?.shouldBypassPermissionChecks === true) {
@@ -1303,15 +1313,12 @@ export class WorkspaceEntityManager extends EntityManager {
return formattedResult;
}
const objectMetadataItemWithFieldMaps =
this.internalContext.objectMetadataMaps.byId[objectMetadataItem.id];
const restrictedFieldNames = new Set(
Object.entries(restrictedFields)
.filter(([_, fieldPermissions]) => fieldPermissions.canRead === false)
.map(([fieldMetadataId]) => {
const fieldMetadata =
objectMetadataItemWithFieldMaps?.fieldsById[fieldMetadataId];
this.internalContext.flatFieldMetadataMaps.byId[fieldMetadataId];
if (!isDefined(fieldMetadata)) {
throw new InternalServerError(
@@ -1407,7 +1414,11 @@ export class WorkspaceEntityManager extends EntityManager {
this.internalContext,
);
const formattedEntity = formatData(entity, objectMetadataItem);
const formattedEntity = formatData(
entity,
objectMetadataItem,
this.internalContext.flatFieldMetadataMaps,
);
const result = new EntityPersistExecutor(
this.connection,
@@ -1424,13 +1435,15 @@ export class WorkspaceEntityManager extends EntityManager {
const formattedResult = formatResult<Entity[]>(
result,
objectMetadataItem,
this.internalContext.objectMetadataMaps,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
);
this.internalContext.eventEmitterService.emitDatabaseBatchEvent(
formatTwentyOrmEventToDatabaseBatchEvent({
action: DatabaseEventAction.DESTROYED,
objectMetadataItem,
flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps,
workspaceId: this.internalContext.workspaceId,
entities: formattedResult,
}),
@@ -1522,7 +1535,11 @@ export class WorkspaceEntityManager extends EntityManager {
this.internalContext,
);
const formattedEntity = formatData(entity, objectMetadataItem);
const formattedEntity = formatData(
entity,
objectMetadataItem,
this.internalContext.flatFieldMetadataMaps,
);
const result = new EntityPersistExecutor(
this.connection,
@@ -1539,13 +1556,15 @@ export class WorkspaceEntityManager extends EntityManager {
const formattedResult = formatResult<Entity[]>(
result,
objectMetadataItem,
this.internalContext.objectMetadataMaps,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
);
this.internalContext.eventEmitterService.emitDatabaseBatchEvent(
formatTwentyOrmEventToDatabaseBatchEvent({
action: DatabaseEventAction.DELETED,
objectMetadataItem,
flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps,
workspaceId: this.internalContext.workspaceId,
entities: formattedResult,
}),
@@ -1633,7 +1652,11 @@ export class WorkspaceEntityManager extends EntityManager {
this.internalContext,
);
const formattedEntity = formatData(entity, objectMetadataItem);
const formattedEntity = formatData(
entity,
objectMetadataItem,
this.internalContext.flatFieldMetadataMaps,
);
const result = new EntityPersistExecutor(
this.connection,
@@ -1650,13 +1673,15 @@ export class WorkspaceEntityManager extends EntityManager {
const formattedResult = formatResult<Entity[]>(
result,
objectMetadataItem,
this.internalContext.objectMetadataMaps,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
);
this.internalContext.eventEmitterService.emitDatabaseBatchEvent(
formatTwentyOrmEventToDatabaseBatchEvent({
action: DatabaseEventAction.RESTORED,
objectMetadataItem,
flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps,
workspaceId: this.internalContext.workspaceId,
entities: formattedResult,
}),
@@ -5,7 +5,7 @@ import { QueryFailedError } from 'typeorm';
import { POSTGRESQL_ERROR_CODES } from 'src/engine/api/graphql/workspace-query-runner/constants/postgres-error-codes.constants';
import { handleDuplicateKeyError } from 'src/engine/api/graphql/workspace-query-runner/utils/handle-duplicate-key-error.util';
import { PostgresException } from 'src/engine/api/graphql/workspace-query-runner/utils/postgres-exception';
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import {
TwentyORMException,
TwentyORMExceptionCode,
@@ -17,7 +17,7 @@ interface QueryFailedErrorWithCode extends QueryFailedError {
export const computeTwentyORMException = (
error: Error,
objectMetadata?: ObjectMetadataItemWithFieldMaps,
objectMetadata?: FlatObjectMetadata,
) => {
if (error instanceof QueryFailedError) {
if (error.message.includes('Query read timeout')) {
@@ -9,12 +9,14 @@ import { type ColumnType, type EntitySchemaColumnOptions } from 'typeorm';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
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 { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value';
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
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 { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
import {
TwentyORMException,
@@ -29,12 +31,14 @@ type EntitySchemaColumnMap = {
@Injectable()
export class EntitySchemaColumnFactory {
create(
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
flatObjectMetadata: FlatObjectMetadata,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
): EntitySchemaColumnMap {
let entitySchemaColumnMap: EntitySchemaColumnMap = {};
const fieldMetadataCollection = Object.values(
objectMetadataItemWithFieldMaps.fieldsById,
const fieldMetadataCollection = getFlatFieldsFromFlatObjectMetadata(
flatObjectMetadata,
flatFieldMetadataMaps,
);
for (const fieldMetadata of fieldMetadataCollection) {
@@ -117,7 +121,7 @@ export class EntitySchemaColumnFactory {
}
private createCompositeColumns(
fieldMetadata: FieldMetadataEntity,
fieldMetadata: FlatFieldMetadata,
): EntitySchemaColumnMap {
const entitySchemaColumnMap: EntitySchemaColumnMap = {};
const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
@@ -1,12 +1,13 @@
import { Injectable } from '@nestjs/common';
import { FieldMetadataType } from 'twenty-shared/types';
import { type EntitySchemaRelationOptions } from 'typeorm';
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 { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
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 { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { determineSchemaRelationDetails } from 'src/engine/twenty-orm/utils/determine-schema-relation-details.util';
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
type EntitySchemaRelationMap = {
[key: string]: EntitySchemaRelationOptions;
@@ -16,52 +17,47 @@ type EntitySchemaRelationMap = {
export class EntitySchemaRelationFactory {
constructor() {}
async create(
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
objectMetadataMaps: ObjectMetadataMaps,
): Promise<EntitySchemaRelationMap> {
create(
flatObjectMetadata: FlatObjectMetadata,
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
): EntitySchemaRelationMap {
const entitySchemaRelationMap: EntitySchemaRelationMap = {};
const fieldMetadataCollection = Object.values(
objectMetadataItemWithFieldMaps.fieldsById,
const flatFieldMetadatas = getFlatFieldsFromFlatObjectMetadata(
flatObjectMetadata,
flatFieldMetadataMaps,
);
for (const fieldMetadata of fieldMetadataCollection) {
const isRelation =
isFieldMetadataEntityOfType(
fieldMetadata,
FieldMetadataType.RELATION,
) ||
isFieldMetadataEntityOfType(
fieldMetadata,
FieldMetadataType.MORPH_RELATION,
);
if (!isRelation) {
for (const flatFieldMetadata of flatFieldMetadatas) {
if (!isMorphOrRelationFlatFieldMetadata(flatFieldMetadata)) {
continue;
}
if (!fieldMetadata.settings) {
if (!flatFieldMetadata.settings) {
throw new Error(
`Field metadata settings are missing for field ${fieldMetadata.name}`,
`Field metadata settings are missing for field ${flatFieldMetadata.name}`,
);
}
const schemaRelationDetails = await determineSchemaRelationDetails(
fieldMetadata,
objectMetadataMaps,
const schemaRelationDetails = determineSchemaRelationDetails(
flatFieldMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
);
const targetObjectMetadata =
objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId];
flatObjectMetadataMaps.byId[
flatFieldMetadata.relationTargetObjectMetadataId
];
if (!targetObjectMetadata) {
throw new Error(
`Target object metadata not found for field ${fieldMetadata.name}`,
`Target object metadata not found for field ${flatFieldMetadata.name}`,
);
}
entitySchemaRelationMap[fieldMetadata.name] = {
entitySchemaRelationMap[flatFieldMetadata.name] = {
type: schemaRelationDetails.relationType,
target: schemaRelationDetails.target,
inverseSide: schemaRelationDetails.inverseSide,
@@ -2,8 +2,9 @@ import { Injectable } from '@nestjs/common';
import { EntitySchema } from 'typeorm';
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 { 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 { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { EntitySchemaColumnFactory } from 'src/engine/twenty-orm/factories/entity-schema-column.factory';
import { EntitySchemaRelationFactory } from 'src/engine/twenty-orm/factories/entity-schema-relation.factory';
import { WorkspaceEntitiesStorage } from 'src/engine/twenty-orm/storage/workspace-entities.storage';
@@ -17,25 +18,30 @@ export class EntitySchemaFactory {
private readonly entitySchemaRelationFactory: EntitySchemaRelationFactory,
) {}
async create(
create(
workspaceId: string,
objectMetadata: ObjectMetadataItemWithFieldMaps,
objectMetadataMaps: ObjectMetadataMaps,
): Promise<EntitySchema> {
const columns = this.entitySchemaColumnFactory.create(objectMetadata);
flatObjectMetadata: FlatObjectMetadata,
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
): EntitySchema {
const columns = this.entitySchemaColumnFactory.create(
flatObjectMetadata,
flatFieldMetadataMaps,
);
const relations = await this.entitySchemaRelationFactory.create(
objectMetadata,
objectMetadataMaps,
const relations = this.entitySchemaRelationFactory.create(
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
);
const schemaName = getWorkspaceSchemaName(workspaceId);
const entitySchema = new EntitySchema({
name: objectMetadata.nameSingular,
name: flatObjectMetadata.nameSingular,
tableName: computeTableName(
objectMetadata.nameSingular,
objectMetadata.isCustom,
flatObjectMetadata.nameSingular,
flatObjectMetadata.isCustom,
),
columns,
relations,
@@ -44,7 +50,7 @@ export class EntitySchemaFactory {
WorkspaceEntitiesStorage.setEntitySchema(
workspaceId,
objectMetadata.nameSingular,
flatObjectMetadata.nameSingular,
entitySchema,
);
@@ -12,8 +12,6 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
import { buildObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-metadata-item-with-field-maps.util';
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service';
import { WorkspacePermissionsCacheStorageService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service';
import {
@@ -144,26 +142,8 @@ export class WorkspaceDatasourceFactory {
},
);
const { idByNameSingular } = buildObjectIdByNameMaps(
flatObjectMetadataMaps,
);
const cachedObjectMetadataMaps: ObjectMetadataMaps = {
byId: {},
idByNameSingular,
};
for (const [id, flatObj] of Object.entries(
flatObjectMetadataMaps.byId,
)) {
if (isDefined(flatObj)) {
cachedObjectMetadataMaps.byId[id] =
buildObjectMetadataItemWithFieldMaps(
flatObj,
flatFieldMetadataMaps,
flatIndexMaps,
);
}
}
const { idByNameSingular: objectIdByNameSingular } =
buildObjectIdByNameMaps(flatObjectMetadataMaps);
const metadataVersionForFinalUpToDateCheck =
await this.workspaceCacheStorageService.getMetadataVersion(
@@ -184,17 +164,16 @@ export class WorkspaceDatasourceFactory {
(option) => new EntitySchema(option),
);
} else {
const entitySchemas = await Promise.all(
Object.values(cachedObjectMetadataMaps.byId)
.filter(isDefined)
.map((objectMetadata) =>
this.entitySchemaFactory.create(
workspaceId,
objectMetadata,
cachedObjectMetadataMaps,
),
const entitySchemas = Object.values(flatObjectMetadataMaps.byId)
.filter(isDefined)
.map((flatObjectMetadata) =>
this.entitySchemaFactory.create(
workspaceId,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
),
);
);
await this.workspaceCacheStorageService.setORMEntitySchema(
workspaceId,
@@ -208,7 +187,10 @@ export class WorkspaceDatasourceFactory {
const workspaceDataSource = new WorkspaceDataSource(
{
workspaceId,
objectMetadataMaps: cachedObjectMetadataMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatIndexMaps,
objectIdByNameSingular,
featureFlagsMap: cachedFeatureFlagMap,
eventEmitterService: this.workspaceEventEmitter,
},
@@ -112,7 +112,10 @@ export class GlobalWorkspaceDataSource extends DataSource {
return new WorkspaceEntityManager(
{
workspaceId: context.authContext.workspace.id,
objectMetadataMaps: context.objectMetadataMaps,
flatObjectMetadataMaps: context.flatObjectMetadataMaps,
flatFieldMetadataMaps: context.flatFieldMetadataMaps,
flatIndexMaps: context.flatIndexMaps,
objectIdByNameSingular: context.objectIdByNameSingular,
featureFlagsMap: context.featureFlagsMap,
eventEmitterService: this.eventEmitterService,
},
@@ -6,9 +6,10 @@ import { EntitySchema, ObjectLiteral } from 'typeorm';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
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 { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
import { buildObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-metadata-item-with-field-maps.util';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service';
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory';
@@ -99,23 +100,8 @@ export class GlobalWorkspaceOrmManager {
},
);
const { idByNameSingular } = buildObjectIdByNameMaps(
flatObjectMetadataMaps,
);
const objectMetadataMaps: ObjectMetadataMaps = {
byId: {},
idByNameSingular,
};
for (const [id, flatObj] of Object.entries(flatObjectMetadataMaps.byId)) {
if (isDefined(flatObj)) {
objectMetadataMaps.byId[id] = buildObjectMetadataItemWithFieldMaps(
flatObj,
flatFieldMetadataMaps,
flatIndexMaps,
);
}
}
const { idByNameSingular: objectIdByNameSingular } =
buildObjectIdByNameMaps(flatObjectMetadataMaps);
const metadataVersion =
await this.workspaceCacheStorageService.getMetadataVersion(workspaceId);
@@ -139,12 +125,16 @@ export class GlobalWorkspaceOrmManager {
const entitySchemas = await this.buildEntitySchemas(
workspaceId,
metadataVersion,
objectMetadataMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
);
return {
authContext,
objectMetadataMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatIndexMaps,
objectIdByNameSingular,
metadataVersion,
featureFlagsMap,
permissionsPerRoleId,
@@ -155,7 +145,8 @@ export class GlobalWorkspaceOrmManager {
private async buildEntitySchemas(
workspaceId: string,
dataSourceMetadataVersion: number,
objectMetadataMaps: ObjectMetadataMaps,
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
) {
const cachedEntitySchemaOptions =
await this.workspaceCacheStorageService.getORMEntitySchema(
@@ -170,17 +161,16 @@ export class GlobalWorkspaceOrmManager {
(option) => new EntitySchema(option),
);
} else {
const entitySchemas = await Promise.all(
Object.values(objectMetadataMaps.byId)
.filter(isDefined)
.map((objectMetadata) =>
this.entitySchemaFactory.create(
workspaceId,
objectMetadata,
objectMetadataMaps,
),
const entitySchemas = Object.values(flatObjectMetadataMaps.byId)
.filter(isDefined)
.map((flatObjectMetadata) =>
this.entitySchemaFactory.create(
workspaceId,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
),
);
);
await this.workspaceCacheStorageService.setORMEntitySchema(
workspaceId,
@@ -1,10 +1,16 @@
import { type FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
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 { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
export interface WorkspaceInternalContext {
workspaceId: string;
objectMetadataMaps: ObjectMetadataMaps;
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
flatIndexMaps: FlatEntityMaps<FlatIndexMetadata>;
objectIdByNameSingular: Record<string, string>;
featureFlagsMap: Record<FeatureFlagKey, boolean>;
eventEmitterService: WorkspaceEventEmitter;
}
@@ -72,12 +72,12 @@ export class RelationNestedQueries {
this.internalContext,
);
const objectMetadataMap = this.internalContext.objectMetadataMaps;
const relationConnectQueryConfigs = computeRelationConnectQueryConfigs(
entities,
objectMetadata,
objectMetadataMap,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
this.internalContext.flatIndexMaps,
relationConnectQueryFieldsByEntityIndex,
);
@@ -1,4 +1,7 @@
import { type ObjectsPermissions } from 'twenty-shared/types';
import {
FieldMetadataType,
type ObjectsPermissions,
} from 'twenty-shared/types';
import {
type DeepPartial,
type FindManyOptions,
@@ -12,6 +15,7 @@ import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interf
import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
@@ -57,13 +61,64 @@ describe('WorkspaceRepository', () => {
clear: jest.fn(),
} as unknown as jest.Mocked<WorkspaceEntityManager>;
const mockFieldMetadata: FlatFieldMetadata = {
id: 'test-field-id',
name: 'id',
type: FieldMetadataType.UUID,
objectMetadataId: 'test-metadata-id',
isActive: true,
isNullable: false,
isUnique: true,
isSystem: true,
isCustom: false,
isUIReadOnly: false,
isLabelSyncedWithName: false,
label: 'ID',
description: 'Record ID',
icon: 'IconKey',
workspaceId: 'test-workspace-id',
universalIdentifier: 'id-field-universal-id',
createdAt: new Date(),
updatedAt: new Date(),
defaultValue: null,
options: null,
settings: null,
morphId: null,
standardId: null,
standardOverrides: null,
applicationId: null,
relationTargetFieldMetadataId: null,
relationTargetObjectMetadataId: null,
calendarViewIds: [],
viewFilterIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
viewFieldIds: [],
};
mockInternalContext = {
workspaceId: 'test-workspace-id',
objectMetadataMaps: {
idByNameSingular: {},
flatObjectMetadataMaps: {
byId: {},
idByUniversalIdentifier: {},
universalIdentifiersByApplicationId: {},
},
flatFieldMetadataMaps: {
byId: {
'test-field-id': mockFieldMetadata,
},
idByUniversalIdentifier: {},
universalIdentifiersByApplicationId: {},
},
flatIndexMaps: {
byId: {},
idByUniversalIdentifier: {},
universalIdentifiersByApplicationId: {},
},
objectIdByNameSingular: {},
featureFlagsMap: {},
} as WorkspaceInternalContext;
eventEmitterService: {} as unknown,
} as unknown as WorkspaceInternalContext;
mockFeatureFlagMap = Object.values(FeatureFlagKey).reduce(
(acc, key) => ({ ...acc, [key]: false }),
@@ -97,6 +152,7 @@ describe('WorkspaceRepository', () => {
id: 'test-metadata-id',
nameSingular: 'test-entity',
namePlural: 'test-entities',
fieldMetadataIds: ['test-field-id'],
fieldIdByName: {
id: 'test-field-id',
},
@@ -9,12 +9,14 @@ import { type QueryExpressionMap } from 'typeorm/query-builder/QueryExpressionMa
import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
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 { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import {
PermissionsException,
PermissionsExceptionCode,
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { getColumnNameToFieldMetadataIdMap } from 'src/engine/twenty-orm/utils/get-column-name-to-field-metadata-id.util';
const getTargetEntityAndOperationType = (
@@ -66,7 +68,9 @@ type ValidateOperationIsPermittedOrThrowArgs = {
entityName: string;
operationType: OperationType;
objectsPermissions: ObjectsPermissions;
objectMetadataMaps: ObjectMetadataMaps;
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
objectIdByNameSingular: Record<string, string>;
selectedColumns: string[] | '*';
allFieldsSelected: boolean;
updatedColumns: string[];
@@ -76,13 +80,14 @@ export const validateOperationIsPermittedOrThrow = ({
entityName,
operationType,
objectsPermissions,
objectMetadataMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
selectedColumns,
allFieldsSelected,
updatedColumns,
}: ValidateOperationIsPermittedOrThrowArgs) => {
const objectMetadataIdForEntity =
objectMetadataMaps.idByNameSingular[entityName];
const objectMetadataIdForEntity = objectIdByNameSingular[entityName];
if (!isNonEmptyString(objectMetadataIdForEntity)) {
throw new PermissionsException(
@@ -91,7 +96,7 @@ export const validateOperationIsPermittedOrThrow = ({
);
}
const objectMetadata = objectMetadataMaps.byId[objectMetadataIdForEntity];
const objectMetadata = flatObjectMetadataMaps.byId[objectMetadataIdForEntity];
if (!isDefined(objectMetadata)) {
throw new PermissionsException(
@@ -106,8 +111,10 @@ export const validateOperationIsPermittedOrThrow = ({
return;
}
const columnNameToFieldMetadataIdMap =
getColumnNameToFieldMetadataIdMap(objectMetadata);
const columnNameToFieldMetadataIdMap = getColumnNameToFieldMetadataIdMap(
objectMetadata,
flatFieldMetadataMaps,
);
const permissionsForEntity = objectsPermissions[objectMetadataIdForEntity];
@@ -194,14 +201,18 @@ export const validateOperationIsPermittedOrThrow = ({
type ValidateQueryIsPermittedOrThrowArgs = {
expressionMap: QueryExpressionMap;
objectsPermissions: ObjectsPermissions;
objectMetadataMaps: ObjectMetadataMaps;
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
objectIdByNameSingular: Record<string, string>;
shouldBypassPermissionChecks: boolean;
};
export const validateQueryIsPermittedOrThrow = ({
expressionMap,
objectsPermissions,
objectMetadataMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
shouldBypassPermissionChecks,
}: ValidateQueryIsPermittedOrThrowArgs) => {
if (shouldBypassPermissionChecks) {
@@ -222,7 +233,9 @@ export const validateQueryIsPermittedOrThrow = ({
validatePermissionsForJoinsAndReturnSelectsWithoutJoins({
expressionMap,
objectsPermissions,
objectMetadataMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
});
expressionMapSelectsOnMainEntity = selectsWithoutJoinedAliases;
@@ -266,7 +279,9 @@ export const validateQueryIsPermittedOrThrow = ({
entityName: mainEntity,
operationType: operationType as OperationType,
objectsPermissions,
objectMetadataMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
selectedColumns,
allFieldsSelected,
updatedColumns,
@@ -276,11 +291,15 @@ export const validateQueryIsPermittedOrThrow = ({
const validatePermissionsForJoinsAndReturnSelectsWithoutJoins = ({
expressionMap,
objectsPermissions,
objectMetadataMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
}: {
expressionMap: QueryExpressionMap;
objectsPermissions: ObjectsPermissions;
objectMetadataMaps: ObjectMetadataMaps;
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
objectIdByNameSingular: Record<string, string>;
}) => {
const joinAttributesAliases = new Set(
expressionMap.joinAttributes.map((join) => join.alias.name),
@@ -313,7 +332,9 @@ const validatePermissionsForJoinsAndReturnSelectsWithoutJoins = ({
entityName: entity.name,
operationType: 'select' as OperationType,
objectsPermissions,
objectMetadataMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
selectedColumns,
allFieldsSelected: false,
updatedColumns: [],
@@ -72,7 +72,9 @@ export class WorkspaceDeleteQueryBuilder<
validateQueryIsPermittedOrThrow({
expressionMap: this.expressionMap,
objectsPermissions: this.objectRecordsPermissions,
objectMetadataMaps: this.internalContext.objectMetadataMaps,
flatObjectMetadataMaps: this.internalContext.flatObjectMetadataMaps,
flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps,
objectIdByNameSingular: this.internalContext.objectIdByNameSingular,
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
});
@@ -110,19 +112,22 @@ export class WorkspaceDeleteQueryBuilder<
const formattedResult = formatResult<T[]>(
result.raw,
objectMetadata,
this.internalContext.objectMetadataMaps,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
);
const formattedBefore = formatResult<T[]>(
before,
objectMetadata,
this.internalContext.objectMetadataMaps,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
);
this.internalContext.eventEmitterService.emitDatabaseBatchEvent(
formatTwentyOrmEventToDatabaseBatchEvent({
action: DatabaseEventAction.DESTROYED,
objectMetadataItem: objectMetadata,
flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps,
workspaceId: this.internalContext.workspaceId,
entities: formattedBefore,
authContext: this.authContext,
@@ -95,7 +95,11 @@ export class WorkspaceInsertQueryBuilder<
this.internalContext,
);
const formattedValues = formatData(values, objectMetadata);
const formattedValues = formatData(
values,
objectMetadata,
this.internalContext.flatFieldMetadataMaps,
);
return super.values(formattedValues);
}
@@ -105,7 +109,9 @@ export class WorkspaceInsertQueryBuilder<
validateQueryIsPermittedOrThrow({
expressionMap: this.expressionMap,
objectsPermissions: this.objectRecordsPermissions,
objectMetadataMaps: this.internalContext.objectMetadataMaps,
flatObjectMetadataMaps: this.internalContext.flatObjectMetadataMaps,
flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps,
objectIdByNameSingular: this.internalContext.objectIdByNameSingular,
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
});
@@ -159,13 +165,15 @@ export class WorkspaceInsertQueryBuilder<
const formattedResultForEvent = formatResult<T[]>(
afterResult,
objectMetadata,
this.internalContext.objectMetadataMaps,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
);
this.internalContext.eventEmitterService.emitDatabaseBatchEvent(
formatTwentyOrmEventToDatabaseBatchEvent({
action: DatabaseEventAction.CREATED,
objectMetadataItem: objectMetadata,
flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps,
workspaceId: this.internalContext.workspaceId,
entities: formattedResultForEvent,
authContext: this.authContext,
@@ -176,6 +184,7 @@ export class WorkspaceInsertQueryBuilder<
formatTwentyOrmEventToDatabaseBatchEvent({
action: DatabaseEventAction.UPSERTED,
objectMetadataItem: objectMetadata,
flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps,
workspaceId: this.internalContext.workspaceId,
entities: formattedResultForEvent,
authContext: this.authContext,
@@ -202,7 +211,8 @@ export class WorkspaceInsertQueryBuilder<
const formattedResult = formatResult<T[]>(
resultWithoutInsertionExtraColumns,
objectMetadata,
this.internalContext.objectMetadataMaps,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
);
return {
@@ -85,7 +85,8 @@ export class WorkspaceSelectQueryBuilder<
const formattedResult = formatResult<T[]>(
result,
objectMetadata,
this.internalContext.objectMetadataMaps,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
);
return {
@@ -114,7 +115,8 @@ export class WorkspaceSelectQueryBuilder<
const formattedResult = formatResult<T[]>(
result,
objectMetadata,
this.internalContext.objectMetadataMaps,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
);
return formattedResult;
@@ -163,7 +165,8 @@ export class WorkspaceSelectQueryBuilder<
const formattedResult = formatResult<T>(
result,
objectMetadata,
this.internalContext.objectMetadataMaps,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
);
return formattedResult;
@@ -188,7 +191,8 @@ export class WorkspaceSelectQueryBuilder<
const formattedResult = formatResult<T>(
result,
objectMetadata,
this.internalContext.objectMetadataMaps,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
);
return formattedResult[0];
@@ -230,7 +234,8 @@ export class WorkspaceSelectQueryBuilder<
const formattedResult = formatResult<T[]>(
result,
objectMetadata,
this.internalContext.objectMetadataMaps,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
);
return [formattedResult, count];
@@ -325,7 +330,9 @@ export class WorkspaceSelectQueryBuilder<
validateQueryIsPermittedOrThrow({
expressionMap: this.expressionMap,
objectsPermissions: this.objectRecordsPermissions,
objectMetadataMaps: this.internalContext.objectMetadataMaps,
flatObjectMetadataMaps: this.internalContext.flatObjectMetadataMaps,
flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps,
objectIdByNameSingular: this.internalContext.objectIdByNameSingular,
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
});
}
@@ -72,7 +72,9 @@ export class WorkspaceSoftDeleteQueryBuilder<
validateQueryIsPermittedOrThrow({
expressionMap: this.expressionMap,
objectsPermissions: this.objectRecordsPermissions,
objectMetadataMaps: this.internalContext.objectMetadataMaps,
flatObjectMetadataMaps: this.internalContext.flatObjectMetadataMaps,
flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps,
objectIdByNameSingular: this.internalContext.objectIdByNameSingular,
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
});
@@ -110,19 +112,22 @@ export class WorkspaceSoftDeleteQueryBuilder<
const formattedAfter = formatResult<T[]>(
after.raw,
objectMetadata,
this.internalContext.objectMetadataMaps,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
);
const formattedBefore = formatResult<T[]>(
before,
objectMetadata,
this.internalContext.objectMetadataMaps,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
);
this.internalContext.eventEmitterService.emitDatabaseBatchEvent(
formatTwentyOrmEventToDatabaseBatchEvent({
action: DatabaseEventAction.DELETED,
objectMetadataItem: objectMetadata,
flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps,
workspaceId: this.internalContext.workspaceId,
entities: formattedBefore,
authContext: this.authContext,
@@ -95,7 +95,9 @@ export class WorkspaceUpdateQueryBuilder<
validateQueryIsPermittedOrThrow({
expressionMap: this.expressionMap,
objectsPermissions: this.objectRecordsPermissions,
objectMetadataMaps: this.internalContext.objectMetadataMaps,
flatObjectMetadataMaps: this.internalContext.flatObjectMetadataMaps,
flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps,
objectIdByNameSingular: this.internalContext.objectIdByNameSingular,
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
});
@@ -164,7 +166,8 @@ export class WorkspaceUpdateQueryBuilder<
const formattedBefore = formatResult<T[]>(
before,
objectMetadata,
this.internalContext.objectMetadataMaps,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
);
const result = await super.execute();
@@ -174,13 +177,15 @@ export class WorkspaceUpdateQueryBuilder<
const formattedAfter = formatResult<T[]>(
after,
objectMetadata,
this.internalContext.objectMetadataMaps,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
);
this.internalContext.eventEmitterService.emitDatabaseBatchEvent(
formatTwentyOrmEventToDatabaseBatchEvent({
action: DatabaseEventAction.UPDATED,
objectMetadataItem: objectMetadata,
flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps,
workspaceId: this.internalContext.workspaceId,
entities: formattedAfter,
beforeEntities: formattedBefore,
@@ -192,6 +197,7 @@ export class WorkspaceUpdateQueryBuilder<
formatTwentyOrmEventToDatabaseBatchEvent({
action: DatabaseEventAction.UPSERTED,
objectMetadataItem: objectMetadata,
flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps,
workspaceId: this.internalContext.workspaceId,
entities: formattedAfter,
beforeEntities: formattedBefore,
@@ -202,7 +208,8 @@ export class WorkspaceUpdateQueryBuilder<
const formattedResult = formatResult<T[]>(
result.raw,
objectMetadata,
this.internalContext.objectMetadataMaps,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
);
return {
@@ -235,7 +242,9 @@ export class WorkspaceUpdateQueryBuilder<
validateQueryIsPermittedOrThrow({
expressionMap: fakeExpressionMapToValidatePermissions,
objectsPermissions: this.objectRecordsPermissions,
objectMetadataMaps: this.internalContext.objectMetadataMaps,
flatObjectMetadataMaps: this.internalContext.flatObjectMetadataMaps,
flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps,
objectIdByNameSingular: this.internalContext.objectIdByNameSingular,
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
});
}
@@ -265,7 +274,8 @@ export class WorkspaceUpdateQueryBuilder<
const formattedBefore = formatResult<T[]>(
beforeRecords,
objectMetadata,
this.internalContext.objectMetadataMaps,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
);
const results: UpdateResult[] = [];
@@ -317,13 +327,15 @@ export class WorkspaceUpdateQueryBuilder<
const formattedAfter = formatResult<T[]>(
afterRecords,
objectMetadata,
this.internalContext.objectMetadataMaps,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
);
this.internalContext.eventEmitterService.emitDatabaseBatchEvent(
formatTwentyOrmEventToDatabaseBatchEvent({
action: DatabaseEventAction.UPDATED,
objectMetadataItem: objectMetadata,
flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps,
workspaceId: this.internalContext.workspaceId,
entities: formattedAfter,
beforeEntities: formattedBefore,
@@ -335,6 +347,7 @@ export class WorkspaceUpdateQueryBuilder<
formatTwentyOrmEventToDatabaseBatchEvent({
action: DatabaseEventAction.UPSERTED,
objectMetadataItem: objectMetadata,
flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps,
workspaceId: this.internalContext.workspaceId,
entities: formattedAfter,
beforeEntities: formattedBefore,
@@ -345,7 +358,8 @@ export class WorkspaceUpdateQueryBuilder<
const formattedResults = formatResult<T[]>(
results.flatMap((result) => result.raw),
objectMetadata,
this.internalContext.objectMetadataMaps,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
);
return {
@@ -395,7 +409,11 @@ export class WorkspaceUpdateQueryBuilder<
mainAliasTarget,
);
const formattedUpdateSet = formatData(_values, objectMetadata);
const formattedUpdateSet = formatData(
_values,
objectMetadata,
this.internalContext.flatFieldMetadataMaps,
);
return super.set(formattedUpdateSet as QueryDeepPartialEntity<T>);
}
@@ -456,7 +474,11 @@ export class WorkspaceUpdateQueryBuilder<
this.manyInputs = inputs.map((input) => ({
criteria: input.criteria,
partialEntity: formatData(input.partialEntity, objectMetadata),
partialEntity: formatData(
input.partialEntity,
objectMetadata,
this.internalContext.flatFieldMetadataMaps,
),
}));
return this;
@@ -37,13 +37,17 @@ import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/g
export class WorkspaceRepository<
T extends ObjectLiteral,
> extends Repository<T> {
private readonly internalContext: WorkspaceInternalContext;
private readonly _internalContext: WorkspaceInternalContext;
private shouldBypassPermissionChecks: boolean;
private featureFlagMap: FeatureFlagMap;
public readonly objectRecordsPermissions?: ObjectsPermissions;
private authContext?: AuthContext;
declare manager: WorkspaceEntityManager;
get internalContext(): WorkspaceInternalContext {
return this._internalContext;
}
constructor(
internalContext: WorkspaceInternalContext,
target: EntityTarget<T>,
@@ -55,7 +59,7 @@ export class WorkspaceRepository<
authContext?: AuthContext,
) {
super(target, manager, queryRunner);
this.internalContext = internalContext;
this._internalContext = internalContext;
this.featureFlagMap = featureFlagMap;
this.objectRecordsPermissions = objectRecordsPermissions;
this.shouldBypassPermissionChecks = shouldBypassPermissionChecks;
@@ -79,7 +83,7 @@ export class WorkspaceRepository<
return new WorkspaceSelectQueryBuilder(
queryBuilder,
this.objectRecordsPermissions,
this.internalContext,
this._internalContext,
this.shouldBypassPermissionChecks,
this.authContext ?? {},
this.featureFlagMap,
@@ -944,7 +948,10 @@ export class WorkspaceRepository<
* PRIVATE METHODS
*/
private async getObjectMetadataFromTarget() {
return getObjectMetadataFromEntityTarget(this.target, this.internalContext);
return getObjectMetadataFromEntityTarget(
this.target,
this._internalContext,
);
}
private async transformOptions<
@@ -968,6 +975,10 @@ export class WorkspaceRepository<
private async formatData<T>(data: T): Promise<T> {
const objectMetadata = await this.getObjectMetadataFromTarget();
return formatData(data, objectMetadata) as T;
return formatData(
data,
objectMetadata,
this._internalContext.flatFieldMetadataMaps,
) as T;
}
}
@@ -6,11 +6,17 @@ import { type EntitySchema } from 'typeorm';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
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 { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
export type WorkspaceContext = {
authContext: WorkspaceAuthContext;
objectMetadataMaps: ObjectMetadataMaps;
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
flatIndexMaps: FlatEntityMaps<FlatIndexMetadata>;
objectIdByNameSingular: Record<string, string>;
metadataVersion: number;
featureFlagsMap: Record<FeatureFlagKey, boolean>;
permissionsPerRoleId: ObjectsPermissionsByRoleId;
@@ -2,147 +2,359 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
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 { 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 { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { computeRelationConnectQueryConfigs } from 'src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util';
describe('computeRelationConnectQueryConfigs', () => {
const personMetadata = {
const personFields: FlatFieldMetadata[] = [
{
id: 'person-id-field-id',
name: 'id',
type: FieldMetadataType.UUID,
label: 'id',
objectMetadataId: 'person-object-metadata-id',
isNullable: false,
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
universalIdentifier: 'person-id-field-id',
viewFieldIds: [],
viewFilterIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
calendarViewIds: [],
applicationId: null,
} as unknown as FlatFieldMetadata,
{
id: 'person-name-field-id',
name: 'name',
type: FieldMetadataType.FULL_NAME,
label: 'name',
objectMetadataId: 'person-object-metadata-id',
isNullable: true,
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
universalIdentifier: 'person-name-field-id',
viewFieldIds: [],
viewFilterIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
calendarViewIds: [],
applicationId: null,
} as unknown as FlatFieldMetadata,
{
id: 'person-company-1-field-id',
name: 'company-related-to-1',
type: FieldMetadataType.RELATION,
label: 'company-related-to-1',
objectMetadataId: 'person-object-metadata-id',
isNullable: true,
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
universalIdentifier: 'person-company-1-field-id',
viewFieldIds: [],
viewFilterIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
calendarViewIds: [],
applicationId: null,
relationTargetObjectMetadataId: 'company-object-metadata-id',
relationTargetFieldMetadataId: 'company-id-field-id',
settings: {
relationType: RelationType.MANY_TO_ONE,
joinColumnName: 'company-related-to-1Id',
},
} as unknown as FlatFieldMetadata,
{
id: 'person-company-2-field-id',
name: 'company-related-to-2',
type: FieldMetadataType.RELATION,
label: 'company-related-to-2',
objectMetadataId: 'person-object-metadata-id',
isNullable: true,
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
universalIdentifier: 'person-company-2-field-id',
viewFieldIds: [],
viewFilterIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
calendarViewIds: [],
applicationId: null,
relationTargetObjectMetadataId: 'company-object-metadata-id',
relationTargetFieldMetadataId: 'company-id-field-id',
settings: {
relationType: RelationType.MANY_TO_ONE,
joinColumnName: 'company-related-to-2Id',
},
} as unknown as FlatFieldMetadata,
];
const companyFields: FlatFieldMetadata[] = [
{
id: 'company-id-field-id',
name: 'id',
type: FieldMetadataType.UUID,
label: 'id',
objectMetadataId: 'company-object-metadata-id',
isNullable: false,
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
universalIdentifier: 'company-id-field-id',
viewFieldIds: [],
viewFilterIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
calendarViewIds: [],
applicationId: null,
} as unknown as FlatFieldMetadata,
{
id: 'company-name-field-id',
name: 'name',
type: FieldMetadataType.TEXT,
label: 'name',
objectMetadataId: 'company-object-metadata-id',
isNullable: true,
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
universalIdentifier: 'company-name-field-id',
viewFieldIds: [],
viewFilterIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
calendarViewIds: [],
applicationId: null,
} as unknown as FlatFieldMetadata,
{
id: 'company-description-field-id',
name: 'description',
type: FieldMetadataType.TEXT,
label: 'description',
objectMetadataId: 'company-object-metadata-id',
isNullable: true,
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
universalIdentifier: 'company-description-field-id',
viewFieldIds: [],
viewFilterIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
calendarViewIds: [],
applicationId: null,
} as unknown as FlatFieldMetadata,
{
id: 'company-domain-name-field-id',
name: 'domainName',
type: FieldMetadataType.LINKS,
label: 'domainName',
objectMetadataId: 'company-object-metadata-id',
isNullable: true,
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
universalIdentifier: 'company-domain-name-field-id',
viewFieldIds: [],
viewFilterIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
calendarViewIds: [],
applicationId: null,
} as unknown as FlatFieldMetadata,
{
id: 'company-address-field-id',
name: 'address',
type: FieldMetadataType.TEXT,
label: 'address',
objectMetadataId: 'company-object-metadata-id',
isNullable: true,
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
universalIdentifier: 'company-address-field-id',
viewFieldIds: [],
viewFilterIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
calendarViewIds: [],
applicationId: null,
} as unknown as FlatFieldMetadata,
];
const allFields = [...personFields, ...companyFields];
const flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata> = {
byId: allFields.reduce(
(acc, field) => {
acc[field.id] = field;
return acc;
},
{} as Record<string, FlatFieldMetadata>,
),
idByUniversalIdentifier: allFields.reduce(
(acc, field) => {
acc[field.universalIdentifier] = field.id;
return acc;
},
{} as Record<string, string>,
),
universalIdentifiersByApplicationId: {},
};
const createFlatObjectMetadata = (
partial: Partial<FlatObjectMetadata> & {
id: string;
nameSingular: string;
fieldMetadataIds: string[];
indexMetadataIds: string[];
},
): FlatObjectMetadata =>
({
namePlural: `${partial.nameSingular}s`,
labelSingular: partial.nameSingular,
labelPlural: `${partial.nameSingular}s`,
icon: 'Icon',
targetTableName: partial.nameSingular,
workspaceId: 'workspace-id',
isCustom: false,
isRemote: false,
isActive: true,
isSystem: false,
isAuditLogged: true,
isSearchable: true,
universalIdentifier: partial.id,
viewIds: [],
applicationId: null,
isLabelSyncedWithName: false,
createdAt: new Date(),
updatedAt: new Date(),
shortcut: null,
description: null,
standardOverrides: null,
isUIReadOnly: false,
standardId: null,
labelIdentifierFieldMetadataId: null,
imageIdentifierFieldMetadataId: null,
duplicateCriteria: null,
...partial,
}) as FlatObjectMetadata;
const personMetadata = createFlatObjectMetadata({
id: 'person-object-metadata-id',
nameSingular: 'person',
fieldsById: {
'person-id-field-id': {
id: 'person-id-field-id',
name: 'id',
type: FieldMetadataType.UUID,
label: 'id',
},
'person-name-field-id': {
id: 'person-name-field-id',
name: 'name',
type: FieldMetadataType.FULL_NAME,
label: 'name',
},
'person-company-1-field-id': {
id: 'person-company-1-field-id',
name: 'company-related-to-1',
type: FieldMetadataType.RELATION,
label: 'company-related-to-1',
relationTargetObjectMetadataId: 'company-object-metadata-id',
relationTargetFieldMetadataId: 'company-id-field-id',
settings: {
relationType: RelationType.MANY_TO_ONE,
},
},
'person-company-2-field-id': {
id: 'person-company-2-field-id',
name: 'company-related-to-2',
type: FieldMetadataType.RELATION,
label: 'company-related-to-2',
relationTargetObjectMetadataId: 'company-object-metadata-id',
relationTargetFieldMetadataId: 'company-id-field-id',
settings: {
relationType: RelationType.MANY_TO_ONE,
},
},
},
fieldIdByName: {
id: 'person-id-field-id',
name: 'person-name-field-id',
'company-related-to-1': 'person-company-1-field-id',
'company-related-to-2': 'person-company-2-field-id',
},
} as unknown as ObjectMetadataItemWithFieldMaps;
indexMetadataIds: [],
fieldMetadataIds: personFields.map((f) => f.id),
});
const companyMetadata = {
const companyMetadata = createFlatObjectMetadata({
id: 'company-object-metadata-id',
nameSingular: 'company',
indexMetadatas: [
{
id: 'company-id-index-metadata-id',
name: 'company-id-index-metadata-name',
indexFieldMetadatas: [
{
fieldMetadataId: 'company-id-field-id',
},
],
isUnique: true,
},
{
id: 'company-domain-index-metadata-id',
name: 'company-domain-index-metadata-name',
indexFieldMetadatas: [
{
fieldMetadataId: 'company-domain-name-field-id',
},
],
isUnique: true,
},
{
id: 'company-composite-index-metadata-id',
name: 'company-composite-index-metadata-name',
indexFieldMetadatas: [
{
fieldMetadataId: 'company-name-field-id',
},
{
fieldMetadataId: 'company-description-field-id',
},
],
isUnique: true,
},
indexMetadataIds: [
'company-id-index-metadata-id',
'company-domain-index-metadata-id',
'company-composite-index-metadata-id',
],
fieldsById: {
'company-id-field-id': {
id: 'company-id-field-id',
name: 'id',
type: FieldMetadataType.UUID,
label: 'id',
},
'company-name-field-id': {
id: 'company-name-field-id',
name: 'name',
type: FieldMetadataType.TEXT,
label: 'name',
},
'company-description-field-id': {
id: 'company-description-field-id',
name: 'description',
type: FieldMetadataType.TEXT,
label: 'description',
},
'company-domain-name-field-id': {
id: 'company-domain-name-field-id',
name: 'domainName',
type: FieldMetadataType.LINKS,
label: 'domainName',
},
'company-address-field-id': {
id: 'company-address-field-id',
name: 'address',
type: FieldMetadataType.TEXT,
label: 'address',
},
},
fieldIdByName: {
id: 'company-id-field-id',
name: 'company-name-field-id',
description: 'company-description-field-id',
domainName: 'company-domain-name-field-id',
address: 'company-address-field-id',
},
} as unknown as ObjectMetadataItemWithFieldMaps;
fieldMetadataIds: companyFields.map((f) => f.id),
});
const objectMetadataMaps = {
const flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata> = {
byId: {
'person-object-metadata-id': personMetadata,
'company-object-metadata-id': companyMetadata,
},
idByNameSingular: {
person: 'person-object-metadata-id',
company: 'company-object-metadata-id',
idByUniversalIdentifier: {
'person-object-metadata-id': 'person-object-metadata-id',
'company-object-metadata-id': 'company-object-metadata-id',
},
} as ObjectMetadataMaps;
universalIdentifiersByApplicationId: {},
};
const createFlatIndexFieldMetadata = (
id: string,
fieldMetadataId: string,
indexMetadataId: string,
order: number,
) => ({
id,
fieldMetadataId,
indexMetadataId,
order,
createdAt: new Date(),
updatedAt: new Date(),
});
const flatIndexMaps: FlatEntityMaps<FlatIndexMetadata> = {
byId: {
'company-id-index-metadata-id': {
id: 'company-id-index-metadata-id',
name: 'company-id-index-metadata-name',
isUnique: true,
objectMetadataId: 'company-object-metadata-id',
universalIdentifier: 'company-id-index-metadata-id',
flatIndexFieldMetadatas: [
createFlatIndexFieldMetadata(
'company-id-index-field-metadata-id',
'company-id-field-id',
'company-id-index-metadata-id',
0,
),
],
} as unknown as FlatIndexMetadata,
'company-domain-index-metadata-id': {
id: 'company-domain-index-metadata-id',
name: 'company-domain-index-metadata-name',
isUnique: true,
objectMetadataId: 'company-object-metadata-id',
universalIdentifier: 'company-domain-index-metadata-id',
flatIndexFieldMetadatas: [
createFlatIndexFieldMetadata(
'company-domain-index-field-metadata-id',
'company-domain-name-field-id',
'company-domain-index-metadata-id',
0,
),
],
} as unknown as FlatIndexMetadata,
'company-composite-index-metadata-id': {
id: 'company-composite-index-metadata-id',
name: 'company-composite-index-metadata-name',
isUnique: true,
objectMetadataId: 'company-object-metadata-id',
universalIdentifier: 'company-composite-index-metadata-id',
flatIndexFieldMetadatas: [
createFlatIndexFieldMetadata(
'company-name-index-field-metadata-id',
'company-name-field-id',
'company-composite-index-metadata-id',
0,
),
createFlatIndexFieldMetadata(
'company-description-index-field-metadata-id',
'company-description-field-id',
'company-composite-index-metadata-id',
1,
),
],
} as unknown as FlatIndexMetadata,
},
idByUniversalIdentifier: {
'company-id-index-metadata-id': 'company-id-index-metadata-id',
'company-domain-index-metadata-id': 'company-domain-index-metadata-id',
'company-composite-index-metadata-id':
'company-composite-index-metadata-id',
},
universalIdentifiersByApplicationId: {},
};
it('should return an empty object if no connect fields are found', () => {
const peopleEntityInputs = [
@@ -159,7 +371,9 @@ describe('computeRelationConnectQueryConfigs', () => {
const result = computeRelationConnectQueryConfigs(
peopleEntityInputs,
personMetadata,
objectMetadataMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatIndexMaps,
{},
);
@@ -187,7 +401,9 @@ describe('computeRelationConnectQueryConfigs', () => {
computeRelationConnectQueryConfigs(
peopleEntityInputs,
personMetadata,
objectMetadataMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatIndexMaps,
relationConnectQueryFieldsByEntityIndex,
);
}).toThrow('Connect is not allowed for name on person');
@@ -213,7 +429,9 @@ describe('computeRelationConnectQueryConfigs', () => {
computeRelationConnectQueryConfigs(
peopleEntityInputs,
personMetadata,
objectMetadataMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatIndexMaps,
relationConnectQueryFieldsByEntityIndex,
);
}).toThrow(
@@ -255,7 +473,9 @@ describe('computeRelationConnectQueryConfigs', () => {
computeRelationConnectQueryConfigs(
peopleEntityInputs,
personMetadata,
objectMetadataMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatIndexMaps,
relationConnectQueryFieldsByEntityIndex,
);
}).toThrow(
@@ -304,7 +524,9 @@ describe('computeRelationConnectQueryConfigs', () => {
computeRelationConnectQueryConfigs(
peopleEntityInputs,
personMetadata,
objectMetadataMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatIndexMaps,
relationConnectQueryFieldsByEntityIndex,
);
}).toThrow(
@@ -374,7 +596,9 @@ describe('computeRelationConnectQueryConfigs', () => {
const result = computeRelationConnectQueryConfigs(
peopleEntityInputs,
personMetadata,
objectMetadataMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatIndexMaps,
relationConnectQueryFieldsByEntityIndex,
);
@@ -392,12 +616,11 @@ describe('computeRelationConnectQueryConfigs', () => {
relationFieldName: 'company-related-to-1Id',
targetObjectName: 'company',
uniqueConstraintFields: [
{
expect.objectContaining({
id: 'company-domain-name-field-id',
label: 'domainName',
name: 'domainName',
type: FieldMetadataType.LINKS,
},
}),
],
},
{
@@ -410,12 +633,11 @@ describe('computeRelationConnectQueryConfigs', () => {
relationFieldName: 'company-related-to-2Id',
targetObjectName: 'company',
uniqueConstraintFields: [
{
expect.objectContaining({
id: 'company-id-field-id',
label: 'id',
name: 'id',
type: FieldMetadataType.UUID,
},
}),
],
},
]);
@@ -2,43 +2,80 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { type ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event';
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 { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import {
TwentyORMException,
TwentyORMExceptionCode,
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util';
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
import { getMockObjectMetadataItemWithFieldsMaps } from 'src/utils/__test__/get-object-metadata-item-with-fields-maps.mock';
describe('formatTwentyOrmEventToDatabaseBatchEvent', () => {
const objectMetadataItemWithFieldMaps =
getMockObjectMetadataItemWithFieldsMaps({
id: 'object-id',
workspaceId: 'workspace-id',
nameSingular: 'person',
namePlural: 'people',
indexMetadatas: [],
fieldIdByJoinColumnName: {},
fieldIdByName: {
name: 'name-id',
age: 'age-id',
fullName: 'fullname-id',
},
fieldsById: {
'name-id': getMockFieldMetadataEntity({
workspaceId: 'workspace-id',
objectMetadataId: 'object-id',
id: 'name-id',
type: FieldMetadataType.TEXT,
name: 'name',
label: 'Name',
isLabelSyncedWithName: true,
isNullable: true,
createdAt: new Date(),
updatedAt: new Date(),
}),
},
});
const workspaceId = 'workspace-id';
const objectMetadataId = 'object-id';
const createMockField = (
overrides: Partial<FlatFieldMetadata> & {
id: string;
name: string;
type: FieldMetadataType;
},
): FlatFieldMetadata =>
({
workspaceId,
objectMetadataId,
isNullable: true,
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
universalIdentifier: overrides.id,
viewFieldIds: [],
viewFilterIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
calendarViewIds: [],
applicationId: null,
label: overrides.name,
...overrides,
}) as FlatFieldMetadata;
const nameField = createMockField({
id: 'name-id',
type: FieldMetadataType.TEXT,
name: 'name',
label: 'Name',
});
const flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata> = {
byId: {
'name-id': nameField,
},
idByUniversalIdentifier: {
'name-id': 'name-id',
},
universalIdentifiersByApplicationId: {},
};
const flatObjectMetadata: FlatObjectMetadata = {
id: objectMetadataId,
workspaceId,
nameSingular: 'person',
namePlural: 'people',
labelSingular: 'Person',
labelPlural: 'People',
isCustom: false,
isRemote: false,
isActive: true,
isSystem: false,
createdAt: new Date(),
updatedAt: new Date(),
universalIdentifier: objectMetadataId,
fieldMetadataIds: ['name-id'],
indexMetadataIds: [],
viewIds: [],
applicationId: null,
} as unknown as FlatObjectMetadata;
const mockWorkspaceId = 'workspace-id';
const mockAuthContext = {
@@ -73,7 +110,8 @@ describe('formatTwentyOrmEventToDatabaseBatchEvent', () => {
try {
formatTwentyOrmEventToDatabaseBatchEvent({
action: DatabaseEventAction.UPDATED,
objectMetadataItem: objectMetadataItemWithFieldMaps,
objectMetadataItem: flatObjectMetadata,
flatFieldMetadataMaps,
workspaceId: mockWorkspaceId,
authContext: mockAuthContext,
entities: afterEntities,
@@ -115,7 +153,8 @@ describe('formatTwentyOrmEventToDatabaseBatchEvent', () => {
const result = formatTwentyOrmEventToDatabaseBatchEvent({
action: DatabaseEventAction.UPDATED,
objectMetadataItem: objectMetadataItemWithFieldMaps,
objectMetadataItem: flatObjectMetadata,
flatFieldMetadataMaps,
workspaceId: mockWorkspaceId,
authContext: mockAuthContext,
entities: afterEntities,
@@ -155,7 +194,8 @@ describe('formatTwentyOrmEventToDatabaseBatchEvent', () => {
const result = formatTwentyOrmEventToDatabaseBatchEvent({
action: DatabaseEventAction.UPDATED,
objectMetadataItem: objectMetadataItemWithFieldMaps,
objectMetadataItem: flatObjectMetadata,
flatFieldMetadataMaps,
workspaceId: mockWorkspaceId,
authContext: mockAuthContext,
entities: afterEntity,
@@ -1,19 +1,20 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
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 { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { getColumnNameToFieldMetadataIdMap } from 'src/engine/twenty-orm/utils/get-column-name-to-field-metadata-id.util';
describe('getColumnNameToFieldMetadataIdMap', () => {
const createMockObjectMetadataItemWithFieldMaps = (
fieldsById: Record<string, any>,
): ObjectMetadataItemWithFieldMaps =>
const createMockFlatObjectMetadata = (
fieldMetadataIds: string[],
): FlatObjectMetadata =>
({
id: 'test-object-id',
nameSingular: 'test',
namePlural: 'tests',
labelSingular: 'Test',
labelPlural: 'Tests',
description: 'Test object',
icon: 'IconTest',
targetTableName: 'test',
isCustom: false,
@@ -22,53 +23,87 @@ describe('getColumnNameToFieldMetadataIdMap', () => {
isSystem: false,
isAuditLogged: false,
isSearchable: false,
labelIdentifierFieldMetadataId: '',
imageIdentifierFieldMetadataId: '',
workspaceId: 'test-workspace-id',
indexMetadatas: [],
fieldsById,
fieldIdByName: {},
fieldIdByJoinColumnName: {},
}) as unknown as ObjectMetadataItemWithFieldMaps;
universalIdentifier: 'test-object-id',
indexMetadataIds: [],
fieldMetadataIds,
viewIds: [],
applicationId: null,
isLabelSyncedWithName: false,
createdAt: new Date(),
updatedAt: new Date(),
shortcut: null,
description: null,
standardOverrides: null,
isUIReadOnly: false,
standardId: null,
labelIdentifierFieldMetadataId: null,
imageIdentifierFieldMetadataId: null,
duplicateCriteria: null,
}) as FlatObjectMetadata;
const createMockFieldMetadata = (
const createMockFlatFieldMetadata = (
id: string,
name: string,
type: FieldMetadataType,
) => ({
id,
name,
type,
label: name,
objectMetadataId: 'test-object-id',
isLabelSyncedWithName: true,
isNullable: true,
isUnique: false,
workspaceId: 'test-workspace-id',
createdAt: new Date(),
updatedAt: new Date(),
settings?: Record<string, unknown>,
): FlatFieldMetadata =>
({
id,
name,
type,
label: name,
objectMetadataId: 'test-object-id',
isLabelSyncedWithName: true,
isNullable: true,
createdAt: new Date(),
updatedAt: new Date(),
universalIdentifier: id,
viewFieldIds: [],
viewFilterIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
calendarViewIds: [],
applicationId: null,
...(settings ? { settings } : {}),
}) as unknown as FlatFieldMetadata;
const buildFlatFieldMetadataMaps = (
fields: FlatFieldMetadata[],
): FlatEntityMaps<FlatFieldMetadata> => ({
byId: fields.reduce(
(acc, field) => {
acc[field.id] = field;
return acc;
},
{} as Record<string, FlatFieldMetadata>,
),
idByUniversalIdentifier: fields.reduce(
(acc, field) => {
acc[field.universalIdentifier] = field.id;
return acc;
},
{} as Record<string, string>,
),
universalIdentifiersByApplicationId: {},
});
describe('with simple field types', () => {
it('should return a map with column name to field metadata id for simple field types', () => {
const fieldsById = {
'field-1': createMockFieldMetadata(
'field-1',
'name',
FieldMetadataType.TEXT,
),
'field-2': createMockFieldMetadata(
'field-2',
'age',
FieldMetadataType.NUMBER,
),
};
const objectMetadataItemWithFieldMaps =
createMockObjectMetadataItemWithFieldMaps(fieldsById);
const fields = [
createMockFlatFieldMetadata('field-1', 'name', FieldMetadataType.TEXT),
createMockFlatFieldMetadata('field-2', 'age', FieldMetadataType.NUMBER),
];
const flatObjectMetadata = createMockFlatObjectMetadata(
fields.map((f) => f.id),
);
const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields);
const result = getColumnNameToFieldMetadataIdMap(
objectMetadataItemWithFieldMaps,
flatObjectMetadata,
flatFieldMetadataMaps,
);
expect(result['name']).toBe('field-1');
@@ -79,19 +114,21 @@ describe('getColumnNameToFieldMetadataIdMap', () => {
describe('with composite field types', () => {
it('should return column names to field metadata id for FULL_NAME composite type', () => {
const fieldsById = {
'field-1': createMockFieldMetadata(
const fields = [
createMockFlatFieldMetadata(
'field-1',
'fullName',
FieldMetadataType.FULL_NAME,
),
};
const objectMetadataItemWithFieldMaps =
createMockObjectMetadataItemWithFieldMaps(fieldsById);
];
const flatObjectMetadata = createMockFlatObjectMetadata(
fields.map((f) => f.id),
);
const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields);
const result = getColumnNameToFieldMetadataIdMap(
objectMetadataItemWithFieldMaps,
flatObjectMetadata,
flatFieldMetadataMaps,
);
expect(result['fullNameFirstName']).toBe('field-1');
@@ -100,19 +137,21 @@ describe('getColumnNameToFieldMetadataIdMap', () => {
});
it('should return column names to field metadata id for CURRENCY composite type', () => {
const fieldsById = {
'field-1': createMockFieldMetadata(
const fields = [
createMockFlatFieldMetadata(
'field-1',
'price',
FieldMetadataType.CURRENCY,
),
};
const objectMetadataItemWithFieldMaps =
createMockObjectMetadataItemWithFieldMaps(fieldsById);
];
const flatObjectMetadata = createMockFlatObjectMetadata(
fields.map((f) => f.id),
);
const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields);
const result = getColumnNameToFieldMetadataIdMap(
objectMetadataItemWithFieldMaps,
flatObjectMetadata,
flatFieldMetadataMaps,
);
expect(result['priceAmountMicros']).toBe('field-1');
@@ -121,29 +160,27 @@ describe('getColumnNameToFieldMetadataIdMap', () => {
});
it('should handle multiple composite fields', () => {
const fieldsById = {
'field-1': createMockFieldMetadata(
const fields = [
createMockFlatFieldMetadata(
'field-1',
'fullName',
FieldMetadataType.FULL_NAME,
),
'field-2': createMockFieldMetadata(
createMockFlatFieldMetadata(
'field-2',
'price',
FieldMetadataType.CURRENCY,
),
'field-3': createMockFieldMetadata(
'field-3',
'name',
FieldMetadataType.TEXT,
),
};
const objectMetadataItemWithFieldMaps =
createMockObjectMetadataItemWithFieldMaps(fieldsById);
createMockFlatFieldMetadata('field-3', 'name', FieldMetadataType.TEXT),
];
const flatObjectMetadata = createMockFlatObjectMetadata(
fields.map((f) => f.id),
);
const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields);
const result = getColumnNameToFieldMetadataIdMap(
objectMetadataItemWithFieldMaps,
flatObjectMetadata,
flatFieldMetadataMaps,
);
expect(result['fullNameFirstName']).toBe('field-1');
@@ -157,34 +194,28 @@ describe('getColumnNameToFieldMetadataIdMap', () => {
describe('with mixed field types', () => {
it('should handle both simple and composite field types', () => {
const fieldsById = {
'field-1': createMockFieldMetadata(
'field-1',
'name',
FieldMetadataType.TEXT,
),
'field-2': createMockFieldMetadata(
const fields = [
createMockFlatFieldMetadata('field-1', 'name', FieldMetadataType.TEXT),
createMockFlatFieldMetadata(
'field-2',
'fullName',
FieldMetadataType.FULL_NAME,
),
'field-3': createMockFieldMetadata(
'field-3',
'age',
FieldMetadataType.NUMBER,
),
'field-4': createMockFieldMetadata(
createMockFlatFieldMetadata('field-3', 'age', FieldMetadataType.NUMBER),
createMockFlatFieldMetadata(
'field-4',
'price',
FieldMetadataType.CURRENCY,
),
};
const objectMetadataItemWithFieldMaps =
createMockObjectMetadataItemWithFieldMaps(fieldsById);
];
const flatObjectMetadata = createMockFlatObjectMetadata(
fields.map((f) => f.id),
);
const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields);
const result = getColumnNameToFieldMetadataIdMap(
objectMetadataItemWithFieldMaps,
flatObjectMetadata,
flatFieldMetadataMaps,
);
expect(result['name']).toBe('field-1');
@@ -199,30 +230,26 @@ describe('getColumnNameToFieldMetadataIdMap', () => {
describe('with relation field types', () => {
it('should handle relation field types with join column name', () => {
const fieldsById = {
'field-1': {
...createMockFieldMetadata(
'field-1',
'company',
FieldMetadataType.RELATION,
),
settings: {
const fields = [
createMockFlatFieldMetadata(
'field-1',
'company',
FieldMetadataType.RELATION,
{
relationType: 'ONE_TO_ONE',
joinColumnName: 'companyId',
},
},
'field-2': createMockFieldMetadata(
'field-2',
'name',
FieldMetadataType.TEXT,
),
};
const objectMetadataItemWithFieldMaps =
createMockObjectMetadataItemWithFieldMaps(fieldsById);
createMockFlatFieldMetadata('field-2', 'name', FieldMetadataType.TEXT),
];
const flatObjectMetadata = createMockFlatObjectMetadata(
fields.map((f) => f.id),
);
const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields);
const result = getColumnNameToFieldMetadataIdMap(
objectMetadataItemWithFieldMaps,
flatObjectMetadata,
flatFieldMetadataMaps,
);
expect(result['companyId']).toBe('field-1');
@@ -232,30 +259,26 @@ describe('getColumnNameToFieldMetadataIdMap', () => {
});
it('should skip ONE_TO_MANY relation field types', () => {
const fieldsById = {
'field-1': {
...createMockFieldMetadata(
'field-1',
'employees',
FieldMetadataType.RELATION,
),
settings: {
const fields = [
createMockFlatFieldMetadata(
'field-1',
'employees',
FieldMetadataType.RELATION,
{
relationType: 'ONE_TO_MANY',
joinColumnName: 'companyId',
},
},
'field-2': createMockFieldMetadata(
'field-2',
'name',
FieldMetadataType.TEXT,
),
};
const objectMetadataItemWithFieldMaps =
createMockObjectMetadataItemWithFieldMaps(fieldsById);
createMockFlatFieldMetadata('field-2', 'name', FieldMetadataType.TEXT),
];
const flatObjectMetadata = createMockFlatObjectMetadata(
fields.map((f) => f.id),
);
const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields);
const result = getColumnNameToFieldMetadataIdMap(
objectMetadataItemWithFieldMaps,
flatObjectMetadata,
flatFieldMetadataMaps,
);
expect(result['name']).toBe('field-2');
@@ -1,19 +1,20 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
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 { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { getFieldMetadataIdToColumnNamesMap } from 'src/engine/twenty-orm/utils/get-field-metadata-id-to-column-names-map.util';
describe('getFieldMetadataIdToColumnNamesMap', () => {
const createMockObjectMetadataItemWithFieldMaps = (
fieldsById: Record<string, any>,
): ObjectMetadataItemWithFieldMaps =>
const createMockFlatObjectMetadata = (
fieldMetadataIds: string[],
): FlatObjectMetadata =>
({
id: 'test-object-id',
nameSingular: 'test',
namePlural: 'tests',
labelSingular: 'Test',
labelPlural: 'Tests',
description: 'Test object',
icon: 'IconTest',
targetTableName: 'test',
isCustom: false,
@@ -22,53 +23,85 @@ describe('getFieldMetadataIdToColumnNamesMap', () => {
isSystem: false,
isAuditLogged: false,
isSearchable: false,
labelIdentifierFieldMetadataId: '',
imageIdentifierFieldMetadataId: '',
workspaceId: 'test-workspace-id',
indexMetadatas: [],
fieldsById,
fieldIdByName: {},
fieldIdByJoinColumnName: {},
}) as unknown as ObjectMetadataItemWithFieldMaps;
universalIdentifier: 'test-object-id',
indexMetadataIds: [],
fieldMetadataIds,
viewIds: [],
applicationId: null,
isLabelSyncedWithName: false,
createdAt: new Date(),
updatedAt: new Date(),
shortcut: null,
description: null,
standardOverrides: null,
isUIReadOnly: false,
standardId: null,
labelIdentifierFieldMetadataId: null,
imageIdentifierFieldMetadataId: null,
duplicateCriteria: null,
}) as FlatObjectMetadata;
const createMockFieldMetadata = (
const createMockFlatFieldMetadata = (
id: string,
name: string,
type: FieldMetadataType,
) => ({
id,
name,
type,
label: name,
objectMetadataId: 'test-object-id',
isLabelSyncedWithName: true,
isNullable: true,
isUnique: false,
workspaceId: 'test-workspace-id',
createdAt: new Date(),
updatedAt: new Date(),
): FlatFieldMetadata =>
({
id,
name,
type,
label: name,
objectMetadataId: 'test-object-id',
isLabelSyncedWithName: true,
isNullable: true,
createdAt: new Date(),
updatedAt: new Date(),
universalIdentifier: id,
viewFieldIds: [],
viewFilterIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
calendarViewIds: [],
applicationId: null,
}) as unknown as FlatFieldMetadata;
const buildFlatFieldMetadataMaps = (
fields: FlatFieldMetadata[],
): FlatEntityMaps<FlatFieldMetadata> => ({
byId: fields.reduce(
(acc, field) => {
acc[field.id] = field;
return acc;
},
{} as Record<string, FlatFieldMetadata>,
),
idByUniversalIdentifier: fields.reduce(
(acc, field) => {
acc[field.universalIdentifier] = field.id;
return acc;
},
{} as Record<string, string>,
),
universalIdentifiersByApplicationId: {},
});
describe('with simple field types', () => {
it('should return a map with single column name for simple field types', () => {
const fieldsById = {
'field-1': createMockFieldMetadata(
'field-1',
'name',
FieldMetadataType.TEXT,
),
'field-2': createMockFieldMetadata(
'field-2',
'age',
FieldMetadataType.NUMBER,
),
};
const objectMetadataItemWithFieldMaps =
createMockObjectMetadataItemWithFieldMaps(fieldsById);
const fields = [
createMockFlatFieldMetadata('field-1', 'name', FieldMetadataType.TEXT),
createMockFlatFieldMetadata('field-2', 'age', FieldMetadataType.NUMBER),
];
const flatObjectMetadata = createMockFlatObjectMetadata(
fields.map((f) => f.id),
);
const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields);
const result = getFieldMetadataIdToColumnNamesMap(
objectMetadataItemWithFieldMaps,
flatObjectMetadata,
flatFieldMetadataMaps,
);
expect(result.get('field-1')).toEqual(['name']);
@@ -79,19 +112,21 @@ describe('getFieldMetadataIdToColumnNamesMap', () => {
describe('with composite field types', () => {
it('should return multiple column names for FULL_NAME composite type', () => {
const fieldsById = {
'field-1': createMockFieldMetadata(
const fields = [
createMockFlatFieldMetadata(
'field-1',
'fullName',
FieldMetadataType.FULL_NAME,
),
};
const objectMetadataItemWithFieldMaps =
createMockObjectMetadataItemWithFieldMaps(fieldsById);
];
const flatObjectMetadata = createMockFlatObjectMetadata(
fields.map((f) => f.id),
);
const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields);
const result = getFieldMetadataIdToColumnNamesMap(
objectMetadataItemWithFieldMaps,
flatObjectMetadata,
flatFieldMetadataMaps,
);
expect(result.get('field-1')).toEqual([
@@ -102,19 +137,21 @@ describe('getFieldMetadataIdToColumnNamesMap', () => {
});
it('should return multiple column names for CURRENCY composite type', () => {
const fieldsById = {
'field-1': createMockFieldMetadata(
const fields = [
createMockFlatFieldMetadata(
'field-1',
'price',
FieldMetadataType.CURRENCY,
),
};
const objectMetadataItemWithFieldMaps =
createMockObjectMetadataItemWithFieldMaps(fieldsById);
];
const flatObjectMetadata = createMockFlatObjectMetadata(
fields.map((f) => f.id),
);
const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields);
const result = getFieldMetadataIdToColumnNamesMap(
objectMetadataItemWithFieldMaps,
flatObjectMetadata,
flatFieldMetadataMaps,
);
expect(result.get('field-1')).toEqual([
@@ -125,29 +162,27 @@ describe('getFieldMetadataIdToColumnNamesMap', () => {
});
it('should handle multiple composite fields', () => {
const fieldsById = {
'field-1': createMockFieldMetadata(
const fields = [
createMockFlatFieldMetadata(
'field-1',
'fullName',
FieldMetadataType.FULL_NAME,
),
'field-2': createMockFieldMetadata(
createMockFlatFieldMetadata(
'field-2',
'price',
FieldMetadataType.CURRENCY,
),
'field-3': createMockFieldMetadata(
'field-3',
'name',
FieldMetadataType.TEXT,
),
};
const objectMetadataItemWithFieldMaps =
createMockObjectMetadataItemWithFieldMaps(fieldsById);
createMockFlatFieldMetadata('field-3', 'name', FieldMetadataType.TEXT),
];
const flatObjectMetadata = createMockFlatObjectMetadata(
fields.map((f) => f.id),
);
const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields);
const result = getFieldMetadataIdToColumnNamesMap(
objectMetadataItemWithFieldMaps,
flatObjectMetadata,
flatFieldMetadataMaps,
);
expect(result.get('field-1')).toEqual([
@@ -165,34 +200,28 @@ describe('getFieldMetadataIdToColumnNamesMap', () => {
describe('with mixed field types', () => {
it('should handle both simple and composite field types', () => {
const fieldsById = {
'field-1': createMockFieldMetadata(
'field-1',
'name',
FieldMetadataType.TEXT,
),
'field-2': createMockFieldMetadata(
const fields = [
createMockFlatFieldMetadata('field-1', 'name', FieldMetadataType.TEXT),
createMockFlatFieldMetadata(
'field-2',
'fullName',
FieldMetadataType.FULL_NAME,
),
'field-3': createMockFieldMetadata(
'field-3',
'age',
FieldMetadataType.NUMBER,
),
'field-4': createMockFieldMetadata(
createMockFlatFieldMetadata('field-3', 'age', FieldMetadataType.NUMBER),
createMockFlatFieldMetadata(
'field-4',
'price',
FieldMetadataType.CURRENCY,
),
};
const objectMetadataItemWithFieldMaps =
createMockObjectMetadataItemWithFieldMaps(fieldsById);
];
const flatObjectMetadata = createMockFlatObjectMetadata(
fields.map((f) => f.id),
);
const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields);
const result = getFieldMetadataIdToColumnNamesMap(
objectMetadataItemWithFieldMaps,
flatObjectMetadata,
flatFieldMetadataMaps,
);
expect(result.get('field-1')).toEqual(['name']);
@@ -5,11 +5,13 @@ import { getUniqueConstraintsFields, isDefined } from 'twenty-shared/utils';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
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';
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
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 { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type ConnectObject } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-nested-relation-fields.type';
import {
type RelationConnectQueryConfig,
@@ -26,8 +28,10 @@ import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-
export const computeRelationConnectQueryConfigs = (
entities: Record<string, unknown>[],
objectMetadata: ObjectMetadataItemWithFieldMaps,
objectMetadataMap: ObjectMetadataMaps,
flatObjectMetadata: FlatObjectMetadata,
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
flatIndexMaps: FlatEntityMaps<FlatIndexMetadata>,
relationConnectQueryFieldsByEntityIndex: RelationConnectQueryFieldsByEntityIndex,
) => {
const allConnectQueryConfigs: Record<string, RelationConnectQueryConfig> = {};
@@ -48,8 +52,10 @@ export const computeRelationConnectQueryConfigs = (
} = computeRecordToConnectCondition(
connectFieldName,
connectObject,
objectMetadata,
objectMetadataMap,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatIndexMaps,
entity,
);
@@ -102,7 +108,7 @@ const updateConnectQueryConfigs = (
const createConnectQueryConfig = (
connectFieldName: string,
recordToConnectCondition: UniqueConstraintCondition,
uniqueConstraintFields: FieldMetadataEntity<FieldMetadataType>[],
uniqueConstraintFields: FlatFieldMetadata<FieldMetadataType>[],
targetObjectNameSingular: string,
entityIndex: number,
) => {
@@ -121,26 +127,33 @@ const createConnectQueryConfig = (
const computeRecordToConnectCondition = (
connectFieldName: string,
connectObject: ConnectObject,
objectMetadata: ObjectMetadataItemWithFieldMaps,
objectMetadataMap: ObjectMetadataMaps,
flatObjectMetadata: FlatObjectMetadata,
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
flatIndexMaps: FlatEntityMaps<FlatIndexMetadata>,
entity: Record<string, unknown>,
): {
recordToConnectCondition: UniqueConstraintCondition;
uniqueConstraintFields: FieldMetadataEntity<FieldMetadataType>[];
uniqueConstraintFields: FlatFieldMetadata<FieldMetadataType>[];
targetObjectNameSingular: string;
} => {
const field =
objectMetadata.fieldsById[objectMetadata.fieldIdByName[connectFieldName]];
const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata(
flatFieldMetadataMaps,
flatObjectMetadata,
);
const field = flatFieldMetadataMaps.byId[fieldIdByName[connectFieldName]];
if (
!isDefined(field) ||
(!isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION) &&
!isFieldMetadataEntityOfType(field, FieldMetadataType.MORPH_RELATION)) ||
field.settings?.relationType !== RelationType.MANY_TO_ONE
) {
const objectMetadataNameSingular = objectMetadata.nameSingular;
const objectMetadataNameSingular = flatObjectMetadata.nameSingular;
throw new TwentyORMException(
`Connect is not allowed for ${connectFieldName} on ${objectMetadata.nameSingular}`,
`Connect is not allowed for ${connectFieldName} on ${flatObjectMetadata.nameSingular}`,
TwentyORMExceptionCode.CONNECT_NOT_ALLOWED,
{
userFriendlyMessage: msg`Connect is not allowed for ${connectFieldName} on ${objectMetadataNameSingular}`,
@@ -150,7 +163,7 @@ const computeRecordToConnectCondition = (
checkNoRelationFieldConflictOrThrow(entity, connectFieldName);
const targetObjectMetadata =
objectMetadataMap.byId[field.relationTargetObjectMetadataId || ''];
flatObjectMetadataMaps.byId[field.relationTargetObjectMetadataId || ''];
if (!isDefined(targetObjectMetadata)) {
throw new TwentyORMException(
@@ -164,6 +177,8 @@ const computeRecordToConnectCondition = (
const uniqueConstraintFields = checkUniqueConstraintFullyPopulated(
targetObjectMetadata,
flatFieldMetadataMaps,
flatIndexMaps,
connectObject,
connectFieldName,
);
@@ -179,16 +194,41 @@ const computeRecordToConnectCondition = (
};
const checkUniqueConstraintFullyPopulated = (
objectMetadata: ObjectMetadataItemWithFieldMaps,
flatObjectMetadata: FlatObjectMetadata,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
flatIndexMaps: FlatEntityMaps<FlatIndexMetadata>,
connectObject: ConnectObject,
connectFieldName: string,
) => {
const fields = getFlatFieldsFromFlatObjectMetadata(
flatObjectMetadata,
flatFieldMetadataMaps,
);
const indexMetadatas = flatObjectMetadata.indexMetadataIds
.map((indexId) => flatIndexMaps.byId[indexId])
.filter(isDefined)
.map((index) => ({
id: index.id,
isUnique: index.isUnique,
indexFieldMetadatas: index.flatIndexFieldMetadatas.map(
(fieldMetadata) => ({
fieldMetadataId: fieldMetadata.fieldMetadataId,
}),
),
}));
const uniqueConstraintsFields = getUniqueConstraintsFields<
FieldMetadataEntity,
ObjectMetadataEntity
FlatFieldMetadata,
{
id: string;
indexMetadatas: typeof indexMetadatas;
fields: FlatFieldMetadata[];
}
>({
...objectMetadata,
fields: Object.values(objectMetadata.fieldsById),
id: flatObjectMetadata.id,
indexMetadatas,
fields,
});
const hasUniqueConstraintFieldFullyPopulated = uniqueConstraintsFields.some(
@@ -243,7 +283,7 @@ const checkNoRelationFieldConflictOrThrow = (
};
const computeUniqueConstraintCondition = (
uniqueConstraintFields: FieldMetadataEntity<FieldMetadataType>[],
uniqueConstraintFields: FlatFieldMetadata<FieldMetadataType>[],
connectObject: ConnectObject,
): UniqueConstraintCondition => {
return uniqueConstraintFields.reduce((acc, uniqueConstraintField) => {
@@ -271,7 +311,7 @@ const computeUniqueConstraintCondition = (
const checkUniqueConstraintsAreSameOrThrow = (
relationConnectQueryConfig: RelationConnectQueryConfig,
uniqueConstraintFields: FieldMetadataEntity<FieldMetadataType>[],
uniqueConstraintFields: FlatFieldMetadata<FieldMetadataType>[],
) => {
if (
!deepEqual(
@@ -1,8 +1,9 @@
import { type FieldMetadataType } from 'twenty-shared/types';
import { type RelationType } from 'typeorm/metadata/types/RelationTypes';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
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 { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import {
RelationException,
RelationExceptionCode,
@@ -16,12 +17,13 @@ interface RelationDetails {
joinColumn: { name: string } | undefined;
}
export async function determineSchemaRelationDetails(
fieldMetadata: FieldMetadataEntity<
export function determineSchemaRelationDetails(
fieldMetadata: FlatFieldMetadata<
FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION
>,
objectMetadataMaps: ObjectMetadataMaps,
): Promise<RelationDetails> {
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
): RelationDetails {
if (!fieldMetadata.settings) {
throw new Error('Field metadata settings are missing');
}
@@ -38,9 +40,9 @@ export async function determineSchemaRelationDetails(
}
const sourceObjectMetadata =
objectMetadataMaps.byId[fieldMetadata.objectMetadataId];
flatObjectMetadataMaps.byId[fieldMetadata.objectMetadataId];
const targetObjectMetadata =
objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId];
flatObjectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId];
if (!sourceObjectMetadata || !targetObjectMetadata) {
throw new RelationException(
@@ -57,9 +59,7 @@ export async function determineSchemaRelationDetails(
}
const targetFieldMetadata =
targetObjectMetadata.fieldsById[
fieldMetadata.relationTargetFieldMetadataId
];
flatFieldMetadataMaps.byId[fieldMetadata.relationTargetFieldMetadataId];
if (!targetFieldMetadata) {
throw new Error('Target field metadata not found');
@@ -4,14 +4,17 @@ import {
} from 'twenty-shared/types';
import { capitalize } from 'twenty-shared/utils';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
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 { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
export function formatData<T>(
data: T,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
flatObjectMetadata: FlatObjectMetadata,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
): T {
if (!data) {
return data;
@@ -19,24 +22,27 @@ export function formatData<T>(
if (Array.isArray(data)) {
return data.map((item) =>
formatData(item, objectMetadataItemWithFieldMaps),
formatData(item, flatObjectMetadata, flatFieldMetadataMaps),
) as T;
}
const { fieldIdByName, fieldIdByJoinColumnName } =
buildFieldMapsFromFlatObjectMetadata(
flatFieldMetadataMaps,
flatObjectMetadata,
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const newData: Record<string, any> = {};
for (const [key, value] of Object.entries(data)) {
const fieldMetadataId =
objectMetadataItemWithFieldMaps.fieldIdByName[key] ||
objectMetadataItemWithFieldMaps.fieldIdByJoinColumnName[key];
const fieldMetadataId = fieldIdByName[key] || fieldIdByJoinColumnName[key];
const fieldMetadata =
objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId];
const fieldMetadata = flatFieldMetadataMaps.byId[fieldMetadataId];
if (!fieldMetadata) {
throw new Error(
`Field metadata for field "${key}" is missing in object metadata ${objectMetadataItemWithFieldMaps.nameSingular}`,
`Field metadata for field "${key}" is missing in object metadata ${flatObjectMetadata.nameSingular}`,
);
}
@@ -58,7 +64,7 @@ export function formatData<T>(
export function formatCompositeField(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any,
fieldMetadata: FieldMetadataEntity,
fieldMetadata: FlatFieldMetadata,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): Record<string, any> {
const compositeType = compositeTypeDefinitions.get(
@@ -81,7 +87,7 @@ export function formatCompositeField(
if (value && value[subFieldKey] !== undefined) {
formattedCompositeField[fullFieldName] = formatFieldMetadataValue(
value[subFieldKey],
property as unknown as FieldMetadataEntity,
property as unknown as FlatFieldMetadata,
);
}
}
@@ -92,7 +98,7 @@ export function formatCompositeField(
function formatFieldMetadataValue(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any,
fieldMetadata: FieldMetadataEntity,
fieldMetadata: FlatFieldMetadata,
) {
if (
fieldMetadata.type === FieldMetadataType.RAW_JSON &&
@@ -12,18 +12,21 @@ import {
DEFAULT_COMPOSITE_FIELDS_NULL_EQUIVALENT_VALUE,
DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE,
} from 'src/engine/api/common/common-args-processors/data-arg-processor/constants/null-equivalent-values.constant';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
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 { 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 { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { getCompositeFieldMetadataCollection } from 'src/engine/twenty-orm/utils/get-composite-field-metadata-collection';
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
export function formatResult<T>(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: any,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps | undefined,
objectMetadataMaps: ObjectMetadataMaps,
flatObjectMetadata: FlatObjectMetadata | undefined,
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
): T {
if (!isDefined(data)) {
return data;
@@ -31,7 +34,12 @@ export function formatResult<T>(
if (Array.isArray(data)) {
return data.map((item) =>
formatResult(item, objectMetadataItemWithFieldMaps, objectMetadataMaps),
formatResult(
item,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
),
) as T;
}
@@ -39,12 +47,18 @@ export function formatResult<T>(
return data;
}
if (!objectMetadataItemWithFieldMaps) {
if (!flatObjectMetadata) {
throw new Error('Object metadata is missing');
}
const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata(
flatFieldMetadataMaps,
flatObjectMetadata,
);
const compositeFieldMetadataMap = getCompositeFieldMetadataMap(
objectMetadataItemWithFieldMaps,
flatObjectMetadata,
flatFieldMetadataMaps,
);
const newData: object = {};
@@ -53,14 +67,12 @@ export function formatResult<T>(
const compositePropertyArgs = compositeFieldMetadataMap.get(key);
const fieldMetadataId =
objectMetadataItemWithFieldMaps.fieldIdByName[key] ||
objectMetadataItemWithFieldMaps.fieldIdByName[
compositePropertyArgs?.parentField ?? ''
];
fieldIdByName[key] ||
fieldIdByName[compositePropertyArgs?.parentField ?? ''];
const fieldMetadata = objectMetadataItemWithFieldMaps.fieldsById[
fieldMetadataId
] as FieldMetadataEntity<FieldMetadataType> | undefined;
const fieldMetadata = flatFieldMetadataMaps.byId[fieldMetadataId] as
| FlatFieldMetadata<FieldMetadataType>
| undefined;
const isRelation = fieldMetadata
? isFieldMetadataEntityOfType(fieldMetadata, FieldMetadataType.RELATION)
@@ -71,8 +83,9 @@ export function formatResult<T>(
// @ts-expect-error legacy noImplicitAny
newData[key] = formatResult(
value,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
);
} else if (fieldMetadata) {
// @ts-expect-error legacy noImplicitAny
@@ -93,7 +106,9 @@ export function formatResult<T>(
}
const targetObjectMetadata =
objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId];
flatObjectMetadataMaps.byId[
fieldMetadata.relationTargetObjectMetadataId
];
if (!targetObjectMetadata) {
throw new Error(
@@ -105,7 +120,8 @@ export function formatResult<T>(
newData[key] = formatResult(
value,
targetObjectMetadata,
objectMetadataMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
);
}
@@ -131,8 +147,9 @@ export function formatResult<T>(
: value;
}
const fieldMetadataItemsOfTypeDateOnly = Object.values(
objectMetadataItemWithFieldMaps.fieldsById,
const fieldMetadataItemsOfTypeDateOnly = getFlatFieldsFromFlatObjectMetadata(
flatObjectMetadata,
flatFieldMetadataMaps,
).filter((field) => field.type === FieldMetadataType.DATE);
for (const dateField of fieldMetadataItemsOfTypeDateOnly) {
@@ -151,10 +168,12 @@ export function formatResult<T>(
}
export function getCompositeFieldMetadataMap(
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
flatObjectMetadata: FlatObjectMetadata,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
) {
const compositeFieldMetadataCollection = getCompositeFieldMetadataCollection(
objectMetadataItemWithFieldMaps,
flatObjectMetadata,
flatFieldMetadataMaps,
);
return new Map(
@@ -211,7 +230,7 @@ function formatFieldMetadataValue(
function transformCompositeFieldNullValue(
value: unknown,
compositePropertyName: string,
fieldMetadata: FieldMetadataEntity,
fieldMetadata: FlatFieldMetadata,
) {
if (!isNull(value)) return value;
@@ -12,7 +12,9 @@ import type { ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/typ
import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event';
import { ObjectRecordUpsertEvent } from 'src/engine/core-modules/event-emitter/types/object-record-upsert.event';
import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-values';
import type { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
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 type { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import {
TwentyORMException,
TwentyORMExceptionCode,
@@ -24,13 +26,15 @@ export const formatTwentyOrmEventToDatabaseBatchEvent = <
>({
action,
objectMetadataItem,
flatFieldMetadataMaps,
workspaceId,
authContext,
entities,
beforeEntities,
}: {
action: DatabaseEventAction;
objectMetadataItem: ObjectMetadataItemWithFieldMaps;
objectMetadataItem: FlatObjectMetadata;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
workspaceId: string;
authContext?: AuthContext;
entities: T | T[];
@@ -41,7 +45,6 @@ export const formatTwentyOrmEventToDatabaseBatchEvent = <
}
const objectMetadataNameSingular = objectMetadataItem.nameSingular;
const fields = Object.values(objectMetadataItem.fieldsById ?? {});
const entityArray = isDefined(entities)
? Array.isArray(entities)
? entities
@@ -89,6 +92,7 @@ export const formatTwentyOrmEventToDatabaseBatchEvent = <
before,
after,
objectMetadataItem,
flatFieldMetadataMaps,
) as Partial<ObjectRecordDiff<T>>;
const updatedFields = Object.keys(diff);
@@ -158,6 +162,7 @@ export const formatTwentyOrmEventToDatabaseBatchEvent = <
before ?? {},
after,
objectMetadataItem,
flatFieldMetadataMaps,
) as Partial<ObjectRecordDiff<T>>;
updatedFields = Object.keys(diff);
@@ -183,7 +188,7 @@ export const formatTwentyOrmEventToDatabaseBatchEvent = <
objectMetadataNameSingular,
action,
events,
objectMetadata: { ...objectMetadataItem, fields },
objectMetadata: objectMetadataItem,
workspaceId,
};
};
@@ -1,16 +1,18 @@
import { isDefined } from 'twenty-shared/utils';
import { type CompositeType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
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 { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import {
type ColumnNameProcessor,
processFieldMetadataForColumnNameMapping,
} from 'src/engine/twenty-orm/utils/process-field-metadata-for-column-name-mapping.util';
export function getColumnNameToFieldMetadataIdMap(
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
flatObjectMetadata: FlatObjectMetadata,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
) {
const columnNameToFieldMetadataIdMap: Record<string, string> = {};
@@ -21,7 +23,7 @@ export function getColumnNameToFieldMetadataIdMap(
compositeType,
}: {
fieldMetadataId: string;
fieldMetadata: FieldMetadataEntity;
fieldMetadata: FlatFieldMetadata;
compositeType: CompositeType;
}) => {
compositeType.properties.forEach((compositeProperty) => {
@@ -59,7 +61,8 @@ export function getColumnNameToFieldMetadataIdMap(
};
processFieldMetadataForColumnNameMapping(
objectMetadataItemWithFieldMaps,
flatObjectMetadata,
flatFieldMetadataMaps,
processor,
);
@@ -1,11 +1,16 @@
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
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 { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
export function getCompositeFieldMetadataCollection(
ObjectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
flatObjectMetadata: FlatObjectMetadata,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
) {
const compositeFieldMetadataCollection = Object.values(
ObjectMetadataItemWithFieldMaps.fieldsById,
const compositeFieldMetadataCollection = getFlatFieldsFromFlatObjectMetadata(
flatObjectMetadata,
flatFieldMetadataMaps,
).filter((fieldMetadataItem) =>
isCompositeFieldMetadataType(fieldMetadataItem.type),
);
@@ -1,15 +1,17 @@
import { type CompositeType } from 'twenty-shared/types';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
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 { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import {
type ColumnNameProcessor,
processFieldMetadataForColumnNameMapping,
} from 'src/engine/twenty-orm/utils/process-field-metadata-for-column-name-mapping.util';
export function getFieldMetadataIdToColumnNamesMap(
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
flatObjectMetadata: FlatObjectMetadata,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
) {
const fieldMetadataToColumnNamesMap = new Map<string, string[]>();
@@ -20,7 +22,7 @@ export function getFieldMetadataIdToColumnNamesMap(
compositeType,
}: {
fieldMetadataId: string;
fieldMetadata: FieldMetadataEntity;
fieldMetadata: FlatFieldMetadata;
compositeType: CompositeType;
}) => {
compositeType.properties.forEach((compositeProperty) => {
@@ -52,7 +54,7 @@ export function getFieldMetadataIdToColumnNamesMap(
columnName,
}: {
fieldMetadataId: string;
fieldMetadata: FieldMetadataEntity;
fieldMetadata: FlatFieldMetadata;
columnName: string;
}) => {
fieldMetadataToColumnNamesMap.set(fieldMetadataId, [columnName]);
@@ -60,7 +62,8 @@ export function getFieldMetadataIdToColumnNamesMap(
};
processFieldMetadataForColumnNameMapping(
objectMetadataItemWithFieldMaps,
flatObjectMetadata,
flatFieldMetadataMaps,
processor,
);
@@ -6,8 +6,7 @@ import {
import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import {
TwentyORMException,
TwentyORMExceptionCode,
@@ -17,7 +16,7 @@ import { WorkspaceEntitiesStorage } from 'src/engine/twenty-orm/storage/workspac
export const getObjectMetadataFromEntityTarget = <T extends ObjectLiteral>(
entityTarget: EntityTarget<T>,
internalContext: WorkspaceInternalContext,
): ObjectMetadataItemWithFieldMaps => {
): FlatObjectMetadata => {
const objectMetadataName =
typeof entityTarget === 'string'
? entityTarget
@@ -33,22 +32,29 @@ export const getObjectMetadataFromEntityTarget = <T extends ObjectLiteral>(
);
}
const objectMetadata = getObjectMetadataMapItemByNameSingular(
internalContext.objectMetadataMaps,
objectMetadataName,
);
const objectMetadataId =
internalContext.objectIdByNameSingular[objectMetadataName];
if (!objectMetadata) {
if (!objectMetadataId) {
throw new TwentyORMException(
`Object metadata for object "${objectMetadataName}" is missing ` +
`in workspace "${internalContext.workspaceId}" ` +
`with object metadata collection length: ${
Object.keys(internalContext.objectMetadataMaps.idByNameSingular)
.length
Object.keys(internalContext.objectIdByNameSingular).length
}`,
TwentyORMExceptionCode.MALFORMED_METADATA,
);
}
const objectMetadata =
internalContext.flatObjectMetadataMaps.byId[objectMetadataId];
if (!objectMetadata) {
throw new TwentyORMException(
`Object metadata for object "${objectMetadataName}" (id: ${objectMetadataId}) is missing`,
TwentyORMExceptionCode.MALFORMED_METADATA,
);
}
return objectMetadata;
};
@@ -1,5 +1,4 @@
import {
type FieldMetadataRelationSettings,
type CompositeType,
compositeTypeDefinitions,
} from 'twenty-shared/types';
@@ -7,15 +6,17 @@ import {
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { extractGraphQLRelationFieldNames } from 'src/engine/api/graphql/workspace-schema-builder/utils/extract-graphql-relation-field-names.util';
import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { computeColumnName } 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 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 { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import {
PermissionsException,
PermissionsExceptionCode,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
export type ColumnNameProcessor = {
processCompositeField: ({
@@ -24,7 +25,7 @@ export type ColumnNameProcessor = {
compositeType,
}: {
fieldMetadataId: string;
fieldMetadata: FieldMetadataEntity;
fieldMetadata: FlatFieldMetadata;
compositeType: CompositeType;
}) => void;
processRelationField: ({
@@ -34,7 +35,7 @@ export type ColumnNameProcessor = {
connectFieldName,
}: {
fieldMetadataId: string;
fieldMetadata: FieldMetadataEntity;
fieldMetadata: FlatFieldMetadata;
joinColumnName: string;
connectFieldName?: string;
}) => void;
@@ -44,18 +45,22 @@ export type ColumnNameProcessor = {
columnName,
}: {
fieldMetadataId: string;
fieldMetadata: FieldMetadataEntity;
fieldMetadata: FlatFieldMetadata;
columnName: string;
}) => void;
};
export function processFieldMetadataForColumnNameMapping(
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
flatObjectMetadata: FlatObjectMetadata,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
processor: ColumnNameProcessor,
) {
for (const [fieldMetadataId, fieldMetadata] of Object.entries(
objectMetadataItemWithFieldMaps.fieldsById,
)) {
for (const fieldMetadataId of flatObjectMetadata.fieldMetadataIds) {
const fieldMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatFieldMetadataMaps,
flatEntityId: fieldMetadataId,
});
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
@@ -72,9 +77,8 @@ export function processFieldMetadataForColumnNameMapping(
compositeType,
});
} else {
if (isFieldMetadataRelationOrMorphRelation(fieldMetadata)) {
const fieldMetadataSettings =
fieldMetadata.settings as FieldMetadataRelationSettings;
if (isMorphOrRelationFlatFieldMetadata(fieldMetadata)) {
const fieldMetadataSettings = fieldMetadata.settings;
if (fieldMetadataSettings?.relationType === RelationType.ONE_TO_MANY) {
continue;