add WorkspaceDuplicateCriteria decorator + update duplicate resolver logic (#10128)

## Context

All objects have '...duplicates' resolver but only companies and people
have duplicate criteria (hard coded constant).
Gql schema and resolver should be created only if duplicate criteria
exist.

## Solution

- Add a new @WorkspaceDuplicateCriteria decorator at object level,
defining duplicate criteria for given object.
- Add a new duplicate criteria field in ObjectMetadata table
- Update schema and resolver building logic
- Update front requests for duplicate check (only for object with
criteria defined)



closes https://github.com/twentyhq/twenty/issues/9828
This commit is contained in:
Etienne
2025-02-12 17:32:59 +01:00
committed by GitHub
parent b66289c44c
commit 0609b31c64
29 changed files with 491 additions and 121 deletions

View File

@ -0,0 +1,14 @@
import { WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type';
import { TypedReflect } from 'src/utils/typed-reflect';
export function WorkspaceDuplicateCriteria(
duplicateCriteria: WorkspaceEntityDuplicateCriteria[],
): ClassDecorator {
return (target) => {
TypedReflect.defineMetadata(
'workspace:duplicate-criteria-metadata-args',
duplicateCriteria,
target,
);
};
}

View File

@ -33,6 +33,11 @@ export function WorkspaceEntity(
'workspace:gate-metadata-args',
target,
);
const duplicateCriteria = TypedReflect.getMetadata(
'workspace:duplicate-criteria-metadata-args',
target,
);
const objectName = convertClassNameToObjectMetadataName(target.name);
metadataArgsStorage.addEntities({
@ -51,6 +56,7 @@ export function WorkspaceEntity(
isAuditLogged,
isSystem,
gate,
duplicateCriteria,
});
};
}

View File

@ -1,5 +1,7 @@
import { Gate } from 'src/engine/twenty-orm/interfaces/gate.interface';
import { WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type';
export interface WorkspaceEntityMetadataArgs {
/**
* Standard id.
@ -65,4 +67,9 @@ export interface WorkspaceEntityMetadataArgs {
* Image identifier.
*/
readonly imageIdentifierStandardId: string | null;
/**
* Duplicate criteria.
*/
readonly duplicateCriteria?: WorkspaceEntityDuplicateCriteria[];
}

View File

@ -40,27 +40,10 @@ export function formatResult<T>(
throw new Error('Object metadata is missing');
}
const compositeFieldMetadataCollection = getCompositeFieldMetadataCollection(
const compositeFieldMetadataMap = getCompositeFieldMetadataMap(
objectMetadataItemWithFieldMaps,
);
const compositeFieldMetadataMap = new Map(
compositeFieldMetadataCollection.flatMap((fieldMetadata) => {
const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
if (!compositeType) return [];
// Map each composite property to a [key, value] pair
return compositeType.properties.map((compositeProperty) => [
computeCompositeColumnName(fieldMetadata.name, compositeProperty),
{
parentField: fieldMetadata.name,
...compositeProperty,
},
]);
}),
);
const relationMetadataMap = new Map(
Object.values(objectMetadataItemWithFieldMaps.fieldsById)
.filter(({ type }) => isRelationFieldMetadataType(type))
@ -199,6 +182,31 @@ export function formatResult<T>(
return newData as T;
}
export function getCompositeFieldMetadataMap(
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
) {
const compositeFieldMetadataCollection = getCompositeFieldMetadataCollection(
objectMetadataItemWithFieldMaps,
);
return new Map(
compositeFieldMetadataCollection.flatMap((fieldMetadata) => {
const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
if (!compositeType) return [];
// Map each composite property to a [key, value] pair
return compositeType.properties.map((compositeProperty) => [
computeCompositeColumnName(fieldMetadata.name, compositeProperty),
{
parentField: fieldMetadata.name,
...compositeProperty,
},
]);
}),
);
}
function formatFieldMetadataValue(
value: any,
fieldMetadata: FieldMetadataInterface,