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:
@ -32,7 +32,7 @@ const documents = {
|
|||||||
"\n mutation DeleteOneObjectMetadataItem($idToDelete: UUID!) {\n deleteOneObject(input: { id: $idToDelete }) {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isActive\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n }\n }\n": types.DeleteOneObjectMetadataItemDocument,
|
"\n mutation DeleteOneObjectMetadataItem($idToDelete: UUID!) {\n deleteOneObject(input: { id: $idToDelete }) {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isActive\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n }\n }\n": types.DeleteOneObjectMetadataItemDocument,
|
||||||
"\n mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) {\n deleteOneField(input: { id: $idToDelete }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n }\n }\n": types.DeleteOneFieldMetadataItemDocument,
|
"\n mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) {\n deleteOneField(input: { id: $idToDelete }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n }\n }\n": types.DeleteOneFieldMetadataItemDocument,
|
||||||
"\n mutation DeleteOneRelationMetadataItem($idToDelete: UUID!) {\n deleteOneRelation(input: { id: $idToDelete }) {\n id\n }\n }\n": types.DeleteOneRelationMetadataItemDocument,
|
"\n mutation DeleteOneRelationMetadataItem($idToDelete: UUID!) {\n deleteOneRelation(input: { id: $idToDelete }) {\n id\n }\n }\n": types.DeleteOneRelationMetadataItemDocument,
|
||||||
"\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n": types.ObjectMetadataItemsDocument,
|
"\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n duplicateCriteria\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n": types.ObjectMetadataItemsDocument,
|
||||||
"\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n syncStatus\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc,
|
"\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n syncStatus\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc,
|
||||||
"\n \n mutation BuildDraftServerlessFunction(\n $input: BuildDraftServerlessFunctionInput!\n ) {\n buildDraftServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.BuildDraftServerlessFunctionDocument,
|
"\n \n mutation BuildDraftServerlessFunction(\n $input: BuildDraftServerlessFunctionInput!\n ) {\n buildDraftServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.BuildDraftServerlessFunctionDocument,
|
||||||
"\n \n mutation CreateOneServerlessFunctionItem(\n $input: CreateServerlessFunctionInput!\n ) {\n createOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.CreateOneServerlessFunctionItemDocument,
|
"\n \n mutation CreateOneServerlessFunctionItem(\n $input: CreateServerlessFunctionInput!\n ) {\n createOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.CreateOneServerlessFunctionItemDocument,
|
||||||
@ -139,7 +139,7 @@ export function graphql(source: "\n mutation DeleteOneRelationMetadataItem($idT
|
|||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
export function graphql(source: "\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"): (typeof documents)["\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"];
|
export function graphql(source: "\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n duplicateCriteria\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"): (typeof documents)["\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n duplicateCriteria\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"];
|
||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -1070,6 +1070,7 @@ export type Object = {
|
|||||||
createdAt: Scalars['DateTime'];
|
createdAt: Scalars['DateTime'];
|
||||||
dataSourceId: Scalars['String'];
|
dataSourceId: Scalars['String'];
|
||||||
description?: Maybe<Scalars['String']>;
|
description?: Maybe<Scalars['String']>;
|
||||||
|
duplicateCriteria?: Maybe<Array<Array<Scalars['String']>>>;
|
||||||
fields: ObjectFieldsConnection;
|
fields: ObjectFieldsConnection;
|
||||||
fieldsList: Array<Field>;
|
fieldsList: Array<Field>;
|
||||||
icon?: Maybe<Scalars['String']>;
|
icon?: Maybe<Scalars['String']>;
|
||||||
|
|||||||
@ -23,6 +23,7 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
|
|||||||
imageIdentifierFieldMetadataId
|
imageIdentifierFieldMetadataId
|
||||||
shortcut
|
shortcut
|
||||||
isLabelSyncedWithName
|
isLabelSyncedWithName
|
||||||
|
duplicateCriteria
|
||||||
indexMetadatas(paging: { first: 100 }) {
|
indexMetadatas(paging: { first: 100 }) {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
|
|||||||
@ -17,6 +17,7 @@ export const useFindDuplicateRecords = <T extends ObjectRecord = ObjectRecord>({
|
|||||||
objectRecordIds = [],
|
objectRecordIds = [],
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
onCompleted,
|
onCompleted,
|
||||||
|
skip,
|
||||||
}: ObjectMetadataItemIdentifier & {
|
}: ObjectMetadataItemIdentifier & {
|
||||||
objectRecordIds: string[] | undefined;
|
objectRecordIds: string[] | undefined;
|
||||||
onCompleted?: (data: RecordGqlConnection[]) => void;
|
onCompleted?: (data: RecordGqlConnection[]) => void;
|
||||||
@ -42,6 +43,7 @@ export const useFindDuplicateRecords = <T extends ObjectRecord = ObjectRecord>({
|
|||||||
useQuery<RecordGqlOperationFindDuplicatesResult>(
|
useQuery<RecordGqlOperationFindDuplicatesResult>(
|
||||||
findDuplicateRecordsQuery,
|
findDuplicateRecordsQuery,
|
||||||
{
|
{
|
||||||
|
skip: !!skip,
|
||||||
variables: {
|
variables: {
|
||||||
ids: objectRecordIds,
|
ids: objectRecordIds,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||||
import { useFindDuplicateRecords } from '@/object-record/hooks/useFindDuplicateRecords';
|
import { useFindDuplicateRecords } from '@/object-record/hooks/useFindDuplicateRecords';
|
||||||
import { RecordDetailRecordsList } from '@/object-record/record-show/record-detail-section/components/RecordDetailRecordsList';
|
import { RecordDetailRecordsList } from '@/object-record/record-show/record-detail-section/components/RecordDetailRecordsList';
|
||||||
import { RecordDetailRecordsListItem } from '@/object-record/record-show/record-detail-section/components/RecordDetailRecordsListItem';
|
import { RecordDetailRecordsListItem } from '@/object-record/record-show/record-detail-section/components/RecordDetailRecordsListItem';
|
||||||
import { RecordDetailSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailSection';
|
import { RecordDetailSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailSection';
|
||||||
import { RecordDetailSectionHeader } from '@/object-record/record-show/record-detail-section/components/RecordDetailSectionHeader';
|
import { RecordDetailSectionHeader } from '@/object-record/record-show/record-detail-section/components/RecordDetailSectionHeader';
|
||||||
|
import { isDefined } from 'twenty-shared';
|
||||||
|
|
||||||
export const RecordDetailDuplicatesSection = ({
|
export const RecordDetailDuplicatesSection = ({
|
||||||
objectRecordId,
|
objectRecordId,
|
||||||
@ -12,9 +14,14 @@ export const RecordDetailDuplicatesSection = ({
|
|||||||
objectRecordId: string;
|
objectRecordId: string;
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
|
objectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
const { results: queryResults } = useFindDuplicateRecords({
|
const { results: queryResults } = useFindDuplicateRecords({
|
||||||
objectRecordIds: [objectRecordId],
|
objectRecordIds: [objectRecordId],
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
|
skip: !isDefined(objectMetadataItem.duplicateCriteria),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!queryResults || !queryResults[0] || queryResults[0].length === 0)
|
if (!queryResults || !queryResults[0] || queryResults[0].length === 0)
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddDuplicateCriteriaColumnInObjectMetadata1738853620654
|
||||||
|
implements MigrationInterface
|
||||||
|
{
|
||||||
|
name = 'AddDuplicateCriteriaColumnInObjectMetadata1738853620654';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."objectMetadata" ADD "duplicateCriteria" jsonb`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."objectMetadata" DROP COLUMN "duplicateCriteria"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,93 @@
|
|||||||
|
import { FieldMetadataType } from 'twenty-shared';
|
||||||
|
|
||||||
|
import { WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type';
|
||||||
|
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||||
|
|
||||||
|
export const mockPersonObjectMetadata = (
|
||||||
|
duplicateCriteria: WorkspaceEntityDuplicateCriteria[],
|
||||||
|
): ObjectMetadataItemWithFieldMaps => ({
|
||||||
|
id: '',
|
||||||
|
standardId: '',
|
||||||
|
nameSingular: 'person',
|
||||||
|
namePlural: 'people',
|
||||||
|
labelSingular: 'Person',
|
||||||
|
labelPlural: 'People',
|
||||||
|
description: 'A person',
|
||||||
|
targetTableName: 'DEPRECATED',
|
||||||
|
isCustom: false,
|
||||||
|
isRemote: false,
|
||||||
|
isActive: true,
|
||||||
|
isSystem: false,
|
||||||
|
isAuditLogged: true,
|
||||||
|
duplicateCriteria: duplicateCriteria,
|
||||||
|
fromRelations: [],
|
||||||
|
toRelations: [],
|
||||||
|
labelIdentifierFieldMetadataId: '',
|
||||||
|
imageIdentifierFieldMetadataId: '',
|
||||||
|
workspaceId: '',
|
||||||
|
fields: [],
|
||||||
|
indexMetadatas: [],
|
||||||
|
fieldsById: {},
|
||||||
|
fieldsByName: {
|
||||||
|
name: {
|
||||||
|
id: '',
|
||||||
|
objectMetadataId: '',
|
||||||
|
type: FieldMetadataType.FULL_NAME,
|
||||||
|
name: 'name',
|
||||||
|
label: 'Name',
|
||||||
|
defaultValue: {
|
||||||
|
lastName: "''",
|
||||||
|
firstName: "''",
|
||||||
|
},
|
||||||
|
description: 'Contact’s name',
|
||||||
|
isCustom: false,
|
||||||
|
isNullable: true,
|
||||||
|
isUnique: false,
|
||||||
|
workspaceId: '',
|
||||||
|
},
|
||||||
|
emails: {
|
||||||
|
id: '',
|
||||||
|
objectMetadataId: '',
|
||||||
|
type: FieldMetadataType.EMAILS,
|
||||||
|
name: 'emails',
|
||||||
|
label: 'Emails',
|
||||||
|
defaultValue: {
|
||||||
|
primaryEmail: "''",
|
||||||
|
additionalEmails: null,
|
||||||
|
},
|
||||||
|
description: 'Contact’s Emails',
|
||||||
|
isCustom: false,
|
||||||
|
workspaceId: '',
|
||||||
|
},
|
||||||
|
linkedinLink: {
|
||||||
|
id: '',
|
||||||
|
objectMetadataId: '',
|
||||||
|
type: FieldMetadataType.LINKS,
|
||||||
|
name: 'linkedinLink',
|
||||||
|
label: 'Linkedin',
|
||||||
|
defaultValue: {
|
||||||
|
primaryLinkUrl: "''",
|
||||||
|
secondaryLinks: "'[]'",
|
||||||
|
primaryLinkLabel: "''",
|
||||||
|
},
|
||||||
|
description: 'Contact’s Linkedin account',
|
||||||
|
isCustom: false,
|
||||||
|
isNullable: true,
|
||||||
|
isUnique: false,
|
||||||
|
workspaceId: '',
|
||||||
|
},
|
||||||
|
jobTitle: {
|
||||||
|
id: '',
|
||||||
|
objectMetadataId: '',
|
||||||
|
type: FieldMetadataType.TEXT,
|
||||||
|
name: 'jobTitle',
|
||||||
|
label: 'Job Title',
|
||||||
|
defaultValue: "''",
|
||||||
|
description: 'Contact’s job title',
|
||||||
|
isCustom: false,
|
||||||
|
isNullable: false,
|
||||||
|
isUnique: false,
|
||||||
|
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||||
|
|
||||||
|
export const mockPersonRecords: Partial<ObjectRecord>[] = [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
firstName: 'Testfirst',
|
||||||
|
lastName: 'Testlast',
|
||||||
|
},
|
||||||
|
emails: {
|
||||||
|
primaryEmail: 'test@test.fr',
|
||||||
|
additionalEmails: [],
|
||||||
|
},
|
||||||
|
linkedinLink: {
|
||||||
|
primaryLinkLabel: '',
|
||||||
|
primaryLinkUrl: '',
|
||||||
|
secondaryLinks: [],
|
||||||
|
},
|
||||||
|
jobTitle: 'Test job',
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -0,0 +1,125 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
|
import { mockPersonObjectMetadata } from 'src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata';
|
||||||
|
import { mockPersonRecords } from 'src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonRecords';
|
||||||
|
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
|
||||||
|
import { GraphqlQueryFindDuplicatesResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service';
|
||||||
|
import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service';
|
||||||
|
import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory';
|
||||||
|
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
|
||||||
|
import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service';
|
||||||
|
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||||
|
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||||
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
|
|
||||||
|
describe('GraphqlQueryFindDuplicatesResolverService', () => {
|
||||||
|
let service: GraphqlQueryFindDuplicatesResolverService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
GraphqlQueryFindDuplicatesResolverService,
|
||||||
|
WorkspaceQueryHookService,
|
||||||
|
QueryRunnerArgsFactory,
|
||||||
|
QueryResultGettersFactory,
|
||||||
|
ApiEventEmitterService,
|
||||||
|
TwentyORMGlobalManager,
|
||||||
|
ProcessNestedRelationsHelper,
|
||||||
|
FeatureFlagService,
|
||||||
|
PermissionsService,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.overrideProvider(WorkspaceQueryHookService)
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(QueryRunnerArgsFactory)
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(QueryResultGettersFactory)
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(ApiEventEmitterService)
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(TwentyORMGlobalManager)
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(ProcessNestedRelationsHelper)
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(FeatureFlagService)
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(PermissionsService)
|
||||||
|
.useValue({})
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
service = module.get<GraphqlQueryFindDuplicatesResolverService>(
|
||||||
|
GraphqlQueryFindDuplicatesResolverService,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buildDuplicateConditions', () => {
|
||||||
|
it('should build conditions based on duplicate criteria from composite field', () => {
|
||||||
|
const duplicateConditons = service.buildDuplicateConditions(
|
||||||
|
mockPersonObjectMetadata([['emailsPrimaryEmail']]),
|
||||||
|
mockPersonRecords,
|
||||||
|
'recordId',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(duplicateConditons).toEqual({
|
||||||
|
or: [
|
||||||
|
{
|
||||||
|
emailsPrimaryEmail: {
|
||||||
|
eq: 'test@test.fr',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: {
|
||||||
|
neq: 'recordId',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should build conditions based on duplicate criteria from basic field', () => {
|
||||||
|
const duplicateConditons = service.buildDuplicateConditions(
|
||||||
|
mockPersonObjectMetadata([['jobTitle']]),
|
||||||
|
mockPersonRecords,
|
||||||
|
'recordId',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(duplicateConditons).toEqual({
|
||||||
|
or: [
|
||||||
|
{
|
||||||
|
jobTitle: {
|
||||||
|
eq: 'Test job',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: {
|
||||||
|
neq: 'recordId',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not build conditions based on duplicate criteria if record value is null or too small', () => {
|
||||||
|
const duplicateConditons = service.buildDuplicateConditions(
|
||||||
|
mockPersonObjectMetadata([['linkedinLinkPrimaryLinkUrl']]),
|
||||||
|
mockPersonRecords,
|
||||||
|
'recordId',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(duplicateConditons).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should build conditions based on duplicate criteria and without recordId filter', () => {
|
||||||
|
const duplicateConditons = service.buildDuplicateConditions(
|
||||||
|
mockPersonObjectMetadata([['jobTitle']]),
|
||||||
|
mockPersonRecords,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(duplicateConditons).toEqual({
|
||||||
|
or: [
|
||||||
|
{
|
||||||
|
jobTitle: {
|
||||||
|
eq: 'Test job',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -23,11 +23,13 @@ import {
|
|||||||
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
|
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
|
||||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||||
import { settings } from 'src/engine/constants/settings';
|
import { settings } from 'src/engine/constants/settings';
|
||||||
import { DUPLICATE_CRITERIA_COLLECTION } from 'src/engine/core-modules/duplicate/constants/duplicate-criteria.constants';
|
|
||||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
import { 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 { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
|
||||||
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
|
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
|
||||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
import {
|
||||||
|
formatResult,
|
||||||
|
getCompositeFieldMetadataMap,
|
||||||
|
} from 'src/engine/twenty-orm/utils/format-result.util';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseResolverService<
|
export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseResolverService<
|
||||||
@ -149,7 +151,7 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR
|
|||||||
return duplicateConnections;
|
return duplicateConnections;
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildDuplicateConditions(
|
buildDuplicateConditions(
|
||||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
|
||||||
records?: Partial<ObjectRecord>[] | undefined,
|
records?: Partial<ObjectRecord>[] | undefined,
|
||||||
filteringByExistingRecordId?: string,
|
filteringByExistingRecordId?: string,
|
||||||
@ -158,13 +160,21 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const criteriaCollection = this.getApplicableDuplicateCriteriaCollection(
|
const criteriaCollection =
|
||||||
|
objectMetadataItemWithFieldMaps.duplicateCriteria || [];
|
||||||
|
|
||||||
|
const formattedRecords = formatData(
|
||||||
|
records,
|
||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
);
|
);
|
||||||
|
|
||||||
const conditions = records.flatMap((record) => {
|
const compositeFieldMetadataMap = getCompositeFieldMetadataMap(
|
||||||
|
objectMetadataItemWithFieldMaps,
|
||||||
|
);
|
||||||
|
|
||||||
|
const conditions = formattedRecords.flatMap((record) => {
|
||||||
const criteriaWithMatchingArgs = criteriaCollection.filter((criteria) =>
|
const criteriaWithMatchingArgs = criteriaCollection.filter((criteria) =>
|
||||||
criteria.columnNames.every((columnName) => {
|
criteria.every((columnName) => {
|
||||||
const value = record[columnName] as string | undefined;
|
const value = record[columnName] as string | undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -176,8 +186,18 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR
|
|||||||
return criteriaWithMatchingArgs.map((criteria) => {
|
return criteriaWithMatchingArgs.map((criteria) => {
|
||||||
const condition = {};
|
const condition = {};
|
||||||
|
|
||||||
criteria.columnNames.forEach((columnName) => {
|
criteria.forEach((columnName) => {
|
||||||
condition[columnName] = { eq: record[columnName] };
|
const compositeFieldMetadata =
|
||||||
|
compositeFieldMetadataMap.get(columnName);
|
||||||
|
|
||||||
|
if (compositeFieldMetadata) {
|
||||||
|
condition[compositeFieldMetadata.parentField] = {
|
||||||
|
...condition[compositeFieldMetadata.parentField],
|
||||||
|
[compositeFieldMetadata.name]: { eq: record[columnName] },
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
condition[columnName] = { eq: record[columnName] };
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return condition;
|
return condition;
|
||||||
@ -197,16 +217,6 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR
|
|||||||
return filter;
|
return filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getApplicableDuplicateCriteriaCollection(
|
|
||||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
|
|
||||||
) {
|
|
||||||
return DUPLICATE_CRITERIA_COLLECTION.filter(
|
|
||||||
(duplicateCriteria) =>
|
|
||||||
duplicateCriteria.objectName ===
|
|
||||||
objectMetadataItemWithFieldMaps.nameSingular,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async validate(
|
async validate(
|
||||||
args: FindDuplicatesResolverArgs,
|
args: FindDuplicatesResolverArgs,
|
||||||
_options: WorkspaceQueryRunnerOptions,
|
_options: WorkspaceQueryRunnerOptions,
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
type columnName = string;
|
||||||
|
|
||||||
|
export type WorkspaceEntityDuplicateCriteria = columnName[];
|
||||||
@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
|||||||
|
|
||||||
import { GraphqlQueryRunnerModule } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module';
|
import { GraphqlQueryRunnerModule } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module';
|
||||||
import { WorkspaceQueryRunnerModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module';
|
import { WorkspaceQueryRunnerModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module';
|
||||||
|
import { WorkspaceResolverBuilderService } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.service';
|
||||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||||
|
|
||||||
import { WorkspaceResolverFactory } from './workspace-resolver.factory';
|
import { WorkspaceResolverFactory } from './workspace-resolver.factory';
|
||||||
@ -14,7 +15,11 @@ import { workspaceResolverBuilderFactories } from './factories/factories';
|
|||||||
GraphqlQueryRunnerModule,
|
GraphqlQueryRunnerModule,
|
||||||
FeatureFlagModule,
|
FeatureFlagModule,
|
||||||
],
|
],
|
||||||
providers: [...workspaceResolverBuilderFactories, WorkspaceResolverFactory],
|
providers: [
|
||||||
exports: [WorkspaceResolverFactory],
|
...workspaceResolverBuilderFactories,
|
||||||
|
WorkspaceResolverFactory,
|
||||||
|
WorkspaceResolverBuilderService,
|
||||||
|
],
|
||||||
|
exports: [WorkspaceResolverFactory, WorkspaceResolverBuilderService],
|
||||||
})
|
})
|
||||||
export class WorkspaceResolverBuilderModule {}
|
export class WorkspaceResolverBuilderModule {}
|
||||||
|
|||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { isDefined } from 'twenty-shared';
|
||||||
|
|
||||||
|
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||||
|
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||||
|
|
||||||
|
import { FindDuplicatesResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WorkspaceResolverBuilderService {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
shouldBuildResolver(
|
||||||
|
objectMetadata: ObjectMetadataInterface,
|
||||||
|
methodName: WorkspaceResolverBuilderMethodNames,
|
||||||
|
) {
|
||||||
|
switch (methodName) {
|
||||||
|
case FindDuplicatesResolverFactory.methodName:
|
||||||
|
return isDefined(objectMetadata.duplicateCriteria);
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ import { RestoreManyResolverFactory } from 'src/engine/api/graphql/workspace-res
|
|||||||
import { RestoreOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-one-resolver.factory';
|
import { RestoreOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-one-resolver.factory';
|
||||||
import { SearchResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/search-resolver-factory';
|
import { SearchResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/search-resolver-factory';
|
||||||
import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory';
|
import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory';
|
||||||
|
import { WorkspaceResolverBuilderService } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.service';
|
||||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||||
import { getResolverName } from 'src/engine/utils/get-resolver-name.util';
|
import { getResolverName } from 'src/engine/utils/get-resolver-name.util';
|
||||||
@ -45,6 +46,7 @@ export class WorkspaceResolverFactory {
|
|||||||
private readonly restoreManyResolverFactory: RestoreManyResolverFactory,
|
private readonly restoreManyResolverFactory: RestoreManyResolverFactory,
|
||||||
private readonly destroyManyResolverFactory: DestroyManyResolverFactory,
|
private readonly destroyManyResolverFactory: DestroyManyResolverFactory,
|
||||||
private readonly searchResolverFactory: SearchResolverFactory,
|
private readonly searchResolverFactory: SearchResolverFactory,
|
||||||
|
private readonly workspaceResolverBuilderService: WorkspaceResolverBuilderService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async create(
|
async create(
|
||||||
@ -92,11 +94,18 @@ export class WorkspaceResolverFactory {
|
|||||||
throw new Error(`Unknown query resolver type: ${methodName}`);
|
throw new Error(`Unknown query resolver type: ${methodName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolvers.Query[resolverName] = resolverFactory.create({
|
if (
|
||||||
authContext,
|
this.workspaceResolverBuilderService.shouldBuildResolver(
|
||||||
objectMetadataMaps,
|
objectMetadata,
|
||||||
objectMetadataItemWithFieldMaps: objectMetadata,
|
methodName,
|
||||||
});
|
)
|
||||||
|
) {
|
||||||
|
resolvers.Query[resolverName] = resolverFactory.create({
|
||||||
|
authContext,
|
||||||
|
objectMetadataMaps,
|
||||||
|
objectMetadataItemWithFieldMaps: objectMetadata,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate mutation resolvers
|
// Generate mutation resolvers
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/work
|
|||||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||||
|
|
||||||
|
import { WorkspaceResolverBuilderService } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.service';
|
||||||
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||||
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
|
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
|
||||||
import { getResolverArgs } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util';
|
import { getResolverArgs } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util';
|
||||||
@ -28,6 +29,7 @@ export class RootTypeFactory {
|
|||||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||||
private readonly typeMapperService: TypeMapperService,
|
private readonly typeMapperService: TypeMapperService,
|
||||||
private readonly argsFactory: ArgsFactory,
|
private readonly argsFactory: ArgsFactory,
|
||||||
|
private readonly workspaceResolverBuilderService: WorkspaceResolverBuilderService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
create(
|
create(
|
||||||
@ -70,53 +72,60 @@ export class RootTypeFactory {
|
|||||||
|
|
||||||
for (const objectMetadata of objectMetadataCollection) {
|
for (const objectMetadata of objectMetadataCollection) {
|
||||||
for (const methodName of workspaceResolverMethodNames) {
|
for (const methodName of workspaceResolverMethodNames) {
|
||||||
const name = getResolverName(objectMetadata, methodName);
|
if (
|
||||||
const args = getResolverArgs(methodName);
|
this.workspaceResolverBuilderService.shouldBuildResolver(
|
||||||
const objectType = this.typeDefinitionsStorage.getObjectTypeByKey(
|
objectMetadata,
|
||||||
objectMetadata.id,
|
methodName,
|
||||||
this.getObjectTypeDefinitionKindByMethodName(methodName),
|
)
|
||||||
);
|
) {
|
||||||
const argsType = this.argsFactory.create(
|
const name = getResolverName(objectMetadata, methodName);
|
||||||
{
|
const args = getResolverArgs(methodName);
|
||||||
args,
|
const objectType = this.typeDefinitionsStorage.getObjectTypeByKey(
|
||||||
objectMetadataId: objectMetadata.id,
|
objectMetadata.id,
|
||||||
},
|
this.getObjectTypeDefinitionKindByMethodName(methodName),
|
||||||
options,
|
);
|
||||||
);
|
const argsType = this.argsFactory.create(
|
||||||
|
|
||||||
if (!objectType) {
|
|
||||||
this.logger.error(
|
|
||||||
`Could not find a GraphQL type for ${objectMetadata.id} for method ${methodName}`,
|
|
||||||
{
|
{
|
||||||
objectMetadata,
|
args,
|
||||||
methodName,
|
objectMetadataId: objectMetadata.id,
|
||||||
options,
|
|
||||||
},
|
},
|
||||||
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new Error(
|
if (!objectType) {
|
||||||
`Could not find a GraphQL type for ${objectMetadata.id} for method ${methodName}`,
|
this.logger.error(
|
||||||
);
|
`Could not find a GraphQL type for ${objectMetadata.id} for method ${methodName}`,
|
||||||
|
{
|
||||||
|
objectMetadata,
|
||||||
|
methodName,
|
||||||
|
options,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Could not find a GraphQL type for ${objectMetadata.id} for method ${methodName}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowedMethodNames = [
|
||||||
|
'updateMany',
|
||||||
|
'deleteMany',
|
||||||
|
'createMany',
|
||||||
|
'findDuplicates',
|
||||||
|
'restoreMany',
|
||||||
|
'destroyMany',
|
||||||
|
];
|
||||||
|
|
||||||
|
const outputType = this.typeMapperService.mapToGqlType(objectType, {
|
||||||
|
isArray: allowedMethodNames.includes(methodName),
|
||||||
|
});
|
||||||
|
|
||||||
|
fieldConfigMap[name] = {
|
||||||
|
type: outputType,
|
||||||
|
args: argsType,
|
||||||
|
resolve: undefined,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowedMethodNames = [
|
|
||||||
'updateMany',
|
|
||||||
'deleteMany',
|
|
||||||
'createMany',
|
|
||||||
'findDuplicates',
|
|
||||||
'restoreMany',
|
|
||||||
'destroyMany',
|
|
||||||
];
|
|
||||||
|
|
||||||
const outputType = this.typeMapperService.mapToGqlType(objectType, {
|
|
||||||
isArray: allowedMethodNames.includes(methodName),
|
|
||||||
});
|
|
||||||
|
|
||||||
fieldConfigMap[name] = {
|
|
||||||
type: outputType,
|
|
||||||
args: argsType,
|
|
||||||
resolve: undefined,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { WorkspaceResolverBuilderModule } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.module';
|
||||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||||
|
|
||||||
@ -11,7 +12,11 @@ import { TypeMapperService } from './services/type-mapper.service';
|
|||||||
import { TypeDefinitionsStorage } from './storages/type-definitions.storage';
|
import { TypeDefinitionsStorage } from './storages/type-definitions.storage';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ObjectMetadataModule, FeatureFlagModule],
|
imports: [
|
||||||
|
ObjectMetadataModule,
|
||||||
|
FeatureFlagModule,
|
||||||
|
WorkspaceResolverBuilderModule,
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
TypeDefinitionsStorage,
|
TypeDefinitionsStorage,
|
||||||
TypeMapperService,
|
TypeMapperService,
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
import { ObjectRecordDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* objectName: directly reference the name of the object from the metadata tables.
|
|
||||||
* columnNames: reference the column names not the field names.
|
|
||||||
* So if we need to reference a custom field, we should directly add the column name like `_customColumn`.
|
|
||||||
* If we need to terence a composite field, we should add all children of the composite like `nameFirstName` and `nameLastName`
|
|
||||||
*/
|
|
||||||
export const DUPLICATE_CRITERIA_COLLECTION: ObjectRecordDuplicateCriteria[] = [
|
|
||||||
{
|
|
||||||
objectName: 'company',
|
|
||||||
columnNames: ['domainName'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
objectName: 'company',
|
|
||||||
columnNames: ['name'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
objectName: 'person',
|
|
||||||
columnNames: ['nameFirstName', 'nameLastName'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
objectName: 'person',
|
|
||||||
columnNames: ['linkedinLinkPrimaryLinkUrl'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
objectName: 'person',
|
|
||||||
columnNames: ['email'],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@ -1,5 +1,7 @@
|
|||||||
import { IndexMetadataInterface } from 'src/engine/metadata-modules/index-metadata/interfaces/index-metadata.interface';
|
import { IndexMetadataInterface } from 'src/engine/metadata-modules/index-metadata/interfaces/index-metadata.interface';
|
||||||
|
|
||||||
|
import { WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type';
|
||||||
|
|
||||||
import { FieldMetadataInterface } from './field-metadata.interface';
|
import { FieldMetadataInterface } from './field-metadata.interface';
|
||||||
import { RelationMetadataInterface } from './relation-metadata.interface';
|
import { RelationMetadataInterface } from './relation-metadata.interface';
|
||||||
|
|
||||||
@ -22,6 +24,7 @@ export interface ObjectMetadataInterface {
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
isRemote: boolean;
|
isRemote: boolean;
|
||||||
isAuditLogged: boolean;
|
isAuditLogged: boolean;
|
||||||
|
duplicateCriteria?: WorkspaceEntityDuplicateCriteria[];
|
||||||
labelIdentifierFieldMetadataId?: string | null;
|
labelIdentifierFieldMetadataId?: string | null;
|
||||||
imageIdentifierFieldMetadataId?: string | null;
|
imageIdentifierFieldMetadataId?: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
QueryOptions,
|
QueryOptions,
|
||||||
} from '@ptc-org/nestjs-query-graphql';
|
} from '@ptc-org/nestjs-query-graphql';
|
||||||
|
|
||||||
|
import { WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type';
|
||||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||||
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
||||||
import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto';
|
import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto';
|
||||||
@ -85,4 +86,7 @@ export class ObjectMetadataDTO {
|
|||||||
|
|
||||||
@Field()
|
@Field()
|
||||||
isLabelSyncedWithName: boolean;
|
isLabelSyncedWithName: boolean;
|
||||||
|
|
||||||
|
@Field(() => [[String]], { nullable: true })
|
||||||
|
duplicateCriteria?: WorkspaceEntityDuplicateCriteria[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
|
|
||||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||||
|
|
||||||
|
import { WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type';
|
||||||
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||||
@ -69,6 +70,9 @@ export class ObjectMetadataEntity implements ObjectMetadataInterface {
|
|||||||
@Column({ default: true })
|
@Column({ default: true })
|
||||||
isAuditLogged: boolean;
|
isAuditLogged: boolean;
|
||||||
|
|
||||||
|
@Column({ type: 'jsonb', nullable: true })
|
||||||
|
duplicateCriteria?: WorkspaceEntityDuplicateCriteria[];
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
shortcut: string;
|
shortcut: string;
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -33,6 +33,11 @@ export function WorkspaceEntity(
|
|||||||
'workspace:gate-metadata-args',
|
'workspace:gate-metadata-args',
|
||||||
target,
|
target,
|
||||||
);
|
);
|
||||||
|
const duplicateCriteria = TypedReflect.getMetadata(
|
||||||
|
'workspace:duplicate-criteria-metadata-args',
|
||||||
|
target,
|
||||||
|
);
|
||||||
|
|
||||||
const objectName = convertClassNameToObjectMetadataName(target.name);
|
const objectName = convertClassNameToObjectMetadataName(target.name);
|
||||||
|
|
||||||
metadataArgsStorage.addEntities({
|
metadataArgsStorage.addEntities({
|
||||||
@ -51,6 +56,7 @@ export function WorkspaceEntity(
|
|||||||
isAuditLogged,
|
isAuditLogged,
|
||||||
isSystem,
|
isSystem,
|
||||||
gate,
|
gate,
|
||||||
|
duplicateCriteria,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { Gate } from 'src/engine/twenty-orm/interfaces/gate.interface';
|
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 {
|
export interface WorkspaceEntityMetadataArgs {
|
||||||
/**
|
/**
|
||||||
* Standard id.
|
* Standard id.
|
||||||
@ -65,4 +67,9 @@ export interface WorkspaceEntityMetadataArgs {
|
|||||||
* Image identifier.
|
* Image identifier.
|
||||||
*/
|
*/
|
||||||
readonly imageIdentifierStandardId: string | null;
|
readonly imageIdentifierStandardId: string | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplicate criteria.
|
||||||
|
*/
|
||||||
|
readonly duplicateCriteria?: WorkspaceEntityDuplicateCriteria[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,27 +40,10 @@ export function formatResult<T>(
|
|||||||
throw new Error('Object metadata is missing');
|
throw new Error('Object metadata is missing');
|
||||||
}
|
}
|
||||||
|
|
||||||
const compositeFieldMetadataCollection = getCompositeFieldMetadataCollection(
|
const compositeFieldMetadataMap = getCompositeFieldMetadataMap(
|
||||||
objectMetadataItemWithFieldMaps,
|
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(
|
const relationMetadataMap = new Map(
|
||||||
Object.values(objectMetadataItemWithFieldMaps.fieldsById)
|
Object.values(objectMetadataItemWithFieldMaps.fieldsById)
|
||||||
.filter(({ type }) => isRelationFieldMetadataType(type))
|
.filter(({ type }) => isRelationFieldMetadataType(type))
|
||||||
@ -199,6 +182,31 @@ export function formatResult<T>(
|
|||||||
return newData as 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(
|
function formatFieldMetadataValue(
|
||||||
value: any,
|
value: any,
|
||||||
fieldMetadata: FieldMetadataInterface,
|
fieldMetadata: FieldMetadataInterface,
|
||||||
|
|||||||
@ -63,9 +63,18 @@ export class WorkspaceObjectComparator {
|
|||||||
for (const difference of objectMetadataDifference) {
|
for (const difference of objectMetadataDifference) {
|
||||||
// We only handle CHANGE here as REMOVE and CREATE are handled earlier.
|
// We only handle CHANGE here as REMOVE and CREATE are handled earlier.
|
||||||
if (difference.type === 'CHANGE') {
|
if (difference.type === 'CHANGE') {
|
||||||
|
// If the old value and the new value are both null, skip
|
||||||
|
// Database is storing null, and we can get undefined here
|
||||||
|
if (
|
||||||
|
difference.oldValue === null &&
|
||||||
|
(difference.value === null || difference.value === undefined)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const property = difference.path[0];
|
const property = difference.path[0];
|
||||||
|
|
||||||
objectPropertiesToUpdate[property] = difference.value;
|
objectPropertiesToUpdate[property] = standardObjectMetadata[property];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import {
|
|||||||
RelationOnDeleteAction,
|
RelationOnDeleteAction,
|
||||||
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||||
|
import { WorkspaceDuplicateCriteria } from 'src/engine/twenty-orm/decorators/workspace-duplicate-criteria.decorator';
|
||||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||||
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
|
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
|
||||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||||
@ -57,6 +58,7 @@ export const SEARCH_FIELDS_FOR_COMPANY: FieldTypeAndNameMetadata[] = [
|
|||||||
shortcut: 'C',
|
shortcut: 'C',
|
||||||
labelIdentifierStandardId: COMPANY_STANDARD_FIELD_IDS.name,
|
labelIdentifierStandardId: COMPANY_STANDARD_FIELD_IDS.name,
|
||||||
})
|
})
|
||||||
|
@WorkspaceDuplicateCriteria([['name'], ['domainNamePrimaryLinkUrl']])
|
||||||
export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
|
export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
|
||||||
@WorkspaceField({
|
@WorkspaceField({
|
||||||
standardId: COMPANY_STANDARD_FIELD_IDS.name,
|
standardId: COMPANY_STANDARD_FIELD_IDS.name,
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import {
|
|||||||
RelationOnDeleteAction,
|
RelationOnDeleteAction,
|
||||||
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||||
|
import { WorkspaceDuplicateCriteria } from 'src/engine/twenty-orm/decorators/workspace-duplicate-criteria.decorator';
|
||||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||||
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
|
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
|
||||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||||
@ -62,6 +63,11 @@ export const SEARCH_FIELDS_FOR_PERSON: FieldTypeAndNameMetadata[] = [
|
|||||||
labelIdentifierStandardId: PERSON_STANDARD_FIELD_IDS.name,
|
labelIdentifierStandardId: PERSON_STANDARD_FIELD_IDS.name,
|
||||||
imageIdentifierStandardId: PERSON_STANDARD_FIELD_IDS.avatarUrl,
|
imageIdentifierStandardId: PERSON_STANDARD_FIELD_IDS.avatarUrl,
|
||||||
})
|
})
|
||||||
|
@WorkspaceDuplicateCriteria([
|
||||||
|
['nameFirstName', 'nameLastName'],
|
||||||
|
['linkedinLinkPrimaryLinkUrl'],
|
||||||
|
['emailsPrimaryEmail'],
|
||||||
|
])
|
||||||
export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
|
export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
|
||||||
@WorkspaceField({
|
@WorkspaceField({
|
||||||
standardId: PERSON_STANDARD_FIELD_IDS.name,
|
standardId: PERSON_STANDARD_FIELD_IDS.name,
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import 'reflect-metadata';
|
|||||||
|
|
||||||
import { Gate } from 'src/engine/twenty-orm/interfaces/gate.interface';
|
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';
|
||||||
import { EnvironmentVariablesMetadataMap } from 'src/engine/core-modules/environment/decorators/environment-variables-metadata.decorator';
|
import { EnvironmentVariablesMetadataMap } from 'src/engine/core-modules/environment/decorators/environment-variables-metadata.decorator';
|
||||||
|
|
||||||
export interface ReflectMetadataTypeMap {
|
export interface ReflectMetadataTypeMap {
|
||||||
@ -12,6 +13,7 @@ export interface ReflectMetadataTypeMap {
|
|||||||
['workspace:is-primary-field-metadata-args']: true;
|
['workspace:is-primary-field-metadata-args']: true;
|
||||||
['workspace:is-deprecated-field-metadata-args']: true;
|
['workspace:is-deprecated-field-metadata-args']: true;
|
||||||
['workspace:is-unique-metadata-args']: true;
|
['workspace:is-unique-metadata-args']: true;
|
||||||
|
['workspace:duplicate-criteria-metadata-args']: WorkspaceEntityDuplicateCriteria[];
|
||||||
['environment-variables']: EnvironmentVariablesMetadataMap;
|
['environment-variables']: EnvironmentVariablesMetadataMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user